From c2f7606d1ecbb28becac2cd1891de5fe9731e41c Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Thu, 10 Nov 2016 18:39:52 +0100 Subject: [PATCH] Refactor --- nemubot/__init__.py | 2 +- nemubot/__main__.py | 85 +++--- nemubot/bot.py | 557 +------------------------------------ nemubot/config/server.py | 10 +- nemubot/consumer.py | 39 +-- nemubot/modulecontext.py | 38 ++- nemubot/server/IRC.py | 11 +- nemubot/server/__init__.py | 39 +-- nemubot/server/abstract.py | 2 +- nemubot/server/socket.py | 4 +- 10 files changed, 135 insertions(+), 652 deletions(-) diff --git a/nemubot/__init__.py b/nemubot/__init__.py index c4e1df9..8ec9c1a 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -19,7 +19,7 @@ __author__ = 'nemunaire' from nemubot.modulecontext import ModuleContext -context = ModuleContext(None, None) +context = ModuleContext(None, None, None) def requires_version(min=None, max=None): diff --git a/nemubot/__main__.py b/nemubot/__main__.py index c39dd2f..e4acd5c 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -15,6 +15,7 @@ # along with this program. If not, see . def main(): + import functools import os import signal import sys @@ -71,8 +72,8 @@ def main(): args.pidfile = os.path.abspath(os.path.expanduser(args.pidfile)) args.socketfile = os.path.abspath(os.path.expanduser(args.socketfile)) args.logfile = os.path.abspath(os.path.expanduser(args.logfile)) - args.files = [ x for x in map(os.path.abspath, args.files)] - args.modules_path = [ x for x in map(os.path.abspath, args.modules_path)] + args.files = [x for x in map(os.path.abspath, args.files)] + args.modules_path = [x for x in map(os.path.abspath, args.modules_path)] # Check if an instance is already launched if args.pidfile is not None and os.path.isfile(args.pidfile): @@ -96,7 +97,7 @@ def main(): with open(args.pidfile, "w+") as f: f.write(str(os.getpid())) - # Setup loggin interface + # Setup logging interface import logging logger = logging.getLogger("nemubot") logger.setLevel(logging.DEBUG) @@ -125,51 +126,59 @@ def main(): # Create bot context from nemubot import datastore - from nemubot.bot import Bot, sync_act - context = Bot(modules_paths=modules_paths, - data_store=datastore.XML(args.data_path), - verbosity=args.verbose) + from nemubot.bot import Bot#, sync_act + context = Bot() if args.no_connect: context.noautoconnect = True # Register the hook for futur import - from nemubot.importer import ModuleFinder - module_finder = ModuleFinder(context.modules_paths, context.add_module) - sys.meta_path.append(module_finder) + #from nemubot.importer import ModuleFinder + #module_finder = ModuleFinder(modules_paths, context.add_module) + #sys.meta_path.append(module_finder) # Load requested configuration files - for path in args.files: - if os.path.isfile(path): - sync_act("loadconf", path) - else: - logger.error("%s is not a readable file", path) + #for path in args.files: + # if os.path.isfile(path): + # sync_act("loadconf", path) + # else: + # logger.error("%s is not a readable file", path) if args.module: for module in args.module: __import__(module) - # Signals handling - def sigtermhandler(signum, frame): - """On SIGTERM and SIGINT, quit nicely""" - context.quit() - signal.signal(signal.SIGINT, sigtermhandler) - signal.signal(signal.SIGTERM, sigtermhandler) + # Signals handling ################################################ - def sighuphandler(signum, frame): - """On SIGHUP, perform a deep reload""" + # SIGINT and SIGTERM + + def ask_exit(signame): + """On SIGTERM and SIGINT, quit nicely""" + context.stop() + + for sig in (signal.SIGINT, signal.SIGTERM): + context._loop.add_signal_handler(sig, functools.partial(ask_exit, sig)) + + + # SIGHUP + + def ask_reload(): + """Perform a deep reload""" nonlocal context logger.debug("SIGHUP receive, iniate reload procedure...") # Reload configuration file - for path in args.files: - if os.path.isfile(path): - sync_act("loadconf", path) - signal.signal(signal.SIGHUP, sighuphandler) + #for path in args.files: + # if os.path.isfile(path): + # sync_act("loadconf", path) + context._loop.add_signal_handler(signal.SIGHUP, ask_reload) - def sigusr1handler(signum, frame): - """On SIGHUSR1, display stacktraces""" + + # SIGUSR1 + + def ask_debug_info(): + """Display debug informations and stacktraces""" import threading, traceback for threadId, stack in sys._current_frames().items(): thName = "#%d" % threadId @@ -180,26 +189,26 @@ def main(): logger.debug("########### Thread %s:\n%s", thName, "".join(traceback.format_stack(stack))) - signal.signal(signal.SIGUSR1, sigusr1handler) + context._loop.add_signal_handler(signal.SIGUSR1, ask_debug_info) - if args.socketfile: - from nemubot.server.socket import UnixSocketListener - context.add_server(UnixSocketListener(new_server_cb=context.add_server, - location=args.socketfile, - name="master_socket")) + #if args.socketfile: + # from nemubot.server.socket import UnixSocketListener + # context.add_server(UnixSocketListener(new_server_cb=context.add_server, + # location=args.socketfile, + # name="master_socket")) # context can change when performing an hotswap, always join the latest context oldcontext = None while oldcontext != context: oldcontext = context - context.start() - context.join() + context.run() # Wait for consumers logger.info("Waiting for other threads shuts down...") if args.debug: - sigusr1handler(0, None) + ask_debug_info() sys.exit(0) + if __name__ == "__main__": main() diff --git a/nemubot/bot.py b/nemubot/bot.py index c8ede40..dc7fa08 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -14,565 +14,34 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from datetime import datetime, timezone import logging -from multiprocessing import JoinableQueue -import threading -import select import sys -from nemubot import __version__ -from nemubot.consumer import Consumer, EventConsumer, MessageConsumer -from nemubot import datastore -import nemubot.hooks -logger = logging.getLogger("nemubot") - -sync_queue = JoinableQueue() - -def sync_act(*args): - sync_queue.put(list(args)) - - -class Bot(threading.Thread): +class Bot: """Class containing the bot context and ensuring key goals""" - def __init__(self, ip="127.0.0.1", modules_paths=list(), - data_store=datastore.Abstract(), verbosity=0): - """Initialize the bot context + logger = logging.getLogger("nemubot") - Keyword arguments: - ip -- The external IP of the bot (default: 127.0.0.1) - modules_paths -- Paths to all directories where looking for modules - data_store -- An instance of the nemubot datastore for bot's modules - verbosity -- verbosity level + def __init__(self): + """Initialize the bot context """ - super().__init__(name="Nemubot main") + from nemubot import __version__ + Bot.logger.info("Initiate nemubot v%s, running on Python %s", + __version__, sys.version.split("\n")[0]) - logger.info("Initiate nemubot v%s (running on Python %s.%s.%s)", - __version__, - sys.version_info.major, sys.version_info.minor, sys.version_info.micro) - - self.verbosity = verbosity - self.stop = None - - # External IP for accessing this bot - import ipaddress - self.ip = ipaddress.ip_address(ip) - - # Context paths - self.modules_paths = modules_paths - self.datastore = data_store - self.datastore.open() - - # Keep global context: servers and modules - self._poll = select.poll() - self.servers = dict() - self.modules = dict() - self.modules_configuration = dict() - - # Events - self.events = list() - self.event_timer = None - - # Own hooks - from nemubot.treatment import MessageTreater - self.treater = MessageTreater() - - import re - def in_ping(msg): - return msg.respond("pong") - self.treater.hm.add_hook(nemubot.hooks.Message(in_ping, - match=lambda msg: re.match("^ *(m[' ]?entends?[ -]+tu|h?ear me|do you copy|ping)", - msg.message, re.I)), - "in", "DirectAsk") - - def in_echo(msg): - from nemubot.message import Text - return Text(msg.nick + ": " + " ".join(msg.args), to=msg.to_response) - self.treater.hm.add_hook(nemubot.hooks.Command(in_echo, "echo"), "in", "Command") - - def _help_msg(msg): - """Parse and response to help messages""" - from more import Response - res = Response(channel=msg.to_response) - if len(msg.args) >= 1: - if msg.args[0] in self.modules: - if hasattr(self.modules[msg.args[0]], "help_full"): - hlp = self.modules[msg.args[0]].help_full() - if isinstance(hlp, Response): - return hlp - else: - res.append_message(hlp) - else: - res.append_message([str(h) for s,h in self.modules[msg.args[0]].__nemubot_context__.hooks], title="Available commands for module " + msg.args[0]) - elif msg.args[0][0] == "!": - from nemubot.message.command import Command - for h in self.treater._in_hooks(Command(msg.args[0][1:])): - if h.help_usage: - lp = ["\x03\x02%s%s\x03\x02: %s" % (msg.args[0], (" " + k if k is not None else ""), h.help_usage[k]) for k in h.help_usage] - jp = h.keywords.help() - return res.append_message(lp + ([". Moreover, you can provides some optional parameters: "] + jp if len(jp) else []), title="Usage for command %s" % msg.args[0]) - elif h.help: - return res.append_message("Command %s: %s" % (msg.args[0], h.help)) - else: - return res.append_message("Sorry, there is currently no help for the command %s. Feel free to make a pull request at https://github.com/nemunaire/nemubot/compare" % msg.args[0]) - res.append_message("Sorry, there is no command %s" % msg.args[0]) - else: - res.append_message("Sorry, there is no module named %s" % msg.args[0]) - 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.treater.hm.add_hook(nemubot.hooks.Command(_help_msg, "help"), "in", "Command") - - from queue import Queue - # Messages to be treated - self.cnsr_queue = Queue() - self.cnsr_thrd = list() - self.cnsr_thrd_size = -1 + import asyncio + self._loop = asyncio.get_event_loop() def run(self): - self._poll.register(sync_queue._reader, select.POLLIN | select.POLLPRI) + self._loop.run_forever() + self._loop.close() - logger.info("Starting main loop") - self.stop = False - while not self.stop: - for fd, flag in self._poll.poll(): - # Handle internal socket passing orders - if fd != sync_queue._reader.fileno() and fd in self.servers: - srv = self.servers[fd] - if flag & (select.POLLERR | select.POLLHUP | select.POLLNVAL): - try: - srv.exception(flag) - except: - logger.exception("Uncatched exception on server exception") - - if srv.fileno() > 0: - if flag & (select.POLLOUT): - try: - srv.async_write() - except: - logger.exception("Uncatched exception on server write") - - if flag & (select.POLLIN | select.POLLPRI): - try: - for i in srv.async_read(): - self.receive_message(srv, i) - except: - logger.exception("Uncatched exception on server read") - - else: - del self.servers[fd] - - - # Always check the sync queue - while not sync_queue.empty(): - args = sync_queue.get() - action = args.pop(0) - - if action == "sckt" and len(args) >= 2: - try: - if args[0] == "write": - self._poll.modify(int(args[1]), select.POLLOUT | select.POLLIN | select.POLLPRI) - elif args[0] == "unwrite": - self._poll.modify(int(args[1]), select.POLLIN | select.POLLPRI) - - elif args[0] == "register": - self._poll.register(int(args[1]), select.POLLIN | select.POLLPRI) - elif args[0] == "unregister": - self._poll.unregister(int(args[1])) - except: - logger.exception("Unhandled excpetion during action:") - - elif action == "exit": - self.quit() - - elif action == "loadconf": - for path in args: - logger.debug("Load configuration from %s", path) - self.load_file(path) - logger.info("Configurations successfully loaded") - - sync_queue.task_done() - - - # 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() - logger.info("Ending main loop") - - - - # Config methods - - def load_file(self, filename): - """Load a configuration file - - Arguments: - filename -- the path to the file to load - """ - - import os - - # Unexisting file, assume a name was passed, import the module! - if not os.path.isfile(filename): - return self.import_module(filename) - - from nemubot.channel import Channel - from nemubot import config - from nemubot.tools.xmlparser import XMLParser - - try: - p = XMLParser({ - "nemubotconfig": config.Nemubot, - "server": config.Server, - "channel": Channel, - "module": config.Module, - "include": config.Include, - }) - config = p.parse_file(filename) - except: - logger.exception("Can't load `%s'; this is not a valid nemubot " - "configuration file." % filename) - return False - - # Preset each server in this file - for server in config.servers: - srv = server.server(config) - # Add the server in the context - if self.add_server(srv, server.autoconnect): - logger.info("Server '%s' successfully added." % srv.name) - else: - logger.error("Can't add server '%s'." % srv.name) - - # Load module and their configuration - for mod in config.modules: - self.modules_configuration[mod.name] = mod - if mod.autoload: - try: - __import__(mod.name) - except: - logger.exception("Exception occurs when loading module" - " '%s'", mod.name) - - - # Load files asked by the configuration file - for load in config.includes: - self.load_file(load.path) - - - # Events methods - - def add_event(self, evt, eid=None, module_src=None): - """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 - """ - - if hasattr(self, "stop") and self.stop: - logger.warn("The bot is stopped, can't register new events") - return - - import uuid - - # Generate the event id if no given - if eid is None: - eid = uuid.uuid1() - - # Fill the id field of the event - if type(eid) is uuid.UUID: - evt.id = str(eid) - else: - # Ok, this is quiet useless... - try: - evt.id = str(uuid.UUID(eid)) - except ValueError: - evt.id = eid - - # TODO: mutex here plz - - # Add the event in its place - t = evt.current - i = 0 # sentinel - for i in range(0, len(self.events)): - if self.events[i].current > t: - break - 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 - - # Register the event in the source module - if module_src is not None: - module_src.__nemubot_context__.events.append(evt.id) - evt.module_src = module_src - - logger.info("New event registered in %d position: %s", i, t) - return evt.id - - - 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) - - from nemubot.event import ModuleEvent - 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]) - self._update_event_timer() - if module_src is not None: - module_src.__nemubot_context__.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.__nemubot_context__.events.remove(evt.id) - return True - return False - - - 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() - - if len(self.events): - remaining = self.events[0].time_left.total_seconds() - logger.debug("Update timer: next event in %d seconds", remaining) - self.event_timer = threading.Timer(remaining if remaining > 0 else 0, self._end_event_timer) - self.event_timer.start() - - else: - logger.debug("Update timer: no timer left") - - - def _end_event_timer(self): - """Function called at the end of the event timer""" - - while len(self.events) > 0 and datetime.now(timezone.utc) >= self.events[0].current: - evt = self.events.pop(0) - self.cnsr_queue.put_nowait(EventConsumer(evt)) - - self._update_event_timer() - - - # Consumers methods - - def add_server(self, srv, autoconnect=True): - """Add a new server to the context - - Arguments: - srv -- a concrete AbstractServer instance - autoconnect -- connect after add? - """ - - fileno = srv.fileno() - if fileno not in self.servers: - self.servers[fileno] = srv - self.servers[srv.name] = srv - if autoconnect and not hasattr(self, "noautoconnect"): - srv.connect() - return True - - else: - return False - - - # Modules methods - - def import_module(self, name): - """Load a module - - Argument: - name -- name of the module to load - """ - - if name in self.modules: - self.unload_module(name) - - __import__(name) - - - def add_module(self, module): - """Add a module to the context, if already exists, unload the - old one before""" - module_name = module.__spec__.name if hasattr(module, "__spec__") else module.__name__ - - if hasattr(self, "stop") and self.stop: - logger.warn("The bot is stopped, can't register new modules") - return - - # Check if the module already exists - if module_name in self.modules: - self.unload_module(module_name) - - # Overwrite print built-in - def prnt(*args): - if hasattr(module, "logger"): - module.logger.info(" ".join([str(s) for s in args])) - else: - logger.info("[%s] %s", module_name, " ".join([str(s) for s in args])) - module.print = prnt - - # Create module context - from nemubot.modulecontext import ModuleContext - module.__nemubot_context__ = ModuleContext(self, module) - - if not hasattr(module, "logger"): - module.logger = logging.getLogger("nemubot.module." + module_name) - - # Replace imported context by real one - for attr in module.__dict__: - if attr != "__nemubot_context__" and type(module.__dict__[attr]) == ModuleContext: - module.__dict__[attr] = module.__nemubot_context__ - - # Register decorated functions - import nemubot.hooks - for s, h in nemubot.hooks.hook.last_registered: - module.__nemubot_context__.add_hook(h, *s if isinstance(s, list) else s) - nemubot.hooks.hook.last_registered = [] - - # Launch the module - if hasattr(module, "load"): - try: - module.load(module.__nemubot_context__) - except: - module.__nemubot_context__.unload() - raise - - # Save a reference to the module - self.modules[module_name] = module - - - def unload_module(self, name): - """Unload a module""" - if name in self.modules: - self.modules[name].print("Unloading module %s" % name) - - # Call the user defined unload method - if hasattr(self.modules[name], "unload"): - self.modules[name].unload(self) - self.modules[name].__nemubot_context__.unload() - - # Remove from the nemubot dict - del self.modules[name] - - # Remove from the Python dict - del sys.modules[name] - for mod in [i for i in sys.modules]: - if mod[:len(name) + 1] == name + ".": - logger.debug("Module '%s' also removed from system modules list.", mod) - del sys.modules[mod] - - logger.info("Module `%s' successfully unloaded.", name) - - return True - return False - - - def receive_message(self, srv, msg): - """Queued the message for treatment - - Arguments: - srv -- The server where the message comes from - msg -- The message not parsed, as simple as possible - """ - - self.cnsr_queue.put_nowait(MessageConsumer(srv, msg)) - - - def quit(self): + def stop(self): """Save and unload modules and disconnect servers""" - if self.event_timer is not None: - logger.info("Stop the event timer...") - self.event_timer.cancel() - - logger.info("Save and unload all modules...") - for mod in self.modules.items(): - self.unload_module(mod) - - logger.info("Close all servers connection...") - for srv in [self.servers[k] for k in self.servers]: - srv.close() - - logger.info("Stop consumers") - k = self.cnsr_thrd - for cnsr in k: - cnsr.stop = True - - self.datastore.close() - - self.stop = True - sync_act("end") - sync_queue.join() - - - # 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) + self._loop.stop() diff --git a/nemubot/config/server.py b/nemubot/config/server.py index 14ca9a8..c08b40c 100644 --- a/nemubot/config/server.py +++ b/nemubot/config/server.py @@ -33,13 +33,11 @@ class Server: return True - def server(self, parent): + def server(self, caps=[], **kwargs): from nemubot.server import factory - for a in ["nick", "owner", "realname", "encoding"]: - if a not in self.args: - self.args[a] = getattr(parent, a) + caps += self.caps - self.caps += parent.caps + kwargs.update(self.args) - return factory(self.uri, caps=self.caps, channels=self.channels, **self.args) + return factory(self.uri, caps=caps, channels=self.channels, **kwargs) diff --git a/nemubot/consumer.py b/nemubot/consumer.py index 2765aff..37234aa 100644 --- a/nemubot/consumer.py +++ b/nemubot/consumer.py @@ -33,6 +33,11 @@ class MessageConsumer: def run(self, context): """Create, parse and treat the message""" + # Dereference weakptr + srv = self.srv() + if srv is None: + return + from nemubot.bot import Bot assert isinstance(context, Bot) @@ -40,7 +45,7 @@ class MessageConsumer: # Parse message try: - for msg in self.srv.parse(self.orig): + for msg in srv.parse(self.orig): msgs.append(msg) except: logger.exception("Error occurred during the processing of the %s: " @@ -52,13 +57,13 @@ class MessageConsumer: # Identify destination to_server = None if isinstance(res, str): - to_server = self.srv + to_server = srv elif not hasattr(res, "server"): logger.error("No server defined for response of type %s: %s", type(res).__name__, res) continue elif res.server is None: - to_server = self.srv - res.server = self.srv.fileno() + to_server = srv + res.server = srv.fileno() elif res.server in context.servers: to_server = context.servers[res.server] else: @@ -89,12 +94,7 @@ class EventConsumer: # Reappend the event in the queue if it has next iteration if self.evt.next is not None: - context.add_event(self.evt, eid=self.evt.id) - - # Or remove reference of this event - elif (hasattr(self.evt, "module_src") and - self.evt.module_src is not None): - self.evt.module_src.__nemubot_context__.events.remove(self.evt.id) + context.add_event(self.evt) @@ -103,20 +103,23 @@ class Consumer(threading.Thread): """Dequeue and exec requested action""" def __init__(self, context): - self.context = context - self.stop = False super().__init__(name="Nemubot consumer") + self.context = context def run(self): try: - while not self.stop: - stm = self.context.cnsr_queue.get(True, 1) - stm.run(self.context) - self.context.cnsr_queue.task_done() + while True: + context = self.context() + if context is None: + break + + stm = context.cnsr_queue.get(True, 1) + stm.run(context) + context.cnsr_queue.task_done() except queue.Empty: pass finally: - self.context.cnsr_thrd_size -= 2 - self.context.cnsr_thrd.remove(self) + if self.context() is not None: + self.context().cnsr_thrd.remove(self) diff --git a/nemubot/modulecontext.py b/nemubot/modulecontext.py index 1d1b3d0..572b14b 100644 --- a/nemubot/modulecontext.py +++ b/nemubot/modulecontext.py @@ -16,19 +16,15 @@ class ModuleContext: - def __init__(self, context, module): + def __init__(self, context, module_name, logger): """Initialize the module context arguments: context -- the bot context - module -- the module + module_name -- the module name + logger -- a logger """ - if module is not None: - module_name = module.__spec__.name if hasattr(module, "__spec__") else module.__name__ - else: - module_name = "" - # Load module configuration if exists if (context is not None and module_name in context.modules_configuration): @@ -60,10 +56,16 @@ class ModuleContext: def subtreat(msg): yield from context.treater.treat_msg(msg) - def add_event(evt, eid=None): - return context.add_event(evt, eid, module_src=module) + def add_event(call=None, **kwargs): + evt = context.add_event(call, **kwargs) + if evt is not None: + self.events.append(evt) + return evt def del_event(evt): - return context.del_event(evt, module_src=module) + if context.del_event(evt): + self._clean_events() + return True + return False def send_response(server, res): if server in context.servers: @@ -72,7 +74,7 @@ class ModuleContext: else: return context.servers[server].send_response(res) else: - module.logger.error("Try to send a message to the unknown server: %s", server) + logger.error("Try to send a message to the unknown server: %s", server) return False else: # Used when using outside of nemubot @@ -88,13 +90,13 @@ class ModuleContext: self.hooks.remove((triggers, hook)) def subtreat(msg): return None - def add_event(evt, eid=None): - return context.add_event(evt, eid, module_src=module) + def add_event(evt): + return context.add_event(evt) def del_event(evt): return context.del_event(evt, module_src=module) def send_response(server, res): - module.logger.info("Send response: %s", res) + logger.info("Send response: %s", res) def save(): context.datastore.save(module_name, self.data) @@ -121,6 +123,14 @@ class ModuleContext: return self._data + def _clean_events(self): + """Look for None weakref in the events list""" + + for i in range(len(self.events), 0, -1): + if self.events[i-1]() is None: + self.events.remove() + + def unload(self): """Perform actions for unloading the module""" diff --git a/nemubot/server/IRC.py b/nemubot/server/IRC.py index 89eeab5..6affe55 100644 --- a/nemubot/server/IRC.py +++ b/nemubot/server/IRC.py @@ -23,9 +23,9 @@ from nemubot.server.message.IRC import IRC as IRCMessage from nemubot.server.socket import SocketServer, SecureSocketServer -class _IRC: +class IRC(): - """Concrete implementation of a connexion to an IRC server""" + """Concrete implementation of a connection to an IRC server""" def __init__(self, host="localhost", port=6667, owner=None, nick="nemubot", username=None, password=None, @@ -273,10 +273,3 @@ class _IRC: def subparse(self, orig, cnt): msg = IRCMessage(("@time=%s :%s!user@host.com PRIVMSG %s :%s" % (orig.date.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), orig.frm, ",".join(orig.to), cnt)).encode(self.encoding), self.encoding) return msg.to_bot_message(self) - - -class IRC(_IRC, SocketServer): - pass - -class IRC_secure(_IRC, SecureSocketServer): - pass diff --git a/nemubot/server/__init__.py b/nemubot/server/__init__.py index 6b583b7..a533491 100644 --- a/nemubot/server/__init__.py +++ b/nemubot/server/__init__.py @@ -16,7 +16,7 @@ def factory(uri, ssl=False, **init_args): - from urllib.parse import urlparse, unquote + from urllib.parse import urlparse, unquote, parse_qs o = urlparse(uri) srv = None @@ -45,25 +45,26 @@ def factory(uri, ssl=False, **init_args): modifiers = o.path.split(",") target = unquote(modifiers.pop(0)[1:]) - queries = o.query.split("&") - for q in queries: - if "=" in q: - key, val = tuple(q.split("=", 1)) - else: - key, val = q, "" - if key == "msg": - if "on_connect" not in args: - args["on_connect"] = [] - args["on_connect"].append("PRIVMSG %s :%s" % (target, unquote(val))) - elif key == "key": - if "channels" not in args: - args["channels"] = [] - args["channels"].append((target, unquote(val))) - elif key == "pass": - args["password"] = unquote(val) - elif key == "charset": - args["encoding"] = unquote(val) + # Read query string + params = parse_qs(o.query) + if "msg" in params: + if "on_connect" not in args: + args["on_connect"] = [] + args["on_connect"].append("PRIVMSG %s :%s" % (target, params["msg"])) + + if "key" in params: + if "channels" not in args: + args["channels"] = [] + args["channels"].append((target, params["key"])) + + if "pass" in params: + args["password"] = params["pass"] + + if "charset" in params: + args["encoding"] = params["charset"] + + # if "channels" not in args and "isnick" not in modifiers: args["channels"] = [ target ] diff --git a/nemubot/server/abstract.py b/nemubot/server/abstract.py index fd25c2d..48c5104 100644 --- a/nemubot/server/abstract.py +++ b/nemubot/server/abstract.py @@ -17,7 +17,7 @@ import logging import queue -from nemubot.bot import sync_act +#from nemubot.bot import sync_act class AbstractServer: diff --git a/nemubot/server/socket.py b/nemubot/server/socket.py index 72c0c7b..f5b9c9a 100644 --- a/nemubot/server/socket.py +++ b/nemubot/server/socket.py @@ -23,7 +23,7 @@ from nemubot.message.printer.socket import Socket as SocketPrinter from nemubot.server.abstract import AbstractServer -class _Socket(AbstractServer): +class _Socket(asyncio.Protocol): """Concrete implementation of a socket connection""" @@ -34,7 +34,7 @@ class _Socket(AbstractServer): super().__init__(**kwargs) self.readbuffer = b'' - self.printer = printer + self.printer = printer # Write