nemubot/bot.py

498 lines
16 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
from hooks.messagehook import MessageHook
from hooks.manager 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
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(message="pong", channel=msg.receivers, nick=msg.nick)
2014-09-22 15:48:41 +00:00
self.hooks.add_hook(MessageHook(in_ping), "in", "PRIVMSG", "ask")
def _help_msg(msg):
"""Parse and response to help messages"""
cmd = msg.cmds
res = response.Response()
if len(cmd) > 1:
if cmd[1] in self.modules:
if len(cmd) > 2:
if hasattr(self.modules[cmd[1]], "HELP_cmd"):
res.append_message(self.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(self.modules[cmd[1]], "help_full"):
res.append_message(self.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",
message=["\x03\x02%s\x03\x02 (%s)" % (im, self.modules[im].__doc__) for im in self.modules if self.modules[im].__doc__])
return res
self.hooks.add_hook(MessageHook(_help_msg, "help"), "in", "PRIVMSG", "cmd")
2012-08-27 22:27:02 +00:00
# Other known bots, making a bots network
self.network = 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:
try:
rl, wl, xl = select(_rlist, _wlist, _xlist, 0.1)
except:
logger.error("Something went wrong in select")
fnd_smth = False
# Looking for invalid server
for r in _rlist:
if not hasattr(r, "fileno") or not isinstance(r.fileno(), int):
_rlist.remove(r)
logger.error("Found invalid object in _rlist: " + r)
fnd_smth = True
for w in _wlist:
if not hasattr(r, "fileno") or not isinstance(w.fileno(), int):
_wlist.remove(w)
logger.error("Found invalid object in _wlist: " + w)
fnd_smth = True
for x in _xlist:
if not hasattr(r, "fileno") or not isinstance(x.fileno(), int):
_xlist.remove(x)
logger.error("Found invalid object in _xlist: " + x)
fnd_smth = True
if not fnd_smth:
logger.exception("Can't continue, sorry")
self.stop = True
continue
for x in xl:
try:
x.exception()
except:
logger.exception("Uncatched exception on server exception")
for w in wl:
try:
w.write_select()
except:
logger.exception("Uncatched exception on server write")
for r in rl:
for i in r.read():
try:
self.receive_message(r, i)
except:
logger.exception("Uncatched exception on server read")
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(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)
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
2014-09-11 15:23:07 +00:00
def receive_message(self, srv, msg, private=False, data=None):
"""Queued the message for treatment"""
#print("READ", raw_msg)
2014-09-11 15:23:07 +00:00
self.cnsr_queue.put_nowait(MessageConsumer(srv, msg))
# 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 hotswap(bak):
2014-09-09 05:02:41 +00:00
bak.stop = True
2014-08-29 09:53:32 +00:00
new = Bot(str(bak.ip), bak.modules_paths, bak.data_path)
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 event
imp.reload(event)
2014-09-09 05:02:41 +00:00
import exception
imp.reload(exception)
import hooks
imp.reload(hooks)
import hooks.manager
imp.reload(hooks.manager)
import hooks.messagehook
imp.reload(hooks.messagehook)
2014-09-09 05:02:41 +00:00
import importer
imp.reload(importer)
import message
imp.reload(message)
2014-09-09 05:02:41 +00:00
import prompt
imp.reload(prompt)
import prompt.builtins
imp.reload(prompt.builtins)
2014-09-09 05:02:41 +00:00
import response
imp.reload(response)
import server
2014-09-09 05:02:41 +00:00
rl,wl,xl = server._rlist,server._wlist,server._xlist
imp.reload(server)
2014-09-09 05:02:41 +00:00
server._rlist,server._wlist,server._xlist = rl,wl,xl
import server.socket
imp.reload(server.socket)
import server.IRC
imp.reload(server.IRC)
import tools
imp.reload(tools)
import tools.countdown
imp.reload(tools.countdown)
import tools.date
imp.reload(tools.date)
import tools.web
imp.reload(tools.web)
import xmlparser
imp.reload(xmlparser)
import xmlparser.node
imp.reload(xmlparser.node)