nemubot/bot.py

550 lines
18 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
# Nemubot is a modulable IRC bot, built around XML configuration files.
# Copyright (C) 2012 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from datetime import datetime
2012-10-15 00:46:23 +00:00
from datetime import timedelta
2014-08-14 10:49:38 +00:00
import logging
from queue import Queue
2014-08-29 09:53:32 +00:00
import re
from select import select
import threading
2012-10-15 00:46:23 +00:00
import time
2014-08-29 09:53:32 +00:00
import uuid
__version__ = '3.4.dev0'
__author__ = 'nemunaire'
2014-08-29 09:53:32 +00:00
from consumer import Consumer, EventConsumer, MessageConsumer
from event import ModuleEvent
import hooks
from hooksmanager import HooksManager
2012-08-27 22:27:02 +00:00
from networkbot import NetworkBot
2014-08-13 15:11:33 +00:00
from server.IRC import IRCServer
from server.DCC import DCC
import response
2014-08-27 05:57:00 +00:00
logger = logging.getLogger("nemubot.bot")
2014-08-14 10:49:38 +00:00
class Bot(threading.Thread):
2014-08-29 09:53:32 +00:00
"""Class containing the bot context and ensuring key goals"""
def __init__(self, ip="127.0.0.1", modules_paths=list(), data_path="./datas/"):
"""Initialize the bot context
Keyword arguments:
ip -- The external IP of the bot (default: 127.0.0.1)
modules_paths -- Paths to all directories where looking for module
data_path -- Path to directory where store bot context data
"""
threading.Thread.__init__(self)
2014-08-29 09:53:32 +00:00
logger.info("Initiate nemubot v%s", __version__)
# External IP for accessing this bot
self.ip = ip
2014-08-29 09:53:32 +00:00
# Context paths
self.modules_paths = modules_paths
self.data_path = data_path
# Save various informations
self.ctcp_capabilities = dict()
self.init_ctcp_capabilities()
2012-08-27 22:27:02 +00:00
# Keep global context: servers and modules
self.servers = dict()
self.modules = dict()
self.modules_configuration = dict()
2012-08-27 22:27:02 +00:00
# Events
self.events = list()
self.event_timer = None
# Own hooks
self.hooks = HooksManager()
def in_ping(msg):
if re.match("^ *(m[' ]?entends?[ -]+tu|h?ear me|do you copy|ping)", msg.text, re.I) is not None:
return response.Response(msg.sender, message="pong", channel=msg.receivers, nick=msg.nick)
self.hooks.add_hook(hooks.Hook(in_ping), "in", "PRIVMSG")
2012-08-27 22:27:02 +00:00
# Other known bots, making a bots network
self.network = dict()
self.hooks_cache = dict()
2012-08-27 22:27:02 +00:00
# Messages to be treated
self.cnsr_queue = Queue()
self.cnsr_thrd = list()
self.cnsr_thrd_size = -1
def run(self):
from server import _rlist, _wlist, _xlist
self.stop = False
while not self.stop:
rl, wl, xl = select(_rlist, _wlist, _xlist, 0.1)
for x in xl:
x.exception()
for w in wl:
w.write_select()
for r in rl:
for i in r.read():
self.receive_message(r, i)
def init_ctcp_capabilities(self):
"""Reset existing CTCP capabilities to default one"""
def _ctcp_clientinfo(srv, msg):
"""Response to CLIENTINFO CTCP message"""
return _ctcp_response(msg.sender,
" ".join(self.ctcp_capabilities.keys()))
def _ctcp_dcc(srv, msg):
"""Response to DCC CTCP message"""
try:
ip = srv.toIP(int(msg.cmds[3]))
port = int(msg.cmds[4])
conn = DCC(srv, msg.sender)
except:
return _ctcp_response(msg.sender, "ERRMSG invalid parameters provided as DCC CTCP request")
logger.info("Receive DCC connection request from %s to %s:%d", conn.sender, ip, port)
if conn.accept_user(ip, port):
srv.dcc_clients[conn.sender] = conn
conn.send_dcc("Hello %s!" % conn.nick)
else:
logger.error("DCC: unable to connect to %s:%d", ip, port)
return _ctcp_response(msg.sender, "ERRMSG unable to connect to %s:%d" % (ip, port))
self.ctcp_capabilities["ACTION"] = lambda srv, msg: print ("ACTION receive: %s" % msg.text)
self.ctcp_capabilities["CLIENTINFO"] = _ctcp_clientinfo
self.ctcp_capabilities["DCC"] = _ctcp_dcc
self.ctcp_capabilities["FINGER"] = lambda srv, msg: _ctcp_response(
msg.sender, "VERSION nemubot v%s" % __version__)
self.ctcp_capabilities["NEMUBOT"] = lambda srv, msg: _ctcp_response(
msg.sender, "NEMUBOT %s" % __version__)
self.ctcp_capabilities["PING"] = lambda srv, msg: _ctcp_response(
msg.sender, "PING %s" % " ".join(msg.cmds[1:]))
self.ctcp_capabilities["SOURCE"] = lambda srv, msg: _ctcp_response(
msg.sender, "SOURCE https://github.com/nemunaire/nemubot")
self.ctcp_capabilities["TIME"] = lambda srv, msg: _ctcp_response(
msg.sender, "TIME %s" % (datetime.now()))
self.ctcp_capabilities["USERINFO"] = lambda srv, msg: _ctcp_response(
msg.sender, "USERINFO %s" % srv.realname)
self.ctcp_capabilities["VERSION"] = lambda srv, msg: _ctcp_response(
msg.sender, "VERSION nemubot v%s" % __version__)
logger.debug("CTCP capabilities setup: %s", ", ".join(self.ctcp_capabilities))
2014-08-29 09:53:32 +00:00
# Events methods
def add_event(self, evt, eid=None, module_src=None):
2014-08-29 09:53:32 +00:00
"""Register an event and return its identifiant for futur update
Return:
None if the event is not in the queue (eg. if it has been executed during the call) or
returns the event ID.
Argument:
evt -- The event object to add
Keyword arguments:
eid -- The desired event ID (object or string UUID)
module_src -- The module to which the event is attached to
"""
# Generate the event id if no given
if eid is None:
2014-08-29 09:53:32 +00:00
eid = uuid.uuid1()
# Fill the id field of the event
if type(eid) is uuid.UUID:
evt.id = str(eid)
else:
2014-08-29 09:53:32 +00:00
# Ok, this is quite useless...
try:
evt.id = str(uuid.UUID(eid))
except ValueError:
evt.id = eid
2014-08-29 09:53:32 +00:00
# Add the event in its place
t = evt.current
2014-08-29 09:53:32 +00:00
i = 0 # sentinel
for i in range(0, len(self.events)):
if self.events[i].current > t:
break
2014-08-29 09:53:32 +00:00
self.events.insert(i, evt)
if i == 0:
# First event changed, reset timer
self._update_event_timer()
if len(self.events) <= 0 or self.events[i] != evt:
# Our event has been executed and removed from queue
return None
2014-08-29 09:53:32 +00:00
# Register the event in the source module
if module_src is not None:
module_src.REGISTERED_EVENTS.append(evt.id)
2014-08-29 09:53:32 +00:00
evt.module_src = module_src
2014-08-27 05:57:00 +00:00
logger.info("New event registered: %s -> %s", evt.id, evt)
return evt.id
2014-08-29 09:53:32 +00:00
def del_event(self, evt, module_src=None):
"""Find and remove an event from list
Return:
True if the event has been found and removed, False else
Argument:
evt -- The ModuleEvent object to remove or just the event identifier
Keyword arguments:
module_src -- The module to which the event is attached to (ignored if evt is a ModuleEvent)
"""
logger.info("Removing event: %s from %s", evt, module_src)
if type(evt) is ModuleEvent:
id = evt.id
module_src = evt.module_src
else:
id = evt
if len(self.events) > 0 and id == self.events[0].id:
self.events.remove(self.events[0])
2014-08-29 09:53:32 +00:00
self._update_event_timer()
if module_src is not None:
module_src.REGISTERED_EVENTS.remove(evt.id)
return True
for evt in self.events:
if evt.id == id:
self.events.remove(evt)
if module_src is not None:
module_src.REGISTERED_EVENTS.remove(evt.id)
return True
return False
2014-08-29 09:53:32 +00:00
def _update_event_timer(self):
"""(Re)launch the timer to end with the closest event"""
# Reset the timer if this is the first item
if self.event_timer is not None:
self.event_timer.cancel()
2014-08-29 09:53:32 +00:00
if len(self.events) > 0:
2014-08-27 05:57:00 +00:00
logger.debug("Update timer: next event in %d seconds",
2014-08-14 10:49:38 +00:00
self.events[0].time_left.seconds)
2012-10-15 00:46:23 +00:00
if datetime.now() + timedelta(seconds=5) >= self.events[0].current:
while datetime.now() < self.events[0].current:
time.sleep(0.6)
2014-08-29 09:53:32 +00:00
self._end_event_timer()
else:
self.event_timer = threading.Timer(
2014-08-29 09:53:32 +00:00
self.events[0].time_left.seconds + 1, self._end_event_timer)
self.event_timer.start()
2014-08-14 10:49:38 +00:00
else:
logger.debug("Update timer: no timer left")
2014-08-29 09:53:32 +00:00
def _end_event_timer(self):
"""Function called at the end of the event timer"""
2014-08-14 10:49:38 +00:00
while len(self.events) > 0 and datetime.now() >= self.events[0].current:
evt = self.events.pop(0)
2014-08-29 09:53:32 +00:00
self.cnsr_queue.put_nowait(EventConsumer(evt))
self._launch_consumers()
self._update_event_timer()
2014-08-29 09:53:32 +00:00
# Consumers methods
def _launch_consumers(self):
"""Launch new consumer threads if necessary"""
while self.cnsr_queue.qsize() > self.cnsr_thrd_size:
# Next launch if two more items in queue
self.cnsr_thrd_size += 2
c = Consumer(self)
self.cnsr_thrd.append(c)
c.start()
def add_server(self, node, nick, owner, realname):
"""Add a new server to the context"""
srv = IRCServer(node, nick, owner, realname)
#srv.register_hooks()
if srv.id not in self.servers:
self.servers[srv.id] = srv
srv.open()
return True
else:
return False
2014-08-29 09:53:32 +00:00
# Modules methods
def add_modules_path(self, path):
"""Add a path to the modules_path array, used by module loader"""
# The path must end by / char
if path[-1] != "/":
path += "/"
2014-08-29 09:53:32 +00:00
if path not in self.modules_paths:
self.modules_paths.append(path)
return True
return False
def add_module(self, module):
"""Add a module to the context, if already exists, unload the
old one before"""
# Check if the module already exists
for mod in self.modules.keys():
if self.modules[mod].name == module.name:
self.unload_module(self.modules[mod].name)
break
self.modules[module.name] = module
return True
2014-08-14 10:49:38 +00:00
def unload_module(self, name):
"""Unload a module"""
if name in self.modules:
self.modules[name].print_debug("Unloading module %s" % name)
self.modules[name].save()
if hasattr(self.modules[name], "unload"):
self.modules[name].unload(self)
# Remove registered hooks
for (s, h) in self.modules[name].REGISTERED_HOOKS:
self.hooks.del_hook(h, s)
# Remove registered events
for e in self.modules[name].REGISTERED_EVENTS:
self.del_event(e)
# Remove from the dict
del self.modules[name]
2014-08-27 05:57:00 +00:00
logger.info("Module `%s' successfully unloaded.", name)
return True
return False
def receive_message(self, srv, raw_msg, private=False, data=None):
"""Queued the message for treatment"""
#print("READ", raw_msg)
2014-08-29 09:53:32 +00:00
self.cnsr_queue.put_nowait(MessageConsumer(srv, raw_msg, datetime.now(), private, data))
# Launch a new thread if necessary
2014-08-29 09:53:32 +00:00
self._launch_consumers()
2012-08-27 22:27:02 +00:00
def add_networkbot(self, srv, dest, dcc=None):
"""Append a new bot into the network"""
id = srv.id + "/" + dest
if id not in self.network:
self.network[id] = NetworkBot(self, srv, dest, dcc)
return self.network[id]
def send_networkbot(self, srv, cmd, data=None):
for bot in self.network:
if self.network[bot].srv == srv:
self.network[bot].send_cmd(cmd, data)
2012-08-27 22:27:02 +00:00
2014-08-14 10:49:38 +00:00
def quit(self):
"""Save and unload modules and disconnect servers"""
2012-08-16 02:53:46 +00:00
if self.event_timer is not None:
2014-08-14 10:49:38 +00:00
logger.info("Stop the event timer...")
2012-08-16 02:53:46 +00:00
self.event_timer.cancel()
2014-08-14 10:49:38 +00:00
logger.info("Save and unload all modules...")
k = list(self.modules.keys())
for mod in k:
2014-08-14 10:49:38 +00:00
self.unload_module(mod)
2014-08-14 10:49:38 +00:00
logger.info("Close all servers connection...")
k = list(self.servers.keys())
for srv in k:
2014-08-31 08:51:44 +00:00
self.servers[srv].close()
self.stop = True
2014-08-29 09:53:32 +00:00
# Treatment
def check_rest_times(self, store, hook):
"""Remove from store the hook if it has been executed given time"""
if hook.times == 0:
if isinstance(store, dict):
store[hook.name].remove(hook)
if len(store) == 0:
del store[hook.name]
elif isinstance(store, list):
store.remove(hook)
def treat_prvmsg_ask(self, msg, srv):
# Treat ping
if re.match("^ *(m[' ]?entends?[ -]+tu|h?ear me|do you copy|ping)",
msg.text, re.I) is not None:
return response.Response(msg.sender, message="pong",
channel=msg.receivers, nick=msg.nick)
# Ask hooks
else:
return self.treat_ask(msg, srv)
def treat_prvmsg(self, msg, srv):
# First, treat CTCP
if msg.is_ctcp:
if msg.cmds[0] in self.ctcp_capabilities:
return self.ctcp_capabilities[msg.cmds[0]](srv, msg)
else:
return _ctcp_response(msg.sender, "ERRMSG Unknown or unimplemented CTCP request")
# Owner commands
if len(msg.text) > 1 and msg.text[0] == '`' and msg.nick == srv.owner:
#TODO: owner commands
pass
elif len(msg.text) > 1 and msg.text[0] == '!':
# Remove the !
msg.cmds[0] = msg.cmds[0][1:]
if msg.cmds[0] == "help":
2012-11-04 13:04:49 +00:00
return _help_msg(msg.sender, self.modules, msg.cmds)
elif msg.cmds[0] == "dcc":
2014-08-27 05:57:00 +00:00
logger.debug("dcctest for %s", msg.sender)
srv.send_dcc("Hello %s!" % msg.nick, msg.sender)
elif msg.cmds[0] == "pvdcctest":
2014-08-14 10:49:38 +00:00
logger.debug("dcctest")
return Response(msg.sender, message="Test DCC")
elif msg.cmds[0] == "dccsendtest":
2014-08-14 10:49:38 +00:00
logger.debug("dccsendtest")
conn = DCC(srv, msg.sender)
conn.send_file("bot_sample.xml")
else:
return self.treat_cmd(msg, srv)
else:
res = self.treat_answer(msg, srv)
# Assume the message starts with nemubot:
if not res and msg.private:
return self.treat_prvmsg_ask(msg, srv)
return res
def _ctcp_response(sndr, msg):
return response.Response(sndr, msg, ctcp=True)
2012-11-04 13:04:49 +00:00
def _help_msg(sndr, modules, cmd):
"""Parse and response to help messages"""
res = response.Response(sndr)
if len(cmd) > 1:
if cmd[1] in modules:
if len(cmd) > 2:
if hasattr(modules[cmd[1]], "HELP_cmd"):
res.append_message(modules[cmd[1]].HELP_cmd(cmd[2]))
else:
res.append_message("No help for command %s in module %s" % (cmd[2], cmd[1]))
elif hasattr(modules[cmd[1]], "help_full"):
res.append_message(modules[cmd[1]].help_full())
else:
res.append_message("No help for module %s" % cmd[1])
else:
res.append_message("No module named %s" % cmd[1])
else:
res.append_message("Pour me demander quelque chose, commencez "
"votre message par mon nom ; je réagis "
"également à certaine commandes commençant par"
" !. Pour plus d'informations, envoyez le "
"message \"!more\".")
res.append_message("Mon code source est libre, publié sous "
"licence AGPL (http://www.gnu.org/licenses/). "
"Vous pouvez le consulter, le dupliquer, "
"envoyer des rapports de bogues ou bien "
"contribuer au projet sur GitHub : "
"http://github.com/nemunaire/nemubot/")
res.append_message(title="Pour plus de détails sur un module, "
"envoyez \"!help nomdumodule\". Voici la liste"
" de tous les modules disponibles localement",
2014-08-27 23:39:31 +00:00
message=["\x03\x02%s\x03\x02 (%s)" % (im, modules[im].__doc__) for im in modules if modules[im].__doc__])
2012-11-04 13:04:49 +00:00
return res
def hotswap(bak):
2014-08-29 09:53:32 +00:00
new = Bot(str(bak.ip), bak.modules_paths, bak.data_path)
new.ctcp_capabilities = bak.ctcp_capabilities
new.servers = bak.servers
new.modules = bak.modules
new.modules_configuration = bak.modules_configuration
new.events = bak.events
new.hooks = bak.hooks
new.network = bak.network
return new
def reload():
import imp
import channel
imp.reload(channel)
import consumer
imp.reload(consumer)
import DCC
imp.reload(DCC)
import event
imp.reload(event)
import hooks
imp.reload(hooks)
import importer
imp.reload(importer)
import message
imp.reload(message)
import prompt.builtins
imp.reload(prompt.builtins)
import server
imp.reload(server)
import xmlparser
imp.reload(xmlparser)
import xmlparser.node
imp.reload(xmlparser.node)