From 57003f9d03802904008c4337b65721d7b6f6c06c Mon Sep 17 00:00:00 2001 From: nemunaire Date: Wed, 22 Apr 2015 16:56:07 +0200 Subject: [PATCH 001/271] Introducing daemon mode --- nemubot/__main__.py | 48 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 992c3ad..0e4433e 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -38,6 +38,9 @@ def main(): default=["./modules/"], help="directory to use as modules store") + parser.add_argument("-d", "--debug", action="store_true", + help="don't deamonize, keep in foreground") + parser.add_argument("-l", "--logfile", default="./nemubot.log", help="Path to store logs") @@ -64,6 +67,38 @@ def main(): 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)] + # Daemonize + if not args.debug: + try: + pid = os.fork() + if pid > 0: + sys.exit(0) + except OSError as err: + sys.stderr.write("Unable to fork: %s" % err) + sys.exit(1) + + os.setsid() + os.umask(0) + os.chdir('/') + + try: + pid = os.fork() + if pid > 0: + sys.exit(0) + except OSError as err: + sys.stderr.write("Unable to fork: %s" % err) + sys.exit(1) + + sys.stdout.flush() + sys.stderr.flush() + si = open(os.devnull, 'r') + so = open(os.devnull, 'a+') + se = open(os.devnull, 'a+') + + os.dup2(si.fileno(), sys.stdin.fileno()) + os.dup2(so.fileno(), sys.stdout.fileno()) + os.dup2(se.fileno(), sys.stderr.fileno()) + # Setup loggin interface import logging logger = logging.getLogger("nemubot") @@ -72,11 +107,12 @@ def main(): formatter = logging.Formatter( '%(asctime)s %(name)s %(levelname)s %(message)s') - ch = logging.StreamHandler() - ch.setFormatter(formatter) - if args.verbose < 2: - ch.setLevel(logging.INFO) - logger.addHandler(ch) + if args.debug: + ch = logging.StreamHandler() + ch.setFormatter(formatter) + if args.verbose < 2: + ch.setLevel(logging.INFO) + logger.addHandler(ch) fh = logging.FileHandler(args.logfile) fh.setFormatter(formatter) @@ -148,7 +184,7 @@ def main(): "the prompt.") context.quit() - print("Waiting for other threads shuts down...") + logger.info("Waiting for other threads shuts down...") sys.exit(0) if __name__ == "__main__": From 93f7061e08f26f41f6dacf7b128717323b0012b8 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Fri, 8 May 2015 00:20:14 +0200 Subject: [PATCH 002/271] Remove prompt at launch --- nemubot/__main__.py | 30 ++---------------------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 0e4433e..80ae7f9 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -155,35 +155,9 @@ def main(): for module in args.module: __import__(module) - print ("Nemubot v%s ready, my PID is %i!" % (nemubot.__version__, - os.getpid())) - while True: - from nemubot.prompt.reset import PromptReset - try: - context.start() - if prmpt.run(context): - break - except PromptReset as e: - if e.type == "quit": - break + context.start() + context.join() - try: - import imp - # Reload all other modules - imp.reload(nemubot) - imp.reload(nemubot.prompt) - nemubot.reload() - import nemubot.bot - context = nemubot.bot.hotswap(context) - prmpt = nemubot.prompt.hotswap(prmpt) - print("\033[1;32mContext reloaded\033[0m, now in Nemubot %s" % - nemubot.__version__) - except: - logger.exception("\033[1;31mUnable to reload the prompt due to " - "errors.\033[0m Fix them before trying to reload " - "the prompt.") - - context.quit() logger.info("Waiting for other threads shuts down...") sys.exit(0) From 3ecab04f195b785ed6e9c5e89ec6195e5043c910 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sat, 9 May 2015 13:20:56 +0200 Subject: [PATCH 003/271] Do a proper close on SIGINT and SIGTERM --- nemubot/__main__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 80ae7f9..8fa2b51 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -18,6 +18,7 @@ def main(): import os + import signal import sys # Parse command line arguments @@ -155,6 +156,12 @@ def main(): for module in args.module: __import__(module) + # Signals handling + def sighandler(signum, frame): + context.quit() + signal.signal(signal.SIGINT, sighandler) + signal.signal(signal.SIGTERM, sighandler) + context.start() context.join() From 3ec1173c00ab75785f64cfc1c9088c3437567e8f Mon Sep 17 00:00:00 2001 From: nemunaire Date: Tue, 12 May 2015 10:41:13 +0200 Subject: [PATCH 004/271] Catch SIGHUP: deep reload --- nemubot/__main__.py | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 8fa2b51..2ec45a9 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -157,14 +157,40 @@ def main(): __import__(module) # Signals handling - def sighandler(signum, frame): + def sigtermhandler(signum, frame): + """On SIGTERM and SIGINT, quit nicely""" context.quit() - signal.signal(signal.SIGINT, sighandler) - signal.signal(signal.SIGTERM, sighandler) + signal.signal(signal.SIGINT, sigtermhandler) + signal.signal(signal.SIGTERM, sigtermhandler) + def sighuphandler(signum, frame): + """On SIGHUP, perform a deep reload""" + import imp + + # Reload nemubot Python modules + imp.reload(nemubot) + nemubot.reload() + + # Hotswap context + import nemubot.bot + context = nemubot.bot.hotswap(context) + + # Reload configuration file + for path in args.files: + if os.path.isfile(path): + context.sync_queue.put_nowait(["loadconf", path]) + signal.signal(signal.SIGHUP, sighuphandler) + + # Here we go! context.start() - context.join() + # context can change when performing an hotswap, always join the latest context + oldcontext = None + while oldcontext != context: + oldcontext = context + context.join() + + # Wait for consumers logger.info("Waiting for other threads shuts down...") sys.exit(0) From e720d3c99ace346119d14ece4de0890a6f659862 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sat, 16 May 2015 10:24:08 +0200 Subject: [PATCH 005/271] Extract deamonize to a dedicated function that can be called from anywhere --- nemubot/__init__.py | 38 ++++++++++++++++++++++++++++++++++++++ nemubot/__main__.py | 31 ++----------------------------- 2 files changed, 40 insertions(+), 29 deletions(-) diff --git a/nemubot/__init__.py b/nemubot/__init__.py index 84403e0..f21ff64 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -40,6 +40,44 @@ def requires_version(min=None, max=None): "but this is nemubot v%s." % (str(max), __version__)) +def daemonize(): + """Detach the running process to run as a daemon + """ + + import os + import sys + + try: + pid = os.fork() + if pid > 0: + sys.exit(0) + except OSError as err: + sys.stderr.write("Unable to fork: %s\n" % err) + sys.exit(1) + + os.setsid() + os.umask(0) + os.chdir('/') + + try: + pid = os.fork() + if pid > 0: + sys.exit(0) + except OSError as err: + sys.stderr.write("Unable to fork: %s\n" % err) + sys.exit(1) + + sys.stdout.flush() + sys.stderr.flush() + si = open(os.devnull, 'r') + so = open(os.devnull, 'a+') + se = open(os.devnull, 'a+') + + os.dup2(si.fileno(), sys.stdin.fileno()) + os.dup2(so.fileno(), sys.stdout.fileno()) + os.dup2(se.fileno(), sys.stderr.fileno()) + + def reload(): """Reload code of all Python modules used by nemubot """ diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 2ec45a9..73cc91a 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -70,35 +70,8 @@ def main(): # Daemonize if not args.debug: - try: - pid = os.fork() - if pid > 0: - sys.exit(0) - except OSError as err: - sys.stderr.write("Unable to fork: %s" % err) - sys.exit(1) - - os.setsid() - os.umask(0) - os.chdir('/') - - try: - pid = os.fork() - if pid > 0: - sys.exit(0) - except OSError as err: - sys.stderr.write("Unable to fork: %s" % err) - sys.exit(1) - - sys.stdout.flush() - sys.stderr.flush() - si = open(os.devnull, 'r') - so = open(os.devnull, 'a+') - se = open(os.devnull, 'a+') - - os.dup2(si.fileno(), sys.stdin.fileno()) - os.dup2(so.fileno(), sys.stdout.fileno()) - os.dup2(se.fileno(), sys.stderr.fileno()) + from nemubot import daemonize + daemonize() # Setup loggin interface import logging From 8380e565a4d29908187c2171913e06a956c1195a Mon Sep 17 00:00:00 2001 From: nemunaire Date: Fri, 15 May 2015 00:05:12 +0200 Subject: [PATCH 006/271] Catch SIGUSR1: log threads stack traces --- nemubot/__main__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 73cc91a..c08e30e 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -154,6 +154,15 @@ def main(): context.sync_queue.put_nowait(["loadconf", path]) signal.signal(signal.SIGHUP, sighuphandler) + def sigusr1handler(signum, frame): + """On SIGHUSR1, display stacktraces""" + import traceback + for threadId, stack in sys._current_frames().items(): + logger.debug("########### Thread %d:\n%s", + threadId, + "".join(traceback.format_stack(stack))) + signal.signal(signal.SIGUSR1, sigusr1handler) + # Here we go! context.start() From 19d8ede570077ad9bdb9612b914c3f854ce058c9 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Mon, 18 May 2015 07:36:49 +0200 Subject: [PATCH 007/271] New CLI argument: --pidfile, path to store the daemon PID --- nemubot/__init__.py | 5 +++++ nemubot/__main__.py | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/nemubot/__init__.py b/nemubot/__init__.py index f21ff64..90a4d01 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -40,6 +40,11 @@ def requires_version(min=None, max=None): "but this is nemubot v%s." % (str(max), __version__)) +def attach(pid): + print("TODO, attach to %d" % pid) + return 0 + + def daemonize(): """Detach the running process to run as a daemon """ diff --git a/nemubot/__main__.py b/nemubot/__main__.py index c08e30e..59e609c 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -42,6 +42,9 @@ def main(): parser.add_argument("-d", "--debug", action="store_true", help="don't deamonize, keep in foreground") + parser.add_argument("-P", "--pidfile", default="./nemubot.pid", + help="Path to the file where store PID") + parser.add_argument("-l", "--logfile", default="./nemubot.log", help="Path to store logs") @@ -64,15 +67,33 @@ def main(): # Resolve relatives paths args.data_path = os.path.abspath(os.path.expanduser(args.data_path)) + args.pidfile = os.path.abspath(os.path.expanduser(args.pidfile)) 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)] + # Check if an instance is already launched + if args.pidfile is not None and os.path.isfile(args.pidfile): + with open(args.pidfile, "r") as f: + pid = int(f.readline()) + try: + os.kill(pid, 0) + except OSError: + pass + else: + from nemubot import attach + sys.exit(attach(pid)) + # Daemonize if not args.debug: from nemubot import daemonize daemonize() + # Store PID to pidfile + if args.pidfile is not None: + with open(args.pidfile, "w+") as f: + f.write(str(os.getpid())) + # Setup loggin interface import logging logger = logging.getLogger("nemubot") From fcca0cd5ec29a56981c26f9b676aa3839fc65c64 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Wed, 20 May 2015 06:12:50 +0200 Subject: [PATCH 008/271] New argument: --socketfile that create a socket for internal communication --- nemubot/__init__.py | 4 ++-- nemubot/__main__.py | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/nemubot/__init__.py b/nemubot/__init__.py index 90a4d01..4c1401c 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -40,8 +40,8 @@ def requires_version(min=None, max=None): "but this is nemubot v%s." % (str(max), __version__)) -def attach(pid): - print("TODO, attach to %d" % pid) +def attach(socketfile): + print("TODO: Attach to Unix socket at: %s" % socketfile) return 0 diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 59e609c..1c9b310 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -45,6 +45,9 @@ def main(): parser.add_argument("-P", "--pidfile", default="./nemubot.pid", help="Path to the file where store PID") + parser.add_argument("-S", "--socketfile", default="./nemubot.sock", + help="path where open the socket for internal communication") + parser.add_argument("-l", "--logfile", default="./nemubot.log", help="Path to store logs") @@ -68,6 +71,7 @@ def main(): # Resolve relatives paths args.data_path = os.path.abspath(os.path.expanduser(args.data_path)) 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)] @@ -187,6 +191,11 @@ def main(): # Here we go! context.start() + if args.socketfile: + from nemubot.server.socket import SocketListener + context.add_server(SocketListener(context.add_server, "master_socket", + sock_location=args.socketfile)) + # context can change when performing an hotswap, always join the latest context oldcontext = None while oldcontext != context: From 5e9056c7a4023eec23e00402b021b80362a8cb89 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sun, 24 May 2015 16:47:22 +0200 Subject: [PATCH 009/271] Fix and improve reload process --- nemubot/__init__.py | 3 +++ nemubot/__main__.py | 15 +++++++++++---- nemubot/bot.py | 12 ++++++++---- nemubot/server/abstract.py | 2 +- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/nemubot/__init__.py b/nemubot/__init__.py index 4c1401c..fee2eda 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -89,6 +89,9 @@ def reload(): import imp + import nemubot.bot + imp.reload(nemubot.bot) + import nemubot.channel imp.reload(nemubot.channel) diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 1c9b310..620f4f6 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -141,7 +141,8 @@ def main(): # Register the hook for futur import from nemubot.importer import ModuleFinder - sys.meta_path.append(ModuleFinder(context.modules_paths, context.add_module)) + module_finder = ModuleFinder(context.modules_paths, context.add_module) + sys.meta_path.append(module_finder) # Load requested configuration files for path in args.files: @@ -164,6 +165,9 @@ def main(): def sighuphandler(signum, frame): """On SIGHUP, perform a deep reload""" import imp + nonlocal nemubot, context, module_finder + + logger.debug("SIGHUP receive, iniate reload procedure...") # Reload nemubot Python modules imp.reload(nemubot) @@ -173,6 +177,11 @@ def main(): import nemubot.bot context = nemubot.bot.hotswap(context) + # Reload ModuleFinder + sys.meta_path.remove(module_finder) + module_finder = ModuleFinder(context.modules_paths, context.add_module) + sys.meta_path.append(module_finder) + # Reload configuration file for path in args.files: if os.path.isfile(path): @@ -188,9 +197,6 @@ def main(): "".join(traceback.format_stack(stack))) signal.signal(signal.SIGUSR1, sigusr1handler) - # Here we go! - context.start() - if args.socketfile: from nemubot.server.socket import SocketListener context.add_server(SocketListener(context.add_server, "master_socket", @@ -200,6 +206,7 @@ def main(): oldcontext = None while oldcontext != context: oldcontext = context + context.start() context.join() # Wait for consumers diff --git a/nemubot/bot.py b/nemubot/bot.py index db2f653..a12dd7a 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -143,6 +143,7 @@ class Bot(threading.Thread): from select import select from nemubot.server import _lock, _rlist, _wlist, _xlist + logger.info("Starting main loop") self.stop = False while not self.stop: with _lock: @@ -208,6 +209,7 @@ class Bot(threading.Thread): from nemubot.tools.config import load_file load_file(path, self) self.sync_queue.task_done() + logger.info("Ending main loop") @@ -509,14 +511,16 @@ def hotswap(bak): bak.stop = True if bak.event_timer is not None: bak.event_timer.cancel() + + # Unload modules + for mod in [k for k in bak.modules.keys()]: + bak.unload_module(mod) + + # Save datastore bak.datastore.close() new = Bot(str(bak.ip), bak.modules_paths, bak.datastore) new.servers = bak.servers - new.modules = bak.modules - new.modules_configuration = bak.modules_configuration - new.events = bak.events - new.hooks = bak.hooks new._update_event_timer() return new diff --git a/nemubot/server/abstract.py b/nemubot/server/abstract.py index 99d10d5..b3c70ca 100644 --- a/nemubot/server/abstract.py +++ b/nemubot/server/abstract.py @@ -115,7 +115,7 @@ class AbstractServer(io.IOBase): """ self._sending_queue.put(self.format(message)) - self.logger.debug("Message '%s' appended to Queue", message) + self.logger.debug("Message '%s' appended to write queue", message) if self not in _wlist: _wlist.append(self) From 27f1b74eef5ae3433235c6a8caa617173d3bb910 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Thu, 16 Jul 2015 20:31:34 +0200 Subject: [PATCH 010/271] Remove legacy prompt --- modules/cmd_server.py | 204 ------------------------------------- nemubot/__init__.py | 5 - nemubot/__main__.py | 4 - nemubot/prompt/__init__.py | 144 -------------------------- nemubot/prompt/builtins.py | 132 ------------------------ nemubot/prompt/error.py | 23 ----- nemubot/prompt/reset.py | 23 ----- setup.py | 1 - 8 files changed, 536 deletions(-) delete mode 100644 modules/cmd_server.py delete mode 100644 nemubot/prompt/__init__.py delete mode 100644 nemubot/prompt/builtins.py delete mode 100644 nemubot/prompt/error.py delete mode 100644 nemubot/prompt/reset.py diff --git a/modules/cmd_server.py b/modules/cmd_server.py deleted file mode 100644 index 8fdadb5..0000000 --- a/modules/cmd_server.py +++ /dev/null @@ -1,204 +0,0 @@ -# -*- coding: utf-8 -*- - -# Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 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 . - -import traceback -import sys - -from nemubot.hooks import hook - -nemubotversion = 3.4 -NODATA = True - - -def getserver(toks, context, prompt, mandatory=False, **kwargs): - """Choose the server in toks or prompt. - This function modify the tokens list passed as argument""" - - if len(toks) > 1 and toks[1] in context.servers: - return context.servers[toks.pop(1)] - elif not mandatory or prompt.selectedServer: - return prompt.selectedServer - else: - from nemubot.prompt.error import PromptError - raise PromptError("Please SELECT a server or give its name in argument.") - - -@hook("prompt_cmd", "close") -def close(toks, context, **kwargs): - """Disconnect and forget (remove from the servers list) the server""" - srv = getserver(toks, context=context, mandatory=True, **kwargs) - - if srv.close(): - del context.servers[srv.id] - return 0 - return 1 - - -@hook("prompt_cmd", "connect") -def connect(toks, **kwargs): - """Make the connexion to a server""" - srv = getserver(toks, mandatory=True, **kwargs) - - return not srv.open() - - -@hook("prompt_cmd", "disconnect") -def disconnect(toks, **kwargs): - """Close the connection to a server""" - srv = getserver(toks, mandatory=True, **kwargs) - - return not srv.close() - - -@hook("prompt_cmd", "discover") -def discover(toks, context, **kwargs): - """Discover a new bot on a server""" - srv = getserver(toks, context=context, mandatory=True, **kwargs) - - if len(toks) > 1 and "!" in toks[1]: - bot = context.add_networkbot(srv, name) - return not bot.connect() - else: - print(" %s is not a valid fullname, for example: " - "nemubot!nemubotV3@bot.nemunai.re" % ''.join(toks[1:1])) - return 1 - - -@hook("prompt_cmd", "join") -@hook("prompt_cmd", "leave") -@hook("prompt_cmd", "part") -def join(toks, **kwargs): - """Join or leave a channel""" - srv = getserver(toks, mandatory=True, **kwargs) - - if len(toks) <= 2: - print("%s: not enough arguments." % toks[0]) - return 1 - - if toks[0] == "join": - if len(toks) > 2: - srv.write("JOIN %s %s" % (toks[1], toks[2])) - else: - srv.write("JOIN %s" % toks[1]) - - elif toks[0] == "leave" or toks[0] == "part": - if len(toks) > 2: - srv.write("PART %s :%s" % (toks[1], " ".join(toks[2:]))) - else: - srv.write("PART %s" % toks[1]) - - return 0 - - -@hook("prompt_cmd", "save") -def save_mod(toks, context, **kwargs): - """Force save module data""" - if len(toks) < 2: - print("save: not enough arguments.") - return 1 - - wrn = 0 - for mod in toks[1:]: - if mod in context.modules: - context.modules[mod].save() - print("save: module `%s´ saved successfully" % mod) - else: - wrn += 1 - print("save: no module named `%s´" % mod) - return wrn - - -@hook("prompt_cmd", "send") -def send(toks, **kwargs): - """Send a message on a channel""" - srv = getserver(toks, mandatory=True, **kwargs) - - # Check the server is connected - if not srv.connected: - print ("send: server `%s' not connected." % srv.id) - return 2 - - if len(toks) <= 3: - print ("send: not enough arguments.") - return 1 - - if toks[1] not in srv.channels: - print ("send: channel `%s' not authorized in server `%s'." - % (toks[1], srv.id)) - return 3 - - from nemubot.message import Text - srv.send_response(Text(" ".join(toks[2:]), server=None, - to=[toks[1]])) - return 0 - - -@hook("prompt_cmd", "zap") -def zap(toks, **kwargs): - """Hard change connexion state""" - srv = getserver(toks, mandatory=True, **kwargs) - - srv.connected = not srv.connected - - -@hook("prompt_cmd", "top") -def top(toks, context, **kwargs): - """Display consumers load information""" - print("Queue size: %d, %d thread(s) running (counter: %d)" % - (context.cnsr_queue.qsize(), - len(context.cnsr_thrd), - context.cnsr_thrd_size)) - if len(context.events) > 0: - print("Events registered: %d, next in %d seconds" % - (len(context.events), - context.events[0].time_left.seconds)) - else: - print("No events registered") - - for th in context.cnsr_thrd: - if th.is_alive(): - print(("#" * 15 + " Stack trace for thread %u " + "#" * 15) % - th.ident) - traceback.print_stack(sys._current_frames()[th.ident]) - - -@hook("prompt_cmd", "netstat") -def netstat(toks, context, **kwargs): - """Display sockets in use and many other things""" - if len(context.network) > 0: - print("Distant bots connected: %d:" % len(context.network)) - for name, bot in context.network.items(): - print("# %s:" % name) - print(" * Declared hooks:") - lvl = 0 - for hlvl in bot.hooks: - lvl += 1 - for hook in (hlvl.all_pre + hlvl.all_post + hlvl.cmd_rgxp + - hlvl.cmd_default + hlvl.ask_rgxp + - hlvl.ask_default + hlvl.msg_rgxp + - hlvl.msg_default): - print(" %s- %s" % (' ' * lvl * 2, hook)) - for kind in ["irc_hook", "cmd_hook", "ask_hook", "msg_hook"]: - print(" %s- <%s> %s" % (' ' * lvl * 2, kind, - ", ".join(hlvl.__dict__[kind].keys()))) - print(" * My tag: %d" % bot.my_tag) - print(" * Tags in use (%d):" % bot.inc_tag) - for tag, (cmd, data) in bot.tags.items(): - print(" - %11s: %s « %s »" % (tag, cmd, data)) - else: - print("No distant bot connected") diff --git a/nemubot/__init__.py b/nemubot/__init__.py index fee2eda..b85ce6c 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -117,11 +117,6 @@ def reload(): nemubot.message.reload() - import nemubot.prompt - imp.reload(nemubot.prompt) - - nemubot.prompt.reload() - import nemubot.server rl, wl, xl = nemubot.server._rlist, nemubot.server._wlist, nemubot.server._xlist imp.reload(nemubot.server) diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 620f4f6..079049c 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -135,10 +135,6 @@ def main(): if args.no_connect: context.noautoconnect = True - # Load the prompt - import nemubot.prompt - prmpt = nemubot.prompt.Prompt() - # Register the hook for futur import from nemubot.importer import ModuleFinder module_finder = ModuleFinder(context.modules_paths, context.add_module) diff --git a/nemubot/prompt/__init__.py b/nemubot/prompt/__init__.py deleted file mode 100644 index c491d99..0000000 --- a/nemubot/prompt/__init__.py +++ /dev/null @@ -1,144 +0,0 @@ -# -*- coding: utf-8 -*- - -# Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 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 . - -import shlex -import sys -import traceback - -from nemubot.prompt import builtins - - -class Prompt: - - def __init__(self): - self.selectedServer = None - self.lastretcode = 0 - - self.HOOKS_CAPS = dict() - self.HOOKS_LIST = dict() - - def add_cap_hook(self, name, call, data=None): - self.HOOKS_CAPS[name] = lambda t, c: call(t, data=data, - context=c, prompt=self) - - def add_list_hook(self, name, call): - self.HOOKS_LIST[name] = call - - def lex_cmd(self, line): - """Return an array of tokens - - Argument: - line -- the line to lex - """ - - try: - cmds = shlex.split(line) - except: - exc_type, exc_value, _ = sys.exc_info() - sys.stderr.write(traceback.format_exception_only(exc_type, - exc_value)[0]) - return - - bgn = 0 - - # Separate commands (command separator: ;) - for i in range(0, len(cmds)): - if cmds[i][-1] == ';': - if i != bgn: - yield cmds[bgn:i] - bgn = i + 1 - - # Return rest of the command (that not end with a ;) - if bgn != len(cmds): - yield cmds[bgn:] - - def exec_cmd(self, toks, context): - """Execute the command - - Arguments: - toks -- lexed tokens to executes - context -- current bot context - """ - - if toks[0] in builtins.CAPS: - self.lastretcode = builtins.CAPS[toks[0]](toks, context, self) - elif toks[0] in self.HOOKS_CAPS: - self.lastretcode = self.HOOKS_CAPS[toks[0]](toks, context) - else: - print("Unknown command: `%s'" % toks[0]) - self.lastretcode = 127 - - def getPS1(self): - """Get the PS1 associated to the selected server""" - if self.selectedServer is None: - return "nemubot" - else: - return self.selectedServer.id - - def run(self, context): - """Launch the prompt - - Argument: - context -- current bot context - """ - - from nemubot.prompt.error import PromptError - from nemubot.prompt.reset import PromptReset - - while True: # Stopped by exception - try: - line = input("\033[0;33m%s\033[0;%dm§\033[0m " % - (self.getPS1(), 31 if self.lastretcode else 32)) - cmds = self.lex_cmd(line.strip()) - for toks in cmds: - try: - self.exec_cmd(toks, context) - except PromptReset: - raise - except PromptError as e: - print(e.message) - self.lastretcode = 128 - except: - exc_type, exc_value, exc_traceback = sys.exc_info() - traceback.print_exception(exc_type, exc_value, - exc_traceback) - except KeyboardInterrupt: - print("") - except EOFError: - print("quit") - return True - - -def hotswap(bak): - p = Prompt() - p.HOOKS_CAPS = bak.HOOKS_CAPS - p.HOOKS_LIST = bak.HOOKS_LIST - return p - - -def reload(): - import imp - - import nemubot.prompt.builtins - imp.reload(nemubot.prompt.builtins) - - import nemubot.prompt.error - imp.reload(nemubot.prompt.error) - - import nemubot.prompt.reset - imp.reload(nemubot.prompt.reset) diff --git a/nemubot/prompt/builtins.py b/nemubot/prompt/builtins.py deleted file mode 100644 index e78c3dd..0000000 --- a/nemubot/prompt/builtins.py +++ /dev/null @@ -1,132 +0,0 @@ -# -*- coding: utf-8 -*- - -# Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 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 . - -def end(toks, context, prompt): - """Quit the prompt for reload or exit""" - from nemubot.prompt.reset import PromptReset - - if toks[0] == "refresh": - raise PromptReset("refresh") - elif toks[0] == "reset": - raise PromptReset("reset") - raise PromptReset("quit") - - -def liste(toks, context, prompt): - """Show some lists""" - if len(toks) > 1: - for l in toks[1:]: - l = l.lower() - if l == "server" or l == "servers": - for srv in context.servers.keys(): - print (" - %s (state: %s) ;" % (srv, - "connected" if context.servers[srv].connected else "disconnected")) - if len(context.servers) == 0: - print (" > No server loaded") - - elif l == "mod" or l == "mods" or l == "module" or l == "modules": - for mod in context.modules.keys(): - print (" - %s ;" % mod) - if len(context.modules) == 0: - print (" > No module loaded") - - elif l in prompt.HOOKS_LIST: - f, d = prompt.HOOKS_LIST[l] - f(d, context, prompt) - - else: - print (" Unknown list `%s'" % l) - return 2 - return 0 - else: - print (" Please give a list to show: servers, ...") - return 1 - - -def load(toks, context, prompt): - """Load an XML configuration file""" - if len(toks) > 1: - from nemubot.tools.config import load_file - - for filename in toks[1:]: - load_file(filename, context) - else: - print ("Not enough arguments. `load' takes a filename.") - return 1 - - -def select(toks, context, prompt): - """Select the current server""" - if (len(toks) == 2 and toks[1] != "None" and - toks[1] != "nemubot" and toks[1] != "none"): - if toks[1] in context.servers: - prompt.selectedServer = context.servers[toks[1]] - else: - print ("select: server `%s' not found." % toks[1]) - return 1 - else: - prompt.selectedServer = None - - -def unload(toks, context, prompt): - """Unload a module""" - if len(toks) == 2 and toks[1] == "all": - for name in context.modules.keys(): - context.unload_module(name) - elif len(toks) > 1: - for name in toks[1:]: - if context.unload_module(name): - print (" Module `%s' successfully unloaded." % name) - else: - print (" No module `%s' loaded, can't unload!" % name) - return 2 - else: - print ("Not enough arguments. `unload' takes a module name.") - return 1 - - -def debug(toks, context, prompt): - """Enable/Disable debug mode on a module""" - if len(toks) > 1: - for name in toks[1:]: - if name in context.modules: - context.modules[name].DEBUG = not context.modules[name].DEBUG - if context.modules[name].DEBUG: - print (" Module `%s' now in DEBUG mode." % name) - else: - print (" Debug for module module `%s' disabled." % name) - else: - print (" No module `%s' loaded, can't debug!" % name) - return 2 - else: - print ("Not enough arguments. `debug' takes a module name.") - return 1 - - -# Register build-ins -CAPS = { - 'quit': end, # Disconnect all server and quit - 'exit': end, # Alias for quit - 'reset': end, # Reload the prompt - 'refresh': end, # Reload the prompt but save modules - 'load': load, # Load a servers or module configuration file - 'unload': unload, # Unload a module and remove it from the list - 'select': select, # Select a server - 'list': liste, # Show lists - 'debug': debug, # Pass a module in debug mode -} diff --git a/nemubot/prompt/error.py b/nemubot/prompt/error.py deleted file mode 100644 index 3d426d6..0000000 --- a/nemubot/prompt/error.py +++ /dev/null @@ -1,23 +0,0 @@ -# coding=utf-8 - -# Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 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 . - -class PromptError(Exception): - - def __init__(self, message): - super(PromptError, self).__init__(message) - self.message = message diff --git a/nemubot/prompt/reset.py b/nemubot/prompt/reset.py deleted file mode 100644 index 57da9f8..0000000 --- a/nemubot/prompt/reset.py +++ /dev/null @@ -1,23 +0,0 @@ -# coding=utf-8 - -# Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 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 . - -class PromptReset(Exception): - - def __init__(self, type): - super(PromptReset, self).__init__("Prompt reset asked") - self.type = type diff --git a/setup.py b/setup.py index 37f4aef..dc448ca 100755 --- a/setup.py +++ b/setup.py @@ -66,7 +66,6 @@ setup( 'nemubot.hooks', 'nemubot.message', 'nemubot.message.printer', - 'nemubot.prompt', 'nemubot.server', 'nemubot.server.message', 'nemubot.tools', From 47ada614fa9c5078b89e4e9351c1875179d4e76c Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sat, 18 Jul 2015 14:01:56 +0200 Subject: [PATCH 011/271] Can attach to the main process --- nemubot/__init__.py | 67 +++++++++++++++++++++++++++++++++++++--- nemubot/__main__.py | 2 +- nemubot/server/socket.py | 13 ++++++++ 3 files changed, 77 insertions(+), 5 deletions(-) diff --git a/nemubot/__init__.py b/nemubot/__init__.py index b85ce6c..9f02039 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -20,6 +20,7 @@ __version__ = '4.0.dev3' __author__ = 'nemunaire' from nemubot.modulecontext import ModuleContext + context = ModuleContext(None, None) @@ -40,8 +41,62 @@ def requires_version(min=None, max=None): "but this is nemubot v%s." % (str(max), __version__)) -def attach(socketfile): - print("TODO: Attach to Unix socket at: %s" % socketfile) +def attach(pid, socketfile): + import socket + import sys + + print("nemubot is already launched with PID %d. Attaching to Unix socket at: %s" % (pid, socketfile)) + + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + try: + sock.connect(socketfile) + except socket.error as e: + sys.stderr.write(str(e)) + sys.stderr.write("\n") + return 1 + + from select import select + try: + print("Connection established.") + while True: + rl, wl, xl = select([sys.stdin, sock], [], []) + + if sys.stdin in rl: + line = sys.stdin.readline().strip() + if line == "exit" or line == "quit": + return 0 + elif line == "reload": + import os, signal + os.kill(pid, signal.SIGHUP) + print("Reload signal sent. Please wait...") + + elif line == "shutdown": + import os, signal + os.kill(pid, signal.SIGTERM) + print("Shutdown signal sent. Please wait...") + + elif line == "kill": + import os, signal + os.kill(pid, signal.SIGKILL) + print("Signal sent...") + return 0 + + elif line == "stack" or line == "stacks": + import os, signal + os.kill(pid, signal.SIGUSR1) + print("Debug signal sent. Consult logs.") + + else: + sock.send(line.encode() + b'\r\n') + + if sock in rl: + sys.stdout.write(sock.recv(2048).decode()) + except KeyboardInterrupt: + pass + except: + return 1 + finally: + sock.close() return 0 @@ -118,9 +173,13 @@ def reload(): nemubot.message.reload() import nemubot.server - rl, wl, xl = nemubot.server._rlist, nemubot.server._wlist, nemubot.server._xlist + rl = nemubot.server._rlist + wl = nemubot.server._wlist + xl = nemubot.server._xlist imp.reload(nemubot.server) - nemubot.server._rlist, nemubot.server._wlist, nemubot.server._xlist = rl, wl, xl + nemubot.server._rlist = rl + nemubot.server._wlist = wl + nemubot.server._xlist = xl nemubot.server.reload() diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 079049c..efc31bd 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -86,7 +86,7 @@ def main(): pass else: from nemubot import attach - sys.exit(attach(pid)) + sys.exit(attach(pid, args.socketfile)) # Daemonize if not args.debug: diff --git a/nemubot/server/socket.py b/nemubot/server/socket.py index 052579b..f810906 100644 --- a/nemubot/server/socket.py +++ b/nemubot/server/socket.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import nemubot.message as message from nemubot.message.printer.socket import Socket as SocketPrinter from nemubot.server.abstract import AbstractServer @@ -132,6 +133,18 @@ class SocketServer(AbstractServer): yield line + def parse(self, line): + import shlex + + line = line.strip().decode() + try: + args = shlex.split(line) + except ValueError: + args = line.split(' ') + + yield message.Command(cmd=args[0], args=args[1:], server=self) + + class SocketListener(AbstractServer): def __init__(self, new_server_cb, id, sock_location=None, host=None, port=None, ssl=None): From e943a556267c5c5ca2958208482ded870afaf929 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Fri, 4 Sep 2015 17:07:21 +0200 Subject: [PATCH 012/271] wip event manager --- nemubot/event/__init__.py | 65 ++++++++++++++++++++------------------- nemubot/modulecontext.py | 6 ++-- 2 files changed, 38 insertions(+), 33 deletions(-) diff --git a/nemubot/event/__init__.py b/nemubot/event/__init__.py index 96f226a..345200d 100644 --- a/nemubot/event/__init__.py +++ b/nemubot/event/__init__.py @@ -24,8 +24,8 @@ class ModuleEvent: """Representation of a event initiated by a bot module""" def __init__(self, call=None, call_data=None, func=None, func_data=None, - cmp=None, cmp_data=None, interval=60, offset=0, times=1): - + cmp=None, cmp_data=None, end_call=None, end_data=None, + interval=60, offset=0, times=1, max_attempt=-1): """Initialize the event Keyword arguments: @@ -35,9 +35,12 @@ class ModuleEvent: func_data -- Argument(s) (single or dict) to pass as argument OR if no func, initial data to watch cmp -- Boolean function called to check changes cmp_data -- Argument(s) (single or dict) to pass as argument OR if no cmp, data compared to previous + end_call -- Function called when times or max_attempt reach 0 (mainly for interaction with the event manager) + end_data -- Argument(s) (single or dict) to pass as argument interval -- Time in seconds between each check (default: 60) offset -- Time in seconds added to interval before the first check (default: 0) times -- Number of times the event has to be realized before being removed; -1 for no limit (default: 1) + max_attempt -- Maximum number of times the event will be checked """ # What have we to check? @@ -46,16 +49,17 @@ class ModuleEvent: # How detect a change? self.cmp = cmp - self.cmp_data = None if cmp_data is not None: self.cmp_data = cmp_data - elif self.func is not None: + elif callable(self.func): if self.func_data is None: self.cmp_data = self.func() elif isinstance(self.func_data, dict): self.cmp_data = self.func(**self.func_data) else: self.cmp_data = self.func(self.func_data) + else: + self.cmp_data = None # What should we call when? self.call = call @@ -64,46 +68,29 @@ class ModuleEvent: else: self.call_data = func_data - # Store times - self.offset = timedelta(seconds=offset) # Time to wait before the first check + # Store time between each event self.interval = timedelta(seconds=interval) - self._end = None # Cache - # How many times do this event? self.times = times - @property - def current(self): - """Return the date of the near check""" - if self.times != 0: - if self._end is None: - self._end = datetime.now(timezone.utc) + self.offset + self.interval - return self._end - return None + # Cache the time of the next occurence + self.next_occur = datetime.now(timezone.utc) + timedelta(seconds=offset) + self.interval - @property - def next(self): - """Return the date of the next check""" - if self.times != 0: - if self._end is None: - return self.current - elif self._end < datetime.now(timezone.utc): - self._end += self.interval - return self._end - return None @property def time_left(self): """Return the time left before/after the near check""" - if self.current is not None: - return self.current - datetime.now(timezone.utc) - return 99999 # TODO: 99999 is not a valid time to return + + return self.next_occur - datetime.now(timezone.utc) + def check(self): """Run a check and realized the event if this is time""" + self.max_attempt -= 1 + # Get initial data - if self.func is None: + if not callable(self.func): d_init = self.func_data elif self.func_data is None: d_init = self.func() @@ -113,7 +100,7 @@ class ModuleEvent: d_init = self.func(self.func_data) # then compare with current data - if self.cmp is None: + if not callable(self.cmp): if self.cmp_data is None: rlz = True else: @@ -138,3 +125,19 @@ class ModuleEvent: self.call(d_init, **self.call_data) else: self.call(d_init, self.call_data) + + # Is it finished? + if self.times == 0 or self.max_attempt == 0: + if not callable(self.end_call): + pass # TODO: log a WARN here + else: + if self.end_data is None: + self.end_call() + elif isinstance(self.end_data, dict): + self.end_call(**self.end_data) + else: + self.end_call(self.end_data) + + # Not finished, ready to next one! + else: + self.next_occur += self.interval diff --git a/nemubot/modulecontext.py b/nemubot/modulecontext.py index 5b47278..0814215 100644 --- a/nemubot/modulecontext.py +++ b/nemubot/modulecontext.py @@ -51,7 +51,7 @@ class ModuleContext: self.config = ModuleState("module") self.hooks = list() - self.events = list() + self.events = list() # Un eventManager, qui contient une liste globale et un thread global et quelques méthodes statique, mais chaque événement est exécuté dans son propre contexte :) self.debug = context.verbosity > 0 if context is not None else False # Define some callbacks @@ -79,7 +79,9 @@ 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) + eid = context.add_event(evt, eid, module_src=module) + self.events.append(eid) + return eid def del_event(evt): return context.del_event(evt, module_src=module) From 56c43179f31e70d2d4e531a197215448f1772134 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Thu, 22 Oct 2015 00:04:40 +0200 Subject: [PATCH 013/271] tools/web: use core xml minidom instead of nemubot xml parser --- nemubot/tools/web.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/nemubot/tools/web.py b/nemubot/tools/web.py index 4cec48a..15f7885 100644 --- a/nemubot/tools/web.py +++ b/nemubot/tools/web.py @@ -1,5 +1,3 @@ -# coding=utf-8 - # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier # @@ -176,8 +174,8 @@ def getXML(url, timeout=7): if cnt is None: return None else: - from nemubot.tools.xmlparser import parse_string - return parse_string(cnt.encode()) + from xml.dom.minidom import parseString + return parseString(cnt) def getJSON(url, timeout=7): @@ -188,12 +186,11 @@ def getJSON(url, timeout=7): timeout -- maximum number of seconds to wait before returning an exception """ - import json - cnt = getURLContent(url, timeout=timeout) if cnt is None: return None else: + import json return json.loads(cnt) From 5141a0dc174a3f3f6a303196aa19db9377340b91 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Fri, 23 Oct 2015 23:18:05 +0200 Subject: [PATCH 014/271] tools/web: simplify regexp and typo --- nemubot/tools/web.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/nemubot/tools/web.py b/nemubot/tools/web.py index 15f7885..95854f8 100644 --- a/nemubot/tools/web.py +++ b/nemubot/tools/web.py @@ -203,7 +203,7 @@ def striphtml(data): data -- the string to strip """ - if not isinstance(data, str) and not isinstance(data, buffer): + if not isinstance(data, str) and not isinstance(data, bytes): return data try: @@ -232,6 +232,5 @@ def striphtml(data): import re - r, _ = re.subn(r' +', ' ', - unescape(re.sub(r'<.*?>', '', data)).replace('\n', ' ')) - return r + return re.sub(r' +', ' ', + unescape(re.sub(r'<.*?>', '', data)).replace('\n', ' ')) From 7ce9b2bb4cdd9da3a0b37990bba04d74bf9b1bab Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 10 Oct 2015 00:18:42 +0100 Subject: [PATCH 015/271] [framalink] Add error handling (invalid URLs) --- modules/framalink.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/modules/framalink.py b/modules/framalink.py index 3ed1214..1b45995 100644 --- a/modules/framalink.py +++ b/modules/framalink.py @@ -23,7 +23,12 @@ def framalink_reducer(url, data): json_data = json.loads(web.getURLContent(url, "lsturl=" + quote(data, "/:%@&=?"), header={"Content-Type": "application/x-www-form-urlencoded"})) - return json_data['short'] + if 'short' in json_data: + return json_data['short'] + elif 'msg' in json_data: + raise IRCException("Error: %s" % json_data['msg']) + else: + IRCException("An error occured while shortening %s." % data) # MODULE VARIABLES #################################################### From aca073faffd220a99f89d9a024b876d532d60fbf Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 11 Oct 2015 00:18:42 +0100 Subject: [PATCH 016/271] [framalink] Fix framalink quoting; add @provider !framalink now allows the provider to be specified using the @provider parameter. --- modules/framalink.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/modules/framalink.py b/modules/framalink.py index 1b45995..a1bf78d 100644 --- a/modules/framalink.py +++ b/modules/framalink.py @@ -21,7 +21,7 @@ def default_reducer(url, data): def framalink_reducer(url, data): json_data = json.loads(web.getURLContent(url, "lsturl=" - + quote(data, "/:%@&=?"), + + quote(data), header={"Content-Type": "application/x-www-form-urlencoded"})) if 'short' in json_data: return json_data['short'] @@ -54,13 +54,13 @@ def load(context): # MODULE CORE ######################################################### -def reduce(url): +def reduce(url, provider=DEFAULT_PROVIDER): """Ask the url shortner website to reduce given URL Argument: url -- the URL to reduce """ - return PROVIDERS[DEFAULT_PROVIDER][0](PROVIDERS[DEFAULT_PROVIDER][1], url) + return PROVIDERS[provider][0](PROVIDERS[provider][1], url) def gen_response(res, msg, srv): if res is None: @@ -105,7 +105,7 @@ def parseresponse(msg): @hook("cmd_hook", "framalink", help="Reduce any given URL", help_usage={None: "Reduce the last URL said on the channel", - "URL [URL ...]": "Reduce the given URL(s)"}) + "[@provider=framalink] URL [URL ...]": "Reduce the given URL(s) using thespecified shortner"}) def cmd_reduceurl(msg): minify = list() @@ -121,10 +121,15 @@ def cmd_reduceurl(msg): else: minify += msg.args + if 'provider' in msg.kwargs and msg.kwargs['provider'] in PROVIDERS: + provider = msg.kwargs['provider'] + else: + provider = DEFAULT_PROVIDER + res = list() for url in minify: o = urlparse(web.getNormalizedURL(url), "http") - minief_url = reduce(url) + minief_url = reduce(url, provider) if o.netloc == "": res.append(gen_response(minief_url, msg, o.scheme)) else: From 2b96c32063661e9b71212106b9609eb94b4c1435 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sat, 24 Oct 2015 14:44:16 +0200 Subject: [PATCH 017/271] [ddg] Split the module in two: ddg for search and urbandict for urbandictionnary --- modules/ddg.py | 138 ++++++++++++++++++++++++++++++++ modules/ddg/DDGSearch.py | 71 ---------------- modules/ddg/UrbanDictionnary.py | 30 ------- modules/ddg/__init__.py | 70 ---------------- modules/urbandict.py | 37 +++++++++ 5 files changed, 175 insertions(+), 171 deletions(-) create mode 100644 modules/ddg.py delete mode 100644 modules/ddg/DDGSearch.py delete mode 100644 modules/ddg/UrbanDictionnary.py delete mode 100644 modules/ddg/__init__.py create mode 100644 modules/urbandict.py diff --git a/modules/ddg.py b/modules/ddg.py new file mode 100644 index 0000000..e11d501 --- /dev/null +++ b/modules/ddg.py @@ -0,0 +1,138 @@ +"""Search around DuckDuckGo search engine""" + +# PYTHON STUFFS ####################################################### + +from urllib.parse import quote + +from nemubot.exception import IRCException +from nemubot.hooks import hook +from nemubot.tools import web + +from more import Response + +# MODULE CORE ######################################################### + +def do_search(terms): + if "!safeoff" in terms: + terms.remove("!safeoff") + safeoff = True + else: + safeoff = False + + sterm = " ".join(terms) + return DDGResult(sterm, web.getJSON( + "https://api.duckduckgo.com/?q=%s&format=json&no_redirect=1%s" % + (quote(sterm), "&kp=-1" if safeoff else ""))) + + +class DDGResult: + + def __init__(self, terms, res): + if res is None: + raise IRCException("An error occurs during search") + + self.terms = terms + self.ddgres = res + + + @property + def type(self): + if not self.ddgres or "Type" not in self.ddgres: + return "" + return self.ddgres["Type"] + + + @property + def definition(self): + if "Definition" not in self.ddgres or not self.ddgres["Definition"]: + return "Sorry, no definition found for %s." % self.terms + return self.ddgres["Definition"] + " <" + self.ddgres["DefinitionURL"] + "> from " + self.ddgres["DefinitionSource"] + + + @property + def relatedTopics(self): + if "RelatedTopics" in self.ddgres: + for rt in self.ddgres["RelatedTopics"]: + if "Text" in rt: + yield rt["Text"] + " <" + rt["FirstURL"] + ">" + elif "Topics" in rt: + yield rt["Name"] + ": " + "; ".join([srt["Text"] + " <" + srt["FirstURL"] + ">" for srt in rt["Topics"]]) + + + @property + def redirect(self): + if "Redirect" not in self.ddgres or not self.ddgres["Redirect"]: + return None + return self.ddgres["Redirect"] + + + @property + def entity(self): + if "Entity" not in self.ddgres or not self.ddgres["Entity"]: + return None + return self.ddgres["Entity"] + + + @property + def heading(self): + if "Heading" not in self.ddgres or not self.ddgres["Heading"]: + return " ".join(self.terms) + return self.ddgres["Heading"] + + + @property + def result(self): + if "Results" in self.ddgres: + for res in self.ddgres["Results"]: + yield res["Text"] + " <" + res["FirstURL"] + ">" + + + @property + def answer(self): + if "Answer" not in self.ddgres or not self.ddgres["Answer"]: + return None + return web.striphtml(self.ddgres["Answer"]) + + + @property + def abstract(self): + if "Abstract" not in self.ddgres or not self.ddgres["Abstract"]: + return None + return self.ddgres["AbstractText"] + " <" + self.ddgres["AbstractURL"] + "> from " + self.ddgres["AbstractSource"] + + +# MODULE INTERFACE #################################################### + +@hook("cmd_hook", "define") +def define(msg): + if not len(msg.args): + raise IRCException("Indicate a term to define") + + s = do_search(msg.args) + + if not s.definition: + raise IRCException("no definition found for '%s'." % " ".join(msg.args)) + + return Response(s.definition, channel=msg.channel) + +@hook("cmd_hook", "search") +def search(msg): + if not len(msg.args): + raise IRCException("Indicate a term to search") + + s = do_search(msg.args) + + res = Response(channel=msg.channel, nomore="No more results", + count=" (%d more results)") + + res.append_message(s.redirect) + res.append_message(s.answer) + res.append_message(s.abstract) + res.append_message([res for res in s.result]) + + for rt in s.relatedTopics: + res.append_message(rt) + + res.append_message(s.definition) + + return res diff --git a/modules/ddg/DDGSearch.py b/modules/ddg/DDGSearch.py deleted file mode 100644 index 174e4a5..0000000 --- a/modules/ddg/DDGSearch.py +++ /dev/null @@ -1,71 +0,0 @@ -# coding=utf-8 - -from urllib.parse import quote - -from nemubot.tools import web -from nemubot.tools.xmlparser import parse_string - - -class DDGSearch: - - def __init__(self, terms, safeoff=False): - self.terms = terms - - self.ddgres = web.getXML( - "https://api.duckduckgo.com/?q=%s&format=xml&no_redirect=1%s" % - (quote(terms), "&kp=-1" if safeoff else ""), - timeout=10) - - @property - def type(self): - if self.ddgres and self.ddgres.hasNode("Type"): - return self.ddgres.getFirstNode("Type").getContent() - else: - return "" - - @property - def definition(self): - if self.ddgres.hasNode("Definition"): - return self.ddgres.getFirstNode("Definition").getContent() - else: - return "Sorry, no definition found for %s" % self.terms - - @property - def relatedTopics(self): - try: - for rt in self.ddgres.getFirstNode("RelatedTopics").getNodes("RelatedTopic"): - yield rt.getFirstNode("Text").getContent() - except: - pass - - @property - def redirect(self): - try: - return self.ddgres.getFirstNode("Redirect").getContent() - except: - return None - - @property - def result(self): - try: - node = self.ddgres.getFirstNode("Results").getFirstNode("Result") - return node.getFirstNode("Text").getContent() + ": " + node.getFirstNode("FirstURL").getContent() - except: - return None - - @property - def answer(self): - try: - return web.striphtml(self.ddgres.getFirstNode("Answer").getContent()) - except: - return None - - @property - def abstract(self): - try: - if self.ddgres.getNode("Abstract").getContent() != "": - return self.ddgres.getNode("Abstract").getContent() + " <" + self.ddgres.getNode("AbstractURL").getContent() + ">" - else: - return None - except: - return None diff --git a/modules/ddg/UrbanDictionnary.py b/modules/ddg/UrbanDictionnary.py deleted file mode 100644 index 25faf39..0000000 --- a/modules/ddg/UrbanDictionnary.py +++ /dev/null @@ -1,30 +0,0 @@ -# coding=utf-8 - -from urllib.parse import quote - -from nemubot.tools import web - - -class UrbanDictionnary: - - def __init__(self, terms): - self.terms = terms - - self.udres = web.getJSON( - "http://api.urbandictionary.com/v0/define?term=%s" % quote(terms), - timeout=10) - - @property - def result_type(self): - if self.udres and "result_type" in self.udres: - return self.udres["result_type"] - else: - return "" - - @property - def definitions(self): - if self.udres and "list" in self.udres: - for d in self.udres["list"]: - yield d["definition"] + "\n" + d["example"] - else: - yield "Sorry, no definition found for %s" % self.terms diff --git a/modules/ddg/__init__.py b/modules/ddg/__init__.py deleted file mode 100644 index e7cfe89..0000000 --- a/modules/ddg/__init__.py +++ /dev/null @@ -1,70 +0,0 @@ -# coding=utf-8 - -"""Search around various search engine or knowledges database""" - -import imp - -from nemubot import context -from nemubot.exception import IRCException -from nemubot.hooks import hook - -nemubotversion = 3.4 - -from more import Response - -from . import DDGSearch -from . import UrbanDictionnary - -@hook("cmd_hook", "define") -def define(msg): - if not len(msg.args): - raise IRCException("Indicate a term to define") - - s = DDGSearch.DDGSearch(' '.join(msg.args)) - - return Response(s.definition, channel=msg.channel) - - -@hook("cmd_hook", "search") -def search(msg): - if not len(msg.args): - raise IRCException("Indicate a term to search") - - if "!safeoff" in msg.args: - msg.args.remove("!safeoff") - safeoff = True - else: - safeoff = False - - s = DDGSearch.DDGSearch(' '.join(msg.args), safeoff) - - res = Response(channel=msg.channel, nomore="No more results", - count=" (%d more results)") - - res.append_message(s.redirect) - res.append_message(s.abstract) - res.append_message(s.result) - res.append_message(s.answer) - - for rt in s.relatedTopics: - res.append_message(rt) - - res.append_message(s.definition) - - return res - - -@hook("cmd_hook", "urbandictionnary") -def udsearch(msg): - if not len(msg.args): - raise IRCException("Indicate a term to search") - - s = UrbanDictionnary.UrbanDictionnary(' '.join(msg.args)) - - res = Response(channel=msg.channel, nomore="No more results", - count=" (%d more definitions)") - - for d in s.definitions: - res.append_message(d.replace("\n", " ")) - - return res diff --git a/modules/urbandict.py b/modules/urbandict.py new file mode 100644 index 0000000..e7474eb --- /dev/null +++ b/modules/urbandict.py @@ -0,0 +1,37 @@ +"""Search definition from urbandictionnary""" + +# PYTHON STUFFS ####################################################### + +from urllib.parse import quote + +from nemubot.exception import IRCException +from nemubot.hooks import hook +from nemubot.tools import web + +from more import Response + +# MODULE CORE ######################################################### + +def search(terms): + return web.getJSON( + "http://api.urbandictionary.com/v0/define?term=%s" + % quote(' '.join(terms))) + + +# MODULE INTERFACE #################################################### + +@hook("cmd_hook", "urbandictionnary") +def udsearch(msg): + if not len(msg.args): + raise IRCException("Indicate a term to search") + + s = search(msg.args) + + res = Response(channel=msg.channel, nomore="No more results", + count=" (%d more definitions)") + + for i in s["list"]: + res.append_message(i["definition"].replace("\n", " "), + title=i["word"]) + + return res From 59ea2e971b8c46dc402ea3e1454da4c6130d479f Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sun, 25 Oct 2015 18:50:18 +0100 Subject: [PATCH 018/271] Refactor modules that used nemubot XML parser due to previous commit --- modules/books.py | 35 +++++++++------- modules/mediawiki.py | 15 +++---- modules/velib.py | 60 ++++++++++++--------------- modules/wolframalpha.py | 92 ++++++++++++++++++++++++----------------- 4 files changed, 108 insertions(+), 94 deletions(-) diff --git a/modules/books.py b/modules/books.py index 260267e..f532a3b 100644 --- a/modules/books.py +++ b/modules/books.py @@ -28,8 +28,8 @@ def get_book(title): """Retrieve a book from its title""" response = web.getXML("https://www.goodreads.com/book/title.xml?key=%s&title=%s" % (context.config["goodreadskey"], urllib.parse.quote(title))) - if response is not None and response.hasNode("book"): - return response.getNode("book") + if response is not None and len(response.getElementsByTagName("book")): + return response.getElementsByTagName("book")[0] else: return None @@ -38,8 +38,8 @@ def search_books(title): """Get a list of book matching given title""" response = web.getXML("https://www.goodreads.com/search.xml?key=%s&q=%s" % (context.config["goodreadskey"], urllib.parse.quote(title))) - if response is not None and response.hasNode("search"): - return response.getNode("search").getNode("results").getNodes("work") + if response is not None and len(response.getElementsByTagName("search")): + return response.getElementsByTagName("search")[0].getElementsByTagName("results")[0].getElementsByTagName("work") else: return [] @@ -48,11 +48,11 @@ def search_author(name): """Looking for an author""" response = web.getXML("https://www.goodreads.com/api/author_url/%s?key=%s" % (urllib.parse.quote(name), context.config["goodreadskey"])) - if response is not None and response.hasNode("author") and response.getNode("author").hasAttribute("id"): + if response is not None and len(response.getElementsByTagName("author")) and response.getElementsByTagName("author")[0].hasAttribute("id"): response = web.getXML("https://www.goodreads.com/author/show/%s.xml?key=%s" % - (urllib.parse.quote(response.getNode("author")["id"]), context.config["goodreadskey"])) - if response is not None and response.hasNode("author"): - return response.getNode("author") + (urllib.parse.quote(response.getElementsByTagName("author")[0].getAttribute("id")), context.config["goodreadskey"])) + if response is not None and len(response.getElementsByTagName("author")): + return response.getElementsByTagName("author")[0] return None @@ -71,9 +71,9 @@ def cmd_book(msg): if book is None: raise IRCException("unable to find book named like this") res = Response(channel=msg.channel) - res.append_message("%s, writed by %s: %s" % (book.getNode("title").getContent(), - book.getNode("authors").getNode("author").getNode("name").getContent(), - web.striphtml(book.getNode("description").getContent()))) + res.append_message("%s, written by %s: %s" % (book.getElementsByTagName("title")[0].firstChild.nodeValue, + book.getElementsByTagName("author")[0].getElementsByTagName("name")[0].firstChild.nodeValue, + web.striphtml(book.getElementsByTagName("description")[0].firstChild.nodeValue if book.getElementsByTagName("description")[0].firstChild else ""))) return res @@ -92,8 +92,8 @@ def cmd_books(msg): count=" (%d more books)") for book in search_books(title): - res.append_message("%s, writed by %s" % (book.getNode("best_book").getNode("title").getContent(), - book.getNode("best_book").getNode("author").getNode("name").getContent())) + res.append_message("%s, writed by %s" % (book.getElementsByTagName("best_book")[0].getElementsByTagName("title")[0].firstChild.nodeValue, + book.getElementsByTagName("best_book")[0].getElementsByTagName("author")[0].getElementsByTagName("name")[0].firstChild.nodeValue)) return res @@ -106,7 +106,10 @@ def cmd_author(msg): if not len(msg.args): raise IRCException("please give me an author to search") - ath = search_author(" ".join(msg.args)) - return Response([b.getNode("title").getContent() for b in ath.getNode("books").getNodes("book")], + name = " ".join(msg.args) + ath = search_author(name) + if ath is None: + raise IRCException("%s does not appear to be a published author." % name) + return Response([b.getElementsByTagName("title")[0].firstChild.nodeValue for b in ath.getElementsByTagName("book")], channel=msg.channel, - title=ath.getNode("name").getContent()) + title=ath.getElementsByTagName("name")[0].firstChild.nodeValue) diff --git a/modules/mediawiki.py b/modules/mediawiki.py index 630afdb..51f65e4 100644 --- a/modules/mediawiki.py +++ b/modules/mediawiki.py @@ -61,17 +61,17 @@ def get_unwikitextified(site, wikitext, ssl=False): def opensearch(site, term, ssl=False): # Built URL - url = "http%s://%s/w/api.php?format=xml&action=opensearch&search=%s" % ( + url = "http%s://%s/w/api.php?format=json&action=opensearch&search=%s" % ( "s" if ssl else "", site, urllib.parse.quote(term)) # Make the request - response = web.getXML(url) + response = web.getJSON(url) - if response is not None and response.hasNode("Section"): - for itm in response.getNode("Section").getNodes("Item"): - yield (itm.getNode("Text").getContent(), - itm.getNode("Description").getContent() if itm.hasNode("Description") else "", - itm.getNode("Url").getContent()) + if response is not None and len(response) >= 4: + for k in range(len(response[1])): + yield (response[1][k], + response[2][k], + response[3][k]) def search(site, term, ssl=False): @@ -167,6 +167,7 @@ def mediawiki_response(site, term, receivers): # Try looking at opensearch os = [x for x, _, _ in opensearch(site, terms[0])] + print(os) # Fallback to global search if not len(os): os = [x for x, _ in search(site, terms[0]) if x is not None and x != ""] diff --git a/modules/velib.py b/modules/velib.py index bdfc8e0..09fa345 100644 --- a/modules/velib.py +++ b/modules/velib.py @@ -1,7 +1,7 @@ -# coding=utf-8 - """Gets information about velib stations""" +# PYTHON STUFFS ####################################################### + import re from nemubot import context @@ -9,11 +9,11 @@ from nemubot.exception import IRCException from nemubot.hooks import hook from nemubot.tools import web -nemubotversion = 4.0 - from more import Response +# LOADING ############################################################# + URL_API = None # http://www.velib.paris.fr/service/stationdetails/paris/%s def load(context): @@ -29,25 +29,14 @@ def load(context): # context.add_event(evt) -def help_full(): - return ("!velib /number/ ...: gives available bikes and slots at " - "the station /number/.") - +# MODULE CORE ######################################################### def station_status(station): """Gets available and free status of a given station""" response = web.getXML(URL_API % station) if response is not None: - available = response.getNode("available").getContent() - if available is not None and len(available) > 0: - available = int(available) - else: - available = 0 - free = response.getNode("free").getContent() - if free is not None and len(free) > 0: - free = int(free) - else: - free = 0 + available = int(response.getElementsByTagName("available")[0].firstChild.nodeValue) + free = int(response.getElementsByTagName("free")[0].firstChild.nodeValue) return (available, free) else: return (None, None) @@ -69,27 +58,30 @@ def print_station_status(msg, station): """Send message with information about the given station""" (available, free) = station_status(station) if available is not None and free is not None: - return Response("à la station %s : %d vélib et %d points d'attache" + return Response("À la station %s : %d vélib et %d points d'attache" " disponibles." % (station, available, free), - channel=msg.channel, nick=msg.nick) + channel=msg.channel) raise IRCException("station %s inconnue." % station) -@hook("cmd_hook", "velib") +# MODULE INTERFACE #################################################### + +@hook("cmd_hook", "velib", + help="gives available bikes and slots at the given station", + help_usage={ + "STATION_ID": "gives available bikes and slots at the station STATION_ID" + }) def ask_stations(msg): - """Hook entry from !velib""" if len(msg.args) > 4: raise IRCException("demande-moi moins de stations à la fois.") - - elif len(msg.args): - for station in msg.args: - if re.match("^[0-9]{4,5}$", station): - return print_station_status(msg, station) - elif station in context.data.index: - return print_station_status(msg, - context.data.index[station]["number"]) - else: - raise IRCException("numéro de station invalide.") - - else: + elif not len(msg.args): raise IRCException("pour quelle station ?") + + for station in msg.args: + if re.match("^[0-9]{4,5}$", station): + return print_station_status(msg, station) + elif station in context.data.index: + return print_station_status(msg, + context.data.index[station]["number"]) + else: + raise IRCException("numéro de station invalide.") diff --git a/modules/wolframalpha.py b/modules/wolframalpha.py index f3bc072..ef1cc82 100644 --- a/modules/wolframalpha.py +++ b/modules/wolframalpha.py @@ -1,16 +1,20 @@ -# coding=utf-8 +"""Performing search and calculation""" + +# PYTHON STUFFS ####################################################### from urllib.parse import quote +import re from nemubot import context from nemubot.exception import IRCException from nemubot.hooks import hook from nemubot.tools import web -nemubotversion = 4.0 - from more import Response + +# LOADING ############################################################# + URL_API = "http://api.wolframalpha.com/v2/query?input=%%s&appid=%s" def load(context): @@ -24,76 +28,90 @@ def load(context): URL_API = URL_API % quote(context.config["apikey"]).replace("%", "%%") -class WFASearch: +# MODULE CORE ######################################################### + +class WFAResults: + def __init__(self, terms): - self.terms = terms self.wfares = web.getXML(URL_API % quote(terms)) + @property def success(self): try: - return self.wfares["success"] == "true" + return self.wfares.documentElement.hasAttribute("success") and self.wfares.documentElement.getAttribute("success") == "true" except: return False + @property def error(self): if self.wfares is None: return "An error occurs during computation." - elif self.wfares["error"] == "true": + elif self.wfares.documentElement.hasAttribute("error") and self.wfares.documentElement.getAttribute("error") == "true": return ("An error occurs during computation: " + - self.wfares.getNode("error").getNode("msg").getContent()) - elif self.wfares.hasNode("didyoumeans"): + self.wfares.getElementsByTagName("error")[0].getElementsByTagName("msg")[0].firstChild.nodeValue) + elif len(self.wfares.getElementsByTagName("didyoumeans")): start = "Did you mean: " tag = "didyoumean" end = "?" - elif self.wfares.hasNode("tips"): + elif len(self.wfares.getElementsByTagName("tips")): start = "Tips: " tag = "tip" end = "" - elif self.wfares.hasNode("relatedexamples"): + elif len(self.wfares.getElementsByTagName("relatedexamples")): start = "Related examples: " tag = "relatedexample" end = "" - elif self.wfares.hasNode("futuretopic"): - return self.wfares.getNode("futuretopic")["msg"] + elif len(self.wfares.getElementsByTagName("futuretopic")): + return self.wfares.getElementsByTagName("futuretopic")[0].getAttribute("msg") else: return "An error occurs during computation" + proposal = list() - for dym in self.wfares.getNode(tag + "s").getNodes(tag): + for dym in self.wfares.getElementsByTagName(tag): if tag == "tip": - proposal.append(dym["text"]) + proposal.append(dym.getAttribute("text")) elif tag == "relatedexample": - proposal.append(dym["desc"]) + proposal.append(dym.getAttribute("desc")) else: - proposal.append(dym.getContent()) + proposal.append(dym.firstChild.nodeValue) + return start + ', '.join(proposal) + end + @property - def nextRes(self): - try: - for node in self.wfares.getNodes("pod"): - for subnode in node.getNodes("subpod"): - if subnode.getFirstNode("plaintext").getContent() != "": - yield (node["title"] + " " + subnode["title"] + ": " + - subnode.getFirstNode("plaintext").getContent()) - except IndexError: - pass + def results(self): + for node in self.wfares.getElementsByTagName("pod"): + for subnode in node.getElementsByTagName("subpod"): + if subnode.getElementsByTagName("plaintext")[0].firstChild: + yield (node.getAttribute("title") + + ((" / " + subnode.getAttribute("title")) if subnode.getAttribute("title") else "") + ": " + + "; ".join(subnode.getElementsByTagName("plaintext")[0].firstChild.nodeValue.split("\n"))) -@hook("cmd_hook", "calculate") +# MODULE INTERFACE #################################################### + +@hook("cmd_hook", "calculate", + help="Perform search and calculation using WolframAlpha", + help_usage={ + "TERM": "Look at the given term on WolframAlpha", + "CALCUL": "Perform the computation over WolframAlpha service", + }) def calculate(msg): if not len(msg.args): raise IRCException("Indicate a calcul to compute") - s = WFASearch(' '.join(msg.args)) + s = WFAResults(' '.join(msg.args)) - if s.success: - res = Response(channel=msg.channel, nomore="No more results") - for result in s.nextRes: - res.append_message(result) - if (len(res.messages) > 0): - res.messages.pop(0) - return res - else: - return Response(s.error, msg.channel) + if not s.success: + raise IRCException(s.error) + + res = Response(channel=msg.channel, nomore="No more results") + + for result in s.results: + res.append_message(re.sub(r' +', ' ', result)) + if len(res.messages): + res.messages.pop(0) + + return res From 92530ef1b2dc358664b3a6da6625a1c539bc73c7 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Mon, 26 Oct 2015 06:23:32 +0100 Subject: [PATCH 019/271] Server factory takes initializer dict --- nemubot/server/__init__.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/nemubot/server/__init__.py b/nemubot/server/__init__.py index 700a198..6bb002d 100644 --- a/nemubot/server/__init__.py +++ b/nemubot/server/__init__.py @@ -26,14 +26,14 @@ _wlist = [] _xlist = [] -def factory(uri): +def factory(uri, **init_args): from urllib.parse import urlparse, unquote o = urlparse(uri) if o.scheme == "irc" or o.scheme == "ircs": # http://www.w3.org/Addressing/draft-mirashi-url-irc-01.txt # http://www-archive.mozilla.org/projects/rt-messaging/chatzilla/irc-urls.html - args = dict() + args = init_args modifiers = o.path.split(",") target = unquote(modifiers.pop(0)[1:]) @@ -51,9 +51,13 @@ def factory(uri): else: key, val = q, "" if key == "msg": - args["on_connect"] = [ "PRIVMSG %s :%s" % (target, unquote(val)) ] + if "on_connect" not in args: + args["on_connect"] = [] + args["on_connect"].append("PRIVMSG %s :%s" % (target, unquote(val))) elif key == "key": - args["channels"] = [ (target, unquote(val)) ] + if "channels" not in args: + args["channels"] = [] + args["channels"].append((target, unquote(val))) elif key == "pass": args["password"] = unquote(val) elif key == "charset": From c560e13f24dde09dc73e7e24d4dca50d10d7d31a Mon Sep 17 00:00:00 2001 From: nemunaire Date: Tue, 27 Oct 2015 18:03:28 +0100 Subject: [PATCH 020/271] Rework XML parser: part 1 This is the first step of the parser refactoring: here we change the configuration, next step will change data saving. --- bot_sample.xml | 4 +- modules/books.py | 2 +- modules/mapquest.py | 2 +- modules/networking/whois.py | 2 +- modules/syno.py | 2 +- modules/tpb.py | 2 +- modules/translate.py | 2 +- modules/velib.py | 2 +- modules/weather.py | 2 +- modules/whois.py | 2 +- modules/wolframalpha.py | 2 +- nemubot/bot.py | 54 +++++- nemubot/channel.py | 6 +- nemubot/prompt/builtins.py | 4 +- nemubot/tools/config.py | 247 +++++++++++++++------------- nemubot/tools/test_xmlparser.py | 82 +++++++++ nemubot/tools/xmlparser/__init__.py | 104 +++++++++++- nemubot/tools/xmlparser/node.py | 7 +- 18 files changed, 388 insertions(+), 140 deletions(-) create mode 100644 nemubot/tools/test_xmlparser.py diff --git a/bot_sample.xml b/bot_sample.xml index ce821d2..ed1a41f 100644 --- a/bot_sample.xml +++ b/bot_sample.xml @@ -1,11 +1,11 @@ - + diff --git a/modules/books.py b/modules/books.py index f532a3b..4a4d5aa 100644 --- a/modules/books.py +++ b/modules/books.py @@ -15,7 +15,7 @@ from more import Response # LOADING ############################################################# def load(context): - if not context.config or not context.config.getAttribute("goodreadskey"): + if not context.config or "goodreadskey" not in context.config: raise ImportError("You need a Goodreads API key in order to use this " "module. Add it to the module configuration file:\n" "\n" diff --git a/modules/mapquest.py b/modules/mapquest.py index 95952ab..f147176 100644 --- a/modules/mapquest.py +++ b/modules/mapquest.py @@ -16,7 +16,7 @@ from more import Response URL_API = "http://open.mapquestapi.com/geocoding/v1/address?key=%s&location=%%s" def load(context): - if not context.config or not context.config.hasAttribute("apikey"): + if not context.config or "apikey" not in context.config: raise ImportError("You need a MapQuest API key in order to use this " "module. Add it to the module configuration file:\n" "\nSample " diff --git a/modules/translate.py b/modules/translate.py index a0d8dc2..7452889 100644 --- a/modules/translate.py +++ b/modules/translate.py @@ -19,7 +19,7 @@ LANG = ["ar", "zh", "cz", "en", "fr", "gr", "it", URL = "http://api.wordreference.com/0.8/%s/json/%%s%%s/%%s" def load(context): - if not context.config or not context.config.hasAttribute("wrapikey"): + if not context.config or "wrapikey" not in context.config: raise ImportError("You need a WordReference API key in order to use " "this module. Add it to the module configuration " "file:\n\n" diff --git a/modules/whois.py b/modules/whois.py index 32c13ea..878d4a2 100644 --- a/modules/whois.py +++ b/modules/whois.py @@ -16,7 +16,7 @@ PASSWD_FILE = None def load(context): global PASSWD_FILE - if not context.config or not context.config.hasAttribute("passwd"): + if not context.config or "passwd" not in context.config: print("No passwd file given") return None PASSWD_FILE = context.config["passwd"] diff --git a/modules/wolframalpha.py b/modules/wolframalpha.py index ef1cc82..7a13200 100644 --- a/modules/wolframalpha.py +++ b/modules/wolframalpha.py @@ -19,7 +19,7 @@ URL_API = "http://api.wolframalpha.com/v2/query?input=%%s&appid=%s" def load(context): global URL_API - if not context.config or not context.config.hasAttribute("apikey"): + if not context.config or "apikey" not in context.config: raise ImportError ("You need a Wolfram|Alpha API key in order to use " "this module. Add it to the module configuration: " "\n 1: - from nemubot.tools.config import load_file - for filename in toks[1:]: - load_file(filename, context) + context.load_file(filename) else: print ("Not enough arguments. `load' takes a filename.") return 1 diff --git a/nemubot/tools/config.py b/nemubot/tools/config.py index 479b96f..33fd3cc 100644 --- a/nemubot/tools/config.py +++ b/nemubot/tools/config.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier # @@ -16,123 +14,146 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import logging - -logger = logging.getLogger("nemubot.tools.config") +def get_boolean(s): + if isinstance(s, bool): + return s + else: + return (s and s != "0" and s.lower() != "false" and s.lower() != "off") -def get_boolean(d, k, default=False): - return ((k in d and d[k].lower() != "false" and d[k].lower() != "off") or - (k not in d and default)) +class GenericNode: + + def __init__(self, tag, **kwargs): + self.tag = tag + self.attrs = kwargs + self.content = "" + self.children = [] + self._cur = None + self._deep_cur = 0 -def _load_server(config, xmlnode): - """Load a server configuration - - Arguments: - config -- the global configuration - xmlnode -- the current server configuration node - """ - - opts = { - "host": xmlnode["host"], - "ssl": xmlnode.hasAttribute("ssl") and xmlnode["ssl"].lower() == "true", - - "nick": xmlnode["nick"] if xmlnode.hasAttribute("nick") else config["nick"], - "owner": xmlnode["owner"] if xmlnode.hasAttribute("owner") else config["owner"], - } - - # Optional keyword arguments - for optional_opt in [ "port", "username", "realname", - "password", "encoding", "caps" ]: - if xmlnode.hasAttribute(optional_opt): - opts[optional_opt] = xmlnode[optional_opt] - elif optional_opt in config: - opts[optional_opt] = config[optional_opt] - - # Command to send on connection - if "on_connect" in xmlnode: - def on_connect(): - yield xmlnode["on_connect"] - opts["on_connect"] = on_connect - - # Channels to autojoin on connection - if xmlnode.hasNode("channel"): - opts["channels"] = list() - for chn in xmlnode.getNodes("channel"): - opts["channels"].append((chn["name"], chn["password"]) - if chn["password"] is not None - else chn["name"]) - - # Server/client capabilities - if "caps" in xmlnode or "caps" in config: - capsl = (xmlnode["caps"] if xmlnode.hasAttribute("caps") - else config["caps"]).lower() - if capsl == "no" or capsl == "off" or capsl == "false": - opts["caps"] = None + def startElement(self, name, attrs): + if self._cur is None: + self._cur = GenericNode(name, **attrs) + self._deep_cur = 0 else: - opts["caps"] = capsl.split(',') - else: - opts["caps"] = list() - - # Bind the protocol asked to the corresponding implementation - if "protocol" not in xmlnode or xmlnode["protocol"] == "irc": - from nemubot.server.IRC import IRC as IRCServer - srvcls = IRCServer - else: - raise Exception("Unhandled protocol '%s'" % - xmlnode["protocol"]) - - # Initialize the server - return srvcls(**opts) + self._deep_cur += 1 + self._cur.startElement(name, attrs) + return True -def load_file(filename, context): - """Load the configuration file - - Arguments: - filename -- the path to the file to load - """ - - import os - - if os.path.isfile(filename): - from nemubot.tools.xmlparser import parse_file - - config = parse_file(filename) - - # This is a true nemubot configuration file, load it! - if config.getName() == "nemubotconfig": - # Preset each server in this file - for server in config.getNodes("server"): - srv = _load_server(config, server) - - # Add the server in the context - if context.add_server(srv, get_boolean(server, "autoconnect")): - logger.info("Server '%s' successfully added." % srv.id) - else: - logger.error("Can't add server '%s'." % srv.id) - - # Load module and their configuration - for mod in config.getNodes("module"): - context.modules_configuration[mod["name"]] = mod - if get_boolean(mod, "autoload", default=True): - 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.getNodes("include"): - load_file(load["path"], context) - - # Other formats + def characters(self, content): + if self._cur is None: + self.content += content else: - logger.error("Can't load `%s'; this is not a valid nemubot " - "configuration file." % filename) + self._cur.characters(content) - # Unexisting file, assume a name was passed, import the module! - else: - context.import_module(filename) + + def endElement(self, name): + if name is None: + return + + if self._deep_cur: + self._deep_cur -= 1 + self._cur.endElement(name) + else: + self.children.append(self._cur) + self._cur = None + return True + + + def hasNode(self, nodename): + return self.getNode(nodename) is not None + + + def getNode(self, nodename): + for c in self.children: + if c is not None and c.tag == nodename: + return c + return None + + + def __getitem__(self, item): + return self.attrs[item] + + def __contains__(self, item): + return item in self.attrs + + +class NemubotConfig: + + def __init__(self, nick="nemubot", realname="nemubot", owner=None, + ip=None, ssl=False, caps=None, encoding="utf-8"): + self.nick = nick + self.realname = realname + self.owner = owner + self.ip = ip + self.caps = caps.split(" ") if caps is not None else [] + self.encoding = encoding + self.servers = [] + self.modules = [] + self.includes = [] + + + def addChild(self, name, child): + if name == "module" and isinstance(child, ModuleConfig): + self.modules.append(child) + return True + elif name == "server" and isinstance(child, ServerConfig): + self.servers.append(child) + return True + elif name == "include" and isinstance(child, IncludeConfig): + self.includes.append(child) + return True + + +class ServerConfig: + + def __init__(self, uri="irc://nemubot@localhost/", autoconnect=True, caps=None, **kwargs): + self.uri = uri + self.autoconnect = autoconnect + self.caps = caps.split(" ") if caps is not None else [] + self.args = kwargs + self.channels = [] + + + def addChild(self, name, child): + if name == "channel" and isinstance(child, Channel): + self.channels.append(child) + return True + + + def server(self, parent): + from nemubot.server import factory + + for a in ["nick", "owner", "realname", "encoding"]: + if a not in self.args: + self.args[a] = getattr(parent, a) + + self.caps += parent.caps + + return factory(self.uri, **self.args) + + +class IncludeConfig: + + def __init__(self, path): + self.path = path + + +class ModuleConfig(GenericNode): + + def __init__(self, name, autoload=True, **kwargs): + super(ModuleConfig, self).__init__(None, **kwargs) + self.name = name + self.autoload = get_boolean(autoload) + +from nemubot.channel import Channel + +config_nodes = { + "nemubotconfig": NemubotConfig, + "server": ServerConfig, + "channel": Channel, + "module": ModuleConfig, + "include": IncludeConfig, +} diff --git a/nemubot/tools/test_xmlparser.py b/nemubot/tools/test_xmlparser.py new file mode 100644 index 0000000..faf5684 --- /dev/null +++ b/nemubot/tools/test_xmlparser.py @@ -0,0 +1,82 @@ +import unittest + +import xml.parsers.expat + +from nemubot.tools.xmlparser import XMLParser + + +class StringNode(): + def __init__(self): + self.string = "" + + def characters(self, content): + self.string += content + + +class TestNode(): + def __init__(self, option=None): + self.option = option + self.mystr = None + + def addChild(self, name, child): + self.mystr = child.string + + +class Test2Node(): + def __init__(self, option=None): + self.option = option + self.mystrs = list() + + def startElement(self, name, attrs): + if name == "string": + self.mystrs.append(attrs["value"]) + return True + + +class TestXMLParser(unittest.TestCase): + + def test_parser1(self): + p = xml.parsers.expat.ParserCreate() + mod = XMLParser({"string": StringNode}) + + p.StartElementHandler = mod.startElement + p.CharacterDataHandler = mod.characters + p.EndElementHandler = mod.endElement + + p.Parse("toto", 1) + + self.assertEqual(mod.root.string, "toto") + + + def test_parser2(self): + p = xml.parsers.expat.ParserCreate() + mod = XMLParser({"string": StringNode, "test": TestNode}) + + p.StartElementHandler = mod.startElement + p.CharacterDataHandler = mod.characters + p.EndElementHandler = mod.endElement + + p.Parse("toto", 1) + + self.assertEqual(mod.root.option, "123") + self.assertEqual(mod.root.mystr, "toto") + + + def test_parser3(self): + p = xml.parsers.expat.ParserCreate() + mod = XMLParser({"string": StringNode, "test": Test2Node}) + + p.StartElementHandler = mod.startElement + p.CharacterDataHandler = mod.characters + p.EndElementHandler = mod.endElement + + p.Parse("", 1) + + self.assertEqual(mod.root.option, None) + self.assertEqual(len(mod.root.mystrs), 2) + self.assertEqual(mod.root.mystrs[0], "toto") + self.assertEqual(mod.root.mystrs[1], "toto2") + + +if __name__ == '__main__': + unittest.main() diff --git a/nemubot/tools/xmlparser/__init__.py b/nemubot/tools/xmlparser/__init__.py index 4617b57..5e546f4 100644 --- a/nemubot/tools/xmlparser/__init__.py +++ b/nemubot/tools/xmlparser/__init__.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier # @@ -48,9 +46,107 @@ class ModuleStatesFile: self.root = child +class XMLParser: + + def __init__(self, knodes): + self.knodes = knodes + + self.stack = list() + self.child = 0 + + + def parse_file(self, path): + p = xml.parsers.expat.ParserCreate() + + p.StartElementHandler = self.startElement + p.CharacterDataHandler = self.characters + p.EndElementHandler = self.endElement + + with open(path, "rb") as f: + p.ParseFile(f) + + return self.root + + + def parse_string(self, s): + p = xml.parsers.expat.ParserCreate() + + p.StartElementHandler = self.startElement + p.CharacterDataHandler = self.characters + p.EndElementHandler = self.endElement + + p.Parse(s, 1) + + return self.root + + + @property + def root(self): + if len(self.stack): + return self.stack[0] + else: + return None + + + @property + def current(self): + if len(self.stack): + return self.stack[-1] + else: + return None + + + def display_stack(self): + return " in ".join([str(type(s).__name__) for s in reversed(self.stack)]) + + + def startElement(self, name, attrs): + if not self.current or not hasattr(self.current, "startElement") or not self.current.startElement(name, attrs): + if name not in self.knodes: + raise TypeError(name + " is not a known type to decode") + else: + self.stack.append(self.knodes[name](**attrs)) + else: + self.child += 1 + + + def characters(self, content): + if self.current and hasattr(self.current, "characters"): + self.current.characters(content) + + + def endElement(self, name): + if self.child: + self.child -= 1 + + if hasattr(self.current, "endElement"): + self.current.endElement(name) + return + + if hasattr(self.current, "endElement"): + self.current.endElement(None) + + # Don't remove root + if len(self.stack) > 1: + last = self.stack.pop() + if hasattr(self.current, "addChild"): + if self.current.addChild(name, last): + return + raise TypeError(name + " tag not expected in " + self.display_stack()) + + def parse_file(filename): - with open(filename, "r") as f: - return parse_string(f.read()) + p = xml.parsers.expat.ParserCreate() + mod = ModuleStatesFile() + + p.StartElementHandler = mod.startElement + p.EndElementHandler = mod.endElement + p.CharacterDataHandler = mod.characters + + with open(filename, "rb") as f: + p.ParseFile(f) + + return mod.root def parse_string(string): diff --git a/nemubot/tools/xmlparser/node.py b/nemubot/tools/xmlparser/node.py index 5f8a509..fa5d0a5 100644 --- a/nemubot/tools/xmlparser/node.py +++ b/nemubot/tools/xmlparser/node.py @@ -1,5 +1,3 @@ -# coding=utf-8 - # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier # @@ -37,7 +35,7 @@ class ModuleState: """Get the name of the current node""" return self.name - def display(self, level = 0): + def display(self, level=0): ret = "" out = list() for k in self.attributes: @@ -51,6 +49,9 @@ class ModuleState: def __str__(self): return self.display() + def __repr__(self): + return self.display() + def __getitem__(self, i): """Return the attribute asked""" return self.getAttribute(i) From 2fdef0afe4e06453336b39ea1814a5681e9d9619 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Wed, 28 Oct 2015 00:20:30 +0100 Subject: [PATCH 021/271] addChild should return a boolean --- nemubot/tools/test_xmlparser.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nemubot/tools/test_xmlparser.py b/nemubot/tools/test_xmlparser.py index faf5684..d7f5a9a 100644 --- a/nemubot/tools/test_xmlparser.py +++ b/nemubot/tools/test_xmlparser.py @@ -20,6 +20,7 @@ class TestNode(): def addChild(self, name, child): self.mystr = child.string + return True class Test2Node(): From e4d67ec345c485eeafc4b073bce0e21f015be1e9 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Thu, 29 Oct 2015 12:35:43 +0100 Subject: [PATCH 022/271] Use Channel class when creating Server --- nemubot/server/IRC.py | 8 ++++---- nemubot/tools/config.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/nemubot/server/IRC.py b/nemubot/server/IRC.py index 672d7af..8dff0f8 100644 --- a/nemubot/server/IRC.py +++ b/nemubot/server/IRC.py @@ -46,7 +46,7 @@ class IRC(SocketServer): realname -- the bot's realname encoding -- the encoding used on the whole server caps -- client capabilities to register on the server - channels -- list of channels to join on connection (if a channel is password protected, give a tuple: (channel_name, password)) + channels -- list of channels to join on connection on_connect -- generator to call when connection is done """ @@ -134,10 +134,10 @@ class IRC(SocketServer): self.write(oc) # Then, JOIN some channels for chn in channels: - if isinstance(chn, tuple): - self.write("JOIN %s %s" % chn) + if chn.password: + self.write("JOIN %s %s" % (chn.name, chn.password)) else: - self.write("JOIN %s" % chn) + self.write("JOIN %s" % chn.name) self.hookscmd["001"] = _on_connect # Respond to ERROR diff --git a/nemubot/tools/config.py b/nemubot/tools/config.py index 33fd3cc..f1305a7 100644 --- a/nemubot/tools/config.py +++ b/nemubot/tools/config.py @@ -132,7 +132,7 @@ class ServerConfig: self.caps += parent.caps - return factory(self.uri, **self.args) + return factory(self.uri, caps=self.caps, channels=self.channels, **self.args) class IncludeConfig: From 497263eaf75554c277316efac288dd0a2f7582a9 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 27 Oct 2015 22:19:12 +0100 Subject: [PATCH 023/271] [suivi] improve the suivi module * Add multiple arguments/tracking numbers support * Make it easier to add new tracking services * Simplified to just one hook to which we can specify trackers using the names variables (correct typo in framalink comments) --- modules/framalink.py | 2 +- modules/suivi.py | 119 +++++++++++++++++++++---------------------- 2 files changed, 60 insertions(+), 61 deletions(-) diff --git a/modules/framalink.py b/modules/framalink.py index a1bf78d..5da446b 100644 --- a/modules/framalink.py +++ b/modules/framalink.py @@ -13,7 +13,7 @@ from nemubot.message import Text from nemubot.tools import web -# MODULE FUCNTIONS #################################################### +# MODULE FUNCTIONS #################################################### def default_reducer(url, data): snd_url = url + quote(data, "/:%@&=?") diff --git a/modules/suivi.py b/modules/suivi.py index 32c39a3..851a6a6 100644 --- a/modules/suivi.py +++ b/modules/suivi.py @@ -9,8 +9,7 @@ from more import Response nemubotversion = 4.0 -def help_full(): - return "Traquez vos courriers La Poste ou Colissimo en utilisant la commande: !laposte ou !colissimo \nCe service se base sur http://www.csuivi.courrier.laposte.fr/suivi/index et http://www.colissimo.fr/portail_colissimo/suivre.do" +# POSTAGE SERVICE PARSERS ############################################ def get_colissimo_info(colissimo_id): colissimo_data = getURLContent("http://www.colissimo.fr/portail_colissimo/suivre.do?colispart=%s" % colissimo_id) @@ -77,66 +76,66 @@ def get_laposte_info(laposte_id): return (poste_type.lower(), poste_id.strip(), poste_status.lower(), poste_location, poste_date) -@hook("cmd_hook", "track") + +# TRACKING HANDLERS ################################################### + +def handle_laposte(tracknum): + info = get_laposte_info(tracknum) + if info: + poste_type, poste_id, poste_status, poste_location, poste_date = info + return ("Le courrier de type \x02%s\x0F : \x02%s\x0F est actuellement \x02%s\x0F dans la zone \x02%s\x0F (Mis à jour le \x02%s\x0F)." % (poste_type, poste_id, poste_status, poste_location, poste_date)) + +def handle_colissimo(tracknum): + info = get_colissimo_info(tracknum) + if info: + date, libelle, site = info + return ("Colissimo: \x02%s\x0F : \x02%s\x0F Dernière mise à jour le \x02%s\x0F au site \x02%s\x0F." % (tracknum, libelle, date, site)) + +def handle_chronopost(tracknum): + info = get_chronopost_info(tracknum) + if info: + date, libelle = info + return ("Colis Chronopost: \x02%s\x0F : \x02%s\x0F. Dernière mise à jour \x02%s\x0F." % (tracknum, libelle, date)) + +def handle_coliprive(tracknum): + info = get_colisprive_info(tracknum) + if info: + return ("Colis Privé: \x02%s\x0F : \x02%s\x0F." % (tracknum, info)) + +TRACKING_HANDLERS = { + 'laposte': handle_laposte, + 'colissimo': handle_colissimo, + 'chronopost': handle_chronopost, + 'coliprive': handle_coliprive +} + +# HOOKS ############################################################## + +@hook("cmd_hook", "track", + help="Track postage", + help_usage={"[@tracker] TRACKING_ID [TRACKING_ID ...]": "Track the specified postage IDs using the specified tracking service or all of them."}) def get_tracking_info(msg): if not len(msg.args): - raise IRCException("Renseignez un identifiant d'envoi,") + raise IRCException("Renseignez un identifiant d'envoi.") - info = get_colisprive_info(msg.args[0]) - if info: - return Response("Colis Privé: \x02%s\x0F : \x02%s\x0F." % (msg.args[0], info), msg.channel) + res = Response(channel=msg.channel, count=" (%d suivis supplémentaires)") - info = get_chronopost_info(msg.args[0]) - if info: - date, libelle = info - return Response("Colis Chronopost: \x02%s\x0F : \x02%s\x0F. Dernière mise à jour \x02%s\x0F." % (msg.args[0], libelle, date), msg.channel) + if 'tracker' in msg.kwargs: + if msg.kwargs['tracker'] in TRACKING_HANDLERS: + trackers = { + msg.kwargs['tracker']: TRACKING_HANDLERS[msg.kwargs['tracker']] + } + else: + raise IRCException("No tracker named \x02{tracker}\x0F, please use one of the following: \x02{trackers}\x0F".format(tracker=msg.kwargs['tracker'], trackers=', '.join(TRACKING_HANDLERS.keys()))) + else: + trackers = TRACKING_HANDLERS - info = get_colissimo_info(msg.args[0]) - if info: - date, libelle, site = info - return Response("Colissimo: \x02%s\x0F : \x02%s\x0F. Dernière mise à jour le \x02%s\x0F au site \x02%s\x0F." % (msg.args[0], libelle, date, site), msg.channel) - - info = get_laposte_info(msg.args[0]) - if info: - poste_type, poste_id, poste_status, poste_location, poste_date = info - return Response("Le courrier de type \x02%s\x0F : \x02%s\x0F est actuellement \x02%s\x0F dans la zone \x02%s\x0F (Mis à jour le \x02%s\x0F)." % (poste_type, poste_id, poste_status, poste_location, poste_date), msg.channel) - return Response("L'identifiant recherché semble incorrect, merci de vérifier son exactitude.", msg.channel) - -@hook("cmd_hook", "colisprive") -def get_colisprive_tracking_info(msg): - if not len(msg.args): - raise IRCException("Renseignez un identifiant d'envoi,") - info = get_colisprive_info(msg.args[0]) - if info: - return Response("Colis: \x02%s\x0F : \x02%s\x0F." % (msg.args[0], info), msg.channel) - return Response("L'identifiant recherché semble incorrect, merci de vérifier son exactitude.", msg.channel) - -@hook("cmd_hook", "chronopost") -def get_chronopost_tracking_info(msg): - if not len(msg.args): - raise IRCException("Renseignez un identifiant d'envoi,") - info = get_chronopost_info(msg.args[0]) - if info: - date, libelle = info - return Response("Colis: \x02%s\x0F : \x02%s\x0F. Dernière mise à jour \x02%s\x0F." % (msg.args[0], libelle, date), msg.channel) - return Response("L'identifiant recherché semble incorrect, merci de vérifier son exactitude.", msg.channel) - -@hook("cmd_hook", "colissimo") -def get_colissimo_tracking_info(msg): - if not len(msg.args): - raise IRCException("Renseignez un identifiant d'envoi,") - info = get_colissimo_info(msg.args[0]) - if info: - date, libelle, site = info - return Response("Colis: \x02%s\x0F : \x02%s\x0F. Dernière mise à jour le \x02%s\x0F au site \x02%s\x0F." % (msg.args[0], libelle, date, site), msg.channel) - return Response("L'identifiant recherché semble incorrect, merci de vérifier son exactitude.", msg.channel) - -@hook("cmd_hook", "laposte") -def get_laposte_tracking_info(msg): - if not len(msg.args): - raise IRCException("Renseignez un identifiant d'envoi,") - info = get_laposte_info(msg.args[0]) - if info: - poste_type, poste_id, poste_status, poste_location, poste_date = info - return Response("Le courrier de type \x02%s\x0F : \x02%s\x0F est actuellement \x02%s\x0F dans la zone \x02%s\x0F (Mis à jour le \x02%s\x0F)." % (poste_type, poste_id, poste_status, poste_location, poste_date), msg.channel) - return Response("L'identifiant recherché semble incorrect, merci de vérifier son exactitude.", msg.channel) + for tracknum in msg.args: + for name,tracker in trackers.items(): + ret = tracker(tracknum) + if ret: + res.append_message(ret) + break + if not ret: + res.append_message("L'identifiant \x02{id}\x0F semble incorrect, merci de vérifier son exactitude.".format(id=tracknum)) + return res From 3cb9a54cee73e9215461b8ad4bb404405c178e70 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 28 Oct 2015 20:55:02 +0100 Subject: [PATCH 024/271] [suivi] Code cleanup --- modules/suivi.py | 65 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 16 deletions(-) diff --git a/modules/suivi.py b/modules/suivi.py index 851a6a6..80e0345 100644 --- a/modules/suivi.py +++ b/modules/suivi.py @@ -1,6 +1,7 @@ import urllib.request import urllib.parse from bs4 import BeautifulSoup +import re from nemubot.hooks import hook from nemubot.exception import IRCException @@ -11,20 +12,25 @@ nemubotversion = 4.0 # POSTAGE SERVICE PARSERS ############################################ + def get_colissimo_info(colissimo_id): - colissimo_data = getURLContent("http://www.colissimo.fr/portail_colissimo/suivre.do?colispart=%s" % colissimo_id) + colissimo_data = getURLContent("http://www.colissimo.fr/portail_colissimo/" + "suivre.do?colispart=%s" % colissimo_id) soup = BeautifulSoup(colissimo_data) dataArray = soup.find(class_='dataArray') if dataArray and dataArray.tbody and dataArray.tbody.tr: date = dataArray.tbody.tr.find(headers="Date").get_text() - libelle = dataArray.tbody.tr.find(headers="Libelle").get_text().replace('\n', '').replace('\t', '').replace('\r', '') + libelle = re.sub(r'[\n\t\r]', '', + dataArray.tbody.tr.find(headers="Libelle").get_text()) site = dataArray.tbody.tr.find(headers="site").get_text().strip() return (date, libelle, site.strip()) + def get_chronopost_info(track_id): data = urllib.parse.urlencode({'listeNumeros': track_id}) - track_baseurl = "http://www.chronopost.fr/expedier/inputLTNumbersNoJahia.do?lang=fr_FR" + track_baseurl = "http://www.chronopost.fr/expedier/" \ + "inputLTNumbersNoJahia.do?lang=fr_FR" track_data = urllib.request.urlopen(track_baseurl, data.encode('utf-8')) soup = BeautifulSoup(track_data) @@ -38,22 +44,28 @@ def get_chronopost_info(track_id): libelle = info[1] return (date, libelle) + def get_colisprive_info(track_id): data = urllib.parse.urlencode({'numColis': track_id}) - track_baseurl = "https://www.colisprive.com/moncolis/pages/detailColis.aspx" + track_baseurl = "https://www.colisprive.com/moncolis/pages/" \ + "detailColis.aspx" track_data = urllib.request.urlopen(track_baseurl, data.encode('utf-8')) soup = BeautifulSoup(track_data) dataArray = soup.find(class_='BandeauInfoColis') - if dataArray and dataArray.find(class_='divStatut') and dataArray.find(class_='divStatut').find(class_='tdText'): - status = dataArray.find(class_='divStatut').find(class_='tdText').get_text() + if (dataArray and dataArray.find(class_='divStatut') + and dataArray.find(class_='divStatut').find(class_='tdText')): + status = dataArray.find(class_='divStatut') \ + .find(class_='tdText').get_text() return status + def get_laposte_info(laposte_id): data = urllib.parse.urlencode({'id': laposte_id}) laposte_baseurl = "http://www.part.csuivi.courrier.laposte.fr/suivi/index" - laposte_data = urllib.request.urlopen(laposte_baseurl, data.encode('utf-8')) + laposte_data = urllib.request.urlopen(laposte_baseurl, + data.encode('utf-8')) soup = BeautifulSoup(laposte_data) search_res = soup.find(class_='resultat_rech_simple_table').tbody.tr if (soup.find(class_='resultat_rech_simple_table').thead @@ -74,28 +86,39 @@ def get_laposte_info(laposte_id): field = field.find_next('td') poste_status = field.get_text() - return (poste_type.lower(), poste_id.strip(), poste_status.lower(), poste_location, poste_date) + return (poste_type.lower(), poste_id.strip(), poste_status.lower(), + poste_location, poste_date) # TRACKING HANDLERS ################################################### + def handle_laposte(tracknum): info = get_laposte_info(tracknum) if info: poste_type, poste_id, poste_status, poste_location, poste_date = info - return ("Le courrier de type \x02%s\x0F : \x02%s\x0F est actuellement \x02%s\x0F dans la zone \x02%s\x0F (Mis à jour le \x02%s\x0F)." % (poste_type, poste_id, poste_status, poste_location, poste_date)) + return ("Le courrier de type \x02%s\x0F : \x02%s\x0F est actuellement " + "\x02%s\x0F dans la zone \x02%s\x0F (Mis à jour le \x02%s\x0F" + ")." % (poste_type, poste_id, poste_status, + poste_location, poste_date)) + def handle_colissimo(tracknum): info = get_colissimo_info(tracknum) if info: date, libelle, site = info - return ("Colissimo: \x02%s\x0F : \x02%s\x0F Dernière mise à jour le \x02%s\x0F au site \x02%s\x0F." % (tracknum, libelle, date, site)) + return ("Colissimo: \x02%s\x0F : \x02%s\x0F Dernière mise à jour le " + "\x02%s\x0F au site \x02%s\x0F." + % (tracknum, libelle, date, site)) + def handle_chronopost(tracknum): info = get_chronopost_info(tracknum) if info: date, libelle = info - return ("Colis Chronopost: \x02%s\x0F : \x02%s\x0F. Dernière mise à jour \x02%s\x0F." % (tracknum, libelle, date)) + return ("Colis Chronopost: \x02%s\x0F : \x02%s\x0F. Dernière mise à " + "jour \x02%s\x0F." % (tracknum, libelle, date)) + def handle_coliprive(tracknum): info = get_colisprive_info(tracknum) @@ -109,11 +132,15 @@ TRACKING_HANDLERS = { 'coliprive': handle_coliprive } + # HOOKS ############################################################## + @hook("cmd_hook", "track", - help="Track postage", - help_usage={"[@tracker] TRACKING_ID [TRACKING_ID ...]": "Track the specified postage IDs using the specified tracking service or all of them."}) + help="Track postage", + help_usage={"[@tracker] TRACKING_ID [TRACKING_ID ...]": "Track the " + "specified postage IDs using the specified tracking service " + "or all of them."}) def get_tracking_info(msg): if not len(msg.args): raise IRCException("Renseignez un identifiant d'envoi.") @@ -126,16 +153,22 @@ def get_tracking_info(msg): msg.kwargs['tracker']: TRACKING_HANDLERS[msg.kwargs['tracker']] } else: - raise IRCException("No tracker named \x02{tracker}\x0F, please use one of the following: \x02{trackers}\x0F".format(tracker=msg.kwargs['tracker'], trackers=', '.join(TRACKING_HANDLERS.keys()))) + raise IRCException("No tracker named \x02{tracker}\x0F, please use" + " one of the following: \x02{trackers}\x0F" + .format(tracker=msg.kwargs['tracker'], + trackers=', ' + .join(TRACKING_HANDLERS.keys()))) else: trackers = TRACKING_HANDLERS for tracknum in msg.args: - for name,tracker in trackers.items(): + for name, tracker in trackers.items(): ret = tracker(tracknum) if ret: res.append_message(ret) break if not ret: - res.append_message("L'identifiant \x02{id}\x0F semble incorrect, merci de vérifier son exactitude.".format(id=tracknum)) + res.append_message("L'identifiant \x02{id}\x0F semble incorrect," + " merci de vérifier son exactitude." + .format(id=tracknum)) return res From 04d5be04fabfdc68990e758761ff2c9e9e57f8c9 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 29 Oct 2015 02:10:46 +0100 Subject: [PATCH 025/271] [suivi] Add TNT support --- modules/suivi.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/modules/suivi.py b/modules/suivi.py index 80e0345..bdd9322 100644 --- a/modules/suivi.py +++ b/modules/suivi.py @@ -13,6 +13,15 @@ nemubotversion = 4.0 # POSTAGE SERVICE PARSERS ############################################ +def get_tnt_info(track_id): + data = getURLContent('www.tnt.fr/public/suivi_colis/recherche/' + 'visubontransport.do?bonTransport=%s' % track_id) + soup = BeautifulSoup(data) + status = soup.find('p', class_='suivi-title-selected') + if status: + return status.get_text() + + def get_colissimo_info(colissimo_id): colissimo_data = getURLContent("http://www.colissimo.fr/portail_colissimo/" "suivre.do?colispart=%s" % colissimo_id) @@ -93,6 +102,13 @@ def get_laposte_info(laposte_id): # TRACKING HANDLERS ################################################### +def handle_tnt(tracknum): + info = get_tnt_info(tracknum) + if info: + return ('Le colis \x02{trackid}\x0f a actuellement le status: ' + '\x02{status}\x0F'.format(trackid=tracknum, status=info)) + + def handle_laposte(tracknum): info = get_laposte_info(tracknum) if info: @@ -129,7 +145,8 @@ TRACKING_HANDLERS = { 'laposte': handle_laposte, 'colissimo': handle_colissimo, 'chronopost': handle_chronopost, - 'coliprive': handle_coliprive + 'coliprive': handle_coliprive, + 'tnt': handle_tnt } From 1e368462656940f2e4fe4a7d9ea4ae28d0d98287 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 25 Oct 2015 12:23:46 +0100 Subject: [PATCH 026/271] [framalink] Fix ycc shortner --- modules/framalink.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/modules/framalink.py b/modules/framalink.py index 5da446b..e4cd944 100644 --- a/modules/framalink.py +++ b/modules/framalink.py @@ -19,9 +19,13 @@ def default_reducer(url, data): snd_url = url + quote(data, "/:%@&=?") return web.getURLContent(snd_url) + +def ycc_reducer(url, data): + snd_url = url + quote(data, "/:%@&=?") + return "http://ycc.fr/%s" % web.getURLContent(snd_url) + def framalink_reducer(url, data): - json_data = json.loads(web.getURLContent(url, "lsturl=" - + quote(data), + json_data = json.loads(web.getURLContent(url, "lsturl=" + quote(data), header={"Content-Type": "application/x-www-form-urlencoded"})) if 'short' in json_data: return json_data['short'] @@ -34,7 +38,7 @@ def framalink_reducer(url, data): PROVIDERS = { "tinyurl": (default_reducer, "http://tinyurl.com/api-create.php?url="), - "ycc": (default_reducer, "http://ycc.fr/redirection/create/"), + "ycc": (ycc_reducer, "http://ycc.fr/redirection/create/"), "framalink": (framalink_reducer, "https://frama.link/a?format=json") } DEFAULT_PROVIDER = "framalink" @@ -43,6 +47,7 @@ PROVIDERS_NETLOC = [urlparse(web.getNormalizedURL(url), "http").netloc for f, ur # LOADING ############################################################# + def load(context): global DEFAULT_PROVIDER @@ -62,6 +67,7 @@ def reduce(url, provider=DEFAULT_PROVIDER): """ return PROVIDERS[provider][0](PROVIDERS[provider][1], url) + def gen_response(res, msg, srv): if res is None: raise IRCException("bad URL : %s" % srv) @@ -90,7 +96,8 @@ def parseresponse(msg): o = urlparse(web._getNormalizedURL(url), "http") # Skip short URLs - if o.netloc == "" or o.netloc in PROVIDERS or len(o.netloc) + len(o.path) < 17: + if (o.netloc == "" or o.netloc in PROVIDERS or + len(o.netloc) + len(o.path) < 17): continue for recv in msg.receivers: From c6e1e9acb2ce7b9e067ac2fac51b3f2a71fc6a8c Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 26 Oct 2015 23:01:44 +0100 Subject: [PATCH 027/271] [framalink] Update regex, clean up code --- modules/framalink.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/framalink.py b/modules/framalink.py index e4cd944..0653129 100644 --- a/modules/framalink.py +++ b/modules/framalink.py @@ -21,8 +21,7 @@ def default_reducer(url, data): def ycc_reducer(url, data): - snd_url = url + quote(data, "/:%@&=?") - return "http://ycc.fr/%s" % web.getURLContent(snd_url) + return "http://ycc.fr/%s" % default_reducer(url, data) def framalink_reducer(url, data): json_data = json.loads(web.getURLContent(url, "lsturl=" + quote(data), @@ -91,7 +90,7 @@ def parselisten(msg): def parseresponse(msg): global LAST_URLS if hasattr(msg, "text") and msg.text: - urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?[^ :]+)", msg.text) + urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?(?:[^ :/]+:[0-9]+)?[^ :]+)", msg.text) for url in urls: o = urlparse(web._getNormalizedURL(url), "http") @@ -112,7 +111,8 @@ def parseresponse(msg): @hook("cmd_hook", "framalink", help="Reduce any given URL", help_usage={None: "Reduce the last URL said on the channel", - "[@provider=framalink] URL [URL ...]": "Reduce the given URL(s) using thespecified shortner"}) + "[@provider=framalink] URL [URL ...]": "Reduce the given " + "URL(s) using the specified shortner"}) def cmd_reduceurl(msg): minify = list() @@ -124,7 +124,7 @@ def cmd_reduceurl(msg): raise IRCException("I have no more URL to reduce.") if len(msg.args) > 4: - raise IRCException("I cannot reduce as much URL at once.") + raise IRCException("I cannot reduce that maby URLs at once.") else: minify += msg.args From f496c31d1cf938bebfa47b30cf35c7c9472b681c Mon Sep 17 00:00:00 2001 From: nemunaire Date: Thu, 29 Oct 2015 22:43:37 +0100 Subject: [PATCH 028/271] Help: don't append space character before ':' when the usage key is None --- nemubot/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nemubot/bot.py b/nemubot/bot.py index 562a099..2fcce60 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -102,7 +102,7 @@ class Bot(threading.Thread): for (s, h) in self.modules[module].__nemubot_context__.hooks: if s == "in_Command" and (h.name is not None or h.regexp is not None) and h.is_matching(msg.args[0][1:]): if h.help_usage: - return res.append_message(["\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], title="Usage for command %s from module %s" % (msg.args[0], module)) + return res.append_message(["\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], title="Usage for command %s from module %s" % (msg.args[0], module)) elif h.help: return res.append_message("Command %s from module %s: %s" % (msg.args[0], module, h.help)) else: From 9935e038fc88140ed34a7888b5da753f8dd4ce99 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Fri, 30 Oct 2015 00:22:52 +0100 Subject: [PATCH 029/271] [man] num variable wasn't used here --- modules/man.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/modules/man.py b/modules/man.py index 31ed9f2..7e7b715 100644 --- a/modules/man.py +++ b/modules/man.py @@ -65,10 +65,6 @@ def cmd_whatis(msg): res.append_message(" ".join(line.decode().split())) if len(res.messages) <= 0: - if num is not None: - res.append_message("There is no entry %s in section %d." % - (msg.args[0], num)) - else: - res.append_message("There is no man page for %s." % msg.args[0]) + res.append_message("There is no man page for %s." % msg.args[0]) return res From ac33ceb579b10700450b3f40728053030d49c96b Mon Sep 17 00:00:00 2001 From: nemunaire Date: Fri, 30 Oct 2015 21:10:06 +0100 Subject: [PATCH 030/271] Remove dead or useless code --- bin/nemubot | 1 - modules/alias.py | 3 +-- modules/cmd_server.py | 2 -- modules/ddg.py | 2 +- modules/github.py | 6 +++--- modules/jsonbot.py | 2 -- modules/mediawiki.py | 1 - modules/more.py | 11 ++++++----- modules/networking/watchWebsite.py | 1 - modules/networking/whois.py | 1 - modules/rnd.py | 2 +- modules/sap.py | 2 -- modules/weather.py | 1 - nemubot/__init__.py | 2 -- nemubot/__main__.py | 2 -- nemubot/bot.py | 4 +--- nemubot/consumer.py | 2 -- nemubot/event/__init__.py | 2 -- nemubot/hooks/__init__.py | 5 ++--- nemubot/hooks/manager.py | 2 -- nemubot/importer.py | 2 -- nemubot/message/printer/IRC.py | 2 -- nemubot/message/printer/__init__.py | 2 -- nemubot/message/printer/socket.py | 2 -- nemubot/message/visitor.py | 2 -- nemubot/prompt/__init__.py | 2 -- nemubot/prompt/builtins.py | 2 -- nemubot/prompt/error.py | 2 -- nemubot/server/DCC.py | 2 -- nemubot/server/IRC.py | 2 -- nemubot/server/__init__.py | 2 -- nemubot/server/abstract.py | 2 -- nemubot/server/factory_test.py | 2 -- nemubot/server/message/IRC.py | 2 -- nemubot/server/message/abstract.py | 2 -- nemubot/server/socket.py | 2 -- nemubot/tools/__init__.py | 2 -- nemubot/tools/countdown.py | 2 -- nemubot/tools/date.py | 2 -- nemubot/tools/feed.py | 1 - nemubot/tools/human.py | 2 -- 41 files changed, 15 insertions(+), 80 deletions(-) diff --git a/bin/nemubot b/bin/nemubot index 97746f1..5cc8bd5 100755 --- a/bin/nemubot +++ b/bin/nemubot @@ -1,5 +1,4 @@ #!/usr/bin/env python3.3 -# -*- coding: utf-8 -*- # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier diff --git a/modules/alias.py b/modules/alias.py index 8d67000..d960610 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -3,7 +3,6 @@ # PYTHON STUFFS ####################################################### import re -import sys from datetime import datetime, timezone import shlex @@ -147,7 +146,7 @@ def replace_variables(cnts, msg=None): resultCnt.append(cnt) for u in sorted(set(unsetCnt), reverse=True): - k = msg.args.pop(u) + msg.args.pop(u) return resultCnt diff --git a/modules/cmd_server.py b/modules/cmd_server.py index 8fdadb5..6580c18 100644 --- a/modules/cmd_server.py +++ b/modules/cmd_server.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier # diff --git a/modules/ddg.py b/modules/ddg.py index e11d501..7e9e918 100644 --- a/modules/ddg.py +++ b/modules/ddg.py @@ -128,7 +128,7 @@ def search(msg): res.append_message(s.redirect) res.append_message(s.answer) res.append_message(s.abstract) - res.append_message([res for res in s.result]) + res.append_message([r for r in s.result]) for rt in s.relatedTopics: res.append_message(rt) diff --git a/modules/github.py b/modules/github.py index b8aa9d2..cb10008 100644 --- a/modules/github.py +++ b/modules/github.py @@ -94,7 +94,7 @@ def cmd_github(msg): @hook("cmd_hook", "github_user") -def cmd_github(msg): +def cmd_github_user(msg): if not len(msg.args): raise IRCException("indicate a user name to search") @@ -127,7 +127,7 @@ def cmd_github(msg): @hook("cmd_hook", "github_issue") -def cmd_github(msg): +def cmd_github_issue(msg): if not len(msg.args): raise IRCException("indicate a repository to view its issues") @@ -165,7 +165,7 @@ def cmd_github(msg): @hook("cmd_hook", "github_commit") -def cmd_github(msg): +def cmd_github_commit(msg): if not len(msg.args): raise IRCException("indicate a repository to view its commits") diff --git a/modules/jsonbot.py b/modules/jsonbot.py index 9061d29..48a61af 100644 --- a/modules/jsonbot.py +++ b/modules/jsonbot.py @@ -1,5 +1,3 @@ -from bs4 import BeautifulSoup - from nemubot.hooks import hook from nemubot.exception import IRCException from nemubot.tools import web diff --git a/modules/mediawiki.py b/modules/mediawiki.py index 51f65e4..cb1187c 100644 --- a/modules/mediawiki.py +++ b/modules/mediawiki.py @@ -2,7 +2,6 @@ """Use MediaWiki API to get pages""" -import json import re import urllib.parse diff --git a/modules/more.py b/modules/more.py index c5b5e49..bab32a5 100644 --- a/modules/more.py +++ b/modules/more.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier # @@ -18,17 +16,18 @@ """Progressive display of very long messages""" +# PYTHON STUFFS ####################################################### + import logging -import sys from nemubot.message import Text, DirectAsk from nemubot.hooks import hook -nemubotversion = 3.4 - logger = logging.getLogger("nemubot.response") +# MODULE CORE ######################################################### + class Response: def __init__(self, message=None, channel=None, nick=None, server=None, @@ -237,6 +236,8 @@ class Response: SERVERS = dict() +# MODULE INTERFACE #################################################### + @hook("all_post") def parseresponse(res): # TODO: handle inter-bot communication NOMORE diff --git a/modules/networking/watchWebsite.py b/modules/networking/watchWebsite.py index 41ea7d3..042751c 100644 --- a/modules/networking/watchWebsite.py +++ b/modules/networking/watchWebsite.py @@ -7,7 +7,6 @@ from urllib.parse import urlparse from nemubot.event import ModuleEvent from nemubot.exception import IRCException -from nemubot.hooks import hook from nemubot.tools.web import getNormalizedURL from nemubot.tools.xmlparser.node import ModuleState diff --git a/modules/networking/whois.py b/modules/networking/whois.py index 7e6b04b..d7f5201 100644 --- a/modules/networking/whois.py +++ b/modules/networking/whois.py @@ -87,7 +87,6 @@ def cmd_whois(msg): js = getJSON(URL_WHOIS % urllib.parse.quote(dom)) if "ErrorMessage" in js: - err = js["ErrorMessage"] raise IRCException(js["ErrorMessage"]["msg"]) whois = js["WhoisRecord"] diff --git a/modules/rnd.py b/modules/rnd.py index 84c5693..d81bd86 100644 --- a/modules/rnd.py +++ b/modules/rnd.py @@ -26,7 +26,7 @@ def cmd_choice(msg): @hook("cmd_hook", "choicecmd") -def cmd_choice(msg): +def cmd_choicecmd(msg): if not len(msg.args): raise IRCException("indicate some command to pick!") diff --git a/modules/sap.py b/modules/sap.py index 19b0f67..affa3d9 100644 --- a/modules/sap.py +++ b/modules/sap.py @@ -2,7 +2,6 @@ """Find information about an SAP transaction codes""" -import re import urllib.parse import urllib.request from bs4 import BeautifulSoup @@ -10,7 +9,6 @@ from bs4 import BeautifulSoup from nemubot.exception import IRCException from nemubot.hooks import hook from nemubot.tools import web -from nemubot.tools.web import striphtml nemubotversion = 4.0 diff --git a/modules/weather.py b/modules/weather.py index 7a60575..d6bda79 100644 --- a/modules/weather.py +++ b/modules/weather.py @@ -4,7 +4,6 @@ import datetime import re -from urllib.parse import quote from nemubot import context from nemubot.exception import IRCException diff --git a/nemubot/__init__.py b/nemubot/__init__.py index 84403e0..044d993 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier # diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 992c3ad..1809bee 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier # diff --git a/nemubot/bot.py b/nemubot/bot.py index 2fcce60..1dbedcd 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier # @@ -16,7 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from datetime import datetime, timedelta, timezone +from datetime import datetime, timezone import logging import threading import sys diff --git a/nemubot/consumer.py b/nemubot/consumer.py index 9c9d90d..886c4cf 100644 --- a/nemubot/consumer.py +++ b/nemubot/consumer.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier # diff --git a/nemubot/event/__init__.py b/nemubot/event/__init__.py index 96f226a..7b2adfd 100644 --- a/nemubot/event/__init__.py +++ b/nemubot/event/__init__.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier # diff --git a/nemubot/hooks/__init__.py b/nemubot/hooks/__init__.py index 15af034..09c77d2 100644 --- a/nemubot/hooks/__init__.py +++ b/nemubot/hooks/__init__.py @@ -14,7 +14,6 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from nemubot.hooks.abstract import Abstract from nemubot.hooks.message import Message last_registered = [] @@ -29,12 +28,12 @@ def hook(store, *args, **kargs): def reload(): - global Abstract, Message + global Message import imp import nemubot.hooks.abstract imp.reload(nemubot.hooks.abstract) - Abstract = nemubot.hooks.abstract.Abstract + import nemubot.hooks.message imp.reload(nemubot.hooks.message) Message = nemubot.hooks.message.Message diff --git a/nemubot/hooks/manager.py b/nemubot/hooks/manager.py index 200091e..8859d19 100644 --- a/nemubot/hooks/manager.py +++ b/nemubot/hooks/manager.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier # diff --git a/nemubot/importer.py b/nemubot/importer.py index 6769ea9..eaf1535 100644 --- a/nemubot/importer.py +++ b/nemubot/importer.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier # diff --git a/nemubot/message/printer/IRC.py b/nemubot/message/printer/IRC.py index d9a1ffc..b874003 100644 --- a/nemubot/message/printer/IRC.py +++ b/nemubot/message/printer/IRC.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier # diff --git a/nemubot/message/printer/__init__.py b/nemubot/message/printer/__init__.py index f906b35..ae6b4df 100644 --- a/nemubot/message/printer/__init__.py +++ b/nemubot/message/printer/__init__.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier # diff --git a/nemubot/message/printer/socket.py b/nemubot/message/printer/socket.py index 2df7d5e..0d6276a 100644 --- a/nemubot/message/printer/socket.py +++ b/nemubot/message/printer/socket.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier # diff --git a/nemubot/message/visitor.py b/nemubot/message/visitor.py index a9630c1..454633a 100644 --- a/nemubot/message/visitor.py +++ b/nemubot/message/visitor.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier # diff --git a/nemubot/prompt/__init__.py b/nemubot/prompt/__init__.py index c491d99..27f7919 100644 --- a/nemubot/prompt/__init__.py +++ b/nemubot/prompt/__init__.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier # diff --git a/nemubot/prompt/builtins.py b/nemubot/prompt/builtins.py index 233345e..a020fb9 100644 --- a/nemubot/prompt/builtins.py +++ b/nemubot/prompt/builtins.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier # diff --git a/nemubot/prompt/error.py b/nemubot/prompt/error.py index 3d426d6..f86b5a1 100644 --- a/nemubot/prompt/error.py +++ b/nemubot/prompt/error.py @@ -1,5 +1,3 @@ -# coding=utf-8 - # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier # diff --git a/nemubot/server/DCC.py b/nemubot/server/DCC.py index 6b8d8c0..6655d52 100644 --- a/nemubot/server/DCC.py +++ b/nemubot/server/DCC.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier # diff --git a/nemubot/server/IRC.py b/nemubot/server/IRC.py index 8dff0f8..9da3235 100644 --- a/nemubot/server/IRC.py +++ b/nemubot/server/IRC.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier # diff --git a/nemubot/server/__init__.py b/nemubot/server/__init__.py index 6bb002d..1f68d74 100644 --- a/nemubot/server/__init__.py +++ b/nemubot/server/__init__.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier # diff --git a/nemubot/server/abstract.py b/nemubot/server/abstract.py index 99d10d5..ebcb427 100644 --- a/nemubot/server/abstract.py +++ b/nemubot/server/abstract.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier # diff --git a/nemubot/server/factory_test.py b/nemubot/server/factory_test.py index 1296414..cc7d35b 100644 --- a/nemubot/server/factory_test.py +++ b/nemubot/server/factory_test.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier # diff --git a/nemubot/server/message/IRC.py b/nemubot/server/message/IRC.py index 6249716..9f69a8c 100644 --- a/nemubot/server/message/IRC.py +++ b/nemubot/server/message/IRC.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier # diff --git a/nemubot/server/message/abstract.py b/nemubot/server/message/abstract.py index 03e10cd..aa3b136 100644 --- a/nemubot/server/message/abstract.py +++ b/nemubot/server/message/abstract.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier # diff --git a/nemubot/server/socket.py b/nemubot/server/socket.py index 052579b..b6c00d4 100644 --- a/nemubot/server/socket.py +++ b/nemubot/server/socket.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier # diff --git a/nemubot/tools/__init__.py b/nemubot/tools/__init__.py index 95be66a..9043466 100644 --- a/nemubot/tools/__init__.py +++ b/nemubot/tools/__init__.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier # diff --git a/nemubot/tools/countdown.py b/nemubot/tools/countdown.py index 58bdc55..afd585f 100644 --- a/nemubot/tools/countdown.py +++ b/nemubot/tools/countdown.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier # diff --git a/nemubot/tools/date.py b/nemubot/tools/date.py index da46756..9c14384 100644 --- a/nemubot/tools/date.py +++ b/nemubot/tools/date.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier # diff --git a/nemubot/tools/feed.py b/nemubot/tools/feed.py index 0e1f313..c3f402a 100644 --- a/nemubot/tools/feed.py +++ b/nemubot/tools/feed.py @@ -1,5 +1,4 @@ #!/usr/bin/python3 -# coding=utf-8 import datetime import time diff --git a/nemubot/tools/human.py b/nemubot/tools/human.py index 588ac1f..a18cde2 100644 --- a/nemubot/tools/human.py +++ b/nemubot/tools/human.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier # From 8b4f08c5bdfe8780ede7661813de12e935d2234c Mon Sep 17 00:00:00 2001 From: nemunaire Date: Fri, 30 Oct 2015 21:57:45 +0100 Subject: [PATCH 031/271] Replace IRCException by IMException, as nemubot is not only built for IRC --- modules/alias.py | 10 +++---- modules/birthday.py | 4 +-- modules/books.py | 12 ++++----- modules/conjugaison.py | 10 +++---- modules/ddg.py | 10 +++---- modules/events.py | 27 ++++++++----------- modules/framalink.py | 12 ++++----- modules/github.py | 16 +++++------ modules/imdb.py | 14 +++++----- modules/jsonbot.py | 8 +++--- modules/mapquest.py | 4 +-- modules/mediawiki.py | 10 +++---- modules/networking/__init__.py | 22 +++++++-------- modules/networking/page.py | 20 +++++++------- modules/networking/w3c.py | 8 +++--- modules/networking/watchWebsite.py | 10 +++---- modules/networking/whois.py | 6 ++--- modules/news.py | 4 +-- modules/nextstop/__init__.py | 10 +++---- modules/reddit.py | 6 ++--- modules/rnd.py | 6 ++--- modules/sap.py | 4 +-- modules/sms.py | 8 +++--- modules/spell/__init__.py | 6 ++--- modules/suivi.py | 6 ++--- modules/syno.py | 12 ++++----- modules/tpb.py | 4 +-- modules/translate.py | 8 +++--- modules/urbandict.py | 4 +-- modules/velib.py | 10 +++---- modules/weather.py | 12 ++++----- modules/whois.py | 6 ++--- modules/wolframalpha.py | 6 ++--- modules/worldcup.py | 14 +++++----- modules/youtube-title.py | 8 +++--- .../{exception.py => exception/__init__.py} | 13 +++++---- nemubot/hooks/abstract.py | 4 +-- nemubot/tools/feed.py | 4 +-- nemubot/tools/web.py | 12 ++++----- setup.py | 1 + 40 files changed, 183 insertions(+), 188 deletions(-) rename nemubot/{exception.py => exception/__init__.py} (83%) diff --git a/modules/alias.py b/modules/alias.py index d960610..871424b 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -7,7 +7,7 @@ from datetime import datetime, timezone import shlex from nemubot import context -from nemubot.exception import IRCException +from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.message import Command from nemubot.tools.xmlparser.node import ModuleState @@ -183,7 +183,7 @@ def cmd_listvars(msg): help_usage={"KEY VALUE": "Define the variable named KEY and fill it with VALUE as content"}) def cmd_set(msg): if len(msg.args) < 2: - raise IRCException("!set take two args: the key and the value.") + raise IMException("!set take two args: the key and the value.") set_variable(msg.args[0], " ".join(msg.args[1:]), msg.nick) return Response("Variable $%s successfully defined." % msg.args[0], channel=msg.channel) @@ -209,7 +209,7 @@ def cmd_listalias(msg): help="Display the replacement command for a given alias") def cmd_alias(msg): if not len(msg.args): - raise IRCException("!alias takes as argument an alias to extend.") + raise IMException("!alias takes as argument an alias to extend.") res = list() for alias in msg.args: if alias[0] == "!": @@ -225,7 +225,7 @@ def cmd_alias(msg): help="Remove a previously created alias") def cmd_unalias(msg): if not len(msg.args): - raise IRCException("Which alias would you want to remove?") + raise IMException("Which alias would you want to remove?") res = list() for alias in msg.args: if alias[0] == "!" and len(alias) > 1: @@ -268,7 +268,7 @@ def parseask(msg): if re.match(".*(register|set|cr[ée]{2}|new|nouvel(le)?) alias.*", msg.text) is not None: result = re.match(".*alias !?([^ ]+) ?(pour|for|=|:) ?(.+)$", msg.text) if result.group(1) in context.data.getNode("aliases").index: - raise IRCException("this alias is already defined.") + raise IMException("this alias is already defined.") else: create_alias(result.group(1), result.group(3), diff --git a/modules/birthday.py b/modules/birthday.py index 34d2c28..f0870ec 100644 --- a/modules/birthday.py +++ b/modules/birthday.py @@ -7,7 +7,7 @@ import sys from datetime import date, datetime from nemubot import context -from nemubot.exception import IRCException +from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools.countdown import countdown_format from nemubot.tools.date import extractDate @@ -131,4 +131,4 @@ def parseask(msg): msg.channel, msg.nick) except: - raise IRCException("la date de naissance ne paraît pas valide.") + raise IMException("la date de naissance ne paraît pas valide.") diff --git a/modules/books.py b/modules/books.py index 4a4d5aa..a5ea1b3 100644 --- a/modules/books.py +++ b/modules/books.py @@ -5,7 +5,7 @@ import urllib from nemubot import context -from nemubot.exception import IRCException +from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web @@ -65,11 +65,11 @@ def search_author(name): }) def cmd_book(msg): if not len(msg.args): - raise IRCException("please give me a title to search") + raise IMException("please give me a title to search") book = get_book(" ".join(msg.args)) if book is None: - raise IRCException("unable to find book named like this") + raise IMException("unable to find book named like this") res = Response(channel=msg.channel) res.append_message("%s, written by %s: %s" % (book.getElementsByTagName("title")[0].firstChild.nodeValue, book.getElementsByTagName("author")[0].getElementsByTagName("name")[0].firstChild.nodeValue, @@ -84,7 +84,7 @@ def cmd_book(msg): }) def cmd_books(msg): if not len(msg.args): - raise IRCException("please give me a title to search") + raise IMException("please give me a title to search") title = " ".join(msg.args) res = Response(channel=msg.channel, @@ -104,12 +104,12 @@ def cmd_books(msg): }) def cmd_author(msg): if not len(msg.args): - raise IRCException("please give me an author to search") + raise IMException("please give me an author to search") name = " ".join(msg.args) ath = search_author(name) if ath is None: - raise IRCException("%s does not appear to be a published author." % name) + raise IMException("%s does not appear to be a published author." % name) return Response([b.getElementsByTagName("title")[0].firstChild.nodeValue for b in ath.getElementsByTagName("book")], channel=msg.channel, title=ath.getElementsByTagName("name")[0].firstChild.nodeValue) diff --git a/modules/conjugaison.py b/modules/conjugaison.py index fdde315..d4405e2 100644 --- a/modules/conjugaison.py +++ b/modules/conjugaison.py @@ -6,7 +6,7 @@ from collections import defaultdict import re from urllib.parse import quote -from nemubot.exception import IRCException +from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web from nemubot.tools.web import striphtml @@ -51,10 +51,10 @@ def compute_line(line, stringTens): try: idTemps = d[stringTens] except: - raise IRCException("le temps demandé n'existe pas") + raise IMException("le temps demandé n'existe pas") if len(idTemps) == 0: - raise IRCException("le temps demandé n'existe pas") + raise IMException("le temps demandé n'existe pas") index = line.index('
0: strnd["end"] = msg.date @@ -144,7 +139,7 @@ def start_countdown(msg): @hook("cmd_hook", "forceend") def end_countdown(msg): if len(msg.args) < 1: - raise IRCException("quel événement terminer ?") + raise IMException("quel événement terminer ?") if msg.args[0] in context.data.index: if context.data.index[msg.args[0]]["proprio"] == msg.nick or (msg.cmd == "forceend" and msg.frm_owner): @@ -155,7 +150,7 @@ def end_countdown(msg): return Response("%s a duré %s." % (msg.args[0], duration), channel=msg.channel, nick=msg.nick) else: - raise IRCException("Vous ne pouvez pas terminer le compteur %s, créé par %s." % (msg.args[0], context.data.index[msg.args[0]]["proprio"])) + raise IMException("Vous ne pouvez pas terminer le compteur %s, créé par %s." % (msg.args[0], context.data.index[msg.args[0]]["proprio"])) else: return Response("%s n'est pas un compteur connu."% (msg.args[0]), channel=msg.channel, nick=msg.nick) @@ -199,15 +194,15 @@ def parseask(msg): if RGXP_ask.match(msg.text) is not None: name = re.match("^.*!([^ \"'@!]+).*$", msg.text) if name is None: - raise IRCException("il faut que tu attribues une commande à l'événement.") + raise IMException("il faut que tu attribues une commande à l'événement.") if name.group(1) in context.data.index: - raise IRCException("un événement portant ce nom existe déjà.") + raise IMException("un événement portant ce nom existe déjà.") texts = re.match("^[^\"]*(avant|après|apres|before|after)?[^\"]*\"([^\"]+)\"[^\"]*((avant|après|apres|before|after)?.*\"([^\"]+)\".*)?$", msg.text, re.I) if texts is not None and texts.group(3) is not None: extDate = extractDate(msg.text) if extDate is None or extDate == "": - raise IRCException("la date de l'événement est invalide !") + raise IMException("la date de l'événement est invalide !") if texts.group(1) is not None and (texts.group(1) == "après" or texts.group(1) == "apres" or texts.group(1) == "after"): msg_after = texts.group (2) @@ -217,7 +212,7 @@ def parseask(msg): msg_after = texts.group (5) if msg_before.find("%s") == -1 or msg_after.find("%s") == -1: - raise IRCException("Pour que l'événement soit valide, ajouter %s à" + raise IMException("Pour que l'événement soit valide, ajouter %s à" " l'endroit où vous voulez que soit ajouté le" " compte à rebours.") @@ -247,4 +242,4 @@ def parseask(msg): channel=msg.channel) else: - raise IRCException("Veuillez indiquez les messages d'attente et d'après événement entre guillemets.") + raise IMException("Veuillez indiquez les messages d'attente et d'après événement entre guillemets.") diff --git a/modules/framalink.py b/modules/framalink.py index 0653129..9e2af2f 100644 --- a/modules/framalink.py +++ b/modules/framalink.py @@ -7,7 +7,7 @@ import json from urllib.parse import urlparse from urllib.parse import quote -from nemubot.exception import IRCException +from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.message import Text from nemubot.tools import web @@ -29,9 +29,9 @@ def framalink_reducer(url, data): if 'short' in json_data: return json_data['short'] elif 'msg' in json_data: - raise IRCException("Error: %s" % json_data['msg']) + raise IMException("Error: %s" % json_data['msg']) else: - IRCException("An error occured while shortening %s." % data) + IMException("An error occured while shortening %s." % data) # MODULE VARIABLES #################################################### @@ -69,7 +69,7 @@ def reduce(url, provider=DEFAULT_PROVIDER): def gen_response(res, msg, srv): if res is None: - raise IRCException("bad URL : %s" % srv) + raise IMException("bad URL : %s" % srv) else: return Text("URL for %s: %s" % (srv, res), server=None, to=msg.to_response) @@ -121,10 +121,10 @@ def cmd_reduceurl(msg): if msg.channel in LAST_URLS and len(LAST_URLS[msg.channel]) > 0: minify.append(LAST_URLS[msg.channel].pop()) else: - raise IRCException("I have no more URL to reduce.") + raise IMException("I have no more URL to reduce.") if len(msg.args) > 4: - raise IRCException("I cannot reduce that maby URLs at once.") + raise IMException("I cannot reduce that maby URLs at once.") else: minify += msg.args diff --git a/modules/github.py b/modules/github.py index cb10008..19eadf9 100644 --- a/modules/github.py +++ b/modules/github.py @@ -5,7 +5,7 @@ import re from urllib.parse import quote -from nemubot.exception import IRCException +from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web @@ -68,7 +68,7 @@ def info_commit(repo, commit=None): @hook("cmd_hook", "github") def cmd_github(msg): if not len(msg.args): - raise IRCException("indicate a repository name to search") + raise IMException("indicate a repository name to search") repos = info_repos(" ".join(msg.args)) @@ -96,7 +96,7 @@ def cmd_github(msg): @hook("cmd_hook", "github_user") def cmd_github_user(msg): if not len(msg.args): - raise IRCException("indicate a user name to search") + raise IMException("indicate a user name to search") res = Response(channel=msg.channel, nomore="No more user") @@ -121,7 +121,7 @@ def cmd_github_user(msg): user["html_url"], kf)) else: - raise IRCException("User not found") + raise IMException("User not found") return res @@ -129,7 +129,7 @@ def cmd_github_user(msg): @hook("cmd_hook", "github_issue") def cmd_github_issue(msg): if not len(msg.args): - raise IRCException("indicate a repository to view its issues") + raise IMException("indicate a repository to view its issues") issue = None @@ -150,7 +150,7 @@ def cmd_github_issue(msg): issues = info_issue(repo, issue) if issues is None: - raise IRCException("Repository not found") + raise IMException("Repository not found") for issue in issues: res.append_message("%s%s issue #%d: \x03\x02%s\x03\x02 opened by %s on %s: %s" % @@ -167,7 +167,7 @@ def cmd_github_issue(msg): @hook("cmd_hook", "github_commit") def cmd_github_commit(msg): if not len(msg.args): - raise IRCException("indicate a repository to view its commits") + raise IMException("indicate a repository to view its commits") commit = None if re.match("^[a-fA-F0-9]+$", msg.args[0]): @@ -185,7 +185,7 @@ def cmd_github_commit(msg): commits = info_commit(repo, commit) if commits is None: - raise IRCException("Repository not found") + raise IMException("Repository not found") for commit in commits: res.append_message("Commit %s by %s on %s: %s" % diff --git a/modules/imdb.py b/modules/imdb.py index 49c4cc9..adea1d8 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -5,7 +5,7 @@ import re import urllib.parse -from nemubot.exception import IRCException +from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web @@ -39,13 +39,13 @@ def get_movie(title=None, year=None, imdbid=None, fullplot=True, tomatoes=False) # Return data if "Error" in data: - raise IRCException(data["Error"]) + raise IMException(data["Error"]) elif "Response" in data and data["Response"] == "True": return data else: - raise IRCException("An error occurs during movie search") + raise IMException("An error occurs during movie search") def find_movies(title): @@ -59,20 +59,20 @@ def find_movies(title): # Return data if "Error" in data: - raise IRCException(data["Error"]) + raise IMException(data["Error"]) elif "Search" in data: return data else: - raise IRCException("An error occurs during movie search") + raise IMException("An error occurs during movie search") @hook("cmd_hook", "imdb") def cmd_imdb(msg): """View movie details with !imdb """ if not len(msg.args): - raise IRCException("precise a movie/serie title!") + raise IMException("precise a movie/serie title!") title = ' '.join(msg.args) @@ -101,7 +101,7 @@ def cmd_imdb(msg): def cmd_search(msg): """!imdbs <approximative title> to search a movie title""" if not len(msg.args): - raise IRCException("precise a movie/serie title!") + raise IMException("precise a movie/serie title!") data = find_movies(' '.join(msg.args)) diff --git a/modules/jsonbot.py b/modules/jsonbot.py index 48a61af..c69cca2 100644 --- a/modules/jsonbot.py +++ b/modules/jsonbot.py @@ -1,5 +1,5 @@ from nemubot.hooks import hook -from nemubot.exception import IRCException +from nemubot.exception import IMException from nemubot.tools import web from more import Response import json @@ -42,15 +42,15 @@ def getJsonKeys(data): @hook("cmd_hook", "json") def get_json_info(msg): if not len(msg.args): - raise IRCException("Please specify a url and a list of JSON keys.") + raise IMException("Please specify a url and a list of JSON keys.") request_data = web.getURLContent(msg.args[0].replace(' ', "%20")) if not request_data: - raise IRCException("Please specify a valid url.") + raise IMException("Please specify a valid url.") json_data = json.loads(request_data) if len(msg.args) == 1: - raise IRCException("Please specify the keys to return (%s)" % ", ".join(getJsonKeys(json_data))) + raise IMException("Please specify the keys to return (%s)" % ", ".join(getJsonKeys(json_data))) tags = ','.join(msg.args[1:]).split(',') response = getRequestedTags(tags, json_data) diff --git a/modules/mapquest.py b/modules/mapquest.py index f147176..40bd40f 100644 --- a/modules/mapquest.py +++ b/modules/mapquest.py @@ -5,7 +5,7 @@ import re from urllib.parse import quote -from nemubot.exception import IRCException +from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web @@ -46,7 +46,7 @@ def where(loc): @hook("cmd_hook", "geocode") def cmd_geocode(msg): if not len(msg.args): - raise IRCException("indicate a name") + raise IMException("indicate a name") res = Response(channel=msg.channel, nick=msg.nick, nomore="No more geocode", count=" (%s more geocode)") diff --git a/modules/mediawiki.py b/modules/mediawiki.py index cb1187c..d2c4488 100644 --- a/modules/mediawiki.py +++ b/modules/mediawiki.py @@ -5,7 +5,7 @@ import re import urllib.parse -from nemubot.exception import IRCException +from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web @@ -42,7 +42,7 @@ def get_raw_page(site, term, ssl=False): try: return data["query"]["pages"][k]["revisions"][0]["*"] except: - raise IRCException("article not found") + raise IMException("article not found") def get_unwikitextified(site, wikitext, ssl=False): @@ -179,7 +179,7 @@ def mediawiki_response(site, term, receivers): def cmd_mediawiki(msg): """Read an article on a MediaWiki""" if len(msg.args) < 2: - raise IRCException("indicate a domain and a term to search") + raise IMException("indicate a domain and a term to search") return mediawiki_response(msg.args[0], " ".join(msg.args[1:]), @@ -190,7 +190,7 @@ def cmd_mediawiki(msg): def cmd_srchmediawiki(msg): """Search an article on a MediaWiki""" if len(msg.args) < 2: - raise IRCException("indicate a domain and a term to search") + raise IMException("indicate a domain and a term to search") res = Response(channel=msg.receivers, nomore="No more results", count=" (%d more results)") @@ -203,7 +203,7 @@ def cmd_srchmediawiki(msg): @hook("cmd_hook", "wikipedia") def cmd_wikipedia(msg): if len(msg.args) < 2: - raise IRCException("indicate a lang and a term to search") + raise IMException("indicate a lang and a term to search") return mediawiki_response(msg.args[0] + ".wikipedia.org", " ".join(msg.args[1:]), diff --git a/modules/networking/__init__.py b/modules/networking/__init__.py index 9688830..26d6470 100644 --- a/modules/networking/__init__.py +++ b/modules/networking/__init__.py @@ -5,7 +5,7 @@ import logging import re -from nemubot.exception import IRCException +from nemubot.exception import IMException from nemubot.hooks import hook from more import Response @@ -43,13 +43,13 @@ def load(context): help_usage={"URL": "Display the title of the given URL"}) def cmd_title(msg): if not len(msg.args): - raise IRCException("Indicate the URL to visit.") + raise IMException("Indicate the URL to visit.") url = " ".join(msg.args) res = re.search("<title>(.*?)", page.fetch(" ".join(msg.args)), re.DOTALL) if res is None: - raise IRCException("The page %s has no title" % url) + raise IMException("The page %s has no title" % url) else: return Response("%s: %s" % (url, res.group(1).replace("\n", " ")), channel=msg.channel) @@ -59,7 +59,7 @@ def cmd_title(msg): help_usage={"URL": "Display HTTP headers of the given URL"}) def cmd_curly(msg): if not len(msg.args): - raise IRCException("Indicate the URL to visit.") + raise IMException("Indicate the URL to visit.") url = " ".join(msg.args) version, status, reason, headers = page.headers(url) @@ -72,7 +72,7 @@ def cmd_curly(msg): help_usage={"URL": "Display raw HTTP body of the given URL"}) def cmd_curl(msg): if not len(msg.args): - raise IRCException("Indicate the URL to visit.") + raise IMException("Indicate the URL to visit.") res = Response(channel=msg.channel) for m in page.fetch(" ".join(msg.args)).split("\n"): @@ -85,7 +85,7 @@ def cmd_curl(msg): help_usage={"URL": "Display and format HTTP content of the given URL"}) def cmd_w3m(msg): if not len(msg.args): - raise IRCException("Indicate the URL to visit.") + raise IMException("Indicate the URL to visit.") res = Response(channel=msg.channel) for line in page.render(" ".join(msg.args)).split("\n"): res.append_message(line) @@ -97,7 +97,7 @@ def cmd_w3m(msg): help_usage={"URL": "Display redirections steps for the given URL"}) def cmd_traceurl(msg): if not len(msg.args): - raise IRCException("Indicate an URL to trace!") + raise IMException("Indicate an URL to trace!") res = list() for url in msg.args[:4]: @@ -114,7 +114,7 @@ def cmd_traceurl(msg): help_usage={"DOMAIN": "Check if a DOMAIN is up"}) def cmd_isup(msg): if not len(msg.args): - raise IRCException("Indicate an domain name to check!") + raise IMException("Indicate an domain name to check!") res = list() for url in msg.args[:4]: @@ -131,7 +131,7 @@ def cmd_isup(msg): help_usage={"URL": "Do W3C HTML validation on the given URL"}) def cmd_w3c(msg): if not len(msg.args): - raise IRCException("Indicate an URL to validate!") + raise IMException("Indicate an URL to validate!") headers, validator = w3c.validator(msg.args[0]) @@ -157,7 +157,7 @@ def cmd_w3c(msg): help_usage={"URL": "Watch the given domain and alert when it availability status changes"}) def cmd_watch(msg, diffType="diff"): if not len(msg.args): - raise IRCException("indicate an URL to watch!") + raise IMException("indicate an URL to watch!") return watchWebsite.add_site(msg.args[0], msg.frm, msg.channel, msg.server, diffType) @@ -178,7 +178,7 @@ def cmd_listwatch(msg): help_usage={"URL": "Unwatch the given URL"}) def cmd_unwatch(msg): if not len(msg.args): - raise IRCException("which URL should I stop watching?") + raise IMException("which URL should I stop watching?") for arg in msg.args: return watchWebsite.del_site(arg, msg.frm, msg.channel, msg.frm_owner) diff --git a/modules/networking/page.py b/modules/networking/page.py index 6179e34..689944b 100644 --- a/modules/networking/page.py +++ b/modules/networking/page.py @@ -5,7 +5,7 @@ import tempfile import urllib from nemubot import __version__ -from nemubot.exception import IRCException +from nemubot.exception import IMException from nemubot.tools import web @@ -23,7 +23,7 @@ def headers(url): o = urllib.parse.urlparse(web.getNormalizedURL(url), "http") if o.netloc == "": - raise IRCException("invalid URL") + raise IMException("invalid URL") if o.scheme == "http": conn = http.client.HTTPConnection(o.hostname, port=o.port, timeout=5) else: @@ -32,18 +32,18 @@ def headers(url): conn.request("HEAD", o.path, None, {"User-agent": "Nemubot v%s" % __version__}) except ConnectionError as e: - raise IRCException(e.strerror) + raise IMException(e.strerror) except socket.timeout: - raise IRCException("request timeout") + raise IMException("request timeout") except socket.gaierror: print (" Unable to receive page %s from %s on %d." % (o.path, o.hostname, o.port if o.port is not None else 0)) - raise IRCException("an unexpected error occurs") + raise IMException("an unexpected error occurs") try: res = conn.getresponse() except http.client.BadStatusLine: - raise IRCException("An error occurs") + raise IMException("An error occurs") finally: conn.close() @@ -51,7 +51,7 @@ def headers(url): def _onNoneDefault(): - raise IRCException("An error occurs when trying to access the page") + raise IMException("An error occurs when trying to access the page") def fetch(url, onNone=_onNoneDefault): @@ -71,11 +71,11 @@ def fetch(url, onNone=_onNoneDefault): else: return None except ConnectionError as e: - raise IRCException(e.strerror) + raise IMException(e.strerror) except socket.timeout: - raise IRCException("The request timeout when trying to access the page") + raise IMException("The request timeout when trying to access the page") except socket.error as e: - raise IRCException(e.strerror) + raise IMException(e.strerror) def _render(cnt): diff --git a/modules/networking/w3c.py b/modules/networking/w3c.py index 3d920ef..83056dd 100644 --- a/modules/networking/w3c.py +++ b/modules/networking/w3c.py @@ -2,7 +2,7 @@ import json import urllib from nemubot import __version__ -from nemubot.exception import IRCException +from nemubot.exception import IMException from nemubot.tools.web import getNormalizedURL def validator(url): @@ -14,19 +14,19 @@ def validator(url): o = urllib.parse.urlparse(getNormalizedURL(url), "http") if o.netloc == "": - raise IRCException("Indicate a valid URL!") + raise IMException("Indicate a valid URL!") try: req = urllib.request.Request("http://validator.w3.org/check?uri=%s&output=json" % (urllib.parse.quote(o.geturl())), headers={ 'User-Agent' : "Nemubot v%s" % __version__}) raw = urllib.request.urlopen(req, timeout=10) except urllib.error.HTTPError as e: - raise IRCException("HTTP error occurs: %s %s" % (e.code, e.reason)) + raise IMException("HTTP error occurs: %s %s" % (e.code, e.reason)) headers = dict() for Hname, Hval in raw.getheaders(): headers[Hname] = Hval if "X-W3C-Validator-Status" not in headers or (headers["X-W3C-Validator-Status"] != "Valid" and headers["X-W3C-Validator-Status"] != "Invalid"): - raise IRCException("Unexpected error on W3C servers" + (" (" + headers["X-W3C-Validator-Status"] + ")" if "X-W3C-Validator-Status" in headers else "")) + raise IMException("Unexpected error on W3C servers" + (" (" + headers["X-W3C-Validator-Status"] + ")" if "X-W3C-Validator-Status" in headers else "")) return headers, json.loads(raw.read().decode()) diff --git a/modules/networking/watchWebsite.py b/modules/networking/watchWebsite.py index 042751c..4945981 100644 --- a/modules/networking/watchWebsite.py +++ b/modules/networking/watchWebsite.py @@ -6,7 +6,7 @@ import urllib.parse from urllib.parse import urlparse from nemubot.event import ModuleEvent -from nemubot.exception import IRCException +from nemubot.exception import IMException from nemubot.tools.web import getNormalizedURL from nemubot.tools.xmlparser.node import ModuleState @@ -61,7 +61,7 @@ def del_site(url, nick, channel, frm_owner): for a in site.getNodes("alert"): if a["channel"] == channel: # if not (nick == a["nick"] or frm_owner): -# raise IRCException("you cannot unwatch this URL.") +# raise IMException("you cannot unwatch this URL.") site.delChild(a) if not site.hasNode("alert"): del_event(site["_evt_id"]) @@ -69,7 +69,7 @@ def del_site(url, nick, channel, frm_owner): save() return Response("I don't watch this URL anymore.", channel=channel, nick=nick) - raise IRCException("I didn't watch this URL!") + raise IMException("I didn't watch this URL!") def add_site(url, nick, channel, server, diffType="diff"): @@ -81,7 +81,7 @@ def add_site(url, nick, channel, server, diffType="diff"): o = urlparse(getNormalizedURL(url), "http") if o.netloc == "": - raise IRCException("sorry, I can't watch this URL :(") + raise IMException("sorry, I can't watch this URL :(") alert = ModuleState("alert") alert["nick"] = nick @@ -219,5 +219,5 @@ def start_watching(site, offset=0): interval=site.getInt("time"), call=alert_change, call_data=site) site["_evt_id"] = add_event(evt) - except IRCException: + except IMException: logger.exception("Unable to watch %s", site["url"]) diff --git a/modules/networking/whois.py b/modules/networking/whois.py index d7f5201..0b8eb9f 100644 --- a/modules/networking/whois.py +++ b/modules/networking/whois.py @@ -1,7 +1,7 @@ import datetime import urllib -from nemubot.exception import IRCException +from nemubot.exception import IMException from nemubot.tools.web import getJSON from more import Response @@ -80,14 +80,14 @@ def whois_entityformat(entity): def cmd_whois(msg): if not len(msg.args): - raise IRCException("Indiquer un domaine ou une IP à whois !") + raise IMException("Indiquer un domaine ou une IP à whois !") dom = msg.args[0] js = getJSON(URL_WHOIS % urllib.parse.quote(dom)) if "ErrorMessage" in js: - raise IRCException(js["ErrorMessage"]["msg"]) + raise IMException(js["ErrorMessage"]["msg"]) whois = js["WhoisRecord"] diff --git a/modules/news.py b/modules/news.py index 7aa323f..dccc77e 100644 --- a/modules/news.py +++ b/modules/news.py @@ -8,7 +8,7 @@ from urllib.parse import urljoin from bs4 import BeautifulSoup -from nemubot.exception import IRCException +from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web @@ -44,7 +44,7 @@ def get_last_news(url): @hook("cmd_hook", "news") def cmd_news(msg): if not len(msg.args): - raise IRCException("Indicate the URL to visit.") + raise IMException("Indicate the URL to visit.") url = " ".join(msg.args) links = [x for x in find_rss_links(url)] diff --git a/modules/nextstop/__init__.py b/modules/nextstop/__init__.py index 65095b2..9a0e5c7 100644 --- a/modules/nextstop/__init__.py +++ b/modules/nextstop/__init__.py @@ -2,7 +2,7 @@ """Informe les usagers des prochains passages des transports en communs de la RATP""" -from nemubot.exception import IRCException +from nemubot.exception import IMException from nemubot.hooks import hook from more import Response @@ -27,7 +27,7 @@ def ask_ratp(msg): times = ratp.getNextStopsAtStation(transport, line, station) if len(times) == 0: - raise IRCException("la station %s n'existe pas sur le %s ligne %s." % (station, transport, line)) + raise IMException("la station %s n'existe pas sur le %s ligne %s." % (station, transport, line)) (time, direction, stationname) = times[0] return Response(message=["\x03\x02%s\x03\x02 direction %s" % (time, direction) for time, direction, stationname in times], @@ -38,11 +38,11 @@ def ask_ratp(msg): stations = ratp.getAllStations(msg.args[0], msg.args[1]) if len(stations) == 0: - raise IRCException("aucune station trouvée.") + raise IMException("aucune station trouvée.") return Response([s for s in stations], title="Stations", channel=msg.channel) else: - raise IRCException("Mauvais usage, merci de spécifier un type de transport et une ligne, ou de consulter l'aide du module.") + raise IMException("Mauvais usage, merci de spécifier un type de transport et une ligne, ou de consulter l'aide du module.") @hook("cmd_hook", "ratp_alert") def ratp_alert(msg): @@ -52,4 +52,4 @@ def ratp_alert(msg): incidents = ratp.getDisturbance(cause, transport) return Response(incidents, channel=msg.channel, nomore="No more incidents", count=" (%d more incidents)") else: - raise IRCException("Mauvais usage, merci de spécifier un type de transport et un type d'alerte (alerte, manif, travaux), ou de consulter l'aide du module.") + raise IMException("Mauvais usage, merci de spécifier un type de transport et un type d'alerte (alerte, manif, travaux), ou de consulter l'aide du module.") diff --git a/modules/reddit.py b/modules/reddit.py index 4c376d3..74eae41 100644 --- a/modules/reddit.py +++ b/modules/reddit.py @@ -4,7 +4,7 @@ import re -from nemubot.exception import IRCException +from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web @@ -26,7 +26,7 @@ def cmd_subreddit(msg): if msg.channel in LAST_SUBS and len(LAST_SUBS[msg.channel]) > 0: subs = [LAST_SUBS[msg.channel].pop()] else: - raise IRCException("Which subreddit? Need inspiration? " + raise IMException("Which subreddit? Need inspiration? " "type !horny or !bored") else: subs = msg.args @@ -44,7 +44,7 @@ def cmd_subreddit(msg): (where, sub.group(2))) if sbr is None: - raise IRCException("subreddit not found") + raise IMException("subreddit not found") if "title" in sbr["data"]: res = Response(channel=msg.channel, diff --git a/modules/rnd.py b/modules/rnd.py index d81bd86..f1f3721 100644 --- a/modules/rnd.py +++ b/modules/rnd.py @@ -6,7 +6,7 @@ import random import shlex from nemubot import context -from nemubot.exception import IRCException +from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.message import Command @@ -18,7 +18,7 @@ from more import Response @hook("cmd_hook", "choice") def cmd_choice(msg): if not len(msg.args): - raise IRCException("indicate some terms to pick!") + raise IMException("indicate some terms to pick!") return Response(random.choice(msg.args), channel=msg.channel, @@ -28,7 +28,7 @@ def cmd_choice(msg): @hook("cmd_hook", "choicecmd") def cmd_choicecmd(msg): if not len(msg.args): - raise IRCException("indicate some command to pick!") + raise IMException("indicate some command to pick!") choice = shlex.split(random.choice(msg.args)) diff --git a/modules/sap.py b/modules/sap.py index affa3d9..a7d65cf 100644 --- a/modules/sap.py +++ b/modules/sap.py @@ -6,7 +6,7 @@ import urllib.parse import urllib.request from bs4 import BeautifulSoup -from nemubot.exception import IRCException +from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web @@ -22,7 +22,7 @@ def help_full(): @hook("cmd_hook", "tcode") def cmd_tcode(msg): if not len(msg.args): - raise IRCException("indicate a transaction code or " + raise IMException("indicate a transaction code or " "a keyword to search!") url = ("http://www.tcodesearch.com/tcodes/search?q=%s" % diff --git a/modules/sms.py b/modules/sms.py index 91a8623..103a938 100644 --- a/modules/sms.py +++ b/modules/sms.py @@ -10,7 +10,7 @@ import urllib.request import urllib.parse from nemubot import context -from nemubot.exception import IRCException +from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools.xmlparser.node import ModuleState @@ -50,15 +50,15 @@ def send_sms(frm, api_usr, api_key, content): @hook("cmd_hook", "sms") def cmd_sms(msg): if not len(msg.args): - raise IRCException("À qui veux-tu envoyer ce SMS ?") + raise IMException("À qui veux-tu envoyer ce SMS ?") # Check dests cur_epoch = time.mktime(time.localtime()); for u in msg.args[0].split(","): if u not in context.data.index: - raise IRCException("Désolé, je sais pas comment envoyer de SMS à %s." % u) + raise IMException("Désolé, je sais pas comment envoyer de SMS à %s." % u) elif cur_epoch - float(context.data.index[u]["lastuse"]) < 42: - raise IRCException("Un peu de calme, %s a déjà reçu un SMS il n'y a pas si longtemps." % u) + raise IMException("Un peu de calme, %s a déjà reçu un SMS il n'y a pas si longtemps." % u) # Go! fails = list() diff --git a/modules/spell/__init__.py b/modules/spell/__init__.py index af08fde..fe5aadd 100644 --- a/modules/spell/__init__.py +++ b/modules/spell/__init__.py @@ -6,7 +6,7 @@ import re from urllib.parse import quote from nemubot import context -from nemubot.exception import IRCException +from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools.xmlparser.node import ModuleState @@ -26,7 +26,7 @@ def load(context): @hook("cmd_hook", "spell") def cmd_spell(msg): if not len(msg.args): - raise IRCException("indique une orthographe approximative du mot dont tu veux vérifier l'orthographe.") + raise IMException("indique une orthographe approximative du mot dont tu veux vérifier l'orthographe.") lang = "fr" strRes = list() @@ -66,7 +66,7 @@ def cmd_score(msg): res = list() unknown = list() if not len(msg.args): - raise IRCException("De qui veux-tu voir les scores ?") + raise IMException("De qui veux-tu voir les scores ?") for cmd in msg.args: if cmd in context.data.index: res.append(Response("%s: %s" % (cmd, " ; ".join(["%s: %d" % (a, context.data.index[cmd].getInt(a)) for a in context.data.index[cmd].attributes.keys() if a != "name"])), channel=msg.channel)) diff --git a/modules/suivi.py b/modules/suivi.py index bdd9322..c2fd645 100644 --- a/modules/suivi.py +++ b/modules/suivi.py @@ -4,7 +4,7 @@ from bs4 import BeautifulSoup import re from nemubot.hooks import hook -from nemubot.exception import IRCException +from nemubot.exception import IMException from nemubot.tools.web import getURLContent from more import Response @@ -160,7 +160,7 @@ TRACKING_HANDLERS = { "or all of them."}) def get_tracking_info(msg): if not len(msg.args): - raise IRCException("Renseignez un identifiant d'envoi.") + raise IMException("Renseignez un identifiant d'envoi.") res = Response(channel=msg.channel, count=" (%d suivis supplémentaires)") @@ -170,7 +170,7 @@ def get_tracking_info(msg): msg.kwargs['tracker']: TRACKING_HANDLERS[msg.kwargs['tracker']] } else: - raise IRCException("No tracker named \x02{tracker}\x0F, please use" + raise IMException("No tracker named \x02{tracker}\x0F, please use" " one of the following: \x02{trackers}\x0F" .format(tracker=msg.kwargs['tracker'], trackers=', ' diff --git a/modules/syno.py b/modules/syno.py index cb1ec84..10bb764 100644 --- a/modules/syno.py +++ b/modules/syno.py @@ -5,7 +5,7 @@ import re from urllib.parse import quote -from nemubot.exception import IRCException +from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web @@ -76,7 +76,7 @@ lang_binding = { 'fr': get_french_synos } @hook("cmd_hook", "antonymes", data="antonymes") def go(msg, what): if not len(msg.args): - raise IRCException("de quel mot veux-tu connaître la liste des synonymes ?") + raise IMException("de quel mot veux-tu connaître la liste des synonymes ?") # Detect lang if msg.args[0] in lang_binding: @@ -86,7 +86,7 @@ def go(msg, what): func = lang_binding["fr"] word = ' '.join(msg.args) # TODO: depreciate usage without lang - #raise IRCException("language %s is not handled yet." % msg.args[0]) + #raise IMException("language %s is not handled yet." % msg.args[0]) try: best, synos, anton = func(word) @@ -100,7 +100,7 @@ def go(msg, what): if len(synos) > 0: res.append_message(synos) return res else: - raise IRCException("Aucun synonyme de %s n'a été trouvé" % word) + raise IMException("Aucun synonyme de %s n'a été trouvé" % word) elif what == "antonymes": if len(anton) > 0: @@ -108,7 +108,7 @@ def go(msg, what): title="Antonymes de %s" % word) return res else: - raise IRCException("Aucun antonyme de %s n'a été trouvé" % word) + raise IMException("Aucun antonyme de %s n'a été trouvé" % word) else: - raise IRCException("WHAT?!") + raise IMException("WHAT?!") diff --git a/modules/tpb.py b/modules/tpb.py index 2704f77..7d30ee1 100644 --- a/modules/tpb.py +++ b/modules/tpb.py @@ -1,7 +1,7 @@ from datetime import datetime import urllib -from nemubot.exception import IRCException +from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import human from nemubot.tools.web import getJSON @@ -25,7 +25,7 @@ def load(context): @hook("cmd_hook", "tpb") def cmd_tpb(msg): if not len(msg.args): - raise IRCException("indicate an item to search!") + raise IMException("indicate an item to search!") torrents = getJSON(URL_TPBAPI + urllib.parse.quote(" ".join(msg.args))) diff --git a/modules/translate.py b/modules/translate.py index 7452889..911f0ea 100644 --- a/modules/translate.py +++ b/modules/translate.py @@ -5,7 +5,7 @@ import re from urllib.parse import quote -from nemubot.exception import IRCException +from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web @@ -36,11 +36,11 @@ def help_full(): @hook("cmd_hook", "translate") def cmd_translate(msg): if not len(msg.args): - raise IRCException("which word would you translate?") + raise IMException("which word would you translate?") if len(msg.args) > 2 and msg.args[0] in LANG and msg.args[1] in LANG: if msg.args[0] != "en" and msg.args[1] != "en": - raise IRCException("sorry, I can only translate to or from english") + raise IMException("sorry, I can only translate to or from english") langFrom = msg.args[0] langTo = msg.args[1] term = ' '.join(msg.args[2:]) @@ -59,7 +59,7 @@ def cmd_translate(msg): wres = web.getJSON(URL % (langFrom, langTo, quote(term))) if "Error" in wres: - raise IRCException(wres["Note"]) + raise IMException(wres["Note"]) else: res = Response(channel=msg.channel, diff --git a/modules/urbandict.py b/modules/urbandict.py index e7474eb..135d240 100644 --- a/modules/urbandict.py +++ b/modules/urbandict.py @@ -4,7 +4,7 @@ from urllib.parse import quote -from nemubot.exception import IRCException +from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web @@ -23,7 +23,7 @@ def search(terms): @hook("cmd_hook", "urbandictionnary") def udsearch(msg): if not len(msg.args): - raise IRCException("Indicate a term to search") + raise IMException("Indicate a term to search") s = search(msg.args) diff --git a/modules/velib.py b/modules/velib.py index d21fb4a..aad5939 100644 --- a/modules/velib.py +++ b/modules/velib.py @@ -5,7 +5,7 @@ import re from nemubot import context -from nemubot.exception import IRCException +from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web @@ -61,7 +61,7 @@ def print_station_status(msg, station): return Response("À la station %s : %d vélib et %d points d'attache" " disponibles." % (station, available, free), channel=msg.channel) - raise IRCException("station %s inconnue." % station) + raise IMException("station %s inconnue." % station) # MODULE INTERFACE #################################################### @@ -73,9 +73,9 @@ def print_station_status(msg, station): }) def ask_stations(msg): if len(msg.args) > 4: - raise IRCException("demande-moi moins de stations à la fois.") + raise IMException("demande-moi moins de stations à la fois.") elif not len(msg.args): - raise IRCException("pour quelle station ?") + raise IMException("pour quelle station ?") for station in msg.args: if re.match("^[0-9]{4,5}$", station): @@ -84,4 +84,4 @@ def ask_stations(msg): return print_station_status(msg, context.data.index[station]["number"]) else: - raise IRCException("numéro de station invalide.") + raise IMException("numéro de station invalide.") diff --git a/modules/weather.py b/modules/weather.py index d6bda79..1d9cf13 100644 --- a/modules/weather.py +++ b/modules/weather.py @@ -6,7 +6,7 @@ import datetime import re from nemubot import context -from nemubot.exception import IRCException +from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web from nemubot.tools.xmlparser.node import ModuleState @@ -120,10 +120,10 @@ def treat_coord(msg): coords.append(geocode[0]["latLng"]["lng"]) return mapquest.where(geocode[0]), coords, specific - raise IRCException("Je ne sais pas où se trouve %s." % city) + raise IMException("Je ne sais pas où se trouve %s." % city) else: - raise IRCException("indique-moi un nom de ville ou des coordonnées.") + raise IMException("indique-moi un nom de ville ou des coordonnées.") def get_json_weather(coords): @@ -131,7 +131,7 @@ def get_json_weather(coords): # First read flags if wth is None or "darksky-unavailable" in wth["flags"]: - raise IRCException("The given location is supported but a temporary error (such as a radar station being down for maintenace) made data unavailable.") + raise IMException("The given location is supported but a temporary error (such as a radar station being down for maintenace) made data unavailable.") return wth @@ -139,11 +139,11 @@ def get_json_weather(coords): @hook("cmd_hook", "coordinates") def cmd_coordinates(msg): if len(msg.args) < 1: - raise IRCException("indique-moi un nom de ville.") + raise IMException("indique-moi un nom de ville.") j = msg.args[0].lower() if j not in context.data.index: - raise IRCException("%s n'est pas une ville connue" % msg.args[0]) + raise IMException("%s n'est pas une ville connue" % msg.args[0]) coords = context.data.index[j] return Response("Les coordonnées de %s sont %s,%s" % (msg.args[0], coords["lat"], coords["long"]), channel=msg.channel) diff --git a/modules/whois.py b/modules/whois.py index 878d4a2..4c43500 100644 --- a/modules/whois.py +++ b/modules/whois.py @@ -3,7 +3,7 @@ import re from nemubot import context -from nemubot.exception import IRCException +from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools.xmlparser.node import ModuleState @@ -75,7 +75,7 @@ def found_login(login): def cmd_whois(msg): if len(msg.args) < 1: - raise IRCException("Provide a name") + raise IMException("Provide a name") res = Response(channel=msg.channel, count=" (%d more logins)") for srch in msg.args: @@ -90,7 +90,7 @@ def cmd_whois(msg): @hook("cmd_hook", "nicks") def cmd_nicks(msg): if len(msg.args) < 1: - raise IRCException("Provide a login") + raise IMException("Provide a login") nick = found_login(msg.args[0]) if nick is None: nick = msg.args[0] diff --git a/modules/wolframalpha.py b/modules/wolframalpha.py index 7a13200..e8421a3 100644 --- a/modules/wolframalpha.py +++ b/modules/wolframalpha.py @@ -6,7 +6,7 @@ from urllib.parse import quote import re from nemubot import context -from nemubot.exception import IRCException +from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web @@ -100,12 +100,12 @@ class WFAResults: }) def calculate(msg): if not len(msg.args): - raise IRCException("Indicate a calcul to compute") + raise IMException("Indicate a calcul to compute") s = WFAResults(' '.join(msg.args)) if not s.success: - raise IRCException(s.error) + raise IMException(s.error) res = Response(channel=msg.channel, nomore="No more results") diff --git a/modules/worldcup.py b/modules/worldcup.py index 1cd49dc..87a182c 100644 --- a/modules/worldcup.py +++ b/modules/worldcup.py @@ -9,7 +9,7 @@ from urllib.parse import quote from urllib.request import urlopen from nemubot import context -from nemubot.exception import IRCException +from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools.xmlparser.node import ModuleState @@ -36,7 +36,7 @@ def start_watch(msg): w["start"] = datetime.now(timezone.utc) context.data.addChild(w) context.save() - raise IRCException("This channel is now watching world cup events!") + raise IMException("This channel is now watching world cup events!") @hook("cmd_hook", "watch_worldcup") def cmd_watch(msg): @@ -52,18 +52,18 @@ def cmd_watch(msg): if msg.args[0] == "stop" and node is not None: context.data.delChild(node) context.save() - raise IRCException("This channel will not anymore receives world cup events.") + raise IMException("This channel will not anymore receives world cup events.") elif msg.args[0] == "start" and node is None: start_watch(msg) else: - raise IRCException("Use only start or stop as first argument") + raise IMException("Use only start or stop as first argument") else: if node is None: start_watch(msg) else: context.data.delChild(node) context.save() - raise IRCException("This channel will not anymore receives world cup events.") + raise IMException("This channel will not anymore receives world cup events.") def current_match_new_action(match_str, osef): context.add_event(ModuleEvent(func=lambda url: urlopen(url).read().decode(), func_data=API_URL % "matches/current?by_date=DESC", call=current_match_new_action, interval=30)) @@ -170,7 +170,7 @@ def get_matches(url): try: raw = urlopen(url) except: - raise IRCException("requête invalide") + raise IMException("requête invalide") matches = json.loads(raw.read().decode()) for match in matches: @@ -194,7 +194,7 @@ def cmd_worldcup(msg): elif is_int(msg.args[0]): url = int(msg.arg[0]) else: - raise IRCException("unrecognized request; choose between 'today', 'tomorrow', a FIFA country code or a match identifier") + raise IMException("unrecognized request; choose between 'today', 'tomorrow', a FIFA country code or a match identifier") if url is None: url = "matches/current?by_date=ASC" diff --git a/modules/youtube-title.py b/modules/youtube-title.py index 4bf115c..e7da2c8 100644 --- a/modules/youtube-title.py +++ b/modules/youtube-title.py @@ -1,7 +1,7 @@ from urllib.parse import urlparse import re, json, subprocess -from nemubot.exception import IRCException +from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools.web import _getNormalizedURL, getURLContent from more import Response @@ -19,7 +19,7 @@ def _get_ytdl(links): res = [] with subprocess.Popen(cmd, stdout=subprocess.PIPE) as p: if p.wait() > 0: - raise IRCException("Error while retrieving video information.") + raise IMException("Error while retrieving video information.") for line in p.stdout.read().split(b"\n"): localres = '' if not line: @@ -46,7 +46,7 @@ def _get_ytdl(links): localres += ' | ' + info['webpage_url'] res.append(localres) if not res: - raise IRCException("No video information to retrieve about this. Sorry!") + raise IMException("No video information to retrieve about this. Sorry!") return res LAST_URLS = dict() @@ -61,7 +61,7 @@ def get_info_yt(msg): if msg.channel in LAST_URLS and len(LAST_URLS[msg.channel]) > 0: links.append(LAST_URLS[msg.channel].pop()) else: - raise IRCException("I don't have any youtube URL for now, please provide me one to get information!") + raise IMException("I don't have any youtube URL for now, please provide me one to get information!") else: for url in msg.args: links.append(url) diff --git a/nemubot/exception.py b/nemubot/exception/__init__.py similarity index 83% rename from nemubot/exception.py rename to nemubot/exception/__init__.py index 93e6a53..84464a0 100644 --- a/nemubot/exception.py +++ b/nemubot/exception/__init__.py @@ -1,5 +1,3 @@ -# coding=utf-8 - # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier # @@ -16,20 +14,21 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -class IRCException(Exception): +class IMException(Exception): + def __init__(self, message, personnal=True): - super(IRCException, self).__init__(message) - self.message = message + super(IMException, self).__init__(message) self.personnal = personnal + def fill_response(self, msg): if self.personnal: from nemubot.message import DirectAsk - return DirectAsk(msg.frm, self.message, + return DirectAsk(msg.frm, *self.args, server=msg.server, to=msg.to_response) else: from nemubot.message import Text - return Text(self.message, + return Text(*self.args, server=msg.server, to=msg.to_response) diff --git a/nemubot/hooks/abstract.py b/nemubot/hooks/abstract.py index 7e9aa72..687ff93 100644 --- a/nemubot/hooks/abstract.py +++ b/nemubot/hooks/abstract.py @@ -57,12 +57,12 @@ class Abstract: def run(self, data1, *args): """Run the hook""" - from nemubot.exception import IRCException + from nemubot.exception import IMException self.times -= 1 try: ret = call_game(self.call, data1, self.data, *args) - except IRCException as e: + except IMException as e: ret = e.fill_response(data1) finally: if self.times == 0: diff --git a/nemubot/tools/feed.py b/nemubot/tools/feed.py index c3f402a..7e63cd2 100644 --- a/nemubot/tools/feed.py +++ b/nemubot/tools/feed.py @@ -110,8 +110,8 @@ class Feed: elif self.feed.tagName == "feed": self._parse_atom_feed() else: - from nemubot.exception import IRCException - raise IRCException("This is not a valid Atom or RSS feed") + from nemubot.exception import IMException + raise IMException("This is not a valid Atom or RSS feed") def _parse_atom_feed(self): diff --git a/nemubot/tools/web.py b/nemubot/tools/web.py index 95854f8..d35740c 100644 --- a/nemubot/tools/web.py +++ b/nemubot/tools/web.py @@ -16,7 +16,7 @@ from urllib.parse import urlparse, urlsplit, urlunsplit -from nemubot.exception import IRCException +from nemubot.exception import IMException def isURL(url): @@ -100,7 +100,7 @@ def getURLContent(url, body=None, timeout=7, header=None): elif o.scheme is None or o.scheme == "": conn = http.client.HTTPConnection(**kwargs) else: - raise IRCException("Invalid URL") + raise IMException("Invalid URL") from nemubot import __version__ if header is None: @@ -121,7 +121,7 @@ def getURLContent(url, body=None, timeout=7, header=None): body, header) except OSError as e: - raise IRCException(e.strerror) + raise IMException(e.strerror) try: res = conn.getresponse() @@ -129,7 +129,7 @@ def getURLContent(url, body=None, timeout=7, header=None): cntype = res.getheader("Content-Type") if size > 524288 or (cntype is not None and cntype[:4] != "text" and cntype[:4] != "appl"): - raise IRCException("Content too large to be retrieved") + raise IMException("Content too large to be retrieved") data = res.read(size) @@ -147,7 +147,7 @@ def getURLContent(url, body=None, timeout=7, header=None): else: charset = cha[0] except http.client.BadStatusLine: - raise IRCException("Invalid HTTP response") + raise IMException("Invalid HTTP response") finally: conn.close() @@ -158,7 +158,7 @@ def getURLContent(url, body=None, timeout=7, header=None): res.getheader("Location") != url): return getURLContent(res.getheader("Location"), timeout=timeout) else: - raise IRCException("A HTTP error occurs: %d - %s" % + raise IMException("A HTTP error occurs: %d - %s" % (res.status, http.client.responses[res.status])) diff --git a/setup.py b/setup.py index 37f4aef..bbd7a52 100755 --- a/setup.py +++ b/setup.py @@ -63,6 +63,7 @@ setup( 'nemubot', 'nemubot.datastore', 'nemubot.event', + 'nemubot.exception', 'nemubot.hooks', 'nemubot.message', 'nemubot.message.printer', From c6aa38147b8d55843f6770eaed66d69ed9a07402 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Fri, 30 Oct 2015 22:18:48 +0100 Subject: [PATCH 032/271] Include some forgotten module in reload process --- nemubot/__init__.py | 5 +++++ nemubot/datastore/__init__.py | 13 +++++++++++++ nemubot/message/printer/__init__.py | 3 +++ nemubot/server/__init__.py | 5 +++++ nemubot/tools/__init__.py | 3 +++ 5 files changed, 29 insertions(+) diff --git a/nemubot/__init__.py b/nemubot/__init__.py index 044d993..005e2b1 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -50,6 +50,11 @@ def reload(): import nemubot.consumer imp.reload(nemubot.consumer) + import nemubot.datastore + imp.reload(nemubot.datastore) + + nemubot.datastore.reload() + import nemubot.event imp.reload(nemubot.event) diff --git a/nemubot/datastore/__init__.py b/nemubot/datastore/__init__.py index 323a160..ed9e829 100644 --- a/nemubot/datastore/__init__.py +++ b/nemubot/datastore/__init__.py @@ -16,3 +16,16 @@ from nemubot.datastore.abstract import Abstract from nemubot.datastore.xml import XML + + +def reload(): + global Abstract, XML + import imp + + import nemubot.datastore.abstract + imp.reload(nemubot.datastore.abstract) + Abstract = nemubot.datastore.abstract.Abstract + + import nemubot.datastore.xml + imp.reload(nemubot.datastore.xml) + XML = nemubot.datastore.xml.XML diff --git a/nemubot/message/printer/__init__.py b/nemubot/message/printer/__init__.py index ae6b4df..bb58338 100644 --- a/nemubot/message/printer/__init__.py +++ b/nemubot/message/printer/__init__.py @@ -19,3 +19,6 @@ def reload(): import nemubot.message.printer.IRC imp.reload(nemubot.message.printer.IRC) + + import nemubot.message.printer.socket + imp.reload(nemubot.message.printer.socket) diff --git a/nemubot/server/__init__.py b/nemubot/server/__init__.py index 1f68d74..b9a8fe4 100644 --- a/nemubot/server/__init__.py +++ b/nemubot/server/__init__.py @@ -81,3 +81,8 @@ def reload(): import nemubot.server.IRC imp.reload(nemubot.server.IRC) + + import nemubot.server.message + imp.reload(nemubot.server.message) + + nemubot.server.message.reload() diff --git a/nemubot/tools/__init__.py b/nemubot/tools/__init__.py index 9043466..127154c 100644 --- a/nemubot/tools/__init__.py +++ b/nemubot/tools/__init__.py @@ -23,6 +23,9 @@ def reload(): import nemubot.tools.countdown imp.reload(nemubot.tools.countdown) + import nemubot.tools.feed + imp.reload(nemubot.tools.feed) + import nemubot.tools.date imp.reload(nemubot.tools.date) From 9790954dfc83d38aef0fab5b91d6ff547e312139 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sat, 31 Oct 2015 14:49:44 +0100 Subject: [PATCH 033/271] Hooks can now contain help on optional keywords --- nemubot/bot.py | 4 +++- nemubot/hooks/message.py | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/nemubot/bot.py b/nemubot/bot.py index 1dbedcd..564ecef 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -100,7 +100,9 @@ class Bot(threading.Thread): for (s, h) in self.modules[module].__nemubot_context__.hooks: if s == "in_Command" and (h.name is not None or h.regexp is not None) and h.is_matching(msg.args[0][1:]): if h.help_usage: - return res.append_message(["\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], title="Usage for command %s from module %s" % (msg.args[0], module)) + 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 = ["\x03\x02@%s\x03\x02: %s" % (k, h.keywords[k]) for k in h.keywords] + return res.append_message(lp + ([". Moreover, you can provides some optional parameters: "] + jp if len(jp) else []), title="Usage for command %s from module %s" % (msg.args[0], module)) elif h.help: return res.append_message("Command %s from module %s: %s" % (msg.args[0], module, h.help)) else: diff --git a/nemubot/hooks/message.py b/nemubot/hooks/message.py index 5f092ad..fffcbb7 100644 --- a/nemubot/hooks/message.py +++ b/nemubot/hooks/message.py @@ -25,7 +25,8 @@ class Message(Abstract): """Class storing hook information, specialized for a generic Message""" def __init__(self, call, name=None, regexp=None, channels=list(), - server=None, help=None, help_usage=dict(), **kargs): + server=None, help=None, help_usage=dict(), keywords=dict(), + **kargs): Abstract.__init__(self, call=call, **kargs) @@ -33,6 +34,7 @@ class Message(Abstract): assert channels is None or type(channels) is list, channels assert server is None or type(server) is str, server assert type(help_usage) is dict, help_usage + assert type(keywords) is dict, keywords self.name = str(name) if name is not None else None self.regexp = regexp @@ -40,6 +42,7 @@ class Message(Abstract): self.channels = channels self.help = help self.help_usage = help_usage + self.keywords = keywords def __str__(self): From 979f1d0c5531e240d9c7209d411c41e4911a1012 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sat, 31 Oct 2015 15:17:58 +0100 Subject: [PATCH 034/271] [more] Don't display the count string if the message is alone --- modules/more.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/more.py b/modules/more.py index bab32a5..08be14b 100644 --- a/modules/more.py +++ b/modules/more.py @@ -209,7 +209,7 @@ class Response: else: if len(elts.encode()) <= maxlen: self.pop() - if self.count is not None: + if self.count is not None and not self.alone: return msg + elts + (self.count % len(self.messages)) else: return msg + elts From 8ff0b626a212ec6ca56d7a9c36eb12e4f2b079be Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sat, 31 Oct 2015 17:40:23 +0100 Subject: [PATCH 035/271] Update help of module using keywords --- modules/framalink.py | 12 ++++++++---- modules/suivi.py | 22 +++++++++++++--------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/modules/framalink.py b/modules/framalink.py index 9e2af2f..6e65ccd 100644 --- a/modules/framalink.py +++ b/modules/framalink.py @@ -109,10 +109,14 @@ def parseresponse(msg): # MODULE INTERFACE #################################################### @hook("cmd_hook", "framalink", - help="Reduce any given URL", - help_usage={None: "Reduce the last URL said on the channel", - "[@provider=framalink] URL [URL ...]": "Reduce the given " - "URL(s) using the specified shortner"}) + help="Reduce any long URL", + help_usage={ + None: "Reduce the last URL said on the channel", + "URL [URL ...]": "Reduce the given URL(s)" + }, + keywords={ + "provider=SMTH": "Change the service provider used (by default: %s) among %s" % (DEFAULT_PROVIDER, ", ".join(PROVIDERS.keys())) + }) def cmd_reduceurl(msg): minify = list() diff --git a/modules/suivi.py b/modules/suivi.py index c2fd645..a0964ac 100644 --- a/modules/suivi.py +++ b/modules/suivi.py @@ -1,3 +1,7 @@ +"""Postal tracking module""" + +# PYTHON STUFF ############################################ + import urllib.request import urllib.parse from bs4 import BeautifulSoup @@ -8,11 +12,9 @@ from nemubot.exception import IMException from nemubot.tools.web import getURLContent from more import Response -nemubotversion = 4.0 # POSTAGE SERVICE PARSERS ############################################ - def get_tnt_info(track_id): data = getURLContent('www.tnt.fr/public/suivi_colis/recherche/' 'visubontransport.do?bonTransport=%s' % track_id) @@ -101,7 +103,6 @@ def get_laposte_info(laposte_id): # TRACKING HANDLERS ################################################### - def handle_tnt(tracknum): info = get_tnt_info(tracknum) if info: @@ -141,23 +142,26 @@ def handle_coliprive(tracknum): if info: return ("Colis Privé: \x02%s\x0F : \x02%s\x0F." % (tracknum, info)) + TRACKING_HANDLERS = { 'laposte': handle_laposte, 'colissimo': handle_colissimo, 'chronopost': handle_chronopost, 'coliprive': handle_coliprive, - 'tnt': handle_tnt + 'tnt': handle_tnt, } # HOOKS ############################################################## - @hook("cmd_hook", "track", - help="Track postage", - help_usage={"[@tracker] TRACKING_ID [TRACKING_ID ...]": "Track the " - "specified postage IDs using the specified tracking service " - "or all of them."}) + help="Track postage delivery", + help_usage={ + "TRACKING_ID [...]": "Track the specified postage IDs on various tracking services." + }, + keywords={ + "tracker=TRK": "Precise the tracker (default: all) among: " + ', '.join(TRACKING_HANDLERS) + }) def get_tracking_info(msg): if not len(msg.args): raise IMException("Renseignez un identifiant d'envoi.") From 70b52d5567bc1d9a950cadb99992be61876eab7b Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sun, 1 Nov 2015 11:23:51 +0100 Subject: [PATCH 036/271] [translate] Refactor module, use keywords --- modules/translate.py | 120 +++++++++++++++++++++++-------------------- 1 file changed, 64 insertions(+), 56 deletions(-) diff --git a/modules/translate.py b/modules/translate.py index 911f0ea..bbca24a 100644 --- a/modules/translate.py +++ b/modules/translate.py @@ -1,23 +1,25 @@ -# coding=utf-8 - """Translation module""" -import re +# PYTHON STUFFS ####################################################### + from urllib.parse import quote from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web -nemubotversion = 4.0 - from more import Response +# GLOBALS ############################################################# + LANG = ["ar", "zh", "cz", "en", "fr", "gr", "it", "ja", "ko", "pl", "pt", "ro", "es", "tr"] URL = "http://api.wordreference.com/0.8/%s/json/%%s%%s/%%s" + +# LOADING ############################################################# + def load(context): if not context.config or "wrapikey" not in context.config: raise ImportError("You need a WordReference API key in order to use " @@ -29,57 +31,7 @@ def load(context): URL = URL % context.config["wrapikey"] -def help_full(): - return "!translate [lang] [ [...]]: Found translation of from/to english to/from . Data © WordReference.com" - - -@hook("cmd_hook", "translate") -def cmd_translate(msg): - if not len(msg.args): - raise IMException("which word would you translate?") - - if len(msg.args) > 2 and msg.args[0] in LANG and msg.args[1] in LANG: - if msg.args[0] != "en" and msg.args[1] != "en": - raise IMException("sorry, I can only translate to or from english") - langFrom = msg.args[0] - langTo = msg.args[1] - term = ' '.join(msg.args[2:]) - elif len(msg.args) > 1 and msg.args[0] in LANG: - langFrom = msg.args[0] - if langFrom == "en": - langTo = "fr" - else: - langTo = "en" - term = ' '.join(msg.args[1:]) - else: - langFrom = "en" - langTo = "fr" - term = ' '.join(msg.args) - - wres = web.getJSON(URL % (langFrom, langTo, quote(term))) - - if "Error" in wres: - raise IMException(wres["Note"]) - - else: - res = Response(channel=msg.channel, - count=" (%d more meanings)", - nomore="No more translation") - for k in sorted(wres.keys()): - t = wres[k] - if len(k) > 4 and k[:4] == "term": - if "Entries" in t: - ent = t["Entries"] - else: - ent = t["PrincipalTranslations"] - - for i in sorted(ent.keys()): - res.append_message("Translation of %s%s: %s" % ( - ent[i]["OriginalTerm"]["term"], - meaning(ent[i]["OriginalTerm"]), - extract_traslation(ent[i]))) - return res - +# MODULE CORE ######################################################### def meaning(entry): ret = list() @@ -101,3 +53,59 @@ def extract_traslation(entry): if "Note" in entry and entry["Note"]: ret.append("note: %s" % entry["Note"]) return ", ".join(ret) + + +def translate(term, langFrom="en", langTo="fr"): + wres = web.getJSON(URL % (langFrom, langTo, quote(term))) + + if "Error" in wres: + raise IMException(wres["Note"]) + + else: + for k in sorted(wres.keys()): + t = wres[k] + if len(k) > 4 and k[:4] == "term": + if "Entries" in t: + ent = t["Entries"] + else: + ent = t["PrincipalTranslations"] + + for i in sorted(ent.keys()): + yield "Translation of %s%s: %s" % ( + ent[i]["OriginalTerm"]["term"], + meaning(ent[i]["OriginalTerm"]), + extract_traslation(ent[i])) + + +# MODULE INTERFACE #################################################### + +@hook("cmd_hook", "translate", + help="Word translation using WordReference.com", + help_usage={ + "TERM": "Found translation of TERM from/to english to/from ." + }, + keywords={ + "from=LANG": "language of the term you asked for translation between: en, " + ", ".join(LANG), + "to=LANG": "language of the translated terms between: en, " + ", ".join(LANG), + }) +def cmd_translate(msg): + if not len(msg.args): + raise IMException("which word would you translate?") + + langFrom = msg.kwargs["from"] if "from" in msg.kwargs else "en" + if "to" in msg.kwargs: + langTo = msg.kwargs["to"] + else: + langTo = "fr" if langFrom == "en" else "en" + + if langFrom not in LANG or langTo not in LANG: + raise IMException("sorry, I can only translate to or from: " + ", ".join(LANG)) + if langFrom != "en" and langTo != "en": + raise IMException("sorry, I can only translate to or from english") + + res = Response(channel=msg.channel, + count=" (%d more meanings)", + nomore="No more translation") + for t in translate(" ".join(msg.args), langFrom=langFrom, langTo=langTo): + res.append_message(t) + return res From ea9829b3419bf78c5752f5859bc9ec7ae55f16aa Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sun, 1 Nov 2015 12:35:46 +0100 Subject: [PATCH 037/271] Check command keywords using keyword help (passed in @hook) --- nemubot/__init__.py | 2 + nemubot/bot.py | 2 +- nemubot/exception/__init__.py | 7 ++++ nemubot/exception/keyword.py | 23 ++++++++++++ nemubot/hooks/__init__.py | 4 ++ nemubot/hooks/abstract.py | 11 +++++- nemubot/hooks/keywords/__init__.py | 36 ++++++++++++++++++ nemubot/hooks/keywords/abstract.py | 35 ++++++++++++++++++ nemubot/hooks/keywords/dict.py | 59 ++++++++++++++++++++++++++++++ nemubot/hooks/message.py | 14 ++++++- setup.py | 1 + 11 files changed, 189 insertions(+), 5 deletions(-) create mode 100644 nemubot/exception/keyword.py create mode 100644 nemubot/hooks/keywords/__init__.py create mode 100644 nemubot/hooks/keywords/abstract.py create mode 100644 nemubot/hooks/keywords/dict.py diff --git a/nemubot/__init__.py b/nemubot/__init__.py index 005e2b1..193ad53 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -61,6 +61,8 @@ def reload(): import nemubot.exception imp.reload(nemubot.exception) + nemubot.exception.reload() + import nemubot.hooks imp.reload(nemubot.hooks) diff --git a/nemubot/bot.py b/nemubot/bot.py index 564ecef..32a2f22 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -101,7 +101,7 @@ class Bot(threading.Thread): if s == "in_Command" and (h.name is not None or h.regexp is not None) and h.is_matching(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 = ["\x03\x02@%s\x03\x02: %s" % (k, h.keywords[k]) for k in h.keywords] + 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 from module %s" % (msg.args[0], module)) elif h.help: return res.append_message("Command %s from module %s: %s" % (msg.args[0], module, h.help)) diff --git a/nemubot/exception/__init__.py b/nemubot/exception/__init__.py index 84464a0..1e34923 100644 --- a/nemubot/exception/__init__.py +++ b/nemubot/exception/__init__.py @@ -32,3 +32,10 @@ class IMException(Exception): from nemubot.message import Text return Text(*self.args, server=msg.server, to=msg.to_response) + + +def reload(): + import imp + + import nemubot.exception.Keyword + imp.reload(nemubot.exception.printer.IRC) diff --git a/nemubot/exception/keyword.py b/nemubot/exception/keyword.py new file mode 100644 index 0000000..6e3c07f --- /dev/null +++ b/nemubot/exception/keyword.py @@ -0,0 +1,23 @@ +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2015 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 . + +from nemubot.exception import IMException + + +class KeywordException(IMException): + + def __init__(self, message): + super(KeywordException, self).__init__(message) diff --git a/nemubot/hooks/__init__.py b/nemubot/hooks/__init__.py index 09c77d2..a9a8a31 100644 --- a/nemubot/hooks/__init__.py +++ b/nemubot/hooks/__init__.py @@ -38,5 +38,9 @@ def reload(): imp.reload(nemubot.hooks.message) Message = nemubot.hooks.message.Message + import nemubot.hooks.keywords + imp.reload(nemubot.hooks.keywords) + nemubot.hooks.keywords.reload() + import nemubot.hooks.manager imp.reload(nemubot.hooks.manager) diff --git a/nemubot/hooks/abstract.py b/nemubot/hooks/abstract.py index 687ff93..5af3f3b 100644 --- a/nemubot/hooks/abstract.py +++ b/nemubot/hooks/abstract.py @@ -50,8 +50,12 @@ class Abstract: self.end_call = end_call + def check(self, data1): + return True + + def match(self, data1, server): - return NotImplemented + return True def run(self, data1, *args): @@ -60,8 +64,11 @@ class Abstract: from nemubot.exception import IMException self.times -= 1 + ret = None + try: - ret = call_game(self.call, data1, self.data, *args) + if self.check(data1): + ret = call_game(self.call, data1, self.data, *args) except IMException as e: ret = e.fill_response(data1) finally: diff --git a/nemubot/hooks/keywords/__init__.py b/nemubot/hooks/keywords/__init__.py new file mode 100644 index 0000000..68250bf --- /dev/null +++ b/nemubot/hooks/keywords/__init__.py @@ -0,0 +1,36 @@ +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2015 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 . + +from nemubot.exception.keyword import KeywordException +from nemubot.hooks.keywords.abstract import Abstract + + +class NoKeyword(Abstract): + + def check(self, mkw): + if len(mkw): + raise KeywordException("This command doesn't take any keyword arguments.") + return super().check(mkw) + + +def reload(): + import imp + + import nemubot.hooks.keywords.abstract + imp.reload(nemubot.hooks.keywords.abstract) + + import nemubot.hooks.keywords.dict + imp.reload(nemubot.hooks.keywords.dict) diff --git a/nemubot/hooks/keywords/abstract.py b/nemubot/hooks/keywords/abstract.py new file mode 100644 index 0000000..0e6dd0b --- /dev/null +++ b/nemubot/hooks/keywords/abstract.py @@ -0,0 +1,35 @@ +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2015 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 . + +class Abstract: + + def __init__(self): + pass + + def check(self, mkw): + """Check that all given message keywords are valid + + Argument: + mkw -- dictionnary of keywords present in the message + """ + + assert type(mkw) is dict, mkw + + return True + + + def help(self): + return "" diff --git a/nemubot/hooks/keywords/dict.py b/nemubot/hooks/keywords/dict.py new file mode 100644 index 0000000..9fc85e3 --- /dev/null +++ b/nemubot/hooks/keywords/dict.py @@ -0,0 +1,59 @@ +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2015 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 . + +from nemubot.exception.keyword import KeywordException +from nemubot.hooks.keywords.abstract import Abstract +from nemubot.tools.human import guess + + +class Dict(Abstract): + + + def __init__(self, d): + super().__init__() + self.d = d + + + @property + def chk_noarg(self): + if not hasattr(self, "_cache_chk_noarg"): + self._cache_chk_noarg = [k for k in self.d if "=" not in k] + return self._cache_chk_noarg + + + @property + def chk_args(self): + if not hasattr(self, "_cache_chk_args"): + self._cache_chk_args = [k.split("=", 1)[0] for k in self.d if "=" in k] + return self._cache_chk_args + + + def check(self, mkw): + for k in mkw: + if (mkw[k] and k not in self.chk_args) or (not mkw[k] and k not in self.chk_noarg): + if mkw[k] and k in self.chk_noarg: + raise KeywordException("Keyword %s doesn't take value." % k) + elif not mkw[k] and k in self.chk_args: + raise KeywordException("Keyword %s requires a value." % k) + else: + ch = [c for c in guess(k, self.d)] + raise KeywordException("Unknown keyword %s." % k + (" Did you mean: " + ", ".join(ch) + "?" if len(ch) else "")) + + return super().check(mkw) + + + def help(self): + return ["\x03\x02@%s\x03\x02: %s" % (k, self.d[k]) for k in self.d] diff --git a/nemubot/hooks/message.py b/nemubot/hooks/message.py index fffcbb7..8033072 100644 --- a/nemubot/hooks/message.py +++ b/nemubot/hooks/message.py @@ -17,6 +17,9 @@ import re from nemubot.hooks.abstract import Abstract +from nemubot.hooks.keywords import NoKeyword +from nemubot.hooks.keywords.abstract import Abstract as AbstractKeywords +from nemubot.hooks.keywords.dict import Dict as DictKeywords import nemubot.message @@ -25,16 +28,19 @@ class Message(Abstract): """Class storing hook information, specialized for a generic Message""" def __init__(self, call, name=None, regexp=None, channels=list(), - server=None, help=None, help_usage=dict(), keywords=dict(), + server=None, help=None, help_usage=dict(), keywords=NoKeyword(), **kargs): Abstract.__init__(self, call=call, **kargs) + if isinstance(keywords, dict): + keywords = DictKeywords(keywords) + assert regexp is None or type(regexp) is str, regexp assert channels is None or type(channels) is list, channels assert server is None or type(server) is str, server assert type(help_usage) is dict, help_usage - assert type(keywords) is dict, keywords + assert isinstance(keywords, AbstractKeywords), keywords self.name = str(name) if name is not None else None self.regexp = regexp @@ -53,6 +59,10 @@ class Message(Abstract): ) + def check(self, msg): + return not hasattr(msg, "kwargs") or self.keywords.check(msg.kwargs) + + def match(self, msg, server=None): if not isinstance(msg, nemubot.message.abstract.Abstract): return True diff --git a/setup.py b/setup.py index bbd7a52..a9b7d3f 100755 --- a/setup.py +++ b/setup.py @@ -65,6 +65,7 @@ setup( 'nemubot.event', 'nemubot.exception', 'nemubot.hooks', + 'nemubot.hooks.keywords', 'nemubot.message', 'nemubot.message.printer', 'nemubot.prompt', From de2e1d621615fd0bab29d1bced2710a730fc175b Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sun, 1 Nov 2015 13:54:59 +0100 Subject: [PATCH 038/271] Remove Message.receivers, long time deprecated --- modules/framalink.py | 8 ++++---- modules/mediawiki.py | 18 +++++++++--------- modules/more.py | 9 +++++---- modules/reddit.py | 8 ++++---- modules/youtube-title.py | 8 ++++---- nemubot/message/abstract.py | 5 ----- 6 files changed, 26 insertions(+), 30 deletions(-) diff --git a/modules/framalink.py b/modules/framalink.py index 6e65ccd..7acc5a5 100644 --- a/modules/framalink.py +++ b/modules/framalink.py @@ -80,13 +80,13 @@ def gen_response(res, msg, srv): LAST_URLS = dict() -@hook("msg_default") +@hook.message() def parselisten(msg): parseresponse(msg) return None -@hook("all_post") +@hook.post() def parseresponse(msg): global LAST_URLS if hasattr(msg, "text") and msg.text: @@ -99,7 +99,7 @@ def parseresponse(msg): len(o.netloc) + len(o.path) < 17): continue - for recv in msg.receivers: + for recv in msg.to: if recv not in LAST_URLS: LAST_URLS[recv] = list() LAST_URLS[recv].append(url) @@ -108,7 +108,7 @@ def parseresponse(msg): # MODULE INTERFACE #################################################### -@hook("cmd_hook", "framalink", +@hook.command("framalink", help="Reduce any long URL", help_usage={ None: "Reduce the last URL said on the channel", diff --git a/modules/mediawiki.py b/modules/mediawiki.py index d2c4488..afc1ecb 100644 --- a/modules/mediawiki.py +++ b/modules/mediawiki.py @@ -151,7 +151,7 @@ def get_page(site, term, ssl=False, subpart=None): # NEMUBOT ############################################################# -def mediawiki_response(site, term, receivers): +def mediawiki_response(site, term, to): ns = get_namespaces(site) terms = term.split("#", 1) @@ -160,7 +160,7 @@ def mediawiki_response(site, term, receivers): # Print the article if it exists return Response(get_page(site, terms[0], subpart=terms[1] if len(terms) > 1 else None), line_treat=lambda line: irc_format(parse_wikitext(site, line, ns)), - channel=receivers) + channel=to) except: pass @@ -171,11 +171,11 @@ def mediawiki_response(site, term, receivers): if not len(os): os = [x for x, _ in search(site, terms[0]) if x is not None and x != ""] return Response(os, - channel=receivers, + channel=to, title="Article not found, would you mean") -@hook("cmd_hook", "mediawiki") +@hook.command("mediawiki") def cmd_mediawiki(msg): """Read an article on a MediaWiki""" if len(msg.args) < 2: @@ -183,16 +183,16 @@ def cmd_mediawiki(msg): return mediawiki_response(msg.args[0], " ".join(msg.args[1:]), - msg.receivers) + msg.to_response) -@hook("cmd_hook", "search_mediawiki") +@hook.command("search_mediawiki") def cmd_srchmediawiki(msg): """Search an article on a MediaWiki""" if len(msg.args) < 2: raise IMException("indicate a domain and a term to search") - res = Response(channel=msg.receivers, nomore="No more results", count=" (%d more results)") + res = Response(channel=msg.to_response, nomore="No more results", count=" (%d more results)") for r in search(msg.args[0], " ".join(msg.args[1:])): res.append_message("%s: %s" % r) @@ -200,11 +200,11 @@ def cmd_srchmediawiki(msg): return res -@hook("cmd_hook", "wikipedia") +@hook.command("wikipedia") def cmd_wikipedia(msg): if len(msg.args) < 2: raise IMException("indicate a lang and a term to search") return mediawiki_response(msg.args[0] + ".wikipedia.org", " ".join(msg.args[1:]), - msg.receivers) + msg.to_response) diff --git a/modules/more.py b/modules/more.py index 08be14b..c8b80a9 100644 --- a/modules/more.py +++ b/modules/more.py @@ -50,7 +50,7 @@ class Response: @property - def receivers(self): + def to(self): if self.channel is None: if self.nick is not None: return [self.nick] @@ -60,6 +60,7 @@ class Response: else: return [self.channel] + def append_message(self, message, title=None, shown_first_count=-1): if type(message) is str: message = message.split('\n') @@ -140,10 +141,10 @@ class Response: if self.nick: return DirectAsk(self.nick, self.get_message(maxlen - len(self.nick) - 2), - server=None, to=self.receivers) + server=None, to=self.to) else: return Text(self.get_message(maxlen), - server=None, to=self.receivers) + server=None, to=self.to) def __str__(self): @@ -245,7 +246,7 @@ def parseresponse(res): if isinstance(res, Response): if res.server not in SERVERS: SERVERS[res.server] = dict() - for receiver in res.receivers: + for receiver in res.to: if receiver in SERVERS[res.server]: nw, bk = SERVERS[res.server][receiver] else: diff --git a/modules/reddit.py b/modules/reddit.py index 74eae41..d3f03a1 100644 --- a/modules/reddit.py +++ b/modules/reddit.py @@ -19,7 +19,7 @@ def help_full(): LAST_SUBS = dict() -@hook("cmd_hook", "subreddit") +@hook.command("subreddit") def cmd_subreddit(msg): global LAST_SUBS if not len(msg.args): @@ -69,20 +69,20 @@ def cmd_subreddit(msg): return all_res -@hook("msg_default") +@hook.message() def parselisten(msg): parseresponse(msg) return None -@hook("all_post") +@hook.post() def parseresponse(msg): global LAST_SUBS if hasattr(msg, "text") and msg.text: urls = re.findall("www.reddit.com(/\w/\w+/?)", msg.text) for url in urls: - for recv in msg.receivers: + for recv in msg.to: if recv not in LAST_SUBS: LAST_SUBS[recv] = list() LAST_SUBS[recv].append(url) diff --git a/modules/youtube-title.py b/modules/youtube-title.py index e7da2c8..ebae4b6 100644 --- a/modules/youtube-title.py +++ b/modules/youtube-title.py @@ -52,7 +52,7 @@ def _get_ytdl(links): LAST_URLS = dict() -@hook("cmd_hook", "yt") +@hook.command("yt") def get_info_yt(msg): links = list() @@ -73,13 +73,13 @@ def get_info_yt(msg): return res -@hook("msg_default") +@hook.message() def parselisten(msg): parseresponse(msg) return None -@hook("all_post") +@hook.post() def parseresponse(msg): global LAST_URLS if hasattr(msg, "text") and msg.text: @@ -89,7 +89,7 @@ def parseresponse(msg): if o.scheme != "": if o.netloc == "" and len(o.path) < 10: continue - for recv in msg.receivers: + for recv in msg.to: if recv not in LAST_URLS: LAST_URLS[recv] = list() LAST_URLS[recv].append(url) diff --git a/nemubot/message/abstract.py b/nemubot/message/abstract.py index 3c69c8d..5d74549 100644 --- a/nemubot/message/abstract.py +++ b/nemubot/message/abstract.py @@ -51,11 +51,6 @@ class Abstract: return self.to - @property - def receivers(self): - # TODO: this is for legacy modules - return self.to_response - @property def channel(self): # TODO: this is for legacy modules From 49d7e4ced6948b494fbeb1e7151bde30bd765c0c Mon Sep 17 00:00:00 2001 From: nemunaire Date: Mon, 2 Nov 2015 19:12:46 +0100 Subject: [PATCH 039/271] Hooks: add global methods to restrict read/write on channels --- nemubot/hooks/abstract.py | 45 +++++++++++++++++++++++++++++++++++++-- nemubot/hooks/message.py | 2 +- nemubot/treatment.py | 6 +++--- 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/nemubot/hooks/abstract.py b/nemubot/hooks/abstract.py index 5af3f3b..e2dc78b 100644 --- a/nemubot/hooks/abstract.py +++ b/nemubot/hooks/abstract.py @@ -42,19 +42,60 @@ class Abstract: """Abstract class for Hook implementation""" - def __init__(self, call, data=None, mtimes=-1, end_call=None): + def __init__(self, call, data=None, channels=None, servers=None, mtimes=-1, + end_call=None): + """Create basis of the hook + + Arguments: + call -- function to call to perform the hook + + Keyword arguments: + data -- optional datas passed to call + """ + + if channels is None: channels = list() + if servers is None: servers = list() + + assert callable(call), call + assert end_call is None or callable(end_call), end_call + assert isinstance(channels, list), channels + assert isinstance(servers, list), servers + assert type(mtimes) is int, mtimes + self.call = call self.data = data + # TODO: find a way to have only one list: a limit is server + channel, not only server or channel + self.channels = channels + self.servers = servers + self.times = mtimes self.end_call = end_call + def can_read(self, receivers=list(), server=None): + assert isinstance(receivers, list), receivers + + if server is None or len(self.servers) == 0 or server in self.servers: + if len(self.channels) == 0: + return True + + for receiver in receivers: + if receiver in self.channels: + return True + + return False + + + def can_write(self, receivers=list(), server=None): + return True + + def check(self, data1): return True - def match(self, data1, server): + def match(self, data1): return True diff --git a/nemubot/hooks/message.py b/nemubot/hooks/message.py index 8033072..a14177a 100644 --- a/nemubot/hooks/message.py +++ b/nemubot/hooks/message.py @@ -54,7 +54,7 @@ class Message(Abstract): def __str__(self): return "\x03\x02%s\x03\x02%s%s" % ( self.name if self.name is not None else "\x03\x1f" + self.regexp + "\x03\x1f" if self.regexp is not None else "", - " (restricted to %s)" % (self.server + ":" if self.server is not None else "") + (self.channels if self.channels else "*") if len(self.channels) or self.server else "", + " (restricted to %:%s)" % ((",".join(self.servers) if self.server else "*") + (",".join(self.channels) if self.channels else "*")) if len(self.channels) or len(self.server) else "", ": %s" % self.help if self.help is not None else "" ) diff --git a/nemubot/treatment.py b/nemubot/treatment.py index 8bbdabb..57eb448 100644 --- a/nemubot/treatment.py +++ b/nemubot/treatment.py @@ -65,7 +65,7 @@ class MessageTreater: """ for h in self.hm.get_hooks("pre", type(msg).__name__): - if h.match(msg): + if h.can_read(msg.to, msg.server) and h.match(msg): res = h.run(msg) if isinstance(res, list): @@ -91,7 +91,7 @@ class MessageTreater: """ for h in self.hm.get_hooks("in", type(msg).__name__): - if h.match(msg): + if h.can_read(msg.to, msg.server) and h.match(msg): res = h.run(msg) if isinstance(res, list): @@ -113,7 +113,7 @@ class MessageTreater: """ for h in self.hm.get_hooks("post"): - if h.match(msg): + if h.can_write(msg.to, msg.server) and h.match(msg): res = h.run(msg) if isinstance(res, list): From f39a0eac56c13bab7da5e21b33dbb2437ce8898b Mon Sep 17 00:00:00 2001 From: nemunaire Date: Mon, 2 Nov 2015 20:19:12 +0100 Subject: [PATCH 040/271] Refactors hooks registration --- modules/alias.py | 14 ++++---- modules/birthday.py | 6 ++-- modules/bonneannee.py | 6 ++-- modules/books.py | 6 ++-- modules/conjugaison.py | 2 +- modules/ctfs.py | 2 +- modules/cve.py | 2 +- modules/ddg.py | 4 +-- modules/events.py | 16 ++++----- modules/github.py | 8 ++--- modules/imdb.py | 4 +-- modules/jsonbot.py | 2 +- modules/man.py | 4 +-- modules/mapquest.py | 2 +- modules/more.py | 6 ++-- modules/networking/__init__.py | 22 ++++++------ modules/networking/whois.py | 6 ++-- modules/news.py | 2 +- modules/nextstop/__init__.py | 4 +-- modules/rnd.py | 4 +-- modules/sap.py | 2 +- modules/sleepytime.py | 2 +- modules/sms.py | 4 +-- modules/spell/__init__.py | 4 +-- modules/suivi.py | 2 +- modules/syno.py | 4 +-- modules/tpb.py | 2 +- modules/translate.py | 2 +- modules/urbandict.py | 2 +- modules/velib.py | 2 +- modules/weather.py | 8 ++--- modules/whois.py | 8 ++--- modules/wolframalpha.py | 2 +- modules/worldcup.py | 4 +-- nemubot/bot.py | 10 +++--- nemubot/hooks/__init__.py | 43 +++++++++++++++++----- nemubot/hooks/abstract.py | 4 +++ nemubot/hooks/command.py | 65 ++++++++++++++++++++++++++++++++++ nemubot/hooks/message.py | 59 +++++------------------------- nemubot/modulecontext.py | 19 ---------- 40 files changed, 202 insertions(+), 168 deletions(-) create mode 100644 nemubot/hooks/command.py diff --git a/modules/alias.py b/modules/alias.py index 871424b..c308608 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -155,7 +155,7 @@ def replace_variables(cnts, msg=None): ## Variables management -@hook("cmd_hook", "listvars", +@hook.command("listvars", help="list defined variables for substitution in input commands", help_usage={ None: "List all known variables", @@ -178,7 +178,7 @@ def cmd_listvars(msg): return Response("There is currently no variable stored.", channel=msg.channel) -@hook("cmd_hook", "set", +@hook.command("set", help="Create or set variables for substitution in input commands", help_usage={"KEY VALUE": "Define the variable named KEY and fill it with VALUE as content"}) def cmd_set(msg): @@ -191,7 +191,7 @@ def cmd_set(msg): ## Alias management -@hook("cmd_hook", "listalias", +@hook.command("listalias", help="List registered aliases", help_usage={ None: "List all registered aliases", @@ -205,7 +205,7 @@ def cmd_listalias(msg): return Response("There is no alias currently.", channel=msg.channel) -@hook("cmd_hook", "alias", +@hook.command("alias", help="Display the replacement command for a given alias") def cmd_alias(msg): if not len(msg.args): @@ -221,7 +221,7 @@ def cmd_alias(msg): return Response(res, channel=msg.channel, nick=msg.nick) -@hook("cmd_hook", "unalias", +@hook.command("unalias", help="Remove a previously created alias") def cmd_unalias(msg): if not len(msg.args): @@ -242,7 +242,7 @@ def cmd_unalias(msg): ## Alias replacement -@hook("pre_Command") +@hook.add("pre_Command") def treat_alias(msg): if msg.cmd in context.data.getNode("aliases").index: txt = context.data.getNode("aliases").index[msg.cmd]["origin"] @@ -263,7 +263,7 @@ def treat_alias(msg): return msg -@hook("ask_default") +@hook.ask() def parseask(msg): if re.match(".*(register|set|cr[ée]{2}|new|nouvel(le)?) alias.*", msg.text) is not None: result = re.match(".*alias !?([^ ]+) ?(pour|for|=|:) ?(.+)$", msg.text) diff --git a/modules/birthday.py b/modules/birthday.py index f0870ec..cb850ac 100644 --- a/modules/birthday.py +++ b/modules/birthday.py @@ -46,7 +46,7 @@ def findName(msg): ## Commands -@hook("cmd_hook", "anniv", +@hook.command("anniv", help="gives the remaining time before the anniversary of known people", help_usage={ None: "Calculate the time remaining before your birthday", @@ -80,7 +80,7 @@ def cmd_anniv(msg): msg.channel, msg.nick) -@hook("cmd_hook", "age", +@hook.command("age", help="Calculate age of known people", help_usage={ None: "Calculate your age", @@ -104,7 +104,7 @@ def cmd_age(msg): ## Input parsing -@hook("ask_default") +@hook.ask() def parseask(msg): res = re.match(r"^(\S+)\s*('s|suis|est|is|was|were)?\s+(birthday|geburtstag|née? |nee? le|born on).*$", msg.text, re.I) if res is not None: diff --git a/modules/bonneannee.py b/modules/bonneannee.py index 18ba637..b3b3934 100644 --- a/modules/bonneannee.py +++ b/modules/bonneannee.py @@ -47,9 +47,9 @@ def load(context): # MODULE INTERFACE #################################################### -@hook("cmd_hook", "newyear", +@hook.command("newyear", help="Display the remaining time before the next new year") -@hook("cmd_hook", str(yrn), +@hook.command(str(yrn), help="Display the remaining time before %d" % yrn) def cmd_newyear(msg): return Response(countdown_format(datetime(yrn, 1, 1, 0, 0, 1, 0, @@ -59,7 +59,7 @@ def cmd_newyear(msg): channel=msg.channel) -@hook("cmd_rgxp", data=yrn, regexp="^[0-9]{4}$", +@hook.command(data=yrn, regexp="^[0-9]{4}$", help="Calculate time remaining/passed before/since the requested year") def cmd_timetoyear(msg, cur): yr = int(msg.cmd) diff --git a/modules/books.py b/modules/books.py index a5ea1b3..df48056 100644 --- a/modules/books.py +++ b/modules/books.py @@ -58,7 +58,7 @@ def search_author(name): # MODULE INTERFACE #################################################### -@hook("cmd_hook", "book", +@hook.command("book", help="Get information about a book from its title", help_usage={ "TITLE": "Get information about a book titled TITLE" @@ -77,7 +77,7 @@ def cmd_book(msg): return res -@hook("cmd_hook", "search_books", +@hook.command("search_books", help="Search book's title", help_usage={ "APPROX_TITLE": "Search for a book approximately titled APPROX_TITLE" @@ -97,7 +97,7 @@ def cmd_books(msg): return res -@hook("cmd_hook", "author_books", +@hook.command("author_books", help="Looking for books writen by a given author", help_usage={ "AUTHOR": "Looking for books writen by AUTHOR" diff --git a/modules/conjugaison.py b/modules/conjugaison.py index d4405e2..25fe242 100644 --- a/modules/conjugaison.py +++ b/modules/conjugaison.py @@ -72,7 +72,7 @@ def compute_line(line, stringTens): # MODULE INTERFACE #################################################### -@hook("cmd_hook", "conjugaison", +@hook.command("conjugaison", help_usage={ "TENS VERB": "give the conjugaison for VERB in TENS." }) diff --git a/modules/ctfs.py b/modules/ctfs.py index 3e02ae9..1526cbc 100644 --- a/modules/ctfs.py +++ b/modules/ctfs.py @@ -16,7 +16,7 @@ URL = 'https://ctftime.org/event/list/upcoming' # MODULE INTERFACE #################################################### -@hook("cmd_hook", "ctfs", +@hook.command("ctfs", help="Display the upcoming CTFs") def get_info_yt(msg): soup = BeautifulSoup(getURLContent(URL)) diff --git a/modules/cve.py b/modules/cve.py index fd28181..c5e125d 100644 --- a/modules/cve.py +++ b/modules/cve.py @@ -20,7 +20,7 @@ def get_cve(cve_id): return desc[17].text.replace("\n", " ") + " Moar at " + search_url -@hook("cmd_hook", "cve") +@hook.command("cve") def get_cve_desc(msg): res = Response(channel=msg.channel) diff --git a/modules/ddg.py b/modules/ddg.py index fc70dd6..78b6022 100644 --- a/modules/ddg.py +++ b/modules/ddg.py @@ -103,7 +103,7 @@ class DDGResult: # MODULE INTERFACE #################################################### -@hook("cmd_hook", "define") +@hook.command("define") def define(msg): if not len(msg.args): raise IMException("Indicate a term to define") @@ -115,7 +115,7 @@ def define(msg): return Response(s.definition, channel=msg.channel) -@hook("cmd_hook", "search") +@hook.command("search") def search(msg): if not len(msg.args): raise IMException("Indicate a term to search") diff --git a/modules/events.py b/modules/events.py index 3354ac6..e1d25d0 100644 --- a/modules/events.py +++ b/modules/events.py @@ -38,7 +38,7 @@ def fini(d, strend): context.data.delChild(context.data.index[strend["name"]]) context.save() -@hook("cmd_hook", "goûter") +@hook.command("goûter") def cmd_gouter(msg): ndate = datetime.now(timezone.utc) ndate = datetime(ndate.year, ndate.month, ndate.day, 16, 42, 0, 0, timezone.utc) @@ -47,7 +47,7 @@ def cmd_gouter(msg): "Nous avons %s de retard pour le goûter :("), channel=msg.channel) -@hook("cmd_hook", "week-end") +@hook.command("week-end") def cmd_we(msg): ndate = datetime.now(timezone.utc) + timedelta(5 - datetime.today().weekday()) ndate = datetime(ndate.year, ndate.month, ndate.day, 0, 0, 1, 0, timezone.utc) @@ -56,7 +56,7 @@ def cmd_we(msg): "Youhou, on est en week-end depuis %s."), channel=msg.channel) -@hook("cmd_hook", "start") +@hook.command("start") def start_countdown(msg): """!start /something/: launch a timer""" if len(msg.args) < 1: @@ -135,8 +135,8 @@ def start_countdown(msg): msg.date.strftime("%A %d %B %Y à %H:%M:%S")), nick=msg.frm) -@hook("cmd_hook", "end") -@hook("cmd_hook", "forceend") +@hook.command("end") +@hook.command("forceend") def end_countdown(msg): if len(msg.args) < 1: raise IMException("quel événement terminer ?") @@ -154,7 +154,7 @@ def end_countdown(msg): else: return Response("%s n'est pas un compteur connu."% (msg.args[0]), channel=msg.channel, nick=msg.nick) -@hook("cmd_hook", "eventslist") +@hook.command("eventslist") def liste(msg): """!eventslist: gets list of timer""" if len(msg.args): @@ -169,7 +169,7 @@ def liste(msg): else: return Response("Compteurs connus : %s." % ", ".join(context.data.index.keys()), channel=msg.channel) -@hook("cmd_default") +@hook.command() def parseanswer(msg): if msg.cmd in context.data.index: res = Response(channel=msg.channel) @@ -189,7 +189,7 @@ def parseanswer(msg): RGXP_ask = re.compile(r"^.*((create|new)\s+(a|an|a\s*new|an\s*other)?\s*(events?|commande?)|(nouvel(le)?|ajoute|cr[ée]{1,3})\s+(un)?\s*([eé]v[ée]nements?|commande?)).*$", re.I) -@hook("ask_default") +@hook.ask() def parseask(msg): if RGXP_ask.match(msg.text) is not None: name = re.match("^.*!([^ \"'@!]+).*$", msg.text) diff --git a/modules/github.py b/modules/github.py index 19eadf9..1a345cd 100644 --- a/modules/github.py +++ b/modules/github.py @@ -65,7 +65,7 @@ def info_commit(repo, commit=None): quote(fullname)) -@hook("cmd_hook", "github") +@hook.command("github") def cmd_github(msg): if not len(msg.args): raise IMException("indicate a repository name to search") @@ -93,7 +93,7 @@ def cmd_github(msg): return res -@hook("cmd_hook", "github_user") +@hook.command("github_user") def cmd_github_user(msg): if not len(msg.args): raise IMException("indicate a user name to search") @@ -126,7 +126,7 @@ def cmd_github_user(msg): return res -@hook("cmd_hook", "github_issue") +@hook.command("github_issue") def cmd_github_issue(msg): if not len(msg.args): raise IMException("indicate a repository to view its issues") @@ -164,7 +164,7 @@ def cmd_github_issue(msg): return res -@hook("cmd_hook", "github_commit") +@hook.command("github_commit") def cmd_github_commit(msg): if not len(msg.args): raise IMException("indicate a repository to view its commits") diff --git a/modules/imdb.py b/modules/imdb.py index adea1d8..1e6c6e9 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -68,7 +68,7 @@ def find_movies(title): raise IMException("An error occurs during movie search") -@hook("cmd_hook", "imdb") +@hook.command("imdb") def cmd_imdb(msg): """View movie details with !imdb """ if not len(msg.args): @@ -97,7 +97,7 @@ def cmd_imdb(msg): return res -@hook("cmd_hook", "imdbs") +@hook.command("imdbs") def cmd_search(msg): """!imdbs <approximative title> to search a movie title""" if not len(msg.args): diff --git a/modules/jsonbot.py b/modules/jsonbot.py index c69cca2..fe25187 100644 --- a/modules/jsonbot.py +++ b/modules/jsonbot.py @@ -39,7 +39,7 @@ def getJsonKeys(data): else: return data.keys() -@hook("cmd_hook", "json") +@hook.command("json") def get_json_info(msg): if not len(msg.args): raise IMException("Please specify a url and a list of JSON keys.") diff --git a/modules/man.py b/modules/man.py index 7e7b715..997b85b 100644 --- a/modules/man.py +++ b/modules/man.py @@ -19,7 +19,7 @@ def help_full(): RGXP_s = re.compile(b'\x1b\\[[0-9]+m') -@hook("cmd_hook", "MAN") +@hook.command("MAN") def cmd_man(msg): args = ["man"] num = None @@ -52,7 +52,7 @@ def cmd_man(msg): return res -@hook("cmd_hook", "man") +@hook.command("man") def cmd_whatis(msg): args = ["whatis", " ".join(msg.args)] diff --git a/modules/mapquest.py b/modules/mapquest.py index 40bd40f..2c42ad7 100644 --- a/modules/mapquest.py +++ b/modules/mapquest.py @@ -43,7 +43,7 @@ def where(loc): "{adminArea1}".format(**loc)).strip() -@hook("cmd_hook", "geocode") +@hook.command("geocode") def cmd_geocode(msg): if not len(msg.args): raise IMException("indicate a name") diff --git a/modules/more.py b/modules/more.py index c8b80a9..4742dfe 100644 --- a/modules/more.py +++ b/modules/more.py @@ -239,7 +239,7 @@ SERVERS = dict() # MODULE INTERFACE #################################################### -@hook("all_post") +@hook.post() def parseresponse(res): # TODO: handle inter-bot communication NOMORE # TODO: check that the response is not the one already saved @@ -256,7 +256,7 @@ def parseresponse(res): return res -@hook("cmd_hook", "more") +@hook.command("more") def cmd_more(msg): """Display next chunck of the message""" res = list() @@ -272,7 +272,7 @@ def cmd_more(msg): return res -@hook("cmd_hook", "next") +@hook.command("next") def cmd_next(msg): """Display the next information include in the message""" res = list() diff --git a/modules/networking/__init__.py b/modules/networking/__init__.py index 26d6470..f0df094 100644 --- a/modules/networking/__init__.py +++ b/modules/networking/__init__.py @@ -38,7 +38,7 @@ def load(context): # MODULE INTERFACE #################################################### -@hook("cmd_hook", "title", +@hook.command("title", help="Retrieve webpage's title", help_usage={"URL": "Display the title of the given URL"}) def cmd_title(msg): @@ -54,7 +54,7 @@ def cmd_title(msg): return Response("%s: %s" % (url, res.group(1).replace("\n", " ")), channel=msg.channel) -@hook("cmd_hook", "curly", +@hook.command("curly", help="Retrieve webpage's headers", help_usage={"URL": "Display HTTP headers of the given URL"}) def cmd_curly(msg): @@ -67,7 +67,7 @@ def cmd_curly(msg): return Response("Entêtes de la page %s : HTTP/%s, statut : %d %s ; headers : %s" % (url, version, status, reason, ", ".join(["\x03\x02" + h + "\x03\x02: " + v for h, v in headers])), channel=msg.channel) -@hook("cmd_hook", "curl", +@hook.command("curl", help="Retrieve webpage's body", help_usage={"URL": "Display raw HTTP body of the given URL"}) def cmd_curl(msg): @@ -80,7 +80,7 @@ def cmd_curl(msg): return res -@hook("cmd_hook", "w3m", +@hook.command("w3m", help="Retrieve and format webpage's content", help_usage={"URL": "Display and format HTTP content of the given URL"}) def cmd_w3m(msg): @@ -92,7 +92,7 @@ def cmd_w3m(msg): return res -@hook("cmd_hook", "traceurl", +@hook.command("traceurl", help="Follow redirections of a given URL and display each step", help_usage={"URL": "Display redirections steps for the given URL"}) def cmd_traceurl(msg): @@ -109,7 +109,7 @@ def cmd_traceurl(msg): return res -@hook("cmd_hook", "isup", +@hook.command("isup", help="Check if a website is up", help_usage={"DOMAIN": "Check if a DOMAIN is up"}) def cmd_isup(msg): @@ -126,7 +126,7 @@ def cmd_isup(msg): return res -@hook("cmd_hook", "w3c", +@hook.command("w3c", help="Perform a w3c HTML validator check", help_usage={"URL": "Do W3C HTML validation on the given URL"}) def cmd_w3c(msg): @@ -149,10 +149,10 @@ def cmd_w3c(msg): -@hook("cmd_hook", "watch", data="diff", +@hook.command("watch", data="diff", help="Alert on webpage change", help_usage={"URL": "Watch the given URL and alert when it changes"}) -@hook("cmd_hook", "updown", data="updown", +@hook.command("updown", data="updown", help="Alert on server availability change", help_usage={"URL": "Watch the given domain and alert when it availability status changes"}) def cmd_watch(msg, diffType="diff"): @@ -162,7 +162,7 @@ def cmd_watch(msg, diffType="diff"): return watchWebsite.add_site(msg.args[0], msg.frm, msg.channel, msg.server, diffType) -@hook("cmd_hook", "listwatch", +@hook.command("listwatch", help="List URL watched for the channel", help_usage={None: "List URL watched for the channel"}) def cmd_listwatch(msg): @@ -173,7 +173,7 @@ def cmd_listwatch(msg): return Response("No URL are currently watched. Use !watch URL to watch one.", channel=msg.channel) -@hook("cmd_hook", "unwatch", +@hook.command("unwatch", help="Unwatch a previously watched URL", help_usage={"URL": "Unwatch the given URL"}) def cmd_unwatch(msg): diff --git a/modules/networking/whois.py b/modules/networking/whois.py index 0b8eb9f..b185cf8 100644 --- a/modules/networking/whois.py +++ b/modules/networking/whois.py @@ -21,9 +21,9 @@ def load(CONF, add_hook): URL_WHOIS = URL_WHOIS % (urllib.parse.quote(CONF.getNode("whoisxmlapi")["username"]), urllib.parse.quote(CONF.getNode("whoisxmlapi")["password"])) import nemubot.hooks - add_hook("cmd_hook", nemubot.hooks.Message(cmd_whois, "netwhois", - help="Get whois information about given domains", - help_usage={"DOMAIN": "Return whois information on the given DOMAIN"})) + add_hook("in_Command", nemubot.hooks.Command(cmd_whois, "netwhois", + help="Get whois information about given domains", + help_usage={"DOMAIN": "Return whois information on the given DOMAIN"})) def extractdate(str): diff --git a/modules/news.py b/modules/news.py index dccc77e..a8fb8de 100644 --- a/modules/news.py +++ b/modules/news.py @@ -41,7 +41,7 @@ def get_last_news(url): # MODULE INTERFACE #################################################### -@hook("cmd_hook", "news") +@hook.command("news") def cmd_news(msg): if not len(msg.args): raise IMException("Indicate the URL to visit.") diff --git a/modules/nextstop/__init__.py b/modules/nextstop/__init__.py index 9a0e5c7..9530ab8 100644 --- a/modules/nextstop/__init__.py +++ b/modules/nextstop/__init__.py @@ -14,7 +14,7 @@ def help_full (): return "!ratp transport line [station]: Donne des informations sur les prochains passages du transport en commun séléctionné à l'arrêt désiré. Si aucune station n'est précisée, les liste toutes." -@hook("cmd_hook", "ratp") +@hook.command("ratp") def ask_ratp(msg): """Hook entry from !ratp""" if len(msg.args) >= 3: @@ -44,7 +44,7 @@ def ask_ratp(msg): else: raise IMException("Mauvais usage, merci de spécifier un type de transport et une ligne, ou de consulter l'aide du module.") -@hook("cmd_hook", "ratp_alert") +@hook.command("ratp_alert") def ratp_alert(msg): if len(msg.args) == 2: transport = msg.args[0] diff --git a/modules/rnd.py b/modules/rnd.py index f1f3721..32c2adf 100644 --- a/modules/rnd.py +++ b/modules/rnd.py @@ -15,7 +15,7 @@ from more import Response # MODULE INTERFACE #################################################### -@hook("cmd_hook", "choice") +@hook.command("choice") def cmd_choice(msg): if not len(msg.args): raise IMException("indicate some terms to pick!") @@ -25,7 +25,7 @@ def cmd_choice(msg): nick=msg.nick) -@hook("cmd_hook", "choicecmd") +@hook.command("choicecmd") def cmd_choicecmd(msg): if not len(msg.args): raise IMException("indicate some command to pick!") diff --git a/modules/sap.py b/modules/sap.py index a7d65cf..8691d6a 100644 --- a/modules/sap.py +++ b/modules/sap.py @@ -19,7 +19,7 @@ def help_full(): return "Retrieve SAP transaction codes and details using tcodes or keywords: !tcode <transaction code|keywords>" -@hook("cmd_hook", "tcode") +@hook.command("tcode") def cmd_tcode(msg): if not len(msg.args): raise IMException("indicate a transaction code or " diff --git a/modules/sleepytime.py b/modules/sleepytime.py index aef2db3..715b3b9 100644 --- a/modules/sleepytime.py +++ b/modules/sleepytime.py @@ -19,7 +19,7 @@ def help_full(): " hh:mm") -@hook("cmd_hook", "sleepytime") +@hook.command("sleepytime") def cmd_sleep(msg): if len(msg.args) and re.match("[0-9]{1,2}[h':.,-]([0-9]{1,2})?[m'\":.,-]?", msg.args[0]) is not None: diff --git a/modules/sms.py b/modules/sms.py index 103a938..3a9727f 100644 --- a/modules/sms.py +++ b/modules/sms.py @@ -47,7 +47,7 @@ def send_sms(frm, api_usr, api_key, content): return None -@hook("cmd_hook", "sms") +@hook.command("sms") def cmd_sms(msg): if not len(msg.args): raise IMException("À qui veux-tu envoyer ce SMS ?") @@ -80,7 +80,7 @@ def cmd_sms(msg): apiuser_ask = re.compile(r"(utilisateur|user|numéro|numero|compte|abonne|abone|abonné|account)\s+(est|is)\s+(?P<user>[0-9]{7,})", re.IGNORECASE) apikey_ask = re.compile(r"(clef|key|password|mot de passe?)\s+(?:est|is)?\s+(?P<key>[a-zA-Z0-9]{10,})", re.IGNORECASE) -@hook("ask_default") +@hook.ask() def parseask(msg): if msg.text.find("Free") >= 0 and ( msg.text.find("API") >= 0 or msg.text.find("api") >= 0) and ( diff --git a/modules/spell/__init__.py b/modules/spell/__init__.py index fe5aadd..ca2c834 100644 --- a/modules/spell/__init__.py +++ b/modules/spell/__init__.py @@ -23,7 +23,7 @@ def help_full(): def load(context): context.data.setIndex("name", "score") -@hook("cmd_hook", "spell") +@hook.command("spell") def cmd_spell(msg): if not len(msg.args): raise IMException("indique une orthographe approximative du mot dont tu veux vérifier l'orthographe.") @@ -61,7 +61,7 @@ def add_score(nick, t): context.data.index[nick][t] = 1 context.save() -@hook("cmd_hook", "spellscore") +@hook.command("spellscore") def cmd_score(msg): res = list() unknown = list() diff --git a/modules/suivi.py b/modules/suivi.py index a0964ac..55c469f 100644 --- a/modules/suivi.py +++ b/modules/suivi.py @@ -154,7 +154,7 @@ TRACKING_HANDLERS = { # HOOKS ############################################################## -@hook("cmd_hook", "track", +@hook.command("track", help="Track postage delivery", help_usage={ "TRACKING_ID [...]": "Track the specified postage IDs on various tracking services." diff --git a/modules/syno.py b/modules/syno.py index 10bb764..650e7e9 100644 --- a/modules/syno.py +++ b/modules/syno.py @@ -72,8 +72,8 @@ def get_english_synos(key, word): lang_binding = { 'fr': get_french_synos } -@hook("cmd_hook", "synonymes", data="synonymes") -@hook("cmd_hook", "antonymes", data="antonymes") +@hook.command("synonymes", data="synonymes") +@hook.command("antonymes", data="antonymes") def go(msg, what): if not len(msg.args): raise IMException("de quel mot veux-tu connaître la liste des synonymes ?") diff --git a/modules/tpb.py b/modules/tpb.py index 7d30ee1..ce98b04 100644 --- a/modules/tpb.py +++ b/modules/tpb.py @@ -22,7 +22,7 @@ def load(context): global URL_TPBAPI URL_TPBAPI = context.config["url"] -@hook("cmd_hook", "tpb") +@hook.command("tpb") def cmd_tpb(msg): if not len(msg.args): raise IMException("indicate an item to search!") diff --git a/modules/translate.py b/modules/translate.py index bbca24a..9d50966 100644 --- a/modules/translate.py +++ b/modules/translate.py @@ -79,7 +79,7 @@ def translate(term, langFrom="en", langTo="fr"): # MODULE INTERFACE #################################################### -@hook("cmd_hook", "translate", +@hook.command("translate", help="Word translation using WordReference.com", help_usage={ "TERM": "Found translation of TERM from/to english to/from <lang>." diff --git a/modules/urbandict.py b/modules/urbandict.py index 135d240..e90c096 100644 --- a/modules/urbandict.py +++ b/modules/urbandict.py @@ -20,7 +20,7 @@ def search(terms): # MODULE INTERFACE #################################################### -@hook("cmd_hook", "urbandictionnary") +@hook.command("urbandictionnary") def udsearch(msg): if not len(msg.args): raise IMException("Indicate a term to search") diff --git a/modules/velib.py b/modules/velib.py index aad5939..8ef6833 100644 --- a/modules/velib.py +++ b/modules/velib.py @@ -66,7 +66,7 @@ def print_station_status(msg, station): # MODULE INTERFACE #################################################### -@hook("cmd_hook", "velib", +@hook.command("velib", help="gives available bikes and slots at the given station", help_usage={ "STATION_ID": "gives available bikes and slots at the station STATION_ID" diff --git a/modules/weather.py b/modules/weather.py index 1d9cf13..34a861a 100644 --- a/modules/weather.py +++ b/modules/weather.py @@ -136,7 +136,7 @@ def get_json_weather(coords): return wth -@hook("cmd_hook", "coordinates") +@hook.command("coordinates") def cmd_coordinates(msg): if len(msg.args) < 1: raise IMException("indique-moi un nom de ville.") @@ -149,7 +149,7 @@ def cmd_coordinates(msg): return Response("Les coordonnées de %s sont %s,%s" % (msg.args[0], coords["lat"], coords["long"]), channel=msg.channel) -@hook("cmd_hook", "alert") +@hook.command("alert") def cmd_alert(msg): loc, coords, specific = treat_coord(msg) wth = get_json_weather(coords) @@ -163,7 +163,7 @@ def cmd_alert(msg): return res -@hook("cmd_hook", "météo") +@hook.command("météo") def cmd_weather(msg): loc, coords, specific = treat_coord(msg) wth = get_json_weather(coords) @@ -217,7 +217,7 @@ def cmd_weather(msg): gps_ask = re.compile(r"^\s*(?P<city>.*\w)\s*(?:(?:se|est)\s+(?:trouve|situ[ée]*)\s+[aà])\s*(?P<lat>-?[0-9]+(?:[,.][0-9]+))[^0-9.](?P<long>-?[0-9]+(?:[,.][0-9]+))\s*$", re.IGNORECASE) -@hook("ask_default") +@hook.ask() def parseask(msg): res = gps_ask.match(msg.text) if res is not None: diff --git a/modules/whois.py b/modules/whois.py index 4c43500..4a13e9c 100644 --- a/modules/whois.py +++ b/modules/whois.py @@ -30,8 +30,8 @@ def load(context): context.data.getNode("pics").setIndex("login", "pict") import nemubot.hooks - context.add_hook("cmd_hook", - nemubot.hooks.Message(cmd_whois, "whois")) + context.add_hook("in_Command", + nemubot.hooks.Command(cmd_whois, "whois")) class Login: @@ -87,7 +87,7 @@ def cmd_whois(msg): res.append_message("Unknown %s :(" % srch) return res -@hook("cmd_hook", "nicks") +@hook.command("nicks") def cmd_nicks(msg): if len(msg.args) < 1: raise IMException("Provide a login") @@ -106,7 +106,7 @@ def cmd_nicks(msg): else: return Response("%s has no known alias." % nick, channel=msg.channel) -@hook("ask_default") +@hook.ask() def parseask(msg): res = re.match(r"^(\S+)\s*('s|suis|est|is|was|were)\s+([a-zA-Z0-9_-]{3,8})$", msg.text, re.I) if res is not None: diff --git a/modules/wolframalpha.py b/modules/wolframalpha.py index e8421a3..a83b500 100644 --- a/modules/wolframalpha.py +++ b/modules/wolframalpha.py @@ -92,7 +92,7 @@ class WFAResults: # MODULE INTERFACE #################################################### -@hook("cmd_hook", "calculate", +@hook.command("calculate", help="Perform search and calculation using WolframAlpha", help_usage={ "TERM": "Look at the given term on WolframAlpha", diff --git a/modules/worldcup.py b/modules/worldcup.py index 87a182c..512a247 100644 --- a/modules/worldcup.py +++ b/modules/worldcup.py @@ -38,7 +38,7 @@ def start_watch(msg): context.save() raise IMException("This channel is now watching world cup events!") -@hook("cmd_hook", "watch_worldcup") +@hook.command("watch_worldcup") def cmd_watch(msg): # Get current state @@ -177,7 +177,7 @@ def get_matches(url): if is_valid(match): yield match -@hook("cmd_hook", "worldcup") +@hook.command("worldcup") def cmd_worldcup(msg): res = Response(channel=msg.channel, nomore="No more match to display", count=" (%d more matches)") diff --git a/nemubot/bot.py b/nemubot/bot.py index 32a2f22..b3dfbc1 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -79,7 +79,7 @@ class Bot(threading.Thread): 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.Message(in_echo, "echo"), "in", "Command") + self.treater.hm.add_hook(nemubot.hooks.Command(in_echo, "echo"), "in", "Command") def _help_msg(msg): """Parse and response to help messages""" @@ -98,7 +98,7 @@ class Bot(threading.Thread): elif msg.args[0][0] == "!": for module in self.modules: for (s, h) in self.modules[module].__nemubot_context__.hooks: - if s == "in_Command" and (h.name is not None or h.regexp is not None) and h.is_matching(msg.args[0][1:]): + if s == "in_Command" and (h.name is not None or h.regexp is not None) and ((h.name is not None and msg.args[0][1:] == h.name) or (h.regexp is not None and re.match(h.regexp, 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() @@ -128,7 +128,7 @@ class Bot(threading.Thread): " 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.Message(_help_msg, "help"), "in", "Command") + self.treater.hm.add_hook(nemubot.hooks.Command(_help_msg, "help"), "in", "Command") from queue import Queue # Messages to be treated @@ -462,9 +462,9 @@ class Bot(threading.Thread): # Register decorated functions import nemubot.hooks - for s, h in nemubot.hooks.last_registered: + for s, h in nemubot.hooks.hook.last_registered: module.__nemubot_context__.add_hook(s, h) - nemubot.hooks.last_registered = [] + nemubot.hooks.hook.last_registered = [] # Launch the module if hasattr(module, "load"): diff --git a/nemubot/hooks/__init__.py b/nemubot/hooks/__init__.py index a9a8a31..9904119 100644 --- a/nemubot/hooks/__init__.py +++ b/nemubot/hooks/__init__.py @@ -14,29 +14,54 @@ # 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 nemubot.hooks.abstract import Abstract +from nemubot.hooks.command import Command from nemubot.hooks.message import Message -last_registered = [] + +class hook: + + last_registered = [] -def hook(store, *args, **kargs): - """Function used as a decorator for module loading""" - def sec(call): - last_registered.append((store, Message(call, *args, **kargs))) - return call - return sec + def _add(store, h, *args, **kwargs): + """Function used as a decorator for module loading""" + def sec(call): + hook.last_registered.append((store, h(call, *args, **kwargs))) + return call + return sec + + + def add(store, *args, **kwargs): + return hook._add(store, Abstract, *args, **kwargs) + + def ask(*args, store="in_DirectAsk", **kwargs): + return hook._add(store, Message, *args, **kwargs) + + def command(*args, store="in_Command", **kwargs): + return hook._add(store, Command, *args, **kwargs) + + def message(*args, store="in_Text", **kwargs): + return hook._add(store, Message, *args, **kwargs) + + def post(*args, store="post", **kwargs): + return hook._add(store, Abstract, *args, **kwargs) + + def pre(*args, store="pre", **kwargs): + return hook._add(store, Abstract, *args, **kwargs) def reload(): - global Message import imp import nemubot.hooks.abstract imp.reload(nemubot.hooks.abstract) + import nemubot.hooks.command + imp.reload(nemubot.hooks.command) + import nemubot.hooks.message imp.reload(nemubot.hooks.message) - Message = nemubot.hooks.message.Message import nemubot.hooks.keywords imp.reload(nemubot.hooks.keywords) diff --git a/nemubot/hooks/abstract.py b/nemubot/hooks/abstract.py index e2dc78b..25efc45 100644 --- a/nemubot/hooks/abstract.py +++ b/nemubot/hooks/abstract.py @@ -87,6 +87,10 @@ class Abstract: return False + def __str__(self): + return "" + + def can_write(self, receivers=list(), server=None): return True diff --git a/nemubot/hooks/command.py b/nemubot/hooks/command.py new file mode 100644 index 0000000..02fdb4d --- /dev/null +++ b/nemubot/hooks/command.py @@ -0,0 +1,65 @@ +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2015 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/>. + +import re + +from nemubot.hooks.message import Message +from nemubot.hooks.keywords import NoKeyword +from nemubot.hooks.keywords.abstract import Abstract as AbstractKeywords +from nemubot.hooks.keywords.dict import Dict as DictKeywords +import nemubot.message + + +class Command(Message): + + """Class storing hook information, specialized for Command messages""" + + def __init__(self, call, name=None, help_usage=dict(), keywords=NoKeyword(), + **kargs): + + super().__init__(call=call, **kargs) + + if isinstance(keywords, dict): + keywords = DictKeywords(keywords) + + assert type(help_usage) is dict, help_usage + assert isinstance(keywords, AbstractKeywords), keywords + + self.name = str(name) if name is not None else None + self.help_usage = help_usage + self.keywords = keywords + + + def __str__(self): + return "\x03\x02%s\x03\x02%s%s" % ( + self.name if self.name is not None else "\x03\x1f" + self.regexp + "\x03\x1f" if self.regexp is not None else "", + " (restricted to %:%s)" % ((",".join(self.servers) if self.server else "*") + (",".join(self.channels) if self.channels else "*")) if len(self.channels) or len(self.servers) else "", + ": %s" % self.help if self.help is not None else "" + ) + + + def check(self, msg): + return self.keywords.check(msg.kwargs) and super().check(msg) + + + def match(self, msg): + if not isinstance(msg, nemubot.message.command.Command): + return False + else: + return ( + (self.name is None or msg.cmd == self.name) and + (self.regexp is None or re.match(self.regexp, msg.cmd)) + ) diff --git a/nemubot/hooks/message.py b/nemubot/hooks/message.py index a14177a..1c245ea 100644 --- a/nemubot/hooks/message.py +++ b/nemubot/hooks/message.py @@ -17,9 +17,6 @@ import re from nemubot.hooks.abstract import Abstract -from nemubot.hooks.keywords import NoKeyword -from nemubot.hooks.keywords.abstract import Abstract as AbstractKeywords -from nemubot.hooks.keywords.dict import Dict as DictKeywords import nemubot.message @@ -27,64 +24,26 @@ class Message(Abstract): """Class storing hook information, specialized for a generic Message""" - def __init__(self, call, name=None, regexp=None, channels=list(), - server=None, help=None, help_usage=dict(), keywords=NoKeyword(), - **kargs): - - Abstract.__init__(self, call=call, **kargs) - - if isinstance(keywords, dict): - keywords = DictKeywords(keywords) + def __init__(self, call, regexp=None, help=None, **kwargs): + super().__init__(call=call, **kwargs) assert regexp is None or type(regexp) is str, regexp - assert channels is None or type(channels) is list, channels - assert server is None or type(server) is str, server - assert type(help_usage) is dict, help_usage - assert isinstance(keywords, AbstractKeywords), keywords - self.name = str(name) if name is not None else None self.regexp = regexp - self.server = server - self.channels = channels self.help = help - self.help_usage = help_usage - self.keywords = keywords def __str__(self): - return "\x03\x02%s\x03\x02%s%s" % ( - self.name if self.name is not None else "\x03\x1f" + self.regexp + "\x03\x1f" if self.regexp is not None else "", - " (restricted to %:%s)" % ((",".join(self.servers) if self.server else "*") + (",".join(self.channels) if self.channels else "*")) if len(self.channels) or len(self.server) else "", - ": %s" % self.help if self.help is not None else "" - ) + # TODO: find a way to name the feature (like command: help) + return self.help if self.help is not None else super().__str__() def check(self, msg): - return not hasattr(msg, "kwargs") or self.keywords.check(msg.kwargs) + return super().check(msg) - def match(self, msg, server=None): - if not isinstance(msg, nemubot.message.abstract.Abstract): - return True - - elif isinstance(msg, nemubot.message.Command): - return self.is_matching(msg.cmd, msg.to, server) - elif isinstance(msg, nemubot.message.Text): - return self.is_matching(msg.message, msg.to, server) - else: + def match(self, msg): + if not isinstance(msg, nemubot.message.text.Text): return False - - - def is_matching(self, strcmp, receivers=list(), server=None): - """Test if the current hook correspond to the message""" - if ((server is None or self.server is None or self.server == server) - and ((self.name is None or strcmp == self.name) and ( - self.regexp is None or re.match(self.regexp, strcmp)))): - - if receivers and self.channels: - for receiver in receivers: - if receiver in self.channels: - return True - else: - return True - return False + else: + return self.regexp is None or re.match(self.regexp, msg.message) diff --git a/nemubot/modulecontext.py b/nemubot/modulecontext.py index 5b47278..b24d94d 100644 --- a/nemubot/modulecontext.py +++ b/nemubot/modulecontext.py @@ -14,21 +14,6 @@ # 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/>. -def convert_legacy_store(old): - if old == "cmd_hook" or old == "cmd_rgxp" or old == "cmd_default": - return "in_Command" - elif old == "ask_hook" or old == "ask_rgxp" or old == "ask_default": - return "in_DirectAsk" - elif old == "msg_hook" or old == "msg_rgxp" or old == "msg_default": - return "in_Text" - elif old == "all_post": - return "post" - elif old == "all_pre": - return "pre" - else: - return old - - class ModuleContext: def __init__(self, context, module): @@ -60,11 +45,9 @@ class ModuleContext: self.data = context.datastore.load(module_name) def add_hook(store, hook): - store = convert_legacy_store(store) self.hooks.append((store, hook)) return context.treater.hm.add_hook(hook, store) def del_hook(store, hook): - store = convert_legacy_store(store) self.hooks.remove((store, hook)) return context.treater.hm.del_hook(hook, store) def call_hook(store, msg): @@ -98,10 +81,8 @@ class ModuleContext: self.data = module_state.ModuleState("nemubotstate") def add_hook(store, hook): - store = convert_legacy_store(store) self.hooks.append((store, hook)) def del_hook(store, hook): - store = convert_legacy_store(store) self.hooks.remove((store, hook)) def call_hook(store, msg): # TODO: what can we do here? From c06fb69c8b5323d18a31bb8312cd95f50034e917 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 3 Nov 2015 08:08:39 +0100 Subject: [PATCH 041/271] Extract tools.config as config module --- nemubot/__init__.py | 5 + nemubot/bot.py | 11 +- nemubot/config/__init__.py | 47 ++++++++ nemubot/config/include.py | 20 ++++ nemubot/config/module.py | 26 ++++ nemubot/config/nemubot.py | 46 +++++++ nemubot/config/server.py | 45 +++++++ nemubot/tools/config.py | 159 ------------------------- nemubot/tools/xmlparser/genericnode.py | 73 ++++++++++++ 9 files changed, 271 insertions(+), 161 deletions(-) create mode 100644 nemubot/config/__init__.py create mode 100644 nemubot/config/include.py create mode 100644 nemubot/config/module.py create mode 100644 nemubot/config/nemubot.py create mode 100644 nemubot/config/server.py delete mode 100644 nemubot/tools/config.py create mode 100644 nemubot/tools/xmlparser/genericnode.py diff --git a/nemubot/__init__.py b/nemubot/__init__.py index 193ad53..d0a2072 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -47,6 +47,11 @@ def reload(): import nemubot.channel imp.reload(nemubot.channel) + import nemubot.config + imp.reload(nemubot.config) + + nemubot.config.reload() + import nemubot.consumer imp.reload(nemubot.consumer) diff --git a/nemubot/bot.py b/nemubot/bot.py index b3dfbc1..54acdee 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -225,11 +225,18 @@ class Bot(threading.Thread): if not os.path.isfile(filename): return self.import_module(filename) - from nemubot.tools.config import config_nodes + from nemubot.channel import Channel + from nemubot import config from nemubot.tools.xmlparser import XMLParser try: - p = XMLParser(config_nodes) + 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 " diff --git a/nemubot/config/__init__.py b/nemubot/config/__init__.py new file mode 100644 index 0000000..497bd9e --- /dev/null +++ b/nemubot/config/__init__.py @@ -0,0 +1,47 @@ +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2015 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/>. + +def get_boolean(s): + if isinstance(s, bool): + return s + else: + return (s and s != "0" and s.lower() != "false" and s.lower() != "off") + +from nemubot.config.include import Include +from nemubot.config.module import Module +from nemubot.config.nemubot import Nemubot +from nemubot.config.server import Server + +def reload(): + global Include, Module, Nemubot, Server + + import imp + + import nemubot.config.include + imp.reload(nemubot.config.include) + Include = nemubot.config.include.Include + + import nemubot.config.module + imp.reload(nemubot.config.module) + Module = nemubot.config.module.Module + + import nemubot.config.nemubot + imp.reload(nemubot.config.nemubot) + Nemubot = nemubot.config.nemubot.Nemubot + + import nemubot.config.server + imp.reload(nemubot.config.server) + Server = nemubot.config.server.Server diff --git a/nemubot/config/include.py b/nemubot/config/include.py new file mode 100644 index 0000000..40bea9a --- /dev/null +++ b/nemubot/config/include.py @@ -0,0 +1,20 @@ +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2015 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/>. + +class Include: + + def __init__(self, path): + self.path = path diff --git a/nemubot/config/module.py b/nemubot/config/module.py new file mode 100644 index 0000000..670e97b --- /dev/null +++ b/nemubot/config/module.py @@ -0,0 +1,26 @@ +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2015 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 nemubot.config import get_boolean +from nemubot.tools.xmlparser.genericnode import GenericNode + + +class Module(GenericNode): + + def __init__(self, name, autoload=True, **kwargs): + super().__init__(None, **kwargs) + self.name = name + self.autoload = get_boolean(autoload) diff --git a/nemubot/config/nemubot.py b/nemubot/config/nemubot.py new file mode 100644 index 0000000..a2548a4 --- /dev/null +++ b/nemubot/config/nemubot.py @@ -0,0 +1,46 @@ +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2015 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 nemubot.config.include import Include +from nemubot.config.module import Module +from nemubot.config.server import Server + + +class Nemubot: + + def __init__(self, nick="nemubot", realname="nemubot", owner=None, + ip=None, ssl=False, caps=None, encoding="utf-8"): + self.nick = nick + self.realname = realname + self.owner = owner + self.ip = ip + self.caps = caps.split(" ") if caps is not None else [] + self.encoding = encoding + self.servers = [] + self.modules = [] + self.includes = [] + + + def addChild(self, name, child): + if name == "module" and isinstance(child, Module): + self.modules.append(child) + return True + elif name == "server" and isinstance(child, Server): + self.servers.append(child) + return True + elif name == "include" and isinstance(child, Include): + self.includes.append(child) + return True diff --git a/nemubot/config/server.py b/nemubot/config/server.py new file mode 100644 index 0000000..c856649 --- /dev/null +++ b/nemubot/config/server.py @@ -0,0 +1,45 @@ +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2015 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 nemubot.channel import Channel + + +class Server: + + def __init__(self, uri="irc://nemubot@localhost/", autoconnect=True, caps=None, **kwargs): + self.uri = uri + self.autoconnect = autoconnect + self.caps = caps.split(" ") if caps is not None else [] + self.args = kwargs + self.channels = [] + + + def addChild(self, name, child): + if name == "channel" and isinstance(child, Channel): + self.channels.append(child) + return True + + + def server(self, parent): + from nemubot.server import factory + + for a in ["nick", "owner", "realname", "encoding"]: + if a not in self.args: + self.args[a] = getattr(parent, a) + + self.caps += parent.caps + + return factory(self.uri, caps=self.caps, channels=self.channels, **self.args) diff --git a/nemubot/tools/config.py b/nemubot/tools/config.py deleted file mode 100644 index f1305a7..0000000 --- a/nemubot/tools/config.py +++ /dev/null @@ -1,159 +0,0 @@ -# Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 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/>. - -def get_boolean(s): - if isinstance(s, bool): - return s - else: - return (s and s != "0" and s.lower() != "false" and s.lower() != "off") - - -class GenericNode: - - def __init__(self, tag, **kwargs): - self.tag = tag - self.attrs = kwargs - self.content = "" - self.children = [] - self._cur = None - self._deep_cur = 0 - - - def startElement(self, name, attrs): - if self._cur is None: - self._cur = GenericNode(name, **attrs) - self._deep_cur = 0 - else: - self._deep_cur += 1 - self._cur.startElement(name, attrs) - return True - - - def characters(self, content): - if self._cur is None: - self.content += content - else: - self._cur.characters(content) - - - def endElement(self, name): - if name is None: - return - - if self._deep_cur: - self._deep_cur -= 1 - self._cur.endElement(name) - else: - self.children.append(self._cur) - self._cur = None - return True - - - def hasNode(self, nodename): - return self.getNode(nodename) is not None - - - def getNode(self, nodename): - for c in self.children: - if c is not None and c.tag == nodename: - return c - return None - - - def __getitem__(self, item): - return self.attrs[item] - - def __contains__(self, item): - return item in self.attrs - - -class NemubotConfig: - - def __init__(self, nick="nemubot", realname="nemubot", owner=None, - ip=None, ssl=False, caps=None, encoding="utf-8"): - self.nick = nick - self.realname = realname - self.owner = owner - self.ip = ip - self.caps = caps.split(" ") if caps is not None else [] - self.encoding = encoding - self.servers = [] - self.modules = [] - self.includes = [] - - - def addChild(self, name, child): - if name == "module" and isinstance(child, ModuleConfig): - self.modules.append(child) - return True - elif name == "server" and isinstance(child, ServerConfig): - self.servers.append(child) - return True - elif name == "include" and isinstance(child, IncludeConfig): - self.includes.append(child) - return True - - -class ServerConfig: - - def __init__(self, uri="irc://nemubot@localhost/", autoconnect=True, caps=None, **kwargs): - self.uri = uri - self.autoconnect = autoconnect - self.caps = caps.split(" ") if caps is not None else [] - self.args = kwargs - self.channels = [] - - - def addChild(self, name, child): - if name == "channel" and isinstance(child, Channel): - self.channels.append(child) - return True - - - def server(self, parent): - from nemubot.server import factory - - for a in ["nick", "owner", "realname", "encoding"]: - if a not in self.args: - self.args[a] = getattr(parent, a) - - self.caps += parent.caps - - return factory(self.uri, caps=self.caps, channels=self.channels, **self.args) - - -class IncludeConfig: - - def __init__(self, path): - self.path = path - - -class ModuleConfig(GenericNode): - - def __init__(self, name, autoload=True, **kwargs): - super(ModuleConfig, self).__init__(None, **kwargs) - self.name = name - self.autoload = get_boolean(autoload) - -from nemubot.channel import Channel - -config_nodes = { - "nemubotconfig": NemubotConfig, - "server": ServerConfig, - "channel": Channel, - "module": ModuleConfig, - "include": IncludeConfig, -} diff --git a/nemubot/tools/xmlparser/genericnode.py b/nemubot/tools/xmlparser/genericnode.py new file mode 100644 index 0000000..efbdda9 --- /dev/null +++ b/nemubot/tools/xmlparser/genericnode.py @@ -0,0 +1,73 @@ +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2015 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/>. + +class GenericNode: + + def __init__(self, tag, **kwargs): + self.tag = tag + self.attrs = kwargs + self.content = "" + self.children = [] + self._cur = None + self._deep_cur = 0 + + + def startElement(self, name, attrs): + if self._cur is None: + self._cur = GenericNode(name, **attrs) + self._deep_cur = 0 + else: + self._deep_cur += 1 + self._cur.startElement(name, attrs) + return True + + + def characters(self, content): + if self._cur is None: + self.content += content + else: + self._cur.characters(content) + + + def endElement(self, name): + if name is None: + return + + if self._deep_cur: + self._deep_cur -= 1 + self._cur.endElement(name) + else: + self.children.append(self._cur) + self._cur = None + return True + + + def hasNode(self, nodename): + return self.getNode(nodename) is not None + + + def getNode(self, nodename): + for c in self.children: + if c is not None and c.tag == nodename: + return c + return None + + + def __getitem__(self, item): + return self.attrs[item] + + def __contains__(self, item): + return item in self.attrs From 3a1ce6c9e8701bb6a4c01cd77a417b2e61699a9e Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 4 Nov 2015 07:31:09 +0100 Subject: [PATCH 042/271] [ddg] Don't include empty definition in global results --- modules/ddg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ddg.py b/modules/ddg.py index 78b6022..d94bd61 100644 --- a/modules/ddg.py +++ b/modules/ddg.py @@ -45,7 +45,7 @@ class DDGResult: @property def definition(self): if "Definition" not in self.ddgres or not self.ddgres["Definition"]: - return "Sorry, no definition found for %s." % self.terms + return None return self.ddgres["Definition"] + " <" + self.ddgres["DefinitionURL"] + "> from " + self.ddgres["DefinitionSource"] From c4a7df7a6f224be383b7e1bb36ea038f2ab6e037 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 5 Nov 2015 08:05:53 +0100 Subject: [PATCH 043/271] [spell] Dusting module --- modules/spell/__init__.py | 94 ++++++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 42 deletions(-) diff --git a/modules/spell/__init__.py b/modules/spell/__init__.py index ca2c834..a70b016 100644 --- a/modules/spell/__init__.py +++ b/modules/spell/__init__.py @@ -1,9 +1,6 @@ -# coding=utf-8 - """Check words spelling""" -import re -from urllib.parse import quote +# PYTHON STUFFS ####################################################### from nemubot import context from nemubot.exception import IMException @@ -13,41 +10,16 @@ from nemubot.tools.xmlparser.node import ModuleState from .pyaspell import Aspell from .pyaspell import AspellError -nemubotversion = 3.4 - from more import Response -def help_full(): - return "!spell [<lang>] <word>: give the correct spelling of <word> in <lang=fr>." + +# LOADING ############################################################# def load(context): context.data.setIndex("name", "score") -@hook.command("spell") -def cmd_spell(msg): - if not len(msg.args): - raise IMException("indique une orthographe approximative du mot dont tu veux vérifier l'orthographe.") - lang = "fr" - strRes = list() - for word in msg.args: - if len(word) <= 2 and len(msg.args) > 2: - lang = word - else: - try: - r = check_spell(word, lang) - except AspellError: - return Response("Je n'ai pas le dictionnaire `%s' :(" % lang, msg.channel, msg.nick) - if r == True: - add_score(msg.nick, "correct") - strRes.append("l'orthographe de `%s' est correcte" % word) - elif len(r) > 0: - add_score(msg.nick, "bad") - strRes.append("suggestions pour `%s' : %s" % (word, ", ".join(r))) - else: - add_score(msg.nick, "bad") - strRes.append("aucune suggestion pour `%s'" % word) - return Response(strRes, channel=msg.channel, nick=msg.nick) +# MODULE CORE ######################################################### def add_score(nick, t): if nick not in context.data.index: @@ -61,7 +33,54 @@ def add_score(nick, t): context.data.index[nick][t] = 1 context.save() -@hook.command("spellscore") + +def check_spell(word, lang='fr'): + a = Aspell([("lang", lang)]) + if a.check(word.encode("utf-8")): + ret = True + else: + ret = a.suggest(word.encode("utf-8")) + a.close() + return ret + + +# MODULE INTERFACE #################################################### + +@hook.command("spell", + help="give the correct spelling of given words", + help_usage={"WORD": "give the correct spelling of the WORD."}, + keywords={"lang=": "change the language use for checking, default fr"}) +def cmd_spell(msg): + if not len(msg.args): + raise IMException("indique une orthographe approximative du mot dont tu veux vérifier l'orthographe.") + + lang = msg.kwargs["lang"] if "lang" in msg.kwargs else "fr" + + res = Response(channel=msg.channel) + for word in msg.args: + try: + r = check_spell(word, lang) + except AspellError: + raise IMException("Je n'ai pas le dictionnaire `%s' :(" % lang) + + if r == True: + add_score(msg.nick, "correct") + res.append_message("l'orthographe de `%s' est correcte" % word) + + elif len(r) > 0: + add_score(msg.nick, "bad") + res.append_message(r, title="suggestions pour `%s'" % word) + + else: + add_score(msg.nick, "bad") + res.append_message("aucune suggestion pour `%s'" % word) + + return res + + +@hook.command("spellscore", + help="Show spell score (tests, mistakes, ...) for someone", + help_usage={"USER": "Display score of USER"}) def cmd_score(msg): res = list() unknown = list() @@ -76,12 +95,3 @@ def cmd_score(msg): res.append(Response("%s inconnus" % ", ".join(unknown), channel=msg.channel)) return res - -def check_spell(word, lang='fr'): - a = Aspell([("lang", lang)]) - if a.check(word.encode("utf-8")): - ret = True - else: - ret = a.suggest(word.encode("utf-8")) - a.close() - return ret From a4e6e4ce84e6ff265fd3319de855e78d507610d6 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 6 Nov 2015 02:27:47 +0100 Subject: [PATCH 044/271] [more] Fix append_content behaviour: initialize a list, don't convert string to char list --- modules/more.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/more.py b/modules/more.py index 4742dfe..d9f121c 100644 --- a/modules/more.py +++ b/modules/more.py @@ -91,7 +91,7 @@ class Response: def append_content(self, message): if message is not None and len(message) > 0: if self.messages is None or len(self.messages) == 0: - self.messages = list(message) + self.messages = [message] self.alone = True else: self.messages[len(self.messages)-1] += message From 6aef54910e051838286dd9bfa99b1a97045a8380 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 7 Nov 2015 11:20:12 +0100 Subject: [PATCH 045/271] [networking/whois] New function to get domain availability status --- modules/networking/whois.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/modules/networking/whois.py b/modules/networking/whois.py index b185cf8..d8ced15 100644 --- a/modules/networking/whois.py +++ b/modules/networking/whois.py @@ -1,3 +1,5 @@ +# PYTHON STUFFS ####################################################### + import datetime import urllib @@ -6,10 +8,14 @@ from nemubot.tools.web import getJSON from more import Response +URL_AVAIL = "https://www.whoisxmlapi.com/whoisserver/WhoisService?cmd=GET_DN_AVAILABILITY&domainName=%%s&outputFormat=json&username=%s&password=%s" URL_WHOIS = "http://www.whoisxmlapi.com/whoisserver/WhoisService?rid=1&domainName=%%s&outputFormat=json&userName=%s&password=%s" + +# LOADING ############################################################# + def load(CONF, add_hook): - global URL_WHOIS + global URL_AVAIL, URL_WHOIS if not CONF or not CONF.hasNode("whoisxmlapi") or "username" not in CONF.getNode("whoisxmlapi") or "password" not in CONF.getNode("whoisxmlapi"): raise ImportError("You need a WhoisXML API account in order to use " @@ -18,14 +24,20 @@ def load(CONF, add_hook): "password=\"XXX\" />\nRegister at " "http://www.whoisxmlapi.com/newaccount.php") + URL_AVAIL = URL_AVAIL % (urllib.parse.quote(CONF.getNode("whoisxmlapi")["username"]), urllib.parse.quote(CONF.getNode("whoisxmlapi")["password"])) URL_WHOIS = URL_WHOIS % (urllib.parse.quote(CONF.getNode("whoisxmlapi")["username"]), urllib.parse.quote(CONF.getNode("whoisxmlapi")["password"])) import nemubot.hooks add_hook("in_Command", nemubot.hooks.Command(cmd_whois, "netwhois", help="Get whois information about given domains", help_usage={"DOMAIN": "Return whois information on the given DOMAIN"})) + add_hook("in_Command", nemubot.hooks.Command(cmd_avail, "domain_available", + help="Domain availability check using whoisxmlapi.com", + help_usage={"DOMAIN": "Check if the given DOMAIN is available or not"})) +# MODULE CORE ######################################################### + def extractdate(str): tries = [ "%Y-%m-%dT%H:%M:%S.0%Z", @@ -77,6 +89,24 @@ def whois_entityformat(entity): return ret.lstrip() +def available(dom): + js = getJSON(URL_AVAIL % urllib.parse.quote(dom)) + + if "ErrorMessage" in js: + raise IMException(js["ErrorMessage"]["msg"]) + + return js["DomainInfo"]["domainAvailability"] == "AVAILABLE" + + +# MODULE INTERFACE #################################################### + +def cmd_avail(msg): + if not len(msg.args): + raise IMException("Indicate a domain name for having its availability status!") + + return Response(["%s: %s" % (dom, "available" if available(dom) else "unavailable") for dom in msg.args], + channel=msg.channel) + def cmd_whois(msg): if not len(msg.args): From 1ef54426bc58d037c39ccc0f5eceecea35acdcbe Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 7 Nov 2015 12:29:53 +0100 Subject: [PATCH 046/271] [networking/whois] improve netwhois response by using normalized API fields --- modules/networking/whois.py | 75 ++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/modules/networking/whois.py b/modules/networking/whois.py index d8ced15..2e2970a 100644 --- a/modules/networking/whois.py +++ b/modules/networking/whois.py @@ -9,7 +9,7 @@ from nemubot.tools.web import getJSON from more import Response URL_AVAIL = "https://www.whoisxmlapi.com/whoisserver/WhoisService?cmd=GET_DN_AVAILABILITY&domainName=%%s&outputFormat=json&username=%s&password=%s" -URL_WHOIS = "http://www.whoisxmlapi.com/whoisserver/WhoisService?rid=1&domainName=%%s&outputFormat=json&userName=%s&password=%s" +URL_WHOIS = "http://www.whoisxmlapi.com/whoisserver/WhoisService?da=2&domainName=%%s&outputFormat=json&userName=%s&password=%s" # LOADING ############################################################# @@ -38,36 +38,12 @@ def load(CONF, add_hook): # MODULE CORE ######################################################### -def extractdate(str): - tries = [ - "%Y-%m-%dT%H:%M:%S.0%Z", - "%Y-%m-%dT%H:%M:%S%Z", - "%Y-%m-%dT%H:%M:%S%z", - "%Y-%m-%dT%H:%M:%SZ", - "%Y-%m-%dT%H:%M:%S.0Z", - "%Y-%m-%dT%H:%M:%S", - "%Y-%m-%d %H:%M:%S.0%Z", - "%Y-%m-%d %H:%M:%S%Z", - "%Y-%m-%d %H:%M:%S%z", - "%Y-%m-%d %H:%M:%S.0Z", - "%Y-%m-%d %H:%M:%SZ", - "%Y-%m-%d %H:%M:%S", - "%Y-%m-%d", - "%d/%m/%Y", - ] - - for t in tries: - try: - return datetime.datetime.strptime(str, t) - except ValueError: - pass - return datetime.datetime.strptime(str, t) - - def whois_entityformat(entity): ret = "" if "organization" in entity: ret += entity["organization"] + if "organization" in entity and "name" in entity: + ret += " " if "name" in entity: ret += entity["name"] @@ -121,15 +97,38 @@ def cmd_whois(msg): whois = js["WhoisRecord"] - res = Response(channel=msg.channel, nomore="No more whois information") + res = [] - res.append_message("%s: %s%s%s%s\x03\x02registered by\x03\x02 %s, \x03\x02administrated by\x03\x02 %s, \x03\x02managed by\x03\x02 %s" % (whois["domainName"], - whois["status"].replace("\n", ", ") + " " if "status" in whois else "", - "\x03\x02created on\x03\x02 " + extractdate(whois["createdDate"]).strftime("%c") + ", " if "createdDate" in whois else "", - "\x03\x02updated on\x03\x02 " + extractdate(whois["updatedDate"]).strftime("%c") + ", " if "updatedDate" in whois else "", - "\x03\x02expires on\x03\x02 " + extractdate(whois["expiresDate"]).strftime("%c") + ", " if "expiresDate" in whois else "", - whois_entityformat(whois["registrant"]) if "registrant" in whois else "unknown", - whois_entityformat(whois["administrativeContact"]) if "administrativeContact" in whois else "unknown", - whois_entityformat(whois["technicalContact"]) if "technicalContact" in whois else "unknown", - )) - return res + if "registrarName" in whois: + res.append("\x03\x02registered by\x03\x02 " + whois["registrarName"]) + + if "domainAvailability" in whois: + res.append(whois["domainAvailability"]) + + if "contactEmail" in whois: + res.append("\x03\x02contact email\x03\x02 " + whois["contactEmail"]) + + if "audit" in whois: + if "createdDate" in whois["audit"] and "$" in whois["audit"]["createdDate"]: + res.append("\x03\x02created on\x03\x02 " + whois["audit"]["createdDate"]["$"]) + if "updatedDate" in whois["audit"] and "$" in whois["audit"]["updatedDate"]: + res.append("\x03\x02updated on\x03\x02 " + whois["audit"]["updatedDate"]["$"]) + + if "registryData" in whois: + if "expiresDateNormalized" in whois["registryData"]: + res.append("\x03\x02expire on\x03\x02 " + whois["registryData"]["expiresDateNormalized"]) + if "registrant" in whois["registryData"]: + res.append("\x03\x02registrant:\x03\x02 " + whois_entityformat(whois["registryData"]["registrant"])) + if "zoneContact" in whois["registryData"]: + res.append("\x03\x02zone contact:\x03\x02 " + whois_entityformat(whois["registryData"]["zoneContact"])) + if "technicalContact" in whois["registryData"]: + res.append("\x03\x02technical contact:\x03\x02 " + whois_entityformat(whois["registryData"]["technicalContact"])) + if "administrativeContact" in whois["registryData"]: + res.append("\x03\x02administrative contact:\x03\x02 " + whois_entityformat(whois["registryData"]["administrativeContact"])) + if "billingContact" in whois["registryData"]: + res.append("\x03\x02billing contact:\x03\x02 " + whois_entityformat(whois["registryData"]["billingContact"])) + + return Response(res, + title=whois["domainName"], + channel=msg.channel, + nomore="No more whois information") From 00fa139e542b42e9e5bfd1f3bdd75c42d282da03 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 8 Nov 2015 01:11:40 +0100 Subject: [PATCH 047/271] [syno] Dusting module --- modules/syno.py | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/modules/syno.py b/modules/syno.py index 650e7e9..13d0250 100644 --- a/modules/syno.py +++ b/modules/syno.py @@ -1,7 +1,7 @@ -# coding=utf-8 - """Find synonyms""" +# PYTHON STUFFS ####################################################### + import re from urllib.parse import quote @@ -9,14 +9,10 @@ from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web -nemubotversion = 4.0 - from more import Response -def help_full(): - return "!syno [LANG] <word>: give a list of synonyms for <word>." - +# LOADING ############################################################# def load(context): global lang_binding @@ -30,6 +26,8 @@ def load(context): lang_binding["en"] = lambda word: get_english_synos(context.config["bighugelabskey"], word) +# MODULE CORE ######################################################### + def get_french_synos(word): url = "http://www.crisco.unicaen.fr/des/synonymes/" + quote(word.encode("ISO-8859-1")) page = web.getURLContent(url) @@ -72,24 +70,29 @@ def get_english_synos(key, word): lang_binding = { 'fr': get_french_synos } -@hook.command("synonymes", data="synonymes") -@hook.command("antonymes", data="antonymes") +# MODULE INTERFACE #################################################### + +@hook.command("synonymes", data="synonymes", + help="give a list of synonyms", + help_usage={"WORD": "give synonyms of the given WORD"}, + keywords={ + "lang=LANG": "change the dictionnary language: default fr, available: " + ", ".join(lang_binding) + }) +@hook.command("antonymes", data="antonymes", + help="give a list of antonyms", + help_usage={"WORD": "give antonyms of the given WORD"}, + keywords={ + "lang=LANG": "change the dictionnary language: default fr, available: " + ", ".join(lang_binding) + }) def go(msg, what): if not len(msg.args): raise IMException("de quel mot veux-tu connaître la liste des synonymes ?") - # Detect lang - if msg.args[0] in lang_binding: - func = lang_binding[msg.args[0]] - word = ' '.join(msg.args[1:]) - else: - func = lang_binding["fr"] - word = ' '.join(msg.args) - # TODO: depreciate usage without lang - #raise IMException("language %s is not handled yet." % msg.args[0]) + lang = msg.kwargs["lang"] if "lang" in msg.kwargs else "fr" + word = ' '.join(msg.args) try: - best, synos, anton = func(word) + best, synos, anton = lang_binding[lang](word) except: best, synos, anton = (list(), list(), list()) From 11bdf8d0a17196b52badebf83b7c034969896210 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 8 Nov 2015 01:11:48 +0100 Subject: [PATCH 048/271] [cve] Dusting module --- modules/cve.py | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/modules/cve.py b/modules/cve.py index c5e125d..637d728 100644 --- a/modules/cve.py +++ b/modules/cve.py @@ -1,26 +1,40 @@ +"""Read CVE in your IM client""" + +# PYTHON STUFFS ####################################################### + from bs4 import BeautifulSoup from urllib.parse import quote from nemubot.hooks import hook -from nemubot.tools.web import getURLContent +from nemubot.tools.web import getURLContent, striphtml + from more import Response -"""CVE description""" +BASEURL_NIST = 'https://web.nvd.nist.gov/view/vuln/detail?vulnId=' -nemubotversion = 4.0 - -BASEURL_MITRE = 'http://cve.mitre.org/cgi-bin/cvename.cgi?name=' +# MODULE CORE ######################################################### def get_cve(cve_id): - search_url = BASEURL_MITRE + quote(cve_id.upper()) + search_url = BASEURL_NIST + quote(cve_id.upper()) soup = BeautifulSoup(getURLContent(search_url)) - desc = soup.body.findAll('td') + vuln = soup.body.find(class_="vulnDetail") + cvss = vuln.find(class_="cvssDetail") - return desc[17].text.replace("\n", " ") + " Moar at " + search_url + return [ + "Base score: " + cvss.findAll('div')[0].findAll('a')[0].text.strip(), + vuln.findAll('p')[0].text, # description + striphtml(vuln.findAll('div')[0].text).strip(), # publication date + striphtml(vuln.findAll('div')[1].text).strip(), # last revised + ] -@hook.command("cve") + +# MODULE INTERFACE #################################################### + +@hook.command("cve", + help="Display given CVE", + help_usage={"CVE_ID": "Display the description of the given CVE"}) def get_cve_desc(msg): res = Response(channel=msg.channel) From 36cfdd88613d7e79726cc9ac27e88237a6f521a5 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 9 Nov 2015 18:57:48 +0100 Subject: [PATCH 049/271] Added check and match module defined functions to hooks --- nemubot/hooks/abstract.py | 11 ++++++++--- nemubot/hooks/command.py | 4 +++- nemubot/hooks/message.py | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/nemubot/hooks/abstract.py b/nemubot/hooks/abstract.py index 25efc45..eac4b20 100644 --- a/nemubot/hooks/abstract.py +++ b/nemubot/hooks/abstract.py @@ -43,7 +43,7 @@ class Abstract: """Abstract class for Hook implementation""" def __init__(self, call, data=None, channels=None, servers=None, mtimes=-1, - end_call=None): + end_call=None, check=None, match=None): """Create basis of the hook Arguments: @@ -58,6 +58,8 @@ class Abstract: assert callable(call), call assert end_call is None or callable(end_call), end_call + assert check is None or callable(check), check + assert match is None or callable(match), match assert isinstance(channels, list), channels assert isinstance(servers, list), servers assert type(mtimes) is int, mtimes @@ -65,6 +67,9 @@ class Abstract: self.call = call self.data = data + self.mod_check = check + self.mod_match = match + # TODO: find a way to have only one list: a limit is server + channel, not only server or channel self.channels = channels self.servers = servers @@ -96,11 +101,11 @@ class Abstract: def check(self, data1): - return True + return self.mod_check(data1) if self.mod_check is not None else True def match(self, data1): - return True + return self.mod_match(data1) if self.mod_match is not None else True def run(self, data1, *args): diff --git a/nemubot/hooks/command.py b/nemubot/hooks/command.py index 02fdb4d..863d672 100644 --- a/nemubot/hooks/command.py +++ b/nemubot/hooks/command.py @@ -17,6 +17,7 @@ import re from nemubot.hooks.message import Message +from nemubot.hooks.abstract import Abstract from nemubot.hooks.keywords import NoKeyword from nemubot.hooks.keywords.abstract import Abstract as AbstractKeywords from nemubot.hooks.keywords.dict import Dict as DictKeywords @@ -61,5 +62,6 @@ class Command(Message): else: return ( (self.name is None or msg.cmd == self.name) and - (self.regexp is None or re.match(self.regexp, msg.cmd)) + (self.regexp is None or re.match(self.regexp, msg.cmd)) and + Abstract.match(self, msg) ) diff --git a/nemubot/hooks/message.py b/nemubot/hooks/message.py index 1c245ea..ee07600 100644 --- a/nemubot/hooks/message.py +++ b/nemubot/hooks/message.py @@ -46,4 +46,4 @@ class Message(Abstract): if not isinstance(msg, nemubot.message.text.Text): return False else: - return self.regexp is None or re.match(self.regexp, msg.message) + return (self.regexp is None or re.match(self.regexp, msg.message)) and super().match(msg) From 0f4a904a7795a6f239e3eb78e050c6d004dd53e9 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 10 Nov 2015 07:05:42 +0100 Subject: [PATCH 050/271] Log configuration loading --- nemubot/bot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nemubot/bot.py b/nemubot/bot.py index 54acdee..44b2bac 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -205,7 +205,9 @@ class Bot(threading.Thread): self.quit() elif action[0] == "loadconf": for path in action[1:]: + logger.debug("Load configuration from %s", path) self.load_file(path) + logger.info("Configurations successfully loaded") self.sync_queue.task_done() From 2ebd86b80f0f8d769d50d462dd8cae887bf31f7c Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 11 Nov 2015 17:57:08 +0100 Subject: [PATCH 051/271] [events] Avoid catchall hook --- modules/events.py | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/modules/events.py b/modules/events.py index e1d25d0..7d79a08 100644 --- a/modules/events.py +++ b/modules/events.py @@ -1,5 +1,3 @@ -# coding=utf-8 - """Create countdowns and reminders""" import re @@ -9,12 +7,11 @@ from nemubot import context from nemubot.exception import IMException from nemubot.event import ModuleEvent from nemubot.hooks import hook +from nemubot.message import Command from nemubot.tools.countdown import countdown_format, countdown from nemubot.tools.date import extractDate from nemubot.tools.xmlparser.node import ModuleState -nemubotversion = 3.4 - from more import Response def help_full (): @@ -169,23 +166,22 @@ def liste(msg): else: return Response("Compteurs connus : %s." % ", ".join(context.data.index.keys()), channel=msg.channel) -@hook.command() +@hook.command(match=lambda msg: isinstance(msg, Command) and msg.cmd in context.data.index) def parseanswer(msg): - if msg.cmd in context.data.index: - res = Response(channel=msg.channel) + res = Response(channel=msg.channel) - # Avoid message starting by ! which can be interpreted as command by other bots - if msg.cmd[0] == "!": - res.nick = msg.nick + # Avoid message starting by ! which can be interpreted as command by other bots + if msg.cmd[0] == "!": + res.nick = msg.nick - if context.data.index[msg.cmd].name == "strend": - if context.data.index[msg.cmd].hasAttribute("end"): - res.append_message("%s commencé il y a %s et se terminera dans %s." % (msg.cmd, countdown(msg.date - context.data.index[msg.cmd].getDate("start")), countdown(context.data.index[msg.cmd].getDate("end") - msg.date))) - else: - res.append_message("%s commencé il y a %s." % (msg.cmd, countdown(msg.date - context.data.index[msg.cmd].getDate("start")))) + if context.data.index[msg.cmd].name == "strend": + if context.data.index[msg.cmd].hasAttribute("end"): + res.append_message("%s commencé il y a %s et se terminera dans %s." % (msg.cmd, countdown(msg.date - context.data.index[msg.cmd].getDate("start")), countdown(context.data.index[msg.cmd].getDate("end") - msg.date))) else: - res.append_message(countdown_format(context.data.index[msg.cmd].getDate("start"), context.data.index[msg.cmd]["msg_before"], context.data.index[msg.cmd]["msg_after"])) - return res + res.append_message("%s commencé il y a %s." % (msg.cmd, countdown(msg.date - context.data.index[msg.cmd].getDate("start")))) + else: + res.append_message(countdown_format(context.data.index[msg.cmd].getDate("start"), context.data.index[msg.cmd]["msg_before"], context.data.index[msg.cmd]["msg_after"])) + return res RGXP_ask = re.compile(r"^.*((create|new)\s+(a|an|a\s*new|an\s*other)?\s*(events?|commande?)|(nouvel(le)?|ajoute|cr[ée]{1,3})\s+(un)?\s*([eé]v[ée]nements?|commande?)).*$", re.I) From 38412c1c16906e7bcdfec8b5e30c56da73b7e39d Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 12 Nov 2015 19:15:09 +0100 Subject: [PATCH 052/271] Suggest command(s) on typo --- nemubot/treatment.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/nemubot/treatment.py b/nemubot/treatment.py index 57eb448..c656856 100644 --- a/nemubot/treatment.py +++ b/nemubot/treatment.py @@ -90,7 +90,10 @@ class MessageTreater: msg -- message to treat """ - for h in self.hm.get_hooks("in", type(msg).__name__): + res = False + + hooks = self.hm.get_hooks("in", type(msg).__name__) + for h in hooks: if h.can_read(msg.to, msg.server) and h.match(msg): res = h.run(msg) @@ -104,6 +107,15 @@ class MessageTreater: yield res + from nemubot.message.command import Command as CommandMessage + if res is False and isinstance(msg, CommandMessage): + from nemubot.hooks import Command as CommandHook + from nemubot.exception import IMException + from nemubot.tools.human import guess + suggest = [s for s in guess(msg.cmd, [h.name for h in hooks if isinstance(h, CommandHook) and h.name is not None])] + if len(suggest) >= 1: + yield IMException("Unknown command %s. Would you mean: %s?" % (msg.cmd, ", ".join(suggest))).fill_response(msg) + def _post_treat(self, msg): """Modify output Messages From f27347f02838bff81b192ce9f9da2dd65b35dc17 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 13 Nov 2015 01:39:30 +0100 Subject: [PATCH 053/271] [grep] Introducing new module that perform grep like action on subcommand --- modules/grep.py | 64 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 modules/grep.py diff --git a/modules/grep.py b/modules/grep.py new file mode 100644 index 0000000..a5395c2 --- /dev/null +++ b/modules/grep.py @@ -0,0 +1,64 @@ +"""Filter messages, displaying lines matching a pattern""" + +# PYTHON STUFFS ####################################################### + +import re + +from nemubot import context +from nemubot.exception import IMException +from nemubot.hooks import hook +from nemubot.message import Command, Text + +from more import Response + + +# MODULE CORE ######################################################### + +def grep(fltr, cmd, args, msg): + """Perform a grep like on known nemubot structures + + Arguments: + fltr -- The filter regexp + cmd -- The subcommand to execute + args -- subcommand arguments + msg -- The original message + """ + + for r in context.subtreat(Command(cmd, + args, + to_response=msg.to_response, + frm=msg.frm, + server=msg.server)): + if isinstance(r, Response): + for i in range(len(r.messages) - 1, -1, -1): + if isinstance(r.messages[i], list): + for j in range(len(r.messages[i]) - 1, -1, -1): + if not re.match(fltr, r.messages[i][j]): + r.messages[i].pop(j) + if len(r.messages[i]) <= 0: + r.messages.pop(i) + elif isinstance(r.messages[i], str) and not re.match(fltr, r.messages[i]): + r.messages.pop(i) + yield r + + elif isinstance(r, Text): + if re.match(fltr, r.message): + yield r + + else: + yield r + + +# MODULE INTERFACE #################################################### + +@hook.command("grep", + help="Display only lines from a subcommand matching the given pattern", + help_usage={"PTRN !SUBCMD": "Filter SUBCMD command using the pattern PTRN"}) +def cmd_grep(msg): + if len(msg.args) < 2: + raise IMException("Please provide a filter and a command") + + return [m for m in grep(msg.args[0] if msg.args[0][0] == "^" else ".*" + msg.args[0] + ".*", + msg.args[1][1:] if msg.args[1][0] == "!" else msg.args[1], + msg.args[2:], + msg)] From 7ae7e381c331c2e83ebcf5f904cc58319df90b48 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 14 Nov 2015 15:47:08 +0100 Subject: [PATCH 054/271] [alias] Forward command keywords --- modules/alias.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/alias.py b/modules/alias.py index c308608..24c8fa3 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -253,7 +253,7 @@ def treat_alias(msg): args = shlex.split(txt) except ValueError: args = txt.split(' ') - nmsg = Command(args[0], replace_variables(args[1:], msg) + msg.args, **msg.export_args()) + nmsg = Command(args[0], args=replace_variables(args[1:], msg) + msg.args, kwargs=msg.kwargs, **msg.export_args()) # Avoid infinite recursion if msg.cmd != nmsg.cmd: From e83c4091bfcd74d3970de7c041c042611604c439 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 14 Nov 2015 16:17:25 +0100 Subject: [PATCH 055/271] Avoid catchall DirectAsk --- modules/events.py | 102 +++++++++++++++++++++++++--------------------- nemubot/bot.py | 8 ++-- 2 files changed, 60 insertions(+), 50 deletions(-) diff --git a/modules/events.py b/modules/events.py index 7d79a08..2887514 100644 --- a/modules/events.py +++ b/modules/events.py @@ -14,9 +14,11 @@ from nemubot.tools.xmlparser.node import ModuleState from more import Response + def help_full (): return "This module store a lot of events: ny, we, " + (", ".join(context.datas.index.keys())) + "\n!eventslist: gets list of timer\n!start /something/: launch a timer" + def load(context): #Define the index context.data.setIndex("name") @@ -35,6 +37,7 @@ def fini(d, strend): context.data.delChild(context.data.index[strend["name"]]) context.save() + @hook.command("goûter") def cmd_gouter(msg): ndate = datetime.now(timezone.utc) @@ -44,6 +47,7 @@ def cmd_gouter(msg): "Nous avons %s de retard pour le goûter :("), channel=msg.channel) + @hook.command("week-end") def cmd_we(msg): ndate = datetime.now(timezone.utc) + timedelta(5 - datetime.today().weekday()) @@ -53,6 +57,7 @@ def cmd_we(msg): "Youhou, on est en week-end depuis %s."), channel=msg.channel) + @hook.command("start") def start_countdown(msg): """!start /something/: launch a timer""" @@ -132,6 +137,7 @@ def start_countdown(msg): msg.date.strftime("%A %d %B %Y à %H:%M:%S")), nick=msg.frm) + @hook.command("end") @hook.command("forceend") def end_countdown(msg): @@ -151,6 +157,7 @@ def end_countdown(msg): else: return Response("%s n'est pas un compteur connu."% (msg.args[0]), channel=msg.channel, nick=msg.nick) + @hook.command("eventslist") def liste(msg): """!eventslist: gets list of timer""" @@ -166,6 +173,7 @@ def liste(msg): else: return Response("Compteurs connus : %s." % ", ".join(context.data.index.keys()), channel=msg.channel) + @hook.command(match=lambda msg: isinstance(msg, Command) and msg.cmd in context.data.index) def parseanswer(msg): res = Response(channel=msg.channel) @@ -183,59 +191,59 @@ def parseanswer(msg): res.append_message(countdown_format(context.data.index[msg.cmd].getDate("start"), context.data.index[msg.cmd]["msg_before"], context.data.index[msg.cmd]["msg_after"])) return res + RGXP_ask = re.compile(r"^.*((create|new)\s+(a|an|a\s*new|an\s*other)?\s*(events?|commande?)|(nouvel(le)?|ajoute|cr[ée]{1,3})\s+(un)?\s*([eé]v[ée]nements?|commande?)).*$", re.I) -@hook.ask() +@hook.ask(match=lambda msg: RGXP_ask.match(msg.text)) def parseask(msg): - if RGXP_ask.match(msg.text) is not None: - name = re.match("^.*!([^ \"'@!]+).*$", msg.text) - if name is None: - raise IMException("il faut que tu attribues une commande à l'événement.") - if name.group(1) in context.data.index: - raise IMException("un événement portant ce nom existe déjà.") + name = re.match("^.*!([^ \"'@!]+).*$", msg.text) + if name is None: + raise IMException("il faut que tu attribues une commande à l'événement.") + if name.group(1) in context.data.index: + raise IMException("un événement portant ce nom existe déjà.") - texts = re.match("^[^\"]*(avant|après|apres|before|after)?[^\"]*\"([^\"]+)\"[^\"]*((avant|après|apres|before|after)?.*\"([^\"]+)\".*)?$", msg.text, re.I) - if texts is not None and texts.group(3) is not None: - extDate = extractDate(msg.text) - if extDate is None or extDate == "": - raise IMException("la date de l'événement est invalide !") + texts = re.match("^[^\"]*(avant|après|apres|before|after)?[^\"]*\"([^\"]+)\"[^\"]*((avant|après|apres|before|after)?.*\"([^\"]+)\".*)?$", msg.text, re.I) + if texts is not None and texts.group(3) is not None: + extDate = extractDate(msg.text) + if extDate is None or extDate == "": + raise IMException("la date de l'événement est invalide !") - if texts.group(1) is not None and (texts.group(1) == "après" or texts.group(1) == "apres" or texts.group(1) == "after"): - msg_after = texts.group (2) - msg_before = texts.group (5) - if (texts.group(4) is not None and (texts.group(4) == "après" or texts.group(4) == "apres" or texts.group(4) == "after")) or texts.group(1) is None: - msg_before = texts.group (2) - msg_after = texts.group (5) + if texts.group(1) is not None and (texts.group(1) == "après" or texts.group(1) == "apres" or texts.group(1) == "after"): + msg_after = texts.group(2) + msg_before = texts.group(5) + if (texts.group(4) is not None and (texts.group(4) == "après" or texts.group(4) == "apres" or texts.group(4) == "after")) or texts.group(1) is None: + msg_before = texts.group(2) + msg_after = texts.group(5) - if msg_before.find("%s") == -1 or msg_after.find("%s") == -1: - raise IMException("Pour que l'événement soit valide, ajouter %s à" - " l'endroit où vous voulez que soit ajouté le" - " compte à rebours.") + if msg_before.find("%s") == -1 or msg_after.find("%s") == -1: + raise IMException("Pour que l'événement soit valide, ajouter %s à" + " l'endroit où vous voulez que soit ajouté le" + " compte à rebours.") - evt = ModuleState("event") - evt["server"] = msg.server - evt["channel"] = msg.channel - evt["proprio"] = msg.nick - evt["name"] = name.group(1) - evt["start"] = extDate - evt["msg_after"] = msg_after - evt["msg_before"] = msg_before - context.data.addChild(evt) - context.save() - return Response("Nouvel événement !%s ajouté avec succès." % name.group(1), - channel=msg.channel) + evt = ModuleState("event") + evt["server"] = msg.server + evt["channel"] = msg.channel + evt["proprio"] = msg.nick + evt["name"] = name.group(1) + evt["start"] = extDate + evt["msg_after"] = msg_after + evt["msg_before"] = msg_before + context.data.addChild(evt) + context.save() + return Response("Nouvel événement !%s ajouté avec succès." % name.group(1), + channel=msg.channel) - elif texts is not None and texts.group (2) is not None: - evt = ModuleState("event") - evt["server"] = msg.server - evt["channel"] = msg.channel - evt["proprio"] = msg.nick - evt["name"] = name.group(1) - evt["msg_before"] = texts.group (2) - context.data.addChild(evt) - context.save() - return Response("Nouvelle commande !%s ajoutée avec succès." % name.group(1), - channel=msg.channel) + elif texts is not None and texts.group(2) is not None: + evt = ModuleState("event") + evt["server"] = msg.server + evt["channel"] = msg.channel + evt["proprio"] = msg.nick + evt["name"] = name.group(1) + evt["msg_before"] = texts.group (2) + context.data.addChild(evt) + context.save() + return Response("Nouvelle commande !%s ajoutée avec succès." % name.group(1), + channel=msg.channel) - else: - raise IMException("Veuillez indiquez les messages d'attente et d'après événement entre guillemets.") + else: + raise IMException("Veuillez indiquez les messages d'attente et d'après événement entre guillemets.") diff --git a/nemubot/bot.py b/nemubot/bot.py index 44b2bac..f9569b7 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -72,9 +72,11 @@ class Bot(threading.Thread): import re def in_ping(msg): - if re.match("^ *(m[' ]?entends?[ -]+tu|h?ear me|do you copy|ping)", msg.message, re.I) is not None: - return msg.respond("pong") - self.treater.hm.add_hook(nemubot.hooks.Message(in_ping), "in", "DirectAsk") + 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 From 31d93734a6841af7a18814bd5502a61c65f8ae49 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 15 Nov 2015 01:58:35 +0100 Subject: [PATCH 056/271] Fixed empty module configuration --- nemubot/modulecontext.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nemubot/modulecontext.py b/nemubot/modulecontext.py index b24d94d..9c1f844 100644 --- a/nemubot/modulecontext.py +++ b/nemubot/modulecontext.py @@ -32,8 +32,8 @@ class ModuleContext: module_name in context.modules_configuration): self.config = context.modules_configuration[module_name] else: - from nemubot.tools.xmlparser.node import ModuleState - self.config = ModuleState("module") + from nemubot.config.module import Module + self.config = Module(module_name) self.hooks = list() self.events = list() From 926648517fdaa2b080440d02a4513a9237b11bd0 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 15 Nov 2015 12:31:58 +0100 Subject: [PATCH 057/271] Add config to package setup --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index a9b7d3f..b39a163 100755 --- a/setup.py +++ b/setup.py @@ -61,6 +61,7 @@ setup( packages=[ 'nemubot', + 'nemubot.config', 'nemubot.datastore', 'nemubot.event', 'nemubot.exception', From 43c42e1397825d3a8b4f9d9fbb7d1dab876727ec Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 16 Nov 2015 07:19:09 +0100 Subject: [PATCH 058/271] Rework hook managment and add some tests --- modules/alias.py | 2 +- modules/networking/whois.py | 14 +++-- modules/whois.py | 4 +- nemubot/bot.py | 2 +- nemubot/hooks/__init__.py | 10 +-- nemubot/hooks/manager.py | 108 +++++++++++++++++++------------ nemubot/hooks/manager_test.py | 115 ++++++++++++++++++++++++++++++++++ nemubot/modulecontext.py | 45 +++++++------ 8 files changed, 222 insertions(+), 78 deletions(-) create mode 100755 nemubot/hooks/manager_test.py diff --git a/modules/alias.py b/modules/alias.py index 24c8fa3..f7aeddb 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -242,7 +242,7 @@ def cmd_unalias(msg): ## Alias replacement -@hook.add("pre_Command") +@hook.add(["pre","Command"]) def treat_alias(msg): if msg.cmd in context.data.getNode("aliases").index: txt = context.data.getNode("aliases").index[msg.cmd]["origin"] diff --git a/modules/networking/whois.py b/modules/networking/whois.py index 2e2970a..d3d30b1 100644 --- a/modules/networking/whois.py +++ b/modules/networking/whois.py @@ -28,12 +28,14 @@ def load(CONF, add_hook): URL_WHOIS = URL_WHOIS % (urllib.parse.quote(CONF.getNode("whoisxmlapi")["username"]), urllib.parse.quote(CONF.getNode("whoisxmlapi")["password"])) import nemubot.hooks - add_hook("in_Command", nemubot.hooks.Command(cmd_whois, "netwhois", - help="Get whois information about given domains", - help_usage={"DOMAIN": "Return whois information on the given DOMAIN"})) - add_hook("in_Command", nemubot.hooks.Command(cmd_avail, "domain_available", - help="Domain availability check using whoisxmlapi.com", - help_usage={"DOMAIN": "Check if the given DOMAIN is available or not"})) + add_hook(nemubot.hooks.Command(cmd_whois, "netwhois", + help="Get whois information about given domains", + help_usage={"DOMAIN": "Return whois information on the given DOMAIN"}), + "in","Command") + add_hook(nemubot.hooks.Command(cmd_avail, "domain_available", + help="Domain availability check using whoisxmlapi.com", + help_usage={"DOMAIN": "Check if the given DOMAIN is available or not"}), + "in","Command") # MODULE CORE ######################################################### diff --git a/modules/whois.py b/modules/whois.py index 4a13e9c..a51b838 100644 --- a/modules/whois.py +++ b/modules/whois.py @@ -30,8 +30,8 @@ def load(context): context.data.getNode("pics").setIndex("login", "pict") import nemubot.hooks - context.add_hook("in_Command", - nemubot.hooks.Command(cmd_whois, "whois")) + context.add_hook(nemubot.hooks.Command(cmd_whois, "whois"), + "in","Command") class Login: diff --git a/nemubot/bot.py b/nemubot/bot.py index f9569b7..8d45f3d 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -474,7 +474,7 @@ class Bot(threading.Thread): # Register decorated functions import nemubot.hooks for s, h in nemubot.hooks.hook.last_registered: - module.__nemubot_context__.add_hook(s, h) + module.__nemubot_context__.add_hook(h, *s if isinstance(s, list) else s) nemubot.hooks.hook.last_registered = [] # Launch the module diff --git a/nemubot/hooks/__init__.py b/nemubot/hooks/__init__.py index 9904119..e9113eb 100644 --- a/nemubot/hooks/__init__.py +++ b/nemubot/hooks/__init__.py @@ -35,19 +35,19 @@ class hook: def add(store, *args, **kwargs): return hook._add(store, Abstract, *args, **kwargs) - def ask(*args, store="in_DirectAsk", **kwargs): + def ask(*args, store=["in","DirectAsk"], **kwargs): return hook._add(store, Message, *args, **kwargs) - def command(*args, store="in_Command", **kwargs): + def command(*args, store=["in","Command"], **kwargs): return hook._add(store, Command, *args, **kwargs) - def message(*args, store="in_Text", **kwargs): + def message(*args, store=["in","Text"], **kwargs): return hook._add(store, Message, *args, **kwargs) - def post(*args, store="post", **kwargs): + def post(*args, store=["post"], **kwargs): return hook._add(store, Abstract, *args, **kwargs) - def pre(*args, store="pre", **kwargs): + def pre(*args, store=["pre"], **kwargs): return hook._add(store, Abstract, *args, **kwargs) diff --git a/nemubot/hooks/manager.py b/nemubot/hooks/manager.py index 8859d19..6a57d2a 100644 --- a/nemubot/hooks/manager.py +++ b/nemubot/hooks/manager.py @@ -14,15 +14,47 @@ # 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/>. +import logging + class HooksManager: """Class to manage hooks""" - def __init__(self): + def __init__(self, name="core"): """Initialize the manager""" self.hooks = dict() + self.logger = logging.getLogger("nemubot.hooks.manager." + name) + + + def _access(self, *triggers): + """Access to the given triggers chain""" + + h = self.hooks + for t in triggers: + if t not in h: + h[t] = dict() + h = h[t] + + if "__end__" not in h: + h["__end__"] = list() + + return h + + + def _search(self, hook, *where, start=None): + """Search all occurence of the given hook""" + + if start is None: + start = self.hooks + + for k in start: + if k == "__end__": + if hook in start[k]: + yield where + else: + yield from self._search(hook, *where + (k,), start=start[k]) def add_hook(self, hook, *triggers): @@ -33,20 +65,19 @@ class HooksManager: triggers -- string that trigger the hook """ - trigger = "_".join(triggers) + assert hook is not None, hook - if trigger not in self.hooks: - self.hooks[trigger] = list() + h = self._access(*triggers) - self.hooks[trigger].append(hook) + h["__end__"].append(hook) + + self.logger.debug("New hook successfully added in %s: %s", + "/".join(triggers), hook) - def del_hook(self, hook=None, *triggers): + def del_hooks(self, *triggers, hook=None): """Remove the given hook from the manager - Return: - Boolean value reporting the deletion success - Argument: triggers -- trigger string to remove @@ -54,15 +85,20 @@ class HooksManager: hook -- a Hook instance to remove from the trigger string """ - trigger = "_".join(triggers) + assert hook is not None or len(triggers) - if trigger in self.hooks: - if hook is None: - del self.hooks[trigger] + self.logger.debug("Trying to delete hook in %s: %s", + "/".join(triggers), hook) + + if hook is not None: + for h in self._search(hook, *triggers, start=self._access(*triggers)): + self._access(*h)["__end__"].remove(hook) + + else: + if len(triggers): + del self._access(*triggers[:-1])[triggers[-1]] else: - self.hooks[trigger].remove(hook) - return True - return False + self.hooks = dict() def get_hooks(self, *triggers): @@ -70,35 +106,29 @@ class HooksManager: Argument: triggers -- the trigger string - - Keyword argument: - data -- Data to pass to the hook as argument """ - trigger = "_".join(triggers) - - res = list() - - for key in self.hooks: - if trigger.find(key) == 0: - res += self.hooks[key] - - return res + for n in range(len(triggers) + 1): + i = self._access(*triggers[:n]) + for h in i["__end__"]: + yield h - def exec_hook(self, *triggers, **data): - """Trigger hooks that match the given trigger string + def get_reverse_hooks(self, *triggers, exclude_first=False): + """Returns list of triggered hooks that are bellow or at the same level Argument: - trigger -- the trigger string + triggers -- the trigger string - Keyword argument: - data -- Data to pass to the hook as argument + Keyword arguments: + exclude_first -- start reporting hook at the next level """ - trigger = "_".join(triggers) - - for key in self.hooks: - if trigger.find(key) == 0: - for hook in self.hooks[key]: - hook.run(**data) + h = self._access(*triggers) + for k in h: + if k == "__end__": + if not exclude_first: + for hk in h[k]: + yield hk + else: + yield from self.get_reverse_hooks(*triggers + (k,)) diff --git a/nemubot/hooks/manager_test.py b/nemubot/hooks/manager_test.py new file mode 100755 index 0000000..a0f38d7 --- /dev/null +++ b/nemubot/hooks/manager_test.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 + +import unittest + +from nemubot.hooks.manager import HooksManager + +class TestHookManager(unittest.TestCase): + + + def test_access(self): + hm = HooksManager() + + h1 = "HOOK1" + h2 = "HOOK2" + h3 = "HOOK3" + + hm.add_hook(h1) + hm.add_hook(h2, "pre") + hm.add_hook(h3, "pre", "Text") + hm.add_hook(h2, "post", "Text") + + self.assertIn("__end__", hm._access()) + self.assertIn("__end__", hm._access("pre")) + self.assertIn("__end__", hm._access("pre", "Text")) + self.assertIn("__end__", hm._access("post", "Text")) + + self.assertFalse(hm._access("inexistant")["__end__"]) + self.assertTrue(hm._access()["__end__"]) + self.assertTrue(hm._access("pre")["__end__"]) + self.assertTrue(hm._access("pre", "Text")["__end__"]) + self.assertTrue(hm._access("post", "Text")["__end__"]) + + + def test_search(self): + hm = HooksManager() + + h1 = "HOOK1" + h2 = "HOOK2" + h3 = "HOOK3" + h4 = "HOOK4" + + hm.add_hook(h1) + hm.add_hook(h2, "pre") + hm.add_hook(h3, "pre", "Text") + hm.add_hook(h2, "post", "Text") + + self.assertTrue([h for h in hm._search(h1)]) + self.assertFalse([h for h in hm._search(h4)]) + self.assertEqual(2, len([h for h in hm._search(h2)])) + self.assertEqual([("pre", "Text")], [h for h in hm._search(h3)]) + + + def test_delete(self): + hm = HooksManager() + + h1 = "HOOK1" + h2 = "HOOK2" + h3 = "HOOK3" + h4 = "HOOK4" + + hm.add_hook(h1) + hm.add_hook(h2, "pre") + hm.add_hook(h3, "pre", "Text") + hm.add_hook(h2, "post", "Text") + + hm.del_hooks(hook=h4) + + self.assertTrue(hm._access("pre")["__end__"]) + self.assertTrue(hm._access("pre", "Text")["__end__"]) + hm.del_hooks("pre") + self.assertFalse(hm._access("pre")["__end__"]) + + self.assertTrue(hm._access("post", "Text")["__end__"]) + hm.del_hooks("post", "Text", hook=h2) + self.assertFalse(hm._access("post", "Text")["__end__"]) + + self.assertTrue(hm._access()["__end__"]) + hm.del_hooks(hook=h1) + self.assertFalse(hm._access()["__end__"]) + + + def test_get(self): + hm = HooksManager() + + h1 = "HOOK1" + h2 = "HOOK2" + h3 = "HOOK3" + + hm.add_hook(h1) + hm.add_hook(h2, "pre") + hm.add_hook(h3, "pre", "Text") + hm.add_hook(h2, "post", "Text") + + self.assertEqual([h1, h2], [h for h in hm.get_hooks("pre")]) + self.assertEqual([h1, h2, h3], [h for h in hm.get_hooks("pre", "Text")]) + + + def test_get_rev(self): + hm = HooksManager() + + h1 = "HOOK1" + h2 = "HOOK2" + h3 = "HOOK3" + + hm.add_hook(h1) + hm.add_hook(h2, "pre") + hm.add_hook(h3, "pre", "Text") + hm.add_hook(h2, "post", "Text") + + self.assertEqual([h2, h3], [h for h in hm.get_reverse_hooks("pre")]) + self.assertEqual([h3], [h for h in hm.get_reverse_hooks("pre", exclude_first=True)]) + + +if __name__ == '__main__': + unittest.main() diff --git a/nemubot/modulecontext.py b/nemubot/modulecontext.py index 9c1f844..d562a98 100644 --- a/nemubot/modulecontext.py +++ b/nemubot/modulecontext.py @@ -26,6 +26,8 @@ class ModuleContext: 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 @@ -39,26 +41,23 @@ class ModuleContext: self.events = list() self.debug = context.verbosity > 0 if context is not None else False + from nemubot.hooks import Abstract as AbstractHook + # Define some callbacks if context is not None: # Load module data self.data = context.datastore.load(module_name) - def add_hook(store, hook): - self.hooks.append((store, hook)) - return context.treater.hm.add_hook(hook, store) - def del_hook(store, hook): - self.hooks.remove((store, hook)) - return context.treater.hm.del_hook(hook, store) - def call_hook(store, msg): - for h in context.treater.hm.get_hooks(store): - if h.match(msg): - res = h.run(msg) - if isinstance(res, list): - for i in res: - yield i - else: - yield res + def add_hook(hook, *triggers): + assert isinstance(hook, AbstractHook), hook + self.hooks.append((triggers, hook)) + return context.treater.hm.add_hook(hook, *triggers) + + def del_hook(hook, *triggers): + assert isinstance(hook, AbstractHook), hook + self.hooks.remove((triggers, hook)) + return context.treater.hm.del_hooks(*triggers, hook=hook) + def subtreat(msg): yield from context.treater.treat_msg(msg) def add_event(evt, eid=None): @@ -80,13 +79,12 @@ class ModuleContext: from nemubot.tools.xmlparser import module_state self.data = module_state.ModuleState("nemubotstate") - def add_hook(store, hook): - self.hooks.append((store, hook)) - def del_hook(store, hook): - self.hooks.remove((store, hook)) - def call_hook(store, msg): - # TODO: what can we do here? - return None + def add_hook(hook, *triggers): + assert isinstance(hook, AbstractHook), hook + self.hooks.append((triggers, hook)) + def del_hook(hook, *triggers): + assert isinstance(hook, AbstractHook), hook + self.hooks.remove((triggers, hook)) def subtreat(msg): return None def add_event(evt, eid=None): @@ -106,7 +104,6 @@ class ModuleContext: self.del_event = del_event self.save = save self.send_response = send_response - self.call_hook = call_hook self.subtreat = subtreat @@ -115,7 +112,7 @@ class ModuleContext: # Remove registered hooks for (s, h) in self.hooks: - self.del_hook(s, h) + self.del_hook(h, *s) # Remove registered events for e in self.events: From 0ba763f8b1d8be6eb362a39e16a0d18d23d84748 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 17 Nov 2015 19:59:38 +0100 Subject: [PATCH 059/271] Display miss string only if no hook match on a full message treatment --- nemubot/treatment.py | 67 +++++++++++++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/nemubot/treatment.py b/nemubot/treatment.py index c656856..2c1955d 100644 --- a/nemubot/treatment.py +++ b/nemubot/treatment.py @@ -36,17 +36,29 @@ class MessageTreater: """ try: + handled = False + # Run pre-treatment: from Message to [ Message ] msg_gen = self._pre_treat(msg) m = next(msg_gen, None) # Run in-treatment: from Message to [ Response ] while m is not None: - for response in self._in_treat(m): - # Run post-treatment: from Response to [ Response ] - yield from self._post_treat(response) + + hook_gen = self._in_hooks(m) + hook = next(hook_gen, None) + if hook is not None: + handled = True + + for response in self._in_treat(m, hook, hook_gen): + # Run post-treatment: from Response to [ Response ] + yield from self._post_treat(response) m = next(msg_gen, None) + + if not handled: + for m in self._in_miss(msg): + yield from self._post_treat(m) except BaseException as e: logger.exception("Error occurred during the processing of the %s: " "%s", type(msg).__name__, msg) @@ -83,38 +95,53 @@ class MessageTreater: yield msg - def _in_treat(self, msg): + def _in_hooks(self, msg): + for h in self.hm.get_hooks("in", type(msg).__name__): + if h.can_read(msg.to, msg.server) and h.match(msg): + yield h + + + def _in_treat(self, msg, hook, hook_gen): """Treats Messages and returns Responses Arguments: msg -- message to treat """ - res = False + while hook is not None: + res = hook.run(msg) - hooks = self.hm.get_hooks("in", type(msg).__name__) - for h in hooks: - if h.can_read(msg.to, msg.server) and h.match(msg): - res = h.run(msg) + if isinstance(res, list): + for r in res: + yield r - if isinstance(res, list): - for r in res: - yield r + elif res is not None: + if not hasattr(res, "server") or res.server is None: + res.server = msg.server - elif res is not None: - if not hasattr(res, "server") or res.server is None: - res.server = msg.server + yield res - yield res + hook = next(hook_gen, None) + + def _in_miss(self, msg): from nemubot.message.command import Command as CommandMessage - if res is False and isinstance(msg, CommandMessage): + from nemubot.message.directask import DirectAsk as DirectAskMessage + + if isinstance(msg, CommandMessage): from nemubot.hooks import Command as CommandHook - from nemubot.exception import IMException from nemubot.tools.human import guess + hooks = self.hm.get_reverse_hooks("in", type(msg).__name__) suggest = [s for s in guess(msg.cmd, [h.name for h in hooks if isinstance(h, CommandHook) and h.name is not None])] if len(suggest) >= 1: - yield IMException("Unknown command %s. Would you mean: %s?" % (msg.cmd, ", ".join(suggest))).fill_response(msg) + yield DirectAskMessage(msg.frm, + "Unknown command %s. Would you mean: %s?" % (msg.cmd, ", ".join(suggest)), + to=msg.to_response) + + elif isinstance(msg, DirectAskMessage): + yield DirectAskMessage(msg.frm, + "Sorry, I'm just a bot and your sentence is too complex for me :( But feel free to teach me some tricks at https://github.com/nemunaire/nemubot/!", + to=msg.to_response) def _post_treat(self, msg): @@ -124,7 +151,7 @@ class MessageTreater: msg -- response to treat """ - for h in self.hm.get_hooks("post"): + for h in self.hm.get_hooks("post", type(msg).__name__): if h.can_write(msg.to, msg.server) and h.match(msg): res = h.run(msg) From ea8656ce0d7542af0fedb0e8aa92931cc5e6bff1 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 18 Nov 2015 21:35:53 +0100 Subject: [PATCH 060/271] Refactor command help: use hookmanager to get command help instead of custom search --- nemubot/bot.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/nemubot/bot.py b/nemubot/bot.py index 8d45f3d..be1d88a 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -98,19 +98,17 @@ class Bot(threading.Thread): 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] == "!": - for module in self.modules: - for (s, h) in self.modules[module].__nemubot_context__.hooks: - if s == "in_Command" and (h.name is not None or h.regexp is not None) and ((h.name is not None and msg.args[0][1:] == h.name) or (h.regexp is not None and re.match(h.regexp, 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 from module %s" % (msg.args[0], module)) - elif h.help: - return res.append_message("Command %s from module %s: %s" % (msg.args[0], module, 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]) - else: - res.append_message("Sorry, there is no command %s" % msg.args[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: From 6fc65611866b37ac700331deb1539da29d161419 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 24 Nov 2015 20:33:02 +0100 Subject: [PATCH 061/271] [alias] Fix parsing error when creating a (not allowed) spaced alias --- modules/alias.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/alias.py b/modules/alias.py index f7aeddb..91ea2bb 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -267,7 +267,9 @@ def treat_alias(msg): def parseask(msg): if re.match(".*(register|set|cr[ée]{2}|new|nouvel(le)?) alias.*", msg.text) is not None: result = re.match(".*alias !?([^ ]+) ?(pour|for|=|:) ?(.+)$", msg.text) - if result.group(1) in context.data.getNode("aliases").index: + if result is None: + raise IMException("Something is wrong with your alias definition. Hint: spaces are not allowed.") + elif result.group(1) in context.data.getNode("aliases").index: raise IMException("this alias is already defined.") else: create_alias(result.group(1), From f47aa8c478db9d6370fa73ae687bedc0e9974f41 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 19 Nov 2015 19:13:27 +0100 Subject: [PATCH 062/271] Load module data on first access --- nemubot/modulecontext.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/nemubot/modulecontext.py b/nemubot/modulecontext.py index d562a98..1321c61 100644 --- a/nemubot/modulecontext.py +++ b/nemubot/modulecontext.py @@ -45,8 +45,8 @@ class ModuleContext: # Define some callbacks if context is not None: - # Load module data - self.data = context.datastore.load(module_name) + def load_data(): + return context.datastore.load(module_name) def add_hook(hook, *triggers): assert isinstance(hook, AbstractHook), hook @@ -76,8 +76,9 @@ class ModuleContext: return False else: # Used when using outside of nemubot - from nemubot.tools.xmlparser import module_state - self.data = module_state.ModuleState("nemubotstate") + def load_data(): + from nemubot.tools.xmlparser import module_state + return module_state.ModuleState("nemubotstate") def add_hook(hook, *triggers): assert isinstance(hook, AbstractHook), hook @@ -98,6 +99,7 @@ class ModuleContext: def save(): context.datastore.save(module_name, self.data) + self.load_data = load_data self.add_hook = add_hook self.del_hook = del_hook self.add_event = add_event @@ -107,6 +109,13 @@ class ModuleContext: self.subtreat = subtreat + @property + def data(self): + if not hasattr(self, "_data"): + self._data = self.load_data() + return self._data + + def unload(self): """Perform actions for unloading the module""" From e03d803ae0ead191339675954c8945f43bc5e0b8 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 20 Nov 2015 22:47:52 +0100 Subject: [PATCH 063/271] [wolframalpha] Servers take a long times to respond theses days :( --- modules/wolframalpha.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/wolframalpha.py b/modules/wolframalpha.py index a83b500..1d09c5b 100644 --- a/modules/wolframalpha.py +++ b/modules/wolframalpha.py @@ -33,7 +33,8 @@ def load(context): class WFAResults: def __init__(self, terms): - self.wfares = web.getXML(URL_API % quote(terms)) + self.wfares = web.getXML(URL_API % quote(terms), + timeout=12) @property From 1e29061bc9392ae3277ed56c5fd96eb331d09114 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 21 Nov 2015 16:26:12 +0100 Subject: [PATCH 064/271] [urlreducer] Framalink is in fact LSTU --- modules/{framalink.py => urlreducer.py} | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) rename modules/{framalink.py => urlreducer.py} (95%) diff --git a/modules/framalink.py b/modules/urlreducer.py similarity index 95% rename from modules/framalink.py rename to modules/urlreducer.py index 7acc5a5..ec03307 100644 --- a/modules/framalink.py +++ b/modules/urlreducer.py @@ -23,7 +23,7 @@ def default_reducer(url, data): def ycc_reducer(url, data): return "http://ycc.fr/%s" % default_reducer(url, data) -def framalink_reducer(url, data): +def lstu_reducer(url, data): json_data = json.loads(web.getURLContent(url, "lsturl=" + quote(data), header={"Content-Type": "application/x-www-form-urlencoded"})) if 'short' in json_data: @@ -38,7 +38,9 @@ def framalink_reducer(url, data): PROVIDERS = { "tinyurl": (default_reducer, "http://tinyurl.com/api-create.php?url="), "ycc": (ycc_reducer, "http://ycc.fr/redirection/create/"), - "framalink": (framalink_reducer, "https://frama.link/a?format=json") + "framalink": (lstu_reducer, "https://frama.link/a?format=json"), + "huitre": (lstu_reducer, "https://huit.re/a?format=json"), + "lstu": (lstu_reducer, "https://lstu.fr/a?format=json"), } DEFAULT_PROVIDER = "framalink" From d59f629dd95ac4f6f2d45e9e77aa59c39bda1cd9 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 22 Nov 2015 14:46:34 +0100 Subject: [PATCH 065/271] Xmlparser: new class that just store one node, futher nodes will be parsed --- nemubot/tools/xmlparser/genericnode.py | 77 ++++++++++++++++---------- 1 file changed, 49 insertions(+), 28 deletions(-) diff --git a/nemubot/tools/xmlparser/genericnode.py b/nemubot/tools/xmlparser/genericnode.py index efbdda9..fbe8f2c 100644 --- a/nemubot/tools/xmlparser/genericnode.py +++ b/nemubot/tools/xmlparser/genericnode.py @@ -14,44 +14,24 @@ # 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/>. -class GenericNode: +class ParsingNode: - def __init__(self, tag, **kwargs): + """Allow any kind of subtags, just keep parsed ones + """ + + def __init__(self, tag=None, **kwargs): self.tag = tag self.attrs = kwargs self.content = "" self.children = [] - self._cur = None - self._deep_cur = 0 - - - def startElement(self, name, attrs): - if self._cur is None: - self._cur = GenericNode(name, **attrs) - self._deep_cur = 0 - else: - self._deep_cur += 1 - self._cur.startElement(name, attrs) - return True def characters(self, content): - if self._cur is None: - self.content += content - else: - self._cur.characters(content) + self.content += content - def endElement(self, name): - if name is None: - return - - if self._deep_cur: - self._deep_cur -= 1 - self._cur.endElement(name) - else: - self.children.append(self._cur) - self._cur = None + def addChild(self, name, child): + self.children.append(child) return True @@ -71,3 +51,44 @@ class GenericNode: def __contains__(self, item): return item in self.attrs + + +class GenericNode(ParsingNode): + + """Consider all subtags as dictionnary + """ + + def __init__(self, tag, **kwargs): + super().__init__(tag, **kwargs) + self._cur = None + self._deep_cur = 0 + + + def startElement(self, name, attrs): + if self._cur is None: + self._cur = GenericNode(name, **attrs) + self._deep_cur = 0 + else: + self._deep_cur += 1 + self._cur.startElement(name, attrs) + return True + + + def characters(self, content): + if self._cur is None: + super().characters(content) + else: + self._cur.characters(content) + + + def endElement(self, name): + if name is None: + return + + if self._deep_cur: + self._deep_cur -= 1 + self._cur.endElement(name) + else: + self.children.append(self._cur) + self._cur = None + return True From cd0dbc4cc29220dd0311d72bda4828f67243f0fb Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 23 Nov 2015 08:57:37 +0100 Subject: [PATCH 066/271] Xmlparser: parser for lists and dicts --- nemubot/tools/xmlparser/basic.py | 108 +++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 nemubot/tools/xmlparser/basic.py diff --git a/nemubot/tools/xmlparser/basic.py b/nemubot/tools/xmlparser/basic.py new file mode 100644 index 0000000..8e61822 --- /dev/null +++ b/nemubot/tools/xmlparser/basic.py @@ -0,0 +1,108 @@ +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2015 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/>. + +class ListNode: + + """XML node representing a Python dictionnnary + """ + + def __init__(self, **kwargs): + self.items = list() + + + def addChild(self, name, child): + self.items.append(child) + return True + + + def __len__(self): + return len(self.items) + + def __getitem__(self, item): + return self.items[item] + + def __setitem__(self, item, v): + self.items[item] = v + + def __contains__(self, item): + return item in self.items + + def __repr__(self): + return self.items.__repr__() + + +class DictNode: + + """XML node representing a Python dictionnnary + """ + + def __init__(self, **kwargs): + self.items = dict() + self._cur = None + + + def startElement(self, name, attrs): + if self._cur is None and "key" in attrs: + self._cur = (attrs["key"], "") + return True + return False + + + def characters(self, content): + if self._cur is not None: + key, cnt = self._cur + if isinstance(cnt, str): + cnt += content + self._cur = key, cnt + + + def endElement(self, name): + if name is None or self._cur is None: + return + + key, cnt = self._cur + if isinstance(cnt, list) and len(cnt) == 1: + self.items[key] = cnt + else: + self.items[key] = cnt + + self._cur = None + return True + + + def addChild(self, name, child): + if self._cur is None: + return False + + key, cnt = self._cur + if not isinstance(cnt, list): + cnt = [] + cnt.append(child) + self._cur = key, cnt + return True + + + def __getitem__(self, item): + return self.items[item] + + def __setitem__(self, item, v): + self.items[item] = v + + def __contains__(self, item): + return item in self.items + + def __repr__(self): + return self.items.__repr__() From 57c460fc9c0fc28eb172a6feb89b2a00824d3c8e Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 25 Nov 2015 00:49:55 +0100 Subject: [PATCH 067/271] Simplify date extraction --- nemubot/tools/date.py | 44 +++++++++++++++++++------------------------ 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/nemubot/tools/date.py b/nemubot/tools/date.py index 9c14384..9e9bbad 100644 --- a/nemubot/tools/date.py +++ b/nemubot/tools/date.py @@ -18,8 +18,23 @@ import re +month_binding = { + "janvier": 1, "january": 1, "januar": 1, + "fevrier": 2, "février": 2, "february": 2, + "march": 3, "mars": 3, + "avril": 4, "april": 4, + "mai": 5, "may": 5, "maï": 5, + "juin": 6, "juni": 6, "junni": 6, + "juillet": 7, "jully": 7, "july": 7, + "aout": 8, "août": 8, "august": 8, + "septembre": 9, "september": 9, + "october": 10, "oktober": 10, "octobre": 10, + "november": 11, "novembre": 11, + "decembre": 12, "décembre": 12, "december": 12, +} + xtrdt = re.compile(r'''^.*? (?P<day>[0-9]{1,4}) .+? - (?P<month>[0-9]{1,2}|janvier|january|fevrier|février|february|mars|march|avril|april|mai|maï|may|juin|juni|juillet|july|jully|august|aout|août|septembre|september|october|octobre|oktober|novembre|november|decembre|décembre|december) + (?P<month>[0-9]{1,2}|"''' + "|".join(month_binding) + '''") (?:.+?(?P<year>[0-9]{1,4}))? (?:[^0-9]+ (?:(?P<hour>[0-9]{1,2})[^0-9]*[h':] (?:[^0-9]*(?P<minute>[0-9]{1,2}) @@ -33,30 +48,9 @@ def extractDate(msg): if result is not None: day = result.group("day") month = result.group("month") - if month == "janvier" or month == "january" or month == "januar": - month = 1 - elif month == "fevrier" or month == "février" or month == "february": - month = 2 - elif month == "mars" or month == "march": - month = 3 - elif month == "avril" or month == "april": - month = 4 - elif month == "mai" or month == "may" or month == "maï": - month = 5 - elif month == "juin" or month == "juni" or month == "junni": - month = 6 - elif month == "juillet" or month == "jully" or month == "july": - month = 7 - elif month == "aout" or month == "août" or month == "august": - month = 8 - elif month == "september" or month == "septembre": - month = 9 - elif month == "october" or month == "october" or month == "oktober": - month = 10 - elif month == "november" or month == "novembre": - month = 11 - elif month == "december" or month == "decembre" or month == "décembre": - month = 12 + + if month in month_binding: + month = month_binding[month] year = result.group("year") From 707131023aca73f1c9975eea5a66277d7e9b8c90 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 26 Nov 2015 20:51:07 +0100 Subject: [PATCH 068/271] [urlreducer] add some checks --- modules/urlreducer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/urlreducer.py b/modules/urlreducer.py index ec03307..cf9ee6b 100644 --- a/modules/urlreducer.py +++ b/modules/urlreducer.py @@ -91,7 +91,7 @@ def parselisten(msg): @hook.post() def parseresponse(msg): global LAST_URLS - if hasattr(msg, "text") and msg.text: + if hasattr(msg, "text") and isinstance(msg.text, str): urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?(?:[^ :/]+:[0-9]+)?[^ :]+)", msg.text) for url in urls: o = urlparse(web._getNormalizedURL(url), "http") From 274836e39aa5faafe0708a659d6482065f5a2505 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 27 Nov 2015 19:07:54 +0100 Subject: [PATCH 069/271] [github] Use default HTTP request timeout --- modules/github.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/modules/github.py b/modules/github.py index 1a345cd..2f79e9f 100644 --- a/modules/github.py +++ b/modules/github.py @@ -21,16 +21,14 @@ def help_full(): def info_repos(repo): return web.getJSON("https://api.github.com/search/repositories?q=%s" % - quote(repo), timeout=10) + quote(repo)) def info_user(username): - user = web.getJSON("https://api.github.com/users/%s" % quote(username), - timeout=10) + user = web.getJSON("https://api.github.com/users/%s" % quote(username)) user["repos"] = web.getJSON("https://api.github.com/users/%s/" - "repos?sort=updated" % quote(username), - timeout=10) + "repos?sort=updated" % quote(username)) return user From a3236cd67adc62f198d6a83977d1022109cfd85c Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 28 Nov 2015 16:19:08 +0100 Subject: [PATCH 070/271] [github] Dusting + fill help --- modules/github.py | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/modules/github.py b/modules/github.py index 2f79e9f..924c06e 100644 --- a/modules/github.py +++ b/modules/github.py @@ -1,7 +1,7 @@ -# coding=utf-8 - """Repositories, users or issues on GitHub""" +# PYTHON STUFFS ####################################################### + import re from urllib.parse import quote @@ -9,15 +9,10 @@ from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web -nemubotversion = 3.4 - from more import Response -def help_full(): - return ("!github /repo/: Display information about /repo/.\n" - "!github_user /user/: Display information about /user/.") - +# MODULE CORE ######################################################### def info_repos(repo): return web.getJSON("https://api.github.com/search/repositories?q=%s" % @@ -63,7 +58,13 @@ def info_commit(repo, commit=None): quote(fullname)) -@hook.command("github") +# MODULE INTERFACE #################################################### + +@hook.command("github", + help="Display information about some repositories", + help_usage={ + "REPO": "Display information about the repository REPO", + }) def cmd_github(msg): if not len(msg.args): raise IMException("indicate a repository name to search") @@ -91,7 +92,11 @@ def cmd_github(msg): return res -@hook.command("github_user") +@hook.command("github_user", + help="Display information about users", + help_usage={ + "USERNAME": "Display information about the user USERNAME", + }) def cmd_github_user(msg): if not len(msg.args): raise IMException("indicate a user name to search") @@ -124,7 +129,12 @@ def cmd_github_user(msg): return res -@hook.command("github_issue") +@hook.command("github_issue", + help="Display repository's issues", + help_usage={ + "REPO": "Display latest issues created on REPO", + "REPO #ISSUE": "Display the issue number #ISSUE for REPO", + }) def cmd_github_issue(msg): if not len(msg.args): raise IMException("indicate a repository to view its issues") @@ -162,7 +172,12 @@ def cmd_github_issue(msg): return res -@hook.command("github_commit") +@hook.command("github_commit", + help="Display repository's commits", + help_usage={ + "REPO": "Display latest commits on REPO", + "REPO COMMIT": "Display details for the COMMIT on REPO", + }) def cmd_github_commit(msg): if not len(msg.args): raise IMException("indicate a repository to view its commits") @@ -183,7 +198,7 @@ def cmd_github_commit(msg): commits = info_commit(repo, commit) if commits is None: - raise IMException("Repository not found") + raise IMException("Repository or commit not found") for commit in commits: res.append_message("Commit %s by %s on %s: %s" % From d4b6283e232b93629b3b05938cdfa03ebbf26733 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 29 Nov 2015 11:40:55 +0100 Subject: [PATCH 071/271] [github] new command to retrieve SSH keys --- modules/github.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/modules/github.py b/modules/github.py index 924c06e..ddd0851 100644 --- a/modules/github.py +++ b/modules/github.py @@ -28,6 +28,11 @@ def info_user(username): return user +def user_keys(username): + keys = web.getURLContent("https://github.com/%s.keys" % quote(username)) + return keys.split('\n') + + def info_issue(repo, issue=None): rp = info_repos(repo) if rp["items"]: @@ -129,6 +134,23 @@ def cmd_github_user(msg): return res +@hook.command("github_user_keys", + help="Display user SSH keys", + help_usage={ + "USERNAME": "Show USERNAME's SSH keys", + }) +def cmd_github_user_keys(msg): + if not len(msg.args): + raise IMException("indicate a user name to search") + + res = Response(channel=msg.channel, nomore="No more keys") + + for k in user_keys(" ".join(msg.args)): + res.append_message(k) + + return res + + @hook.command("github_issue", help="Display repository's issues", help_usage={ From a089efff1a2f7fd722de5e07dd20c796221b7fbf Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 30 Nov 2015 07:09:27 +0100 Subject: [PATCH 072/271] [more] Don't append space after a cut not ended by space --- modules/more.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/more.py b/modules/more.py index d9f121c..be0fb55 100644 --- a/modules/more.py +++ b/modules/more.py @@ -192,7 +192,9 @@ class Response: msg += self.title + ": " elif self.elt > 0: - msg += "[…] " + msg += "[…]" + if self.messages[0][self.elt - 1] == ' ': + msg += " " elts = self.messages[0][self.elt:] if isinstance(elts, list): From c9801ee2f7368976863f4178a07b62f0c4d23153 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 1 Dec 2015 00:49:09 +0100 Subject: [PATCH 073/271] [mapquest] Dusting + fill help --- modules/mapquest.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/modules/mapquest.py b/modules/mapquest.py index 2c42ad7..55b87c0 100644 --- a/modules/mapquest.py +++ b/modules/mapquest.py @@ -1,7 +1,7 @@ -# coding=utf-8 - """Transform name location to GPS coordinates""" +# PYTHON STUFFS ####################################################### + import re from urllib.parse import quote @@ -9,12 +9,15 @@ from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web -nemubotversion = 4.0 - from more import Response +# GLOBALS ############################################################# + URL_API = "http://open.mapquestapi.com/geocoding/v1/address?key=%s&location=%%s" + +# LOADING ############################################################# + def load(context): if not context.config or "apikey" not in context.config: raise ImportError("You need a MapQuest API key in order to use this " @@ -25,9 +28,7 @@ def load(context): URL_API = URL_API % context.config["apikey"].replace("%", "%%") -def help_full(): - return "!geocode /place/: get coordinate of /place/." - +# MODULE CORE ######################################################### def geocode(location): obj = web.getJSON(URL_API % quote(location)) @@ -43,7 +44,13 @@ def where(loc): "{adminArea1}".format(**loc)).strip() -@hook.command("geocode") +# MODULE INTERFACE #################################################### + +@hook.command("geocode", + help="Get GPS coordinates of a place", + help_usage={ + "PLACE": "Get GPS coordinates of PLACE" + }) def cmd_geocode(msg): if not len(msg.args): raise IMException("indicate a name") From 313c693d487da2ab1f948401a30405809cb7df53 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 2 Dec 2015 01:18:15 +0100 Subject: [PATCH 074/271] [imdb] Dusting + fill help --- modules/imdb.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/modules/imdb.py b/modules/imdb.py index 1e6c6e9..2434a3c 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -1,7 +1,7 @@ -# coding=utf-8 - """Show many information about a movie or serie""" +# PYTHON STUFFS ####################################################### + import re import urllib.parse @@ -9,14 +9,10 @@ from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web -nemubotversion = 3.4 - from more import Response -def help_full(): - return "Search a movie title with: !imdbs <approximative title> ; View movie details with !imdb <title>" - +# MODULE CORE ######################################################### def get_movie(title=None, year=None, imdbid=None, fullplot=True, tomatoes=False): """Returns the information about the matching movie""" @@ -68,9 +64,15 @@ def find_movies(title): raise IMException("An error occurs during movie search") -@hook.command("imdb") +# MODULE INTERFACE #################################################### + +@hook.command("imdb", + help="View movie/serie details, using OMDB", + help_usage={ + "TITLE": "Look for a movie titled TITLE", + "IMDB_ID": "Look for the movie with the given IMDB_ID", + }) def cmd_imdb(msg): - """View movie details with !imdb <title>""" if not len(msg.args): raise IMException("precise a movie/serie title!") @@ -97,9 +99,12 @@ def cmd_imdb(msg): return res -@hook.command("imdbs") +@hook.command("imdbs", + help="Search a movie/serie by title", + help_usage={ + "TITLE": "Search a movie/serie by TITLE", + }) def cmd_search(msg): - """!imdbs <approximative title> to search a movie title""" if not len(msg.args): raise IMException("precise a movie/serie title!") From 009ab088214f4ecac863123ac4ab94e9f6f486fa Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 3 Dec 2015 00:29:16 +0100 Subject: [PATCH 075/271] [man] Dusting + fill help --- modules/man.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/modules/man.py b/modules/man.py index 997b85b..f45e30d 100644 --- a/modules/man.py +++ b/modules/man.py @@ -1,6 +1,6 @@ -# coding=utf-8 +"""Read manual pages on IRC""" -"Read manual pages on IRC" +# PYTHON STUFFS ####################################################### import subprocess import re @@ -8,18 +8,22 @@ import os from nemubot.hooks import hook -nemubotversion = 3.4 - from more import Response -def help_full(): - return "!man [0-9] /what/: gives informations about /what/." +# GLOBALS ############################################################# RGXP_s = re.compile(b'\x1b\\[[0-9]+m') -@hook.command("MAN") +# MODULE INTERFACE #################################################### + +@hook.command("MAN", + help="Show man pages", + help_usage={ + "SUBJECT": "Display the default man page for SUBJECT", + "SECTION SUBJECT": "Display the man page in SECTION for SUBJECT" + }) def cmd_man(msg): args = ["man"] num = None @@ -52,7 +56,11 @@ def cmd_man(msg): return res -@hook.command("man") +@hook.command("man", + help="Show man pages synopsis (in one line)", + help_usage={ + "SUBJECT": "Display man page synopsis for SUBJECT", + }) def cmd_whatis(msg): args = ["whatis", " ".join(msg.args)] From 1d18305870fd0c5fefe78eb870768e692d487c12 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 12 Jan 2016 16:39:01 +0100 Subject: [PATCH 076/271] [grep] Add -o option --- modules/grep.py | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/modules/grep.py b/modules/grep.py index a5395c2..59d84e3 100644 --- a/modules/grep.py +++ b/modules/grep.py @@ -14,7 +14,7 @@ from more import Response # MODULE CORE ######################################################### -def grep(fltr, cmd, args, msg): +def grep(fltr, cmd, args, msg, only=False): """Perform a grep like on known nemubot structures Arguments: @@ -22,8 +22,11 @@ def grep(fltr, cmd, args, msg): cmd -- The subcommand to execute args -- subcommand arguments msg -- The original message + only -- like the --only-matching parameter of grep """ + fltr = re.compile(fltr) + for r in context.subtreat(Command(cmd, args, to_response=msg.to_response, @@ -33,16 +36,26 @@ def grep(fltr, cmd, args, msg): for i in range(len(r.messages) - 1, -1, -1): if isinstance(r.messages[i], list): for j in range(len(r.messages[i]) - 1, -1, -1): - if not re.match(fltr, r.messages[i][j]): + res = fltr.match(r.messages[i][j]) + if not res: r.messages[i].pop(j) + elif only: + r.messages[i][j] = res.group(1) if fltr.groups else res.group(0) if len(r.messages[i]) <= 0: r.messages.pop(i) - elif isinstance(r.messages[i], str) and not re.match(fltr, r.messages[i]): - r.messages.pop(i) + elif isinstance(r.messages[i], str): + res = fltr.match(r.messages[i]) + if not res: + r.messages.pop(i) + elif only: + r.messages[i] = res.group(1) if fltr.groups else res.group(0) yield r elif isinstance(r, Text): - if re.match(fltr, r.message): + res = fltr.match(r.message) + if res: + if only: + r.message = res.group(1) if fltr.groups else res.group(0) yield r else: @@ -53,12 +66,18 @@ def grep(fltr, cmd, args, msg): @hook.command("grep", help="Display only lines from a subcommand matching the given pattern", - help_usage={"PTRN !SUBCMD": "Filter SUBCMD command using the pattern PTRN"}) + help_usage={"PTRN !SUBCMD": "Filter SUBCMD command using the pattern PTRN"}, + keywords={ + "only": "Print only the matched parts of a matching line", + }) def cmd_grep(msg): if len(msg.args) < 2: raise IMException("Please provide a filter and a command") - return [m for m in grep(msg.args[0] if msg.args[0][0] == "^" else ".*" + msg.args[0] + ".*", + only = "only" in msg.kwargs + + return [m for m in grep(msg.args[0] if msg.args[0][0] == "^" else ".*?(" + msg.args[0] + ").*?", msg.args[1][1:] if msg.args[1][0] == "!" else msg.args[1], msg.args[2:], - msg)] + msg, + only=only)] From 9ff8a3a02b7f25846658e49e3cb5e5ce61c5fab5 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 12 Jan 2016 16:49:51 +0100 Subject: [PATCH 077/271] [grep] raise an IMException if pattern not found --- modules/grep.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/modules/grep.py b/modules/grep.py index 59d84e3..a9a4adc 100644 --- a/modules/grep.py +++ b/modules/grep.py @@ -76,8 +76,13 @@ def cmd_grep(msg): only = "only" in msg.kwargs - return [m for m in grep(msg.args[0] if msg.args[0][0] == "^" else ".*?(" + msg.args[0] + ").*?", - msg.args[1][1:] if msg.args[1][0] == "!" else msg.args[1], - msg.args[2:], - msg, - only=only)] + l = [m for m in grep(msg.args[0] if msg.args[0][0] == "^" else ".*?(" + msg.args[0] + ").*?", + msg.args[1][1:] if msg.args[1][0] == "!" else msg.args[1], + msg.args[2:], + msg, + only=only) if m is not None] + + if len(l) <= 0: + raise IMException("Pattern not found in output") + + return l From d705d351c09ebad0be227cbbc6a5d8d2e263613c Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 12 Jan 2016 16:50:49 +0100 Subject: [PATCH 078/271] [grep] Add @nocase option, --ignore-case like --- modules/grep.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/modules/grep.py b/modules/grep.py index a9a4adc..df1b794 100644 --- a/modules/grep.py +++ b/modules/grep.py @@ -14,7 +14,7 @@ from more import Response # MODULE CORE ######################################################### -def grep(fltr, cmd, args, msg, only=False): +def grep(fltr, cmd, args, msg, icase=False, only=False): """Perform a grep like on known nemubot structures Arguments: @@ -22,10 +22,11 @@ def grep(fltr, cmd, args, msg, only=False): cmd -- The subcommand to execute args -- subcommand arguments msg -- The original message + icase -- like the --ignore-case parameter of grep only -- like the --only-matching parameter of grep """ - fltr = re.compile(fltr) + fltr = re.compile(fltr, re.I if icase else 0) for r in context.subtreat(Command(cmd, args, @@ -68,6 +69,7 @@ def grep(fltr, cmd, args, msg, only=False): help="Display only lines from a subcommand matching the given pattern", help_usage={"PTRN !SUBCMD": "Filter SUBCMD command using the pattern PTRN"}, keywords={ + "nocase": "Perform case-insensitive matching", "only": "Print only the matched parts of a matching line", }) def cmd_grep(msg): @@ -80,6 +82,7 @@ def cmd_grep(msg): msg.args[1][1:] if msg.args[1][0] == "!" else msg.args[1], msg.args[2:], msg, + icase="nocase" in msg.kwargs, only=only) if m is not None] if len(l) <= 0: From 277d55d5219725240823a861150b8df90a5805c1 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 12 Jan 2016 18:09:01 +0100 Subject: [PATCH 079/271] Add subparse method in context, that use server parser --- nemubot/modulecontext.py | 5 +++++ nemubot/server/IRC.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/nemubot/modulecontext.py b/nemubot/modulecontext.py index 1321c61..1d1b3d0 100644 --- a/nemubot/modulecontext.py +++ b/nemubot/modulecontext.py @@ -99,6 +99,10 @@ class ModuleContext: def save(): context.datastore.save(module_name, self.data) + def subparse(orig, cnt): + if orig.server in context.servers: + return context.servers[orig.server].subparse(orig, cnt) + self.load_data = load_data self.add_hook = add_hook self.del_hook = del_hook @@ -107,6 +111,7 @@ class ModuleContext: self.save = save self.send_response = send_response self.subtreat = subtreat + self.subparse = subparse @property diff --git a/nemubot/server/IRC.py b/nemubot/server/IRC.py index 9da3235..e433176 100644 --- a/nemubot/server/IRC.py +++ b/nemubot/server/IRC.py @@ -268,3 +268,8 @@ class IRC(SocketServer): mes = msg.to_bot_message(self) if mes is not None: yield mes + + + 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) From 1d13d56dced13ae94c96a06f5404eb5aa8eb6931 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 13 Jan 2016 00:32:05 +0100 Subject: [PATCH 080/271] [cat] New module performing cat like action --- modules/cat.py | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 modules/cat.py diff --git a/modules/cat.py b/modules/cat.py new file mode 100644 index 0000000..0619cee --- /dev/null +++ b/modules/cat.py @@ -0,0 +1,55 @@ +"""Concatenate commands""" + +# PYTHON STUFFS ####################################################### + +from nemubot import context +from nemubot.exception import IMException +from nemubot.hooks import hook +from nemubot.message import Command, DirectAsk, Text + +from more import Response + + +# MODULE CORE ######################################################### + +def cat(msg, *terms): + res = Response(channel=msg.to_response, server=msg.server) + for term in terms: + m = context.subparse(msg, term) + if isinstance(m, Command) or isinstance(m, DirectAsk): + for r in context.subtreat(m): + if isinstance(r, Response): + for t in range(len(r.messages)): + res.append_message(r.messages[t], + title=r.rawtitle if not isinstance(r.rawtitle, list) else r.rawtitle[t]) + + elif isinstance(r, Text): + res.append_message(r.message) + + elif isinstance(r, str): + res.append_message(r) + + else: + res.append_message(term) + + return res + + +# MODULE INTERFACE #################################################### + +@hook.command("cat", + help="Concatenate responses of commands given as argument", + help_usage={"!SUBCMD [!SUBCMD [...]]": "Concatenate response of subcommands"}, + keywords={ + "merge": "Merge messages into the same", + }) +def cmd_cat(msg): + if len(msg.args) < 1: + raise IMException("No subcommand to concatenate") + + r = cat(msg, *msg.args) + + if "merge" in msg.kwargs and len(r.messages) > 1: + r.messages = [ r.messages ] + + return r From 645c18c981c46cb648a6795b477c36efb0fd8ca3 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 14 Jan 2016 23:54:11 +0100 Subject: [PATCH 081/271] [grep] use subparse feature --- modules/grep.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/modules/grep.py b/modules/grep.py index df1b794..6a26c02 100644 --- a/modules/grep.py +++ b/modules/grep.py @@ -14,13 +14,12 @@ from more import Response # MODULE CORE ######################################################### -def grep(fltr, cmd, args, msg, icase=False, only=False): +def grep(fltr, cmd, msg, icase=False, only=False): """Perform a grep like on known nemubot structures Arguments: fltr -- The filter regexp cmd -- The subcommand to execute - args -- subcommand arguments msg -- The original message icase -- like the --ignore-case parameter of grep only -- like the --only-matching parameter of grep @@ -28,11 +27,7 @@ def grep(fltr, cmd, args, msg, icase=False, only=False): fltr = re.compile(fltr, re.I if icase else 0) - for r in context.subtreat(Command(cmd, - args, - to_response=msg.to_response, - frm=msg.frm, - server=msg.server)): + for r in context.subtreat(context.subparse(msg, cmd)): if isinstance(r, Response): for i in range(len(r.messages) - 1, -1, -1): if isinstance(r.messages[i], list): @@ -79,8 +74,7 @@ def cmd_grep(msg): only = "only" in msg.kwargs l = [m for m in grep(msg.args[0] if msg.args[0][0] == "^" else ".*?(" + msg.args[0] + ").*?", - msg.args[1][1:] if msg.args[1][0] == "!" else msg.args[1], - msg.args[2:], + " ".join(msg.args[1:]), msg, icase="nocase" in msg.kwargs, only=only) if m is not None] From bd2eff83b751f855a586c1a582989a1fe769423c Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 15 Jan 2016 19:18:17 +0100 Subject: [PATCH 082/271] [alias] Use alias command to display and define new aliases --- modules/alias.py | 43 ++++++++++++++++++------------------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/modules/alias.py b/modules/alias.py index 91ea2bb..3ec97d9 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -206,19 +206,31 @@ def cmd_listalias(msg): @hook.command("alias", - help="Display the replacement command for a given alias") + help="Display or define the replacement command for a given alias", + help_usage={ + "ALIAS": "Extends the given alias", + "ALIAS COMMAND [ARGS ...]": "Create a new alias named ALIAS as replacement to the given COMMAND and ARGS", + }) def cmd_alias(msg): if not len(msg.args): raise IMException("!alias takes as argument an alias to extend.") - res = list() - for alias in msg.args: + + elif len(msg.args) == 1: + alias = msg.args[0] if alias[0] == "!": alias = alias[1:] if alias in context.data.getNode("aliases").index: - res.append("!%s correspond to %s" % (alias, context.data.getNode("aliases").index[alias]["origin"])) + return Response("!%s correspond to %s" % (alias, context.data.getNode("aliases").index[alias]["origin"]), channel=msg.channel, nick=msg.nick) else: - res.append("!%s is not an alias" % alias) - return Response(res, channel=msg.channel, nick=msg.nick) + return Response("!%s is not an alias" % alias, channel=msg.channel, nick=msg.nick) + + else: + create_alias(msg.args[0], + " ".join(msg.args[1:]), + channel=msg.channel, + creator=msg.nick) + return Response("New alias %s successfully registered." % + msg.args[0], channel=msg.channel) @hook.command("unalias", @@ -261,22 +273,3 @@ def treat_alias(msg): return [msg, nmsg] return msg - - -@hook.ask() -def parseask(msg): - if re.match(".*(register|set|cr[ée]{2}|new|nouvel(le)?) alias.*", msg.text) is not None: - result = re.match(".*alias !?([^ ]+) ?(pour|for|=|:) ?(.+)$", msg.text) - if result is None: - raise IMException("Something is wrong with your alias definition. Hint: spaces are not allowed.") - elif result.group(1) in context.data.getNode("aliases").index: - raise IMException("this alias is already defined.") - else: - create_alias(result.group(1), - result.group(3), - channel=msg.channel, - creator=msg.nick) - res = Response("New alias %s successfully registered." % - result.group(1), channel=msg.channel) - return res - return None From d028afd09e330183b1ecef77ddc4e656769a3216 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 16 Jan 2016 14:50:43 +0100 Subject: [PATCH 083/271] [alias] use subparse method --- modules/alias.py | 44 +++++++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/modules/alias.py b/modules/alias.py index 3ec97d9..0e361b3 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -215,22 +215,24 @@ def cmd_alias(msg): if not len(msg.args): raise IMException("!alias takes as argument an alias to extend.") - elif len(msg.args) == 1: - alias = msg.args[0] - if alias[0] == "!": - alias = alias[1:] - if alias in context.data.getNode("aliases").index: - return Response("!%s correspond to %s" % (alias, context.data.getNode("aliases").index[alias]["origin"]), channel=msg.channel, nick=msg.nick) - else: - return Response("!%s is not an alias" % alias, channel=msg.channel, nick=msg.nick) + alias = context.subparse(msg, msg.args[0]) + if alias is None or not isinstance(alias, Command): + raise IMException("%s is not a valid alias" % msg.args[0]) - else: - create_alias(msg.args[0], + if alias.cmd in context.data.getNode("aliases").index: + return Response("%s corresponds to %s" % (alias.cmd, context.data.getNode("aliases").index[alias.cmd]["origin"]), + channel=msg.channel, nick=msg.nick) + + elif len(msg.args) > 1: + create_alias(alias.cmd, " ".join(msg.args[1:]), channel=msg.channel, creator=msg.nick) - return Response("New alias %s successfully registered." % - msg.args[0], channel=msg.channel) + return Response("New alias %s successfully registered." % alias.cmd, + channel=msg.channel) + + else: + raise IMException("%s is not an alias" % msg.args[0]) @hook.command("unalias", @@ -257,19 +259,15 @@ def cmd_unalias(msg): @hook.add(["pre","Command"]) def treat_alias(msg): if msg.cmd in context.data.getNode("aliases").index: - txt = context.data.getNode("aliases").index[msg.cmd]["origin"] - # TODO: for legacy compatibility - if txt[0] == "!": - txt = txt[1:] - try: - args = shlex.split(txt) - except ValueError: - args = txt.split(' ') - nmsg = Command(args[0], args=replace_variables(args[1:], msg) + msg.args, kwargs=msg.kwargs, **msg.export_args()) + origin = context.data.getNode("aliases").index[msg.cmd]["origin"] + rpl_cmd = context.subparse(msg, origin) + rpl_cmd.args = replace_variables(rpl_cmd.args, msg) + rpl_cmd.args += msg.args + rpl_cmd.kwargs.update(msg.kwargs) # Avoid infinite recursion - if msg.cmd != nmsg.cmd: + if msg.cmd != rpl_cmd.cmd: # Also return origin message, if it can be treated as well - return [msg, nmsg] + return [msg, rpl_cmd] return msg From ff6460b92e1baf5e1ca955fd6593230fda0e8cd6 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 17 Jan 2016 15:45:03 +0100 Subject: [PATCH 084/271] Fix IRC message parameter escape --- nemubot/server/message/IRC.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nemubot/server/message/IRC.py b/nemubot/server/message/IRC.py index 9f69a8c..9be010d 100644 --- a/nemubot/server/message/IRC.py +++ b/nemubot/server/message/IRC.py @@ -180,7 +180,7 @@ class IRC(Abstract): for i in range(len(args) - 1, 0, -1): arg = args[i] if len(arg) > 2: - if arg[0:1] == '\\@': + if arg[0:2] == '\\@': args[i] = arg[1:] elif arg[0] == '@': arsp = arg[1:].split("=", 1) From 09e3b082c19a98e43798f43aa5d2fe638688ad3b Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 21 Jan 2016 18:43:34 +0100 Subject: [PATCH 085/271] [alias] Give near alias in case of error --- modules/alias.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/alias.py b/modules/alias.py index 0e361b3..2ed48cb 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -4,12 +4,12 @@ import re from datetime import datetime, timezone -import shlex from nemubot import context from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.message import Command +from nemubot.tools.human import guess from nemubot.tools.xmlparser.node import ModuleState from more import Response @@ -232,7 +232,8 @@ def cmd_alias(msg): channel=msg.channel) else: - raise IMException("%s is not an alias" % msg.args[0]) + wym = guess(alias.cmd, context.data.getNode("aliases").index) + raise IMException(msg.args[0] + " is not an alias." + (" Would you mean: %s?" % ", ".join(wym)) if len(wym) else "") @hook.command("unalias", From 6ad979a5eb63f7aca902faba5249ce8086735049 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 22 Jan 2016 19:52:21 +0100 Subject: [PATCH 086/271] Fix event/timer issue if very close to 0 --- nemubot/bot.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/nemubot/bot.py b/nemubot/bot.py index be1d88a..a874e7b 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -380,11 +380,9 @@ class Bot(threading.Thread): self.event_timer.cancel() if len(self.events): - logger.debug("Update timer: next event in %d seconds", - self.events[0].time_left.seconds) - self.event_timer = threading.Timer( - self.events[0].time_left.seconds + self.events[0].time_left.microseconds / 1000000 if datetime.now(timezone.utc) < self.events[0].current else 0, - self._end_event_timer) + remaining = self.events[0].time_left.seconds + self.events[0].time_left.microseconds / 1000000 + 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: From 663e5e720708e236d0795ce4e207a8ff1f206c3e Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 27 Feb 2016 16:55:29 +0100 Subject: [PATCH 087/271] Don't force python3.3 --- bin/nemubot | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/nemubot b/bin/nemubot index 5cc8bd5..1c2e681 100755 --- a/bin/nemubot +++ b/bin/nemubot @@ -1,4 +1,4 @@ -#!/usr/bin/env python3.3 +#!/usr/bin/env python3 # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier From 26668c81b10658cf88171cefb2f42790ad42cd32 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 27 Feb 2016 16:56:00 +0100 Subject: [PATCH 088/271] Update README --- README.md | 41 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e93cbaf..aa3b141 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ -# *nemubot* +nemubot +======= An extremely modulable IRC bot, built around XML configuration files! -## Requirements +Requirements +------------ *nemubot* requires at least Python 3.3 to work. @@ -12,6 +14,37 @@ Some modules (like `cve`, `nextstop` or `laposte`) require the but the core and framework has no dependency. -## Documentation +Installation +------------ -Have a look to the wiki at https://github.com/nemunaire/nemubot/wiki +Use the `setup.py` file: `python setup.py install`. + +### VirtualEnv setup + +The easiest way to do this is through a virtualenv: + +```sh +virtualenv venv +. venv/bin/activate +python setup.py install +``` + +### Create a new configuration file + +There is a sample configuration file, called `bot_sample.xml`. You can +create your own configuration file from it. + + +Usage +----- + +Don't forget to activate your virtualenv in further terminals, if you +use it. + +To launch the bot, run: + +```sh +nemubot bot.xml +``` + +Where `bot.xml` is your configuration file. From a05821620db6e89dbd11985226e584e0ba6bea35 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 4 Mar 2016 19:02:56 +0100 Subject: [PATCH 089/271] [alias] Allow arguments only on Command --- modules/alias.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/modules/alias.py b/modules/alias.py index 2ed48cb..19f38b7 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -262,9 +262,12 @@ def treat_alias(msg): if msg.cmd in context.data.getNode("aliases").index: origin = context.data.getNode("aliases").index[msg.cmd]["origin"] rpl_cmd = context.subparse(msg, origin) - rpl_cmd.args = replace_variables(rpl_cmd.args, msg) - rpl_cmd.args += msg.args - rpl_cmd.kwargs.update(msg.kwargs) + if isinstance(rpl_cmd, Command): + rpl_cmd.args = replace_variables(rpl_cmd.args, msg) + rpl_cmd.args += msg.args + rpl_cmd.kwargs.update(msg.kwargs) + elif len(msg.args) or len(msg.kwargs): + raise IMException("This kind of alias doesn't take any argument (haven't you forgotten the '!'?).") # Avoid infinite recursion if msg.cmd != rpl_cmd.cmd: From 2c3d61495fa9beeaecf5866ef44a4a557a408a71 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 31 Jan 2016 20:45:44 +0100 Subject: [PATCH 090/271] Welcome in 2016... Happy new year! --- bin/nemubot | 2 +- nemubot/bot.py | 2 +- nemubot/channel.py | 2 +- nemubot/config/__init__.py | 2 +- nemubot/config/include.py | 2 +- nemubot/config/module.py | 2 +- nemubot/config/nemubot.py | 2 +- nemubot/config/server.py | 2 +- nemubot/datastore/__init__.py | 2 +- nemubot/datastore/abstract.py | 2 +- nemubot/hooks/keywords/__init__.py | 2 +- nemubot/hooks/keywords/abstract.py | 2 +- nemubot/hooks/keywords/dict.py | 2 +- nemubot/message/printer/IRC.py | 2 +- nemubot/message/printer/__init__.py | 2 +- nemubot/message/printer/socket.py | 2 +- nemubot/message/printer/test_socket.py | 2 +- nemubot/server/message/IRC.py | 2 +- nemubot/server/message/abstract.py | 2 +- nemubot/tools/xmlparser/__init__.py | 2 +- nemubot/tools/xmlparser/basic.py | 2 +- nemubot/tools/xmlparser/genericnode.py | 2 +- nemubot/tools/xmlparser/node.py | 2 +- 23 files changed, 23 insertions(+), 23 deletions(-) diff --git a/bin/nemubot b/bin/nemubot index 1c2e681..c248802 100755 --- a/bin/nemubot +++ b/bin/nemubot @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 Mercier Pierre-Olivier +# Copyright (C) 2012-2016 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 diff --git a/nemubot/bot.py b/nemubot/bot.py index a874e7b..f244b7a 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -1,5 +1,5 @@ # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 Mercier Pierre-Olivier +# Copyright (C) 2012-2016 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 diff --git a/nemubot/channel.py b/nemubot/channel.py index 506251e..a070131 100644 --- a/nemubot/channel.py +++ b/nemubot/channel.py @@ -1,5 +1,5 @@ # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 Mercier Pierre-Olivier +# Copyright (C) 2012-2016 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 diff --git a/nemubot/config/__init__.py b/nemubot/config/__init__.py index 497bd9e..7e0b74a 100644 --- a/nemubot/config/__init__.py +++ b/nemubot/config/__init__.py @@ -1,5 +1,5 @@ # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 Mercier Pierre-Olivier +# Copyright (C) 2012-2016 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 diff --git a/nemubot/config/include.py b/nemubot/config/include.py index 40bea9a..408c09a 100644 --- a/nemubot/config/include.py +++ b/nemubot/config/include.py @@ -1,5 +1,5 @@ # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 Mercier Pierre-Olivier +# Copyright (C) 2012-2016 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 diff --git a/nemubot/config/module.py b/nemubot/config/module.py index 670e97b..ab51971 100644 --- a/nemubot/config/module.py +++ b/nemubot/config/module.py @@ -1,5 +1,5 @@ # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 Mercier Pierre-Olivier +# Copyright (C) 2012-2016 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 diff --git a/nemubot/config/nemubot.py b/nemubot/config/nemubot.py index a2548a4..992cd8e 100644 --- a/nemubot/config/nemubot.py +++ b/nemubot/config/nemubot.py @@ -1,5 +1,5 @@ # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 Mercier Pierre-Olivier +# Copyright (C) 2012-2016 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 diff --git a/nemubot/config/server.py b/nemubot/config/server.py index c856649..14ca9a8 100644 --- a/nemubot/config/server.py +++ b/nemubot/config/server.py @@ -1,5 +1,5 @@ # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 Mercier Pierre-Olivier +# Copyright (C) 2012-2016 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 diff --git a/nemubot/datastore/__init__.py b/nemubot/datastore/__init__.py index ed9e829..411eab1 100644 --- a/nemubot/datastore/__init__.py +++ b/nemubot/datastore/__init__.py @@ -1,5 +1,5 @@ # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 Mercier Pierre-Olivier +# Copyright (C) 2012-2016 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 diff --git a/nemubot/datastore/abstract.py b/nemubot/datastore/abstract.py index 6162d52..96e2c0d 100644 --- a/nemubot/datastore/abstract.py +++ b/nemubot/datastore/abstract.py @@ -1,5 +1,5 @@ # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 Mercier Pierre-Olivier +# Copyright (C) 2012-2016 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 diff --git a/nemubot/hooks/keywords/__init__.py b/nemubot/hooks/keywords/__init__.py index 68250bf..4b6419a 100644 --- a/nemubot/hooks/keywords/__init__.py +++ b/nemubot/hooks/keywords/__init__.py @@ -1,5 +1,5 @@ # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 Mercier Pierre-Olivier +# Copyright (C) 2012-2016 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 diff --git a/nemubot/hooks/keywords/abstract.py b/nemubot/hooks/keywords/abstract.py index 0e6dd0b..a990cf3 100644 --- a/nemubot/hooks/keywords/abstract.py +++ b/nemubot/hooks/keywords/abstract.py @@ -1,5 +1,5 @@ # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 Mercier Pierre-Olivier +# Copyright (C) 2012-2016 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 diff --git a/nemubot/hooks/keywords/dict.py b/nemubot/hooks/keywords/dict.py index 9fc85e3..e1429fc 100644 --- a/nemubot/hooks/keywords/dict.py +++ b/nemubot/hooks/keywords/dict.py @@ -1,5 +1,5 @@ # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 Mercier Pierre-Olivier +# Copyright (C) 2012-2016 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 diff --git a/nemubot/message/printer/IRC.py b/nemubot/message/printer/IRC.py index b874003..320366c 100644 --- a/nemubot/message/printer/IRC.py +++ b/nemubot/message/printer/IRC.py @@ -1,5 +1,5 @@ # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 Mercier Pierre-Olivier +# Copyright (C) 2012-2016 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 diff --git a/nemubot/message/printer/__init__.py b/nemubot/message/printer/__init__.py index bb58338..060118b 100644 --- a/nemubot/message/printer/__init__.py +++ b/nemubot/message/printer/__init__.py @@ -1,5 +1,5 @@ # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 Mercier Pierre-Olivier +# Copyright (C) 2012-2016 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 diff --git a/nemubot/message/printer/socket.py b/nemubot/message/printer/socket.py index 0d6276a..cb9bc4c 100644 --- a/nemubot/message/printer/socket.py +++ b/nemubot/message/printer/socket.py @@ -1,5 +1,5 @@ # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 Mercier Pierre-Olivier +# Copyright (C) 2012-2016 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 diff --git a/nemubot/message/printer/test_socket.py b/nemubot/message/printer/test_socket.py index aa8d833..41f74b0 100644 --- a/nemubot/message/printer/test_socket.py +++ b/nemubot/message/printer/test_socket.py @@ -1,5 +1,5 @@ # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 Mercier Pierre-Olivier +# Copyright (C) 2012-2016 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 diff --git a/nemubot/server/message/IRC.py b/nemubot/server/message/IRC.py index 9be010d..67eb2c1 100644 --- a/nemubot/server/message/IRC.py +++ b/nemubot/server/message/IRC.py @@ -1,5 +1,5 @@ # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 Mercier Pierre-Olivier +# Copyright (C) 2012-2016 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 diff --git a/nemubot/server/message/abstract.py b/nemubot/server/message/abstract.py index aa3b136..624e453 100644 --- a/nemubot/server/message/abstract.py +++ b/nemubot/server/message/abstract.py @@ -1,5 +1,5 @@ # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 Mercier Pierre-Olivier +# Copyright (C) 2012-2016 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 diff --git a/nemubot/tools/xmlparser/__init__.py b/nemubot/tools/xmlparser/__init__.py index 5e546f4..abc5bb9 100644 --- a/nemubot/tools/xmlparser/__init__.py +++ b/nemubot/tools/xmlparser/__init__.py @@ -1,5 +1,5 @@ # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 Mercier Pierre-Olivier +# Copyright (C) 2012-2016 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 diff --git a/nemubot/tools/xmlparser/basic.py b/nemubot/tools/xmlparser/basic.py index 8e61822..8456629 100644 --- a/nemubot/tools/xmlparser/basic.py +++ b/nemubot/tools/xmlparser/basic.py @@ -1,5 +1,5 @@ # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 Mercier Pierre-Olivier +# Copyright (C) 2012-2016 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 diff --git a/nemubot/tools/xmlparser/genericnode.py b/nemubot/tools/xmlparser/genericnode.py index fbe8f2c..9c29a23 100644 --- a/nemubot/tools/xmlparser/genericnode.py +++ b/nemubot/tools/xmlparser/genericnode.py @@ -1,5 +1,5 @@ # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 Mercier Pierre-Olivier +# Copyright (C) 2012-2016 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 diff --git a/nemubot/tools/xmlparser/node.py b/nemubot/tools/xmlparser/node.py index fa5d0a5..965a475 100644 --- a/nemubot/tools/xmlparser/node.py +++ b/nemubot/tools/xmlparser/node.py @@ -1,5 +1,5 @@ # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 Mercier Pierre-Olivier +# Copyright (C) 2012-2016 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 From 5fae67255b9d12fc883287cfecc52880c5272f8a Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 6 Mar 2016 17:07:20 +0100 Subject: [PATCH 091/271] Log Python version --- nemubot/bot.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nemubot/bot.py b/nemubot/bot.py index f244b7a..0adb587 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -43,7 +43,9 @@ class Bot(threading.Thread): threading.Thread.__init__(self) - logger.info("Initiate nemubot v%s", __version__) + 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 From 358499e6d51fdc43dc1681d81bbfe32c7f6a3a40 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 3 Apr 2016 17:40:20 +0200 Subject: [PATCH 092/271] Expect IM keyword argument in command to be at the begining of the args list --- nemubot/server/message/IRC.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/nemubot/server/message/IRC.py b/nemubot/server/message/IRC.py index 67eb2c1..f6d562f 100644 --- a/nemubot/server/message/IRC.py +++ b/nemubot/server/message/IRC.py @@ -175,20 +175,23 @@ class IRC(Abstract): except ValueError: args = text.split(' ') - # Extract explicit named arguments: @key=value or just @key + # Extract explicit named arguments: @key=value or just @key, only at begening kwargs = {} - for i in range(len(args) - 1, 0, -1): - arg = args[i] + while len(args) > 1: + arg = args[1] if len(arg) > 2: if arg[0:2] == '\\@': - args[i] = arg[1:] + args[1] = arg[1:] elif arg[0] == '@': arsp = arg[1:].split("=", 1) if len(arsp) == 2: kwargs[arsp[0]] = arsp[1] else: kwargs[arg[1:]] = None - args.pop(i) + args.pop(1) + continue + # Futher argument are considered as normal argument (this helps for subcommand treatment) + break return message.Command(cmd=args[0], args=args[1:], From abf810209e1274c695a94a823af76c7f4b75d542 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 6 Apr 2016 02:00:32 +0200 Subject: [PATCH 093/271] [alias] Fix empty error message --- modules/alias.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/alias.py b/modules/alias.py index 19f38b7..5089759 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -232,8 +232,8 @@ def cmd_alias(msg): channel=msg.channel) else: - wym = guess(alias.cmd, context.data.getNode("aliases").index) - raise IMException(msg.args[0] + " is not an alias." + (" Would you mean: %s?" % ", ".join(wym)) if len(wym) else "") + wym = [m for m in guess(alias.cmd, context.data.getNode("aliases").index)] + raise IMException(msg.args[0] + " is not an alias." + (" Would you mean: %s?" % ", ".join(wym) if len(wym) else "")) @hook.command("unalias", From 91b550754fcdb500c15f144622717c1b7f503ed9 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 30 May 2016 17:22:44 +0200 Subject: [PATCH 094/271] [cve] Reflects site changes --- modules/cve.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/cve.py b/modules/cve.py index 637d728..23a0302 100644 --- a/modules/cve.py +++ b/modules/cve.py @@ -19,8 +19,8 @@ def get_cve(cve_id): search_url = BASEURL_NIST + quote(cve_id.upper()) soup = BeautifulSoup(getURLContent(search_url)) - vuln = soup.body.find(class_="vulnDetail") - cvss = vuln.find(class_="cvssDetail") + vuln = soup.body.find(class_="vuln-detail") + cvss = vuln.findAll('div')[4] return [ "Base score: " + cvss.findAll('div')[0].findAll('a')[0].text.strip(), From f15ebd7c02dc42a624ef7d306e25264e6f42fa63 Mon Sep 17 00:00:00 2001 From: Max <max@23.tf> Date: Fri, 30 Oct 2015 20:55:02 +0100 Subject: [PATCH 095/271] [suivi] Fix TNT tracking --- modules/suivi.py | 21 +++++++++++++++++---- modules/urlreducer.py | 5 +++-- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/modules/suivi.py b/modules/suivi.py index 55c469f..19e4d20 100644 --- a/modules/suivi.py +++ b/modules/suivi.py @@ -16,12 +16,19 @@ from more import Response # POSTAGE SERVICE PARSERS ############################################ def get_tnt_info(track_id): + values = [] data = getURLContent('www.tnt.fr/public/suivi_colis/recherche/' 'visubontransport.do?bonTransport=%s' % track_id) soup = BeautifulSoup(data) - status = soup.find('p', class_='suivi-title-selected') - if status: - return status.get_text() + status_list = soup.find('div', class_='result__content') + if not status_list: + return None + last_status = status_list.find('div', class_='roster') + if last_status: + for info in last_status.find_all('div', class_='roster__item'): + values.append(info.get_text().strip()) + if len(values) == 3: + return (values[0], values[1], values[2]) def get_colissimo_info(colissimo_id): @@ -106,8 +113,14 @@ def get_laposte_info(laposte_id): def handle_tnt(tracknum): info = get_tnt_info(tracknum) if info: + status, date, place = info + placestr = '' + if place: + placestr = ' à \x02{place}\x0f' return ('Le colis \x02{trackid}\x0f a actuellement le status: ' - '\x02{status}\x0F'.format(trackid=tracknum, status=info)) + '\x02{status}\x0F mis à jour le \x02{date}\x0f{place}.' + .format(trackid=tracknum, status=status, + date=re.sub(r'\s+', ' ', date), place=placestr)) def handle_laposte(tracknum): diff --git a/modules/urlreducer.py b/modules/urlreducer.py index cf9ee6b..bd5dc9a 100644 --- a/modules/urlreducer.py +++ b/modules/urlreducer.py @@ -92,7 +92,8 @@ def parselisten(msg): def parseresponse(msg): global LAST_URLS if hasattr(msg, "text") and isinstance(msg.text, str): - urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?(?:[^ :/]+:[0-9]+)?[^ :]+)", msg.text) + urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?(?:[^ :/]+:[0-9]+)?[^ :]+)", + msg.text) for url in urls: o = urlparse(web._getNormalizedURL(url), "http") @@ -130,7 +131,7 @@ def cmd_reduceurl(msg): raise IMException("I have no more URL to reduce.") if len(msg.args) > 4: - raise IMException("I cannot reduce that maby URLs at once.") + raise IMException("I cannot reduce that many URLs at once.") else: minify += msg.args From f2c44a1108b2a206e17dcaf1acfefd2def01df46 Mon Sep 17 00:00:00 2001 From: Max <max@23.tf> Date: Fri, 22 Jul 2016 23:46:28 +0200 Subject: [PATCH 096/271] Add virtual radar flight tracking module --- modules/virtualradar.py | 100 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 modules/virtualradar.py diff --git a/modules/virtualradar.py b/modules/virtualradar.py new file mode 100644 index 0000000..ffd5a67 --- /dev/null +++ b/modules/virtualradar.py @@ -0,0 +1,100 @@ +"""Retrieve flight information from VirtualRadar APIs""" + +# PYTHON STUFFS ####################################################### + +import re +from urllib.parse import quote +import time + +from nemubot.exception import IMException +from nemubot.hooks import hook +from nemubot.tools import web + +from more import Response +import mapquest + +# GLOBALS ############################################################# + +URL_API = "http://public-api.adsbexchange.com/VirtualRadar/AircraftList.json?fcallC=%s" + +SPEED_TYPES = { + 0: 'Ground speed', + 1: 'Ground speed reversing', + 2: 'Indicated air speed', + 3: 'True air speed'} + +WTC_CAT = { + 0: 'None', + 1: 'Light', + 2: 'Medium', + 3: 'Heavy' + } + +SPECIES = { + 1: 'Land plane', + 2: 'Sea plane', + 3: 'Amphibian', + 4: 'Helicopter', + 5: 'Gyrocopter', + 6: 'Tiltwing', + 7: 'Ground vehicle', + 8: 'Tower'} + +HANDLER_TABLE = { + 'From': lambda x: 'From: \x02%s\x0F' % x, + 'To': lambda x: 'To: \x02%s\x0F' % x, + 'Op': lambda x: 'Airline: \x02%s\x0F' % x, + 'Mdl': lambda x: 'Model: \x02%s\x0F' % x, + 'Call': lambda x: 'Flight: \x02%s\x0F' % x, + 'PosTime': lambda x: 'Last update: \x02%s\x0F' % (time.ctime(int(x)/1000)), + 'Alt': lambda x: 'Altitude: \x02%s\x0F ft' % x, + 'Spd': lambda x: 'Speed: \x02%s\x0F kn' % x, + 'SpdTyp': lambda x: 'Speed type: \x02%s\x0F' % SPEED_TYPES[x] if x in SPEED_TYPES else None, + 'Engines': lambda x: 'Engines: \x02%s\x0F' % x, + 'Gnd': lambda x: 'On the ground' if x else None, + 'Mil': lambda x: 'Military aicraft' if x else None, + 'Species': lambda x: 'Aircraft species: \x02%s\x0F' % SPECIES[x] if x in SPECIES else None, + 'WTC': lambda x: 'Turbulence level: \x02%s\x0F' % WTC_CAT[x] if x in WTC_CAT else None, + } + +# MODULE CORE ######################################################### + +def virtual_radar(flight_call): + obj = web.getJSON(URL_API % quote(flight_call)) + + if "acList" in obj: + for flight in obj["acList"]: + yield flight + +def flight_info(flight): + for prop in HANDLER_TABLE: + if prop in flight: + yield HANDLER_TABLE[prop](flight[prop]) + +# MODULE INTERFACE #################################################### + +@hook.command("flight", + help="Get flight information", + help_usage={ "FLIGHT": "Get information on FLIGHT" }) +def cmd_flight(msg): + if not len(msg.args): + raise IMException("please indicate a flight") + + res = Response(channel=msg.channel, nick=msg.nick, + nomore="No more flights", count=" (%s more flights)") + + for param in msg.args: + for flight in virtual_radar(param): + if 'Lat' in flight and 'Long' in flight: + loc = None + for location in mapquest.geocode('{Lat},{Long}'.format(**flight)): + loc = location + break + if loc: + res.append_message('\x02{0}\x0F: Position: \x02{1}\x0F, {2}'.format(flight['Call'], \ + mapquest.where(loc), \ + ', '.join(filter(None, flight_info(flight))))) + continue + res.append_message('\x02{0}\x0F: {1}'.format(flight['Call'], \ + ', '.join(filter(None, flight_info(flight))))) + return res From e9cea5d010366e90cc15fbd08b4f25e538d0b5a4 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 29 Jul 2016 02:55:24 +0200 Subject: [PATCH 097/271] Fix events expiration --- modules/worldcup.py | 2 +- nemubot/bot.py | 4 ++-- nemubot/event/__init__.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/worldcup.py b/modules/worldcup.py index 512a247..7b4f53d 100644 --- a/modules/worldcup.py +++ b/modules/worldcup.py @@ -133,7 +133,7 @@ def prettify(match): if match["status"] == "completed": msg += "Match (%s) du %s terminé : " % (match["match_number"], matchdate.strftime("%A %d à %H:%M")) else: - msg += "Match en cours (%s) depuis %d minutes : " % (match["match_number"], (datetime.now(matchdate.tzinfo) - matchdate_local).seconds / 60) + msg += "Match en cours (%s) depuis %d minutes : " % (match["match_number"], (datetime.now(matchdate.tzinfo) - matchdate_local).total_seconds() / 60) msg += "%s %d - %d %s" % (match["home_team"]["country"], match["home_team"]["goals"], match["away_team"]["goals"], match["away_team"]["country"]) diff --git a/nemubot/bot.py b/nemubot/bot.py index 0adb587..e449f35 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -331,7 +331,7 @@ class Bot(threading.Thread): module_src.__nemubot_context__.events.append(evt.id) evt.module_src = module_src - logger.info("New event registered: %s -> %s", evt.id, evt) + logger.info("New event registered in %d position: %s", i, t) return evt.id @@ -382,7 +382,7 @@ class Bot(threading.Thread): self.event_timer.cancel() if len(self.events): - remaining = self.events[0].time_left.seconds + self.events[0].time_left.microseconds / 1000000 + 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() diff --git a/nemubot/event/__init__.py b/nemubot/event/__init__.py index 7b2adfd..c45081c 100644 --- a/nemubot/event/__init__.py +++ b/nemubot/event/__init__.py @@ -95,7 +95,7 @@ class ModuleEvent: """Return the time left before/after the near check""" if self.current is not None: return self.current - datetime.now(timezone.utc) - return 99999 # TODO: 99999 is not a valid time to return + return timedelta.max def check(self): """Run a check and realized the event if this is time""" From 3f2b18cae83579e73ceac5a83468ddfce82da4c8 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 29 Jul 2016 22:58:16 +0200 Subject: [PATCH 098/271] [mediawiki] Permit control of ssl and absolute path through keywords --- modules/mediawiki.py | 72 ++++++++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/modules/mediawiki.py b/modules/mediawiki.py index afc1ecb..a335c9e 100644 --- a/modules/mediawiki.py +++ b/modules/mediawiki.py @@ -16,10 +16,10 @@ from more import Response # MEDIAWIKI REQUESTS ################################################## -def get_namespaces(site, ssl=False): +def get_namespaces(site, ssl=False, path="/w/api.php"): # Built URL - url = "http%s://%s/w/api.php?format=json&action=query&meta=siteinfo&siprop=namespaces" % ( - "s" if ssl else "", site) + url = "http%s://%s%s?format=json&action=query&meta=siteinfo&siprop=namespaces" % ( + "s" if ssl else "", site, path) # Make the request data = web.getJSON(url) @@ -30,10 +30,10 @@ def get_namespaces(site, ssl=False): return namespaces -def get_raw_page(site, term, ssl=False): +def get_raw_page(site, term, ssl=False, path="/w/api.php"): # Built URL - url = "http%s://%s/w/api.php?format=json&redirects&action=query&prop=revisions&rvprop=content&titles=%s" % ( - "s" if ssl else "", site, urllib.parse.quote(term)) + url = "http%s://%s%s?format=json&redirects&action=query&prop=revisions&rvprop=content&titles=%s" % ( + "s" if ssl else "", site, path, urllib.parse.quote(term)) # Make the request data = web.getJSON(url) @@ -45,10 +45,10 @@ def get_raw_page(site, term, ssl=False): raise IMException("article not found") -def get_unwikitextified(site, wikitext, ssl=False): +def get_unwikitextified(site, wikitext, ssl=False, path="/w/api.php"): # Built URL - url = "http%s://%s/w/api.php?format=json&action=expandtemplates&text=%s" % ( - "s" if ssl else "", site, urllib.parse.quote(wikitext)) + url = "http%s://%s%s?format=json&action=expandtemplates&text=%s" % ( + "s" if ssl else "", site, path, urllib.parse.quote(wikitext)) # Make the request data = web.getJSON(url) @@ -58,10 +58,10 @@ def get_unwikitextified(site, wikitext, ssl=False): ## Search -def opensearch(site, term, ssl=False): +def opensearch(site, term, ssl=False, path="/w/api.php"): # Built URL - url = "http%s://%s/w/api.php?format=json&action=opensearch&search=%s" % ( - "s" if ssl else "", site, urllib.parse.quote(term)) + url = "http%s://%s%s?format=json&action=opensearch&search=%s" % ( + "s" if ssl else "", site, path, urllib.parse.quote(term)) # Make the request response = web.getJSON(url) @@ -73,10 +73,10 @@ def opensearch(site, term, ssl=False): response[3][k]) -def search(site, term, ssl=False): +def search(site, term, ssl=False, path="/w/api.php"): # Built URL - url = "http%s://%s/w/api.php?format=json&action=query&list=search&srsearch=%s&srprop=titlesnippet|snippet" % ( - "s" if ssl else "", site, urllib.parse.quote(term)) + url = "http%s://%s%s?format=json&action=query&list=search&srsearch=%s&srprop=titlesnippet|snippet" % ( + "s" if ssl else "", site, path, urllib.parse.quote(term)) # Make the request data = web.getJSON(url) @@ -108,9 +108,9 @@ def strip_model(cnt): return cnt -def parse_wikitext(site, cnt, namespaces=dict(), ssl=False): +def parse_wikitext(site, cnt, namespaces=dict(), **kwargs): for i, _, _, _ in re.findall(r"({{([^{]|\s|({{(.|\s|{{.*?}})*?}})*?)*?}})", cnt): - cnt = cnt.replace(i, get_unwikitextified(site, i, ssl), 1) + cnt = cnt.replace(i, get_unwikitextified(site, i, **kwargs), 1) # Strip [[...]] for full, args, lnk in re.findall(r"(\[\[(.*?|)?([^|]*?)\]\])", cnt): @@ -139,8 +139,8 @@ def irc_format(cnt): return cnt.replace("'''", "\x03\x02").replace("''", "\x03\x1f") -def get_page(site, term, ssl=False, subpart=None): - raw = get_raw_page(site, term, ssl) +def get_page(site, term, subpart=None, **kwargs): + raw = get_raw_page(site, term, **kwargs) if subpart is not None: subpart = subpart.replace("_", " ") @@ -151,50 +151,62 @@ def get_page(site, term, ssl=False, subpart=None): # NEMUBOT ############################################################# -def mediawiki_response(site, term, to): - ns = get_namespaces(site) +def mediawiki_response(site, term, to, **kwargs): + ns = get_namespaces(site, **kwargs) terms = term.split("#", 1) try: # Print the article if it exists - return Response(get_page(site, terms[0], subpart=terms[1] if len(terms) > 1 else None), + return Response(get_page(site, terms[0], subpart=terms[1] if len(terms) > 1 else None, **kwargs), line_treat=lambda line: irc_format(parse_wikitext(site, line, ns)), channel=to) except: pass # Try looking at opensearch - os = [x for x, _, _ in opensearch(site, terms[0])] + os = [x for x, _, _ in opensearch(site, terms[0], **kwargs)] print(os) # Fallback to global search if not len(os): - os = [x for x, _ in search(site, terms[0]) if x is not None and x != ""] + os = [x for x, _ in search(site, terms[0], **kwargs) if x is not None and x != ""] return Response(os, channel=to, title="Article not found, would you mean") -@hook.command("mediawiki") +@hook.command("mediawiki", + help="Read an article on a MediaWiki", + keywords={ + "ssl": "query over https instead of http", + "path=PATH": "absolute path to the API", + }) def cmd_mediawiki(msg): - """Read an article on a MediaWiki""" if len(msg.args) < 2: raise IMException("indicate a domain and a term to search") return mediawiki_response(msg.args[0], " ".join(msg.args[1:]), - msg.to_response) + msg.to_response, + ssl="ssl" in msg.kwargs, + path=msg.kwargs["path"] if "path" in msg.kwargs else "/w/api.php") -@hook.command("search_mediawiki") +@hook.command("search_mediawiki", + help="Search an article on a MediaWiki", + keywords={ + "ssl": "query over https instead of http", + "path=PATH": "absolute path to the API", + }) def cmd_srchmediawiki(msg): - """Search an article on a MediaWiki""" if len(msg.args) < 2: raise IMException("indicate a domain and a term to search") res = Response(channel=msg.to_response, nomore="No more results", count=" (%d more results)") - for r in search(msg.args[0], " ".join(msg.args[1:])): + for r in search(msg.args[0], " ".join(msg.args[1:]), + ssl="ssl" in msg.kwargs, + path=msg.kwargs["path"] if "path" in msg.kwargs else "/w/api.php"): res.append_message("%s: %s" % r) return res From 3301fb87c2afa9fed89acf78a4970d29037a5885 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 30 Jul 2016 07:16:23 +0200 Subject: [PATCH 099/271] [more] line_treat over an array --- modules/more.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/modules/more.py b/modules/more.py index be0fb55..018a1ae 100644 --- a/modules/more.py +++ b/modules/more.py @@ -181,8 +181,13 @@ class Response: return self.nomore if self.line_treat is not None and self.elt == 0: - self.messages[0] = (self.line_treat(self.messages[0]) - .replace("\n", " ").strip()) + if isinstance(self.messages[0], list): + for x in self.messages[0]: + print(x, self.line_treat(x)) + self.messages[0] = [self.line_treat(x).replace("\n", " ").strip() for x in self.messages[0]] + else: + self.messages[0] = (self.line_treat(self.messages[0]) + .replace("\n", " ").strip()) msg = "" if self.title is not None: From 0efee0cb839f78cc405126749b4ccd1713a694f4 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 30 Jul 2016 20:24:55 +0200 Subject: [PATCH 100/271] [mediawiki] parse Infobox --- modules/mediawiki.py | 45 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/modules/mediawiki.py b/modules/mediawiki.py index a335c9e..cb3d1da 100644 --- a/modules/mediawiki.py +++ b/modules/mediawiki.py @@ -89,6 +89,11 @@ def search(site, term, ssl=False, path="/w/api.php"): # PARSING FUNCTIONS ################################################### +def get_model(cnt, model="Infobox"): + for full in re.findall(r"(\{\{" + model + " .*?(?:\{\{.*?}}.*?)*}})", cnt, flags=re.DOTALL): + return full[3 + len(model):-2].replace("\n", " ").strip() + + def strip_model(cnt): # Strip models at begin: mostly useless cnt = re.sub(r"^(({{([^{]|\s|({{([^{]|\s|{{.*?}})*?}})*?)*?}}|\[\[([^[]|\s|\[\[.*?\]\])*?\]\])\s*)+", "", cnt, flags=re.DOTALL) @@ -139,6 +144,14 @@ def irc_format(cnt): return cnt.replace("'''", "\x03\x02").replace("''", "\x03\x1f") +def parse_infobox(cnt): + for v in cnt.split("|"): + try: + yield re.sub(r"^\s*([^=]*[^=\s])\s*=\s*(.+)\s*$", "\x03\x02" + r"\1" + ":\x03\x02 " + r"\2", v).replace("<br />", ", ").replace("<br/>", ", ").strip() + except: + yield re.sub(r"^\s+(.+)\s+$", "\x03\x02" + r"\1" + "\x03\x02", v).replace("<br />", ", ").replace("<br/>", ", ").strip() + + def get_page(site, term, subpart=None, **kwargs): raw = get_raw_page(site, term, **kwargs) @@ -146,7 +159,7 @@ def get_page(site, term, subpart=None, **kwargs): subpart = subpart.replace("_", " ") raw = re.sub(r"^.*(?P<title>==+)\s*(" + subpart + r")\s*(?P=title)", r"\1 \2 \1", raw, flags=re.DOTALL) - return strip_model(raw) + return raw # NEMUBOT ############################################################# @@ -158,8 +171,8 @@ def mediawiki_response(site, term, to, **kwargs): try: # Print the article if it exists - return Response(get_page(site, terms[0], subpart=terms[1] if len(terms) > 1 else None, **kwargs), - line_treat=lambda line: irc_format(parse_wikitext(site, line, ns)), + return Response(strip_model(get_page(site, terms[0], subpart=terms[1] if len(terms) > 1 else None, **kwargs)), + line_treat=lambda line: irc_format(parse_wikitext(site, line, ns, **kwargs)), channel=to) except: pass @@ -188,11 +201,10 @@ def cmd_mediawiki(msg): return mediawiki_response(msg.args[0], " ".join(msg.args[1:]), msg.to_response, - ssl="ssl" in msg.kwargs, - path=msg.kwargs["path"] if "path" in msg.kwargs else "/w/api.php") + **msg.kwargs) -@hook.command("search_mediawiki", +@hook.command("mediawiki_search", help="Search an article on a MediaWiki", keywords={ "ssl": "query over https instead of http", @@ -204,14 +216,29 @@ def cmd_srchmediawiki(msg): res = Response(channel=msg.to_response, nomore="No more results", count=" (%d more results)") - for r in search(msg.args[0], " ".join(msg.args[1:]), - ssl="ssl" in msg.kwargs, - path=msg.kwargs["path"] if "path" in msg.kwargs else "/w/api.php"): + for r in search(msg.args[0], " ".join(msg.args[1:]), **msg.kwargs): res.append_message("%s: %s" % r) return res +@hook.command("mediawiki_infobox", + help="Highlight information from an article on a MediaWiki", + keywords={ + "ssl": "query over https instead of http", + "path=PATH": "absolute path to the API", + }) +def cmd_infobox(msg): + if len(msg.args) < 2: + raise IMException("indicate a domain and a term to search") + + ns = get_namespaces(msg.args[0], **msg.kwargs) + + return Response(", ".join([x for x in parse_infobox(get_model(get_page(msg.args[0], " ".join(msg.args[1:]), **msg.kwargs), "Infobox"))]), + line_treat=lambda line: irc_format(parse_wikitext(msg.args[0], line, ns, **msg.kwargs)), + channel=msg.to_response) + + @hook.command("wikipedia") def cmd_wikipedia(msg): if len(msg.args) < 2: From 7a48dc2cef5dbadbba516f1d973b6b8755ff0840 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Tue, 13 Sep 2016 19:16:37 +0200 Subject: [PATCH 101/271] whois: new URL to pick picts --- modules/whois.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/whois.py b/modules/whois.py index a51b838..27a8aed 100644 --- a/modules/whois.py +++ b/modules/whois.py @@ -49,7 +49,7 @@ class Login: def get_photo(self): if self.login in context.data.getNode("pics").index: return context.data.getNode("pics").index[self.login]["url"] - for url in [ "https://static.acu.epita.fr/photos/%s", "https://static.acu.epita.fr/photos/%s/%%s" % self.gid, "https://intra-bocal.epitech.eu/trombi/%s.jpg", "http://whois.23.tf/p/%s/%%s.jpg" % self.gid ]: + for url in [ "https://photos.cri.epita.net/%s", "https://static.acu.epita.fr/photos/%s", "https://static.acu.epita.fr/photos/%s/%%s" % self.gid, "https://intra-bocal.epitech.eu/trombi/%s.jpg", "http://whois.23.tf/p/%s/%%s.jpg" % self.gid ]: url = url % self.login try: _, status, _, _ = headers(url) From c8c9112b0f5631d3bcfc2f6c891142c1974435e8 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Sat, 1 Jul 2017 19:03:07 +0200 Subject: [PATCH 102/271] syno: CRISCO now speaks utf-8 --- modules/syno.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/syno.py b/modules/syno.py index 13d0250..4bdc990 100644 --- a/modules/syno.py +++ b/modules/syno.py @@ -29,7 +29,7 @@ def load(context): # MODULE CORE ######################################################### def get_french_synos(word): - url = "http://www.crisco.unicaen.fr/des/synonymes/" + quote(word.encode("ISO-8859-1")) + url = "http://www.crisco.unicaen.fr/des/synonymes/" + quote(word) page = web.getURLContent(url) best = list(); synos = list(); anton = list() From 52b3bfa945520d82102746814930c4b36541eaba Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Sun, 2 Jul 2017 19:08:01 +0200 Subject: [PATCH 103/271] suivi: add postnl tracking --- modules/suivi.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/modules/suivi.py b/modules/suivi.py index 19e4d20..79910d4 100644 --- a/modules/suivi.py +++ b/modules/suivi.py @@ -108,6 +108,30 @@ def get_laposte_info(laposte_id): poste_location, poste_date) +def get_postnl_info(postnl_id): + data = urllib.parse.urlencode({'barcodes': postnl_id}) + postnl_baseurl = "http://www.postnl.post/details/" + + postnl_data = urllib.request.urlopen(postnl_baseurl, + data.encode('utf-8')) + soup = BeautifulSoup(postnl_data) + if (soup.find(id='datatables') + and soup.find(id='datatables').tbody + and soup.find(id='datatables').tbody.tr): + search_res = soup.find(id='datatables').tbody.tr + if len(search_res.find_all('td')) >= 3: + field = field.find_next('td') + post_date = field.get_text() + + field = field.find_next('td') + post_status = field.get_text() + + field = field.find_next('td') + post_destination = field.get_text() + + return (post_status.lower(), post_destination, post_date) + + # TRACKING HANDLERS ################################################### def handle_tnt(tracknum): @@ -133,6 +157,15 @@ def handle_laposte(tracknum): poste_location, poste_date)) +def handle_postnl(tracknum): + info = get_postnl_info(tracknum) + if info: + post_status, post_destination, post_date = info + return ("PostNL \x02%s\x0F est actuellement " + "\x02%s\x0F vers le pays \x02%s\x0F (Mis à jour le \x02%s\x0F" + ")." % (tracknum, post_status, post_destination, post_date)) + + def handle_colissimo(tracknum): info = get_colissimo_info(tracknum) if info: @@ -158,6 +191,7 @@ def handle_coliprive(tracknum): TRACKING_HANDLERS = { 'laposte': handle_laposte, + 'postnl': handle_postnl, 'colissimo': handle_colissimo, 'chronopost': handle_chronopost, 'coliprive': handle_coliprive, From aefc0bb53440966c055234544a989553ebeb8574 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 10 Jul 2017 06:36:17 +0200 Subject: [PATCH 104/271] event: don't forward d_init if None --- nemubot/event/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nemubot/event/__init__.py b/nemubot/event/__init__.py index c45081c..981cf4b 100644 --- a/nemubot/event/__init__.py +++ b/nemubot/event/__init__.py @@ -134,5 +134,7 @@ class ModuleEvent: self.call(d_init) elif isinstance(self.call_data, dict): self.call(d_init, **self.call_data) + elif d_init is None: + self.call(self.call_data) else: self.call(d_init, self.call_data) From 920506c702d5301091f9d5706009ea37b58b5ed4 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 9 Jul 2017 15:03:09 +0200 Subject: [PATCH 105/271] wolframalpha: avoid content that is not plaintext --- modules/wolframalpha.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/wolframalpha.py b/modules/wolframalpha.py index 1d09c5b..e6bf86c 100644 --- a/modules/wolframalpha.py +++ b/modules/wolframalpha.py @@ -15,7 +15,7 @@ from more import Response # LOADING ############################################################# -URL_API = "http://api.wolframalpha.com/v2/query?input=%%s&appid=%s" +URL_API = "http://api.wolframalpha.com/v2/query?input=%%s&format=plaintext&appid=%s" def load(context): global URL_API From c3f2c89c7cc753c21c137937b224b5082a2546da Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 14 Jul 2017 12:30:15 +0200 Subject: [PATCH 106/271] alias: only perform alias expansion on Command --- modules/alias.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/alias.py b/modules/alias.py index 5089759..b4ab4ca 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -261,17 +261,17 @@ def cmd_unalias(msg): def treat_alias(msg): if msg.cmd in context.data.getNode("aliases").index: origin = context.data.getNode("aliases").index[msg.cmd]["origin"] - rpl_cmd = context.subparse(msg, origin) - if isinstance(rpl_cmd, Command): - rpl_cmd.args = replace_variables(rpl_cmd.args, msg) - rpl_cmd.args += msg.args - rpl_cmd.kwargs.update(msg.kwargs) + rpl_msg = context.subparse(msg, origin) + if isinstance(rpl_msg, Command): + rpl_msg.args = replace_variables(rpl_msg.args, msg) + rpl_msg.args += msg.args + rpl_msg.kwargs.update(msg.kwargs) elif len(msg.args) or len(msg.kwargs): raise IMException("This kind of alias doesn't take any argument (haven't you forgotten the '!'?).") # Avoid infinite recursion - if msg.cmd != rpl_cmd.cmd: + if not isinstance(rpl_msg, Command) or msg.cmd != rpl_msg.cmd: # Also return origin message, if it can be treated as well - return [msg, rpl_cmd] + return [msg, rpl_msg] return msg From 2334bc502af2ed2b19b327963c048c3161120977 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 11 Jul 2017 07:31:53 +0200 Subject: [PATCH 107/271] alias: add syntax to handle default variable replacement --- modules/alias.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/modules/alias.py b/modules/alias.py index b4ab4ca..701639c 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -76,7 +76,7 @@ def get_variable(name, msg=None): elif name in context.data.getNode("variables").index: return context.data.getNode("variables").index[name]["value"] else: - return "" + return None def list_variables(user=None): @@ -108,12 +108,12 @@ def set_variable(name, value, creator): context.save() -def replace_variables(cnts, msg=None): +def replace_variables(cnts, msg): """Replace variables contained in the content Arguments: cnt -- content where search variables - msg -- optional message where pick some variables + msg -- Message where pick some variables """ unsetCnt = list() @@ -122,12 +122,12 @@ def replace_variables(cnts, msg=None): resultCnt = list() for cnt in cnts: - for res in re.findall("\\$\{(?P<name>[a-zA-Z0-9:]+)\}", cnt): - rv = re.match("([0-9]+)(:([0-9]*))?", res) + for res, name, default in re.findall("\\$\{(([a-zA-Z0-9:]+)(?:-([^}]+))?)\}", cnt): + rv = re.match("([0-9]+)(:([0-9]*))?", name) if rv is not None: varI = int(rv.group(1)) - 1 - if varI > len(msg.args): - cnt = cnt.replace("${%s}" % res, "", 1) + if varI >= len(msg.args): + cnt = cnt.replace("${%s}" % res, default, 1) elif rv.group(2) is not None: if rv.group(3) is not None and len(rv.group(3)): varJ = int(rv.group(3)) - 1 @@ -142,9 +142,10 @@ def replace_variables(cnts, msg=None): cnt = cnt.replace("${%s}" % res, msg.args[varI], 1) unsetCnt.append(varI) else: - cnt = cnt.replace("${%s}" % res, get_variable(res), 1) + cnt = cnt.replace("${%s}" % res, get_variable(name) or default, 1) resultCnt.append(cnt) + # Remove used content for u in sorted(set(unsetCnt), reverse=True): msg.args.pop(u) From 679f50b730c1460e31de17baf799e7e7ddae374e Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 12 Jul 2017 08:08:39 +0200 Subject: [PATCH 108/271] alias: fix lookup replacement when empty list --- modules/alias.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/alias.py b/modules/alias.py index 701639c..5053783 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -260,7 +260,7 @@ def cmd_unalias(msg): @hook.add(["pre","Command"]) def treat_alias(msg): - if msg.cmd in context.data.getNode("aliases").index: + if context.data.getNode("aliases") is not None and msg.cmd in context.data.getNode("aliases").index: origin = context.data.getNode("aliases").index[msg.cmd]["origin"] rpl_msg = context.subparse(msg, origin) if isinstance(rpl_msg, Command): From be9492c151e80fbaa144cf164fce1f67a4b3cb1a Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 6 Jul 2017 00:36:12 +0200 Subject: [PATCH 109/271] weather: don't show expire date if not provided --- modules/weather.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/modules/weather.py b/modules/weather.py index 34a861a..1fadc71 100644 --- a/modules/weather.py +++ b/modules/weather.py @@ -158,7 +158,10 @@ def cmd_alert(msg): if "alerts" in wth: for alert in wth["alerts"]: - res.append_message("\x03\x02%s\x03\x02 (see %s expire on %s): %s" % (alert["title"], alert["uri"], format_timestamp(int(alert["expires"]), wth["timezone"], wth["offset"]), alert["description"].replace("\n", " "))) + if "expires" in alert: + res.append_message("\x03\x02%s\x03\x02 (see %s expire on %s): %s" % (alert["title"], alert["uri"], format_timestamp(int(alert["expires"]), wth["timezone"], wth["offset"]), alert["description"].replace("\n", " "))) + else: + res.append_message("\x03\x02%s\x03\x02 (see %s): %s" % (alert["title"], alert["uri"], alert["description"].replace("\n", " "))) return res @@ -173,7 +176,10 @@ def cmd_weather(msg): if "alerts" in wth: alert_msgs = list() for alert in wth["alerts"]: - alert_msgs.append("\x03\x02%s\x03\x02 expire on %s" % (alert["title"], format_timestamp(int(alert["expires"]), wth["timezone"], wth["offset"]))) + if "expires" in alert: + alert_msgs.append("\x03\x02%s\x03\x02 expire on %s" % (alert["title"], format_timestamp(int(alert["expires"]), wth["timezone"], wth["offset"]))) + else: + alert_msgs.append("\x03\x02%s\x03\x02" % (alert["title"])) res.append_message("\x03\x16\x03\x02/!\\\x03\x02 Alert%s:\x03\x16 " % ("s" if len(alert_msgs) > 1 else "") + ", ".join(alert_msgs)) if specific is not None: From 08a77012389864e4f09a9f8ed41aae273a85342f Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 5 Jul 2017 22:54:07 +0200 Subject: [PATCH 110/271] whois: add @lookup keyword to perform research in the list --- modules/whois.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/modules/whois.py b/modules/whois.py index 27a8aed..52344d1 100644 --- a/modules/whois.py +++ b/modules/whois.py @@ -30,7 +30,7 @@ def load(context): context.data.getNode("pics").setIndex("login", "pict") import nemubot.hooks - context.add_hook(nemubot.hooks.Command(cmd_whois, "whois"), + context.add_hook(nemubot.hooks.Command(cmd_whois, "whois", keywords={"lookup": "Perform a lookup of the begining of the login instead of an exact search."}), "in","Command") class Login: @@ -60,31 +60,38 @@ class Login: return None -def found_login(login): +def found_login(login, search=False): if login in context.data.getNode("aliases").index: login = context.data.getNode("aliases").index[login]["to"] - login_ = login + ":" + login_ = login + (":" if not search else "") lsize = len(login_) with open(PASSWD_FILE, encoding="iso-8859-15") as f: for l in f.readlines(): if l[:lsize] == login_: - return Login(l.strip()) - return None + yield Login(l.strip()) def cmd_whois(msg): if len(msg.args) < 1: raise IMException("Provide a name") - res = Response(channel=msg.channel, count=" (%d more logins)") - for srch in msg.args: - l = found_login(srch) - if l is not None: + def format_response(t): + srch, l = t + if type(l) is Login: pic = l.get_photo() - res.append_message("%s is %s (%s %s): %s%s" % (srch, l.cn.capitalize(), l.login, l.uid, l.get_promo(), " and looks like %s" % pic if pic is not None else "")) + return "%s is %s (%s %s): %s%s" % (srch, l.cn.capitalize(), l.login, l.uid, l.get_promo(), " and looks like %s" % pic if pic is not None else "") else: - res.append_message("Unknown %s :(" % srch) + return l % srch + + res = Response(channel=msg.channel, count=" (%d more logins)", line_treat=format_response) + for srch in msg.args: + found = False + for l in found_login(srch, "lookup" in msg.kwargs): + found = True + res.append_message((srch, l)) + if not found: + res.append_message((srch, "Unknown %s :(")) return res @hook.command("nicks") From 1b108428c252fd4a68a5fc5682558737932141be Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 13 Jul 2017 01:21:43 +0200 Subject: [PATCH 111/271] Fix issue with some non-text messages --- modules/reddit.py | 2 +- modules/youtube-title.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/reddit.py b/modules/reddit.py index d3f03a1..7d481b7 100644 --- a/modules/reddit.py +++ b/modules/reddit.py @@ -79,7 +79,7 @@ def parselisten(msg): def parseresponse(msg): global LAST_SUBS - if hasattr(msg, "text") and msg.text: + if hasattr(msg, "text") and msg.text and type(msg.text) == str: urls = re.findall("www.reddit.com(/\w/\w+/?)", msg.text) for url in urls: for recv in msg.to: diff --git a/modules/youtube-title.py b/modules/youtube-title.py index ebae4b6..fe62cda 100644 --- a/modules/youtube-title.py +++ b/modules/youtube-title.py @@ -82,7 +82,7 @@ def parselisten(msg): @hook.post() def parseresponse(msg): global LAST_URLS - if hasattr(msg, "text") and msg.text: + if hasattr(msg, "text") and msg.text and type(msg.text) == str: urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?[^ :]+)", msg.text) for url in urls: o = urlparse(_getNormalizedURL(url)) From 1858a045ccb8c16716211d797d617af96eec5def Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 22 Apr 2015 16:56:07 +0200 Subject: [PATCH 112/271] Introducing daemon mode --- nemubot/__main__.py | 48 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 1809bee..f3903ee 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -36,6 +36,9 @@ def main(): default=["./modules/"], help="directory to use as modules store") + parser.add_argument("-d", "--debug", action="store_true", + help="don't deamonize, keep in foreground") + parser.add_argument("-l", "--logfile", default="./nemubot.log", help="Path to store logs") @@ -62,6 +65,38 @@ def main(): 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)] + # Daemonize + if not args.debug: + try: + pid = os.fork() + if pid > 0: + sys.exit(0) + except OSError as err: + sys.stderr.write("Unable to fork: %s" % err) + sys.exit(1) + + os.setsid() + os.umask(0) + os.chdir('/') + + try: + pid = os.fork() + if pid > 0: + sys.exit(0) + except OSError as err: + sys.stderr.write("Unable to fork: %s" % err) + sys.exit(1) + + sys.stdout.flush() + sys.stderr.flush() + si = open(os.devnull, 'r') + so = open(os.devnull, 'a+') + se = open(os.devnull, 'a+') + + os.dup2(si.fileno(), sys.stdin.fileno()) + os.dup2(so.fileno(), sys.stdout.fileno()) + os.dup2(se.fileno(), sys.stderr.fileno()) + # Setup loggin interface import logging logger = logging.getLogger("nemubot") @@ -70,11 +105,12 @@ def main(): formatter = logging.Formatter( '%(asctime)s %(name)s %(levelname)s %(message)s') - ch = logging.StreamHandler() - ch.setFormatter(formatter) - if args.verbose < 2: - ch.setLevel(logging.INFO) - logger.addHandler(ch) + if args.debug: + ch = logging.StreamHandler() + ch.setFormatter(formatter) + if args.verbose < 2: + ch.setLevel(logging.INFO) + logger.addHandler(ch) fh = logging.FileHandler(args.logfile) fh.setFormatter(formatter) @@ -146,7 +182,7 @@ def main(): "the prompt.") context.quit() - print("Waiting for other threads shuts down...") + logger.info("Waiting for other threads shuts down...") sys.exit(0) if __name__ == "__main__": From 7bc37617b0cfe04a02c50925c8c036ae81882709 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 8 May 2015 00:20:14 +0200 Subject: [PATCH 113/271] Remove prompt at launch --- nemubot/__main__.py | 30 ++---------------------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/nemubot/__main__.py b/nemubot/__main__.py index f3903ee..259096b 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -153,35 +153,9 @@ def main(): for module in args.module: __import__(module) - print ("Nemubot v%s ready, my PID is %i!" % (nemubot.__version__, - os.getpid())) - while True: - from nemubot.prompt.reset import PromptReset - try: - context.start() - if prmpt.run(context): - break - except PromptReset as e: - if e.type == "quit": - break + context.start() + context.join() - try: - import imp - # Reload all other modules - imp.reload(nemubot) - imp.reload(nemubot.prompt) - nemubot.reload() - import nemubot.bot - context = nemubot.bot.hotswap(context) - prmpt = nemubot.prompt.hotswap(prmpt) - print("\033[1;32mContext reloaded\033[0m, now in Nemubot %s" % - nemubot.__version__) - except: - logger.exception("\033[1;31mUnable to reload the prompt due to " - "errors.\033[0m Fix them before trying to reload " - "the prompt.") - - context.quit() logger.info("Waiting for other threads shuts down...") sys.exit(0) From ec512fc5401da69b4e1f43b831e045ba1d362117 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 9 May 2015 13:20:56 +0200 Subject: [PATCH 114/271] Do a proper close on SIGINT and SIGTERM --- nemubot/__main__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 259096b..4f739f8 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -16,6 +16,7 @@ def main(): import os + import signal import sys # Parse command line arguments @@ -153,6 +154,12 @@ def main(): for module in args.module: __import__(module) + # Signals handling + def sighandler(signum, frame): + context.quit() + signal.signal(signal.SIGINT, sighandler) + signal.signal(signal.SIGTERM, sighandler) + context.start() context.join() From 57275f573543e6f3106dac421810712241984a5d Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 12 May 2015 10:41:13 +0200 Subject: [PATCH 115/271] Catch SIGHUP: deep reload --- nemubot/__main__.py | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 4f739f8..757d3b0 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -155,14 +155,40 @@ def main(): __import__(module) # Signals handling - def sighandler(signum, frame): + def sigtermhandler(signum, frame): + """On SIGTERM and SIGINT, quit nicely""" context.quit() - signal.signal(signal.SIGINT, sighandler) - signal.signal(signal.SIGTERM, sighandler) + signal.signal(signal.SIGINT, sigtermhandler) + signal.signal(signal.SIGTERM, sigtermhandler) + def sighuphandler(signum, frame): + """On SIGHUP, perform a deep reload""" + import imp + + # Reload nemubot Python modules + imp.reload(nemubot) + nemubot.reload() + + # Hotswap context + import nemubot.bot + context = nemubot.bot.hotswap(context) + + # Reload configuration file + for path in args.files: + if os.path.isfile(path): + context.sync_queue.put_nowait(["loadconf", path]) + signal.signal(signal.SIGHUP, sighuphandler) + + # Here we go! context.start() - context.join() + # context can change when performing an hotswap, always join the latest context + oldcontext = None + while oldcontext != context: + oldcontext = context + context.join() + + # Wait for consumers logger.info("Waiting for other threads shuts down...") sys.exit(0) From b0678ceb846fd78a2555974b64aba718579322c5 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 16 May 2015 10:24:08 +0200 Subject: [PATCH 116/271] Extract deamonize to a dedicated function that can be called from anywhere --- nemubot/__init__.py | 38 ++++++++++++++++++++++++++++++++++++++ nemubot/__main__.py | 31 ++----------------------------- 2 files changed, 40 insertions(+), 29 deletions(-) diff --git a/nemubot/__init__.py b/nemubot/__init__.py index d0a2072..4a2e789 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -38,6 +38,44 @@ def requires_version(min=None, max=None): "but this is nemubot v%s." % (str(max), __version__)) +def daemonize(): + """Detach the running process to run as a daemon + """ + + import os + import sys + + try: + pid = os.fork() + if pid > 0: + sys.exit(0) + except OSError as err: + sys.stderr.write("Unable to fork: %s\n" % err) + sys.exit(1) + + os.setsid() + os.umask(0) + os.chdir('/') + + try: + pid = os.fork() + if pid > 0: + sys.exit(0) + except OSError as err: + sys.stderr.write("Unable to fork: %s\n" % err) + sys.exit(1) + + sys.stdout.flush() + sys.stderr.flush() + si = open(os.devnull, 'r') + so = open(os.devnull, 'a+') + se = open(os.devnull, 'a+') + + os.dup2(si.fileno(), sys.stdin.fileno()) + os.dup2(so.fileno(), sys.stdout.fileno()) + os.dup2(se.fileno(), sys.stderr.fileno()) + + def reload(): """Reload code of all Python modules used by nemubot """ diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 757d3b0..8e9320e 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -68,35 +68,8 @@ def main(): # Daemonize if not args.debug: - try: - pid = os.fork() - if pid > 0: - sys.exit(0) - except OSError as err: - sys.stderr.write("Unable to fork: %s" % err) - sys.exit(1) - - os.setsid() - os.umask(0) - os.chdir('/') - - try: - pid = os.fork() - if pid > 0: - sys.exit(0) - except OSError as err: - sys.stderr.write("Unable to fork: %s" % err) - sys.exit(1) - - sys.stdout.flush() - sys.stderr.flush() - si = open(os.devnull, 'r') - so = open(os.devnull, 'a+') - se = open(os.devnull, 'a+') - - os.dup2(si.fileno(), sys.stdin.fileno()) - os.dup2(so.fileno(), sys.stdout.fileno()) - os.dup2(se.fileno(), sys.stderr.fileno()) + from nemubot import daemonize + daemonize() # Setup loggin interface import logging From f160411f718344055e43ab2e2cb9cc2cfcc88d4a Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 15 May 2015 00:05:12 +0200 Subject: [PATCH 117/271] Catch SIGUSR1: log threads stack traces --- nemubot/__main__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 8e9320e..114b83c 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -152,6 +152,15 @@ def main(): context.sync_queue.put_nowait(["loadconf", path]) signal.signal(signal.SIGHUP, sighuphandler) + def sigusr1handler(signum, frame): + """On SIGHUSR1, display stacktraces""" + import traceback + for threadId, stack in sys._current_frames().items(): + logger.debug("########### Thread %d:\n%s", + threadId, + "".join(traceback.format_stack(stack))) + signal.signal(signal.SIGUSR1, sigusr1handler) + # Here we go! context.start() From 150d069dfb614fb193b0976e35d632fe2444d7a3 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 18 May 2015 07:36:49 +0200 Subject: [PATCH 118/271] New CLI argument: --pidfile, path to store the daemon PID --- nemubot/__init__.py | 5 +++++ nemubot/__main__.py | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/nemubot/__init__.py b/nemubot/__init__.py index 4a2e789..668dd6c 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -38,6 +38,11 @@ def requires_version(min=None, max=None): "but this is nemubot v%s." % (str(max), __version__)) +def attach(pid): + print("TODO, attach to %d" % pid) + return 0 + + def daemonize(): """Detach the running process to run as a daemon """ diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 114b83c..7db715b 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -40,6 +40,9 @@ def main(): parser.add_argument("-d", "--debug", action="store_true", help="don't deamonize, keep in foreground") + parser.add_argument("-P", "--pidfile", default="./nemubot.pid", + help="Path to the file where store PID") + parser.add_argument("-l", "--logfile", default="./nemubot.log", help="Path to store logs") @@ -62,15 +65,33 @@ def main(): # Resolve relatives paths args.data_path = os.path.abspath(os.path.expanduser(args.data_path)) + args.pidfile = os.path.abspath(os.path.expanduser(args.pidfile)) 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)] + # Check if an instance is already launched + if args.pidfile is not None and os.path.isfile(args.pidfile): + with open(args.pidfile, "r") as f: + pid = int(f.readline()) + try: + os.kill(pid, 0) + except OSError: + pass + else: + from nemubot import attach + sys.exit(attach(pid)) + # Daemonize if not args.debug: from nemubot import daemonize daemonize() + # Store PID to pidfile + if args.pidfile is not None: + with open(args.pidfile, "w+") as f: + f.write(str(os.getpid())) + # Setup loggin interface import logging logger = logging.getLogger("nemubot") From 9dc385a32aa3a47fecf97eef3935868a102769dc Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 20 May 2015 06:12:50 +0200 Subject: [PATCH 119/271] New argument: --socketfile that create a socket for internal communication --- nemubot/__init__.py | 4 ++-- nemubot/__main__.py | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/nemubot/__init__.py b/nemubot/__init__.py index 668dd6c..7b3d21f 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -38,8 +38,8 @@ def requires_version(min=None, max=None): "but this is nemubot v%s." % (str(max), __version__)) -def attach(pid): - print("TODO, attach to %d" % pid) +def attach(socketfile): + print("TODO: Attach to Unix socket at: %s" % socketfile) return 0 diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 7db715b..456dc3a 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -43,6 +43,9 @@ def main(): parser.add_argument("-P", "--pidfile", default="./nemubot.pid", help="Path to the file where store PID") + parser.add_argument("-S", "--socketfile", default="./nemubot.sock", + help="path where open the socket for internal communication") + parser.add_argument("-l", "--logfile", default="./nemubot.log", help="Path to store logs") @@ -66,6 +69,7 @@ def main(): # Resolve relatives paths args.data_path = os.path.abspath(os.path.expanduser(args.data_path)) 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)] @@ -185,6 +189,11 @@ def main(): # Here we go! context.start() + if args.socketfile: + from nemubot.server.socket import SocketListener + context.add_server(SocketListener(context.add_server, "master_socket", + sock_location=args.socketfile)) + # context can change when performing an hotswap, always join the latest context oldcontext = None while oldcontext != context: From a7d7013639d8e1538c65c254de52d6fa77e54af2 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 24 May 2015 16:47:22 +0200 Subject: [PATCH 120/271] Fix and improve reload process --- nemubot/__init__.py | 3 +++ nemubot/__main__.py | 15 +++++++++++---- nemubot/bot.py | 12 ++++++++---- nemubot/server/abstract.py | 2 +- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/nemubot/__init__.py b/nemubot/__init__.py index 7b3d21f..7b6949e 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -87,6 +87,9 @@ def reload(): import imp + import nemubot.bot + imp.reload(nemubot.bot) + import nemubot.channel imp.reload(nemubot.channel) diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 456dc3a..5c30695 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -139,7 +139,8 @@ def main(): # Register the hook for futur import from nemubot.importer import ModuleFinder - sys.meta_path.append(ModuleFinder(context.modules_paths, context.add_module)) + module_finder = ModuleFinder(context.modules_paths, context.add_module) + sys.meta_path.append(module_finder) # Load requested configuration files for path in args.files: @@ -162,6 +163,9 @@ def main(): def sighuphandler(signum, frame): """On SIGHUP, perform a deep reload""" import imp + nonlocal nemubot, context, module_finder + + logger.debug("SIGHUP receive, iniate reload procedure...") # Reload nemubot Python modules imp.reload(nemubot) @@ -171,6 +175,11 @@ def main(): import nemubot.bot context = nemubot.bot.hotswap(context) + # Reload ModuleFinder + sys.meta_path.remove(module_finder) + module_finder = ModuleFinder(context.modules_paths, context.add_module) + sys.meta_path.append(module_finder) + # Reload configuration file for path in args.files: if os.path.isfile(path): @@ -186,9 +195,6 @@ def main(): "".join(traceback.format_stack(stack))) signal.signal(signal.SIGUSR1, sigusr1handler) - # Here we go! - context.start() - if args.socketfile: from nemubot.server.socket import SocketListener context.add_server(SocketListener(context.add_server, "master_socket", @@ -198,6 +204,7 @@ def main(): oldcontext = None while oldcontext != context: oldcontext = context + context.start() context.join() # Wait for consumers diff --git a/nemubot/bot.py b/nemubot/bot.py index e449f35..8caf0ed 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -145,6 +145,7 @@ class Bot(threading.Thread): from select import select from nemubot.server import _lock, _rlist, _wlist, _xlist + logger.info("Starting main loop") self.stop = False while not self.stop: with _lock: @@ -211,6 +212,7 @@ class Bot(threading.Thread): self.load_file(path) logger.info("Configurations successfully loaded") self.sync_queue.task_done() + logger.info("Ending main loop") @@ -568,14 +570,16 @@ def hotswap(bak): bak.stop = True if bak.event_timer is not None: bak.event_timer.cancel() + + # Unload modules + for mod in [k for k in bak.modules.keys()]: + bak.unload_module(mod) + + # Save datastore bak.datastore.close() new = Bot(str(bak.ip), bak.modules_paths, bak.datastore) new.servers = bak.servers - new.modules = bak.modules - new.modules_configuration = bak.modules_configuration - new.events = bak.events - new.hooks = bak.hooks new._update_event_timer() return new diff --git a/nemubot/server/abstract.py b/nemubot/server/abstract.py index ebcb427..8e3dc3b 100644 --- a/nemubot/server/abstract.py +++ b/nemubot/server/abstract.py @@ -113,7 +113,7 @@ class AbstractServer(io.IOBase): """ self._sending_queue.put(self.format(message)) - self.logger.debug("Message '%s' appended to Queue", message) + self.logger.debug("Message '%s' appended to write queue", message) if self not in _wlist: _wlist.append(self) From 38fd9e5091bbf224b36a5865c711e0cb20f1e67e Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 16 Jul 2015 20:31:34 +0200 Subject: [PATCH 121/271] Remove legacy prompt --- modules/cmd_server.py | 202 ------------------------------------- nemubot/__init__.py | 5 - nemubot/__main__.py | 4 - nemubot/prompt/__init__.py | 142 -------------------------- nemubot/prompt/builtins.py | 128 ----------------------- nemubot/prompt/error.py | 21 ---- nemubot/prompt/reset.py | 23 ----- setup.py | 1 - 8 files changed, 526 deletions(-) delete mode 100644 modules/cmd_server.py delete mode 100644 nemubot/prompt/__init__.py delete mode 100644 nemubot/prompt/builtins.py delete mode 100644 nemubot/prompt/error.py delete mode 100644 nemubot/prompt/reset.py diff --git a/modules/cmd_server.py b/modules/cmd_server.py deleted file mode 100644 index 6580c18..0000000 --- a/modules/cmd_server.py +++ /dev/null @@ -1,202 +0,0 @@ -# Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 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/>. - -import traceback -import sys - -from nemubot.hooks import hook - -nemubotversion = 3.4 -NODATA = True - - -def getserver(toks, context, prompt, mandatory=False, **kwargs): - """Choose the server in toks or prompt. - This function modify the tokens list passed as argument""" - - if len(toks) > 1 and toks[1] in context.servers: - return context.servers[toks.pop(1)] - elif not mandatory or prompt.selectedServer: - return prompt.selectedServer - else: - from nemubot.prompt.error import PromptError - raise PromptError("Please SELECT a server or give its name in argument.") - - -@hook("prompt_cmd", "close") -def close(toks, context, **kwargs): - """Disconnect and forget (remove from the servers list) the server""" - srv = getserver(toks, context=context, mandatory=True, **kwargs) - - if srv.close(): - del context.servers[srv.id] - return 0 - return 1 - - -@hook("prompt_cmd", "connect") -def connect(toks, **kwargs): - """Make the connexion to a server""" - srv = getserver(toks, mandatory=True, **kwargs) - - return not srv.open() - - -@hook("prompt_cmd", "disconnect") -def disconnect(toks, **kwargs): - """Close the connection to a server""" - srv = getserver(toks, mandatory=True, **kwargs) - - return not srv.close() - - -@hook("prompt_cmd", "discover") -def discover(toks, context, **kwargs): - """Discover a new bot on a server""" - srv = getserver(toks, context=context, mandatory=True, **kwargs) - - if len(toks) > 1 and "!" in toks[1]: - bot = context.add_networkbot(srv, name) - return not bot.connect() - else: - print(" %s is not a valid fullname, for example: " - "nemubot!nemubotV3@bot.nemunai.re" % ''.join(toks[1:1])) - return 1 - - -@hook("prompt_cmd", "join") -@hook("prompt_cmd", "leave") -@hook("prompt_cmd", "part") -def join(toks, **kwargs): - """Join or leave a channel""" - srv = getserver(toks, mandatory=True, **kwargs) - - if len(toks) <= 2: - print("%s: not enough arguments." % toks[0]) - return 1 - - if toks[0] == "join": - if len(toks) > 2: - srv.write("JOIN %s %s" % (toks[1], toks[2])) - else: - srv.write("JOIN %s" % toks[1]) - - elif toks[0] == "leave" or toks[0] == "part": - if len(toks) > 2: - srv.write("PART %s :%s" % (toks[1], " ".join(toks[2:]))) - else: - srv.write("PART %s" % toks[1]) - - return 0 - - -@hook("prompt_cmd", "save") -def save_mod(toks, context, **kwargs): - """Force save module data""" - if len(toks) < 2: - print("save: not enough arguments.") - return 1 - - wrn = 0 - for mod in toks[1:]: - if mod in context.modules: - context.modules[mod].save() - print("save: module `%s´ saved successfully" % mod) - else: - wrn += 1 - print("save: no module named `%s´" % mod) - return wrn - - -@hook("prompt_cmd", "send") -def send(toks, **kwargs): - """Send a message on a channel""" - srv = getserver(toks, mandatory=True, **kwargs) - - # Check the server is connected - if not srv.connected: - print ("send: server `%s' not connected." % srv.id) - return 2 - - if len(toks) <= 3: - print ("send: not enough arguments.") - return 1 - - if toks[1] not in srv.channels: - print ("send: channel `%s' not authorized in server `%s'." - % (toks[1], srv.id)) - return 3 - - from nemubot.message import Text - srv.send_response(Text(" ".join(toks[2:]), server=None, - to=[toks[1]])) - return 0 - - -@hook("prompt_cmd", "zap") -def zap(toks, **kwargs): - """Hard change connexion state""" - srv = getserver(toks, mandatory=True, **kwargs) - - srv.connected = not srv.connected - - -@hook("prompt_cmd", "top") -def top(toks, context, **kwargs): - """Display consumers load information""" - print("Queue size: %d, %d thread(s) running (counter: %d)" % - (context.cnsr_queue.qsize(), - len(context.cnsr_thrd), - context.cnsr_thrd_size)) - if len(context.events) > 0: - print("Events registered: %d, next in %d seconds" % - (len(context.events), - context.events[0].time_left.seconds)) - else: - print("No events registered") - - for th in context.cnsr_thrd: - if th.is_alive(): - print(("#" * 15 + " Stack trace for thread %u " + "#" * 15) % - th.ident) - traceback.print_stack(sys._current_frames()[th.ident]) - - -@hook("prompt_cmd", "netstat") -def netstat(toks, context, **kwargs): - """Display sockets in use and many other things""" - if len(context.network) > 0: - print("Distant bots connected: %d:" % len(context.network)) - for name, bot in context.network.items(): - print("# %s:" % name) - print(" * Declared hooks:") - lvl = 0 - for hlvl in bot.hooks: - lvl += 1 - for hook in (hlvl.all_pre + hlvl.all_post + hlvl.cmd_rgxp + - hlvl.cmd_default + hlvl.ask_rgxp + - hlvl.ask_default + hlvl.msg_rgxp + - hlvl.msg_default): - print(" %s- %s" % (' ' * lvl * 2, hook)) - for kind in ["irc_hook", "cmd_hook", "ask_hook", "msg_hook"]: - print(" %s- <%s> %s" % (' ' * lvl * 2, kind, - ", ".join(hlvl.__dict__[kind].keys()))) - print(" * My tag: %d" % bot.my_tag) - print(" * Tags in use (%d):" % bot.inc_tag) - for tag, (cmd, data) in bot.tags.items(): - print(" - %11s: %s « %s »" % (tag, cmd, data)) - else: - print("No distant bot connected") diff --git a/nemubot/__init__.py b/nemubot/__init__.py index 7b6949e..d831445 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -127,11 +127,6 @@ def reload(): nemubot.message.reload() - import nemubot.prompt - imp.reload(nemubot.prompt) - - nemubot.prompt.reload() - import nemubot.server rl, wl, xl = nemubot.server._rlist, nemubot.server._wlist, nemubot.server._xlist imp.reload(nemubot.server) diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 5c30695..64652ab 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -133,10 +133,6 @@ def main(): if args.no_connect: context.noautoconnect = True - # Load the prompt - import nemubot.prompt - prmpt = nemubot.prompt.Prompt() - # Register the hook for futur import from nemubot.importer import ModuleFinder module_finder = ModuleFinder(context.modules_paths, context.add_module) diff --git a/nemubot/prompt/__init__.py b/nemubot/prompt/__init__.py deleted file mode 100644 index 27f7919..0000000 --- a/nemubot/prompt/__init__.py +++ /dev/null @@ -1,142 +0,0 @@ -# Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 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/>. - -import shlex -import sys -import traceback - -from nemubot.prompt import builtins - - -class Prompt: - - def __init__(self): - self.selectedServer = None - self.lastretcode = 0 - - self.HOOKS_CAPS = dict() - self.HOOKS_LIST = dict() - - def add_cap_hook(self, name, call, data=None): - self.HOOKS_CAPS[name] = lambda t, c: call(t, data=data, - context=c, prompt=self) - - def add_list_hook(self, name, call): - self.HOOKS_LIST[name] = call - - def lex_cmd(self, line): - """Return an array of tokens - - Argument: - line -- the line to lex - """ - - try: - cmds = shlex.split(line) - except: - exc_type, exc_value, _ = sys.exc_info() - sys.stderr.write(traceback.format_exception_only(exc_type, - exc_value)[0]) - return - - bgn = 0 - - # Separate commands (command separator: ;) - for i in range(0, len(cmds)): - if cmds[i][-1] == ';': - if i != bgn: - yield cmds[bgn:i] - bgn = i + 1 - - # Return rest of the command (that not end with a ;) - if bgn != len(cmds): - yield cmds[bgn:] - - def exec_cmd(self, toks, context): - """Execute the command - - Arguments: - toks -- lexed tokens to executes - context -- current bot context - """ - - if toks[0] in builtins.CAPS: - self.lastretcode = builtins.CAPS[toks[0]](toks, context, self) - elif toks[0] in self.HOOKS_CAPS: - self.lastretcode = self.HOOKS_CAPS[toks[0]](toks, context) - else: - print("Unknown command: `%s'" % toks[0]) - self.lastretcode = 127 - - def getPS1(self): - """Get the PS1 associated to the selected server""" - if self.selectedServer is None: - return "nemubot" - else: - return self.selectedServer.id - - def run(self, context): - """Launch the prompt - - Argument: - context -- current bot context - """ - - from nemubot.prompt.error import PromptError - from nemubot.prompt.reset import PromptReset - - while True: # Stopped by exception - try: - line = input("\033[0;33m%s\033[0;%dm§\033[0m " % - (self.getPS1(), 31 if self.lastretcode else 32)) - cmds = self.lex_cmd(line.strip()) - for toks in cmds: - try: - self.exec_cmd(toks, context) - except PromptReset: - raise - except PromptError as e: - print(e.message) - self.lastretcode = 128 - except: - exc_type, exc_value, exc_traceback = sys.exc_info() - traceback.print_exception(exc_type, exc_value, - exc_traceback) - except KeyboardInterrupt: - print("") - except EOFError: - print("quit") - return True - - -def hotswap(bak): - p = Prompt() - p.HOOKS_CAPS = bak.HOOKS_CAPS - p.HOOKS_LIST = bak.HOOKS_LIST - return p - - -def reload(): - import imp - - import nemubot.prompt.builtins - imp.reload(nemubot.prompt.builtins) - - import nemubot.prompt.error - imp.reload(nemubot.prompt.error) - - import nemubot.prompt.reset - imp.reload(nemubot.prompt.reset) diff --git a/nemubot/prompt/builtins.py b/nemubot/prompt/builtins.py deleted file mode 100644 index a020fb9..0000000 --- a/nemubot/prompt/builtins.py +++ /dev/null @@ -1,128 +0,0 @@ -# Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 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/>. - -def end(toks, context, prompt): - """Quit the prompt for reload or exit""" - from nemubot.prompt.reset import PromptReset - - if toks[0] == "refresh": - raise PromptReset("refresh") - elif toks[0] == "reset": - raise PromptReset("reset") - raise PromptReset("quit") - - -def liste(toks, context, prompt): - """Show some lists""" - if len(toks) > 1: - for l in toks[1:]: - l = l.lower() - if l == "server" or l == "servers": - for srv in context.servers.keys(): - print (" - %s (state: %s) ;" % (srv, - "connected" if context.servers[srv].connected else "disconnected")) - if len(context.servers) == 0: - print (" > No server loaded") - - elif l == "mod" or l == "mods" or l == "module" or l == "modules": - for mod in context.modules.keys(): - print (" - %s ;" % mod) - if len(context.modules) == 0: - print (" > No module loaded") - - elif l in prompt.HOOKS_LIST: - f, d = prompt.HOOKS_LIST[l] - f(d, context, prompt) - - else: - print (" Unknown list `%s'" % l) - return 2 - return 0 - else: - print (" Please give a list to show: servers, ...") - return 1 - - -def load(toks, context, prompt): - """Load an XML configuration file""" - if len(toks) > 1: - for filename in toks[1:]: - context.load_file(filename) - else: - print ("Not enough arguments. `load' takes a filename.") - return 1 - - -def select(toks, context, prompt): - """Select the current server""" - if (len(toks) == 2 and toks[1] != "None" and - toks[1] != "nemubot" and toks[1] != "none"): - if toks[1] in context.servers: - prompt.selectedServer = context.servers[toks[1]] - else: - print ("select: server `%s' not found." % toks[1]) - return 1 - else: - prompt.selectedServer = None - - -def unload(toks, context, prompt): - """Unload a module""" - if len(toks) == 2 and toks[1] == "all": - for name in context.modules.keys(): - context.unload_module(name) - elif len(toks) > 1: - for name in toks[1:]: - if context.unload_module(name): - print (" Module `%s' successfully unloaded." % name) - else: - print (" No module `%s' loaded, can't unload!" % name) - return 2 - else: - print ("Not enough arguments. `unload' takes a module name.") - return 1 - - -def debug(toks, context, prompt): - """Enable/Disable debug mode on a module""" - if len(toks) > 1: - for name in toks[1:]: - if name in context.modules: - context.modules[name].DEBUG = not context.modules[name].DEBUG - if context.modules[name].DEBUG: - print (" Module `%s' now in DEBUG mode." % name) - else: - print (" Debug for module module `%s' disabled." % name) - else: - print (" No module `%s' loaded, can't debug!" % name) - return 2 - else: - print ("Not enough arguments. `debug' takes a module name.") - return 1 - - -# Register build-ins -CAPS = { - 'quit': end, # Disconnect all server and quit - 'exit': end, # Alias for quit - 'reset': end, # Reload the prompt - 'refresh': end, # Reload the prompt but save modules - 'load': load, # Load a servers or module configuration file - 'unload': unload, # Unload a module and remove it from the list - 'select': select, # Select a server - 'list': liste, # Show lists - 'debug': debug, # Pass a module in debug mode -} diff --git a/nemubot/prompt/error.py b/nemubot/prompt/error.py deleted file mode 100644 index f86b5a1..0000000 --- a/nemubot/prompt/error.py +++ /dev/null @@ -1,21 +0,0 @@ -# Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 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/>. - -class PromptError(Exception): - - def __init__(self, message): - super(PromptError, self).__init__(message) - self.message = message diff --git a/nemubot/prompt/reset.py b/nemubot/prompt/reset.py deleted file mode 100644 index 57da9f8..0000000 --- a/nemubot/prompt/reset.py +++ /dev/null @@ -1,23 +0,0 @@ -# coding=utf-8 - -# Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 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/>. - -class PromptReset(Exception): - - def __init__(self, type): - super(PromptReset, self).__init__("Prompt reset asked") - self.type = type diff --git a/setup.py b/setup.py index b39a163..36dddb4 100755 --- a/setup.py +++ b/setup.py @@ -69,7 +69,6 @@ setup( 'nemubot.hooks.keywords', 'nemubot.message', 'nemubot.message.printer', - 'nemubot.prompt', 'nemubot.server', 'nemubot.server.message', 'nemubot.tools', From 24eb9a6911856fa5e5ee7542ab970eb4a4412ae3 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 18 Jul 2015 14:01:56 +0200 Subject: [PATCH 122/271] Can attach to the main process --- nemubot/__init__.py | 66 +++++++++++++++++++++++++++++++++++++--- nemubot/__main__.py | 2 +- nemubot/server/socket.py | 13 ++++++++ 3 files changed, 76 insertions(+), 5 deletions(-) diff --git a/nemubot/__init__.py b/nemubot/__init__.py index d831445..80c4e74 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -18,6 +18,7 @@ __version__ = '4.0.dev3' __author__ = 'nemunaire' from nemubot.modulecontext import ModuleContext + context = ModuleContext(None, None) @@ -38,8 +39,61 @@ def requires_version(min=None, max=None): "but this is nemubot v%s." % (str(max), __version__)) -def attach(socketfile): - print("TODO: Attach to Unix socket at: %s" % socketfile) +def attach(pid, socketfile): + import socket + import sys + + print("nemubot is launched with PID %d. Attaching to Unix socket at: %s" % (pid, socketfile)) + + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + try: + sock.connect(socketfile) + except socket.error as e: + sys.stderr.write(str(e)) + sys.stderr.write("\n") + return 1 + + from select import select + try: + while True: + rl, wl, xl = select([sys.stdin, sock], [], []) + + if sys.stdin in rl: + line = sys.stdin.readline().strip() + if line == "exit" or line == "quit": + return 0 + elif line == "reload": + import os, signal + os.kill(pid, signal.SIGHUP) + print("Reload signal sent. Please wait...") + + elif line == "shutdown": + import os, signal + os.kill(pid, signal.SIGTERM) + print("Shutdown signal sent. Please wait...") + + elif line == "kill": + import os, signal + os.kill(pid, signal.SIGKILL) + print("Signal sent...") + return 0 + + elif line == "stack" or line == "stacks": + import os, signal + os.kill(pid, signal.SIGUSR1) + print("Debug signal sent. Consult logs.") + + else: + sock.send(line.encode() + b'\r\n') + + if sock in rl: + sys.stdout.write(sock.recv(2048).decode()) + except KeyboardInterrupt: + pass + except: + return 1 + finally: + sock.close() return 0 @@ -128,9 +182,13 @@ def reload(): nemubot.message.reload() import nemubot.server - rl, wl, xl = nemubot.server._rlist, nemubot.server._wlist, nemubot.server._xlist + rl = nemubot.server._rlist + wl = nemubot.server._wlist + xl = nemubot.server._xlist imp.reload(nemubot.server) - nemubot.server._rlist, nemubot.server._wlist, nemubot.server._xlist = rl, wl, xl + nemubot.server._rlist = rl + nemubot.server._wlist = wl + nemubot.server._xlist = xl nemubot.server.reload() diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 64652ab..cc15408 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -84,7 +84,7 @@ def main(): pass else: from nemubot import attach - sys.exit(attach(pid)) + sys.exit(attach(pid, args.socketfile)) # Daemonize if not args.debug: diff --git a/nemubot/server/socket.py b/nemubot/server/socket.py index b6c00d4..907b3c3 100644 --- a/nemubot/server/socket.py +++ b/nemubot/server/socket.py @@ -14,6 +14,7 @@ # 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/>. +import nemubot.message as message from nemubot.message.printer.socket import Socket as SocketPrinter from nemubot.server.abstract import AbstractServer @@ -130,6 +131,18 @@ class SocketServer(AbstractServer): yield line + def parse(self, line): + import shlex + + line = line.strip().decode() + try: + args = shlex.split(line) + except ValueError: + args = line.split(' ') + + yield message.Command(cmd=args[0], args=args[1:], server=self.id, to=["you"], frm="you") + + class SocketListener(AbstractServer): def __init__(self, new_server_cb, id, sock_location=None, host=None, port=None, ssl=None): From fc14c76b6d03a45cc94fe69fcbeb2db14cbe5b95 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 3 Mar 2016 19:11:24 +0100 Subject: [PATCH 123/271] [rnd] Add new function choiceres which pick a random response returned by a given subcommand --- modules/rnd.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/modules/rnd.py b/modules/rnd.py index 32c2adf..5329b06 100644 --- a/modules/rnd.py +++ b/modules/rnd.py @@ -8,7 +8,6 @@ import shlex from nemubot import context from nemubot.exception import IMException from nemubot.hooks import hook -from nemubot.message import Command from more import Response @@ -32,8 +31,24 @@ def cmd_choicecmd(msg): choice = shlex.split(random.choice(msg.args)) - return [x for x in context.subtreat(Command(choice[0][1:], - choice[1:], - to_response=msg.to_response, - frm=msg.frm, - server=msg.server))] + return [x for x in context.subtreat(context.subparse(msg, choice))] + + +@hook.command("choiceres") +def cmd_choiceres(msg): + if not len(msg.args): + raise IMException("indicate some command to pick a message from!") + + rl = [x for x in context.subtreat(context.subparse(msg, " ".join(msg.args)))] + if len(rl) <= 0: + return rl + + r = random.choice(rl) + + if isinstance(r, Response): + for i in range(len(r.messages) - 1, -1, -1): + if isinstance(r.messages[i], list): + r.messages = [ random.choice(random.choice(r.messages)) ] + elif isinstance(r.messages[i], str): + r.messages = [ random.choice(r.messages) ] + return r From 6cd299ab60e32ee9476bb839e12f6f8cb8271b40 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 3 Mar 2016 19:19:25 +0100 Subject: [PATCH 124/271] New keywords class that accepts any keywords --- nemubot/hooks/keywords/__init__.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/nemubot/hooks/keywords/__init__.py b/nemubot/hooks/keywords/__init__.py index 4b6419a..95984e8 100644 --- a/nemubot/hooks/keywords/__init__.py +++ b/nemubot/hooks/keywords/__init__.py @@ -26,6 +26,27 @@ class NoKeyword(Abstract): return super().check(mkw) +class AnyKeyword(Abstract): + + def __init__(self, h): + """Class that accepts any passed keywords + + Arguments: + h -- Help string + """ + + super().__init__() + self.h = h + + + def check(self, mkw): + return super().check(mkw) + + + def help(self): + return self.h + + def reload(): import imp From 7cf73fb84a75f93decf350f2a7635d2f24337df2 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 27 Mar 2016 20:34:12 +0100 Subject: [PATCH 125/271] Remove reload feature As reload shoudl be done in a particular order, to keep valid types, and because maintaining such system is too complex (currently, it doesn't work for a while), now, a reload is just reload configuration file (and possibly modules) --- nemubot/__init__.py | 63 ----------------------------- nemubot/__main__.py | 16 +------- nemubot/bot.py | 19 --------- nemubot/config/__init__.py | 21 ---------- nemubot/datastore/__init__.py | 13 ------ nemubot/exception/__init__.py | 7 ---- nemubot/hooks/__init__.py | 20 --------- nemubot/hooks/keywords/__init__.py | 10 ----- nemubot/message/__init__.py | 24 ----------- nemubot/message/printer/__init__.py | 9 ----- nemubot/server/__init__.py | 18 --------- nemubot/tools/__init__.py | 26 ------------ 12 files changed, 1 insertion(+), 245 deletions(-) diff --git a/nemubot/__init__.py b/nemubot/__init__.py index 80c4e74..a56c472 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -133,66 +133,3 @@ def daemonize(): os.dup2(si.fileno(), sys.stdin.fileno()) os.dup2(so.fileno(), sys.stdout.fileno()) os.dup2(se.fileno(), sys.stderr.fileno()) - - -def reload(): - """Reload code of all Python modules used by nemubot - """ - - import imp - - import nemubot.bot - imp.reload(nemubot.bot) - - import nemubot.channel - imp.reload(nemubot.channel) - - import nemubot.config - imp.reload(nemubot.config) - - nemubot.config.reload() - - import nemubot.consumer - imp.reload(nemubot.consumer) - - import nemubot.datastore - imp.reload(nemubot.datastore) - - nemubot.datastore.reload() - - import nemubot.event - imp.reload(nemubot.event) - - import nemubot.exception - imp.reload(nemubot.exception) - - nemubot.exception.reload() - - import nemubot.hooks - imp.reload(nemubot.hooks) - - nemubot.hooks.reload() - - import nemubot.importer - imp.reload(nemubot.importer) - - import nemubot.message - imp.reload(nemubot.message) - - nemubot.message.reload() - - import nemubot.server - rl = nemubot.server._rlist - wl = nemubot.server._wlist - xl = nemubot.server._xlist - imp.reload(nemubot.server) - nemubot.server._rlist = rl - nemubot.server._wlist = wl - nemubot.server._xlist = xl - - nemubot.server.reload() - - import nemubot.tools - imp.reload(nemubot.tools) - - nemubot.tools.reload() diff --git a/nemubot/__main__.py b/nemubot/__main__.py index cc15408..64e4c74 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -158,24 +158,10 @@ def main(): def sighuphandler(signum, frame): """On SIGHUP, perform a deep reload""" - import imp - nonlocal nemubot, context, module_finder + nonlocal context logger.debug("SIGHUP receive, iniate reload procedure...") - # Reload nemubot Python modules - imp.reload(nemubot) - nemubot.reload() - - # Hotswap context - import nemubot.bot - context = nemubot.bot.hotswap(context) - - # Reload ModuleFinder - sys.meta_path.remove(module_finder) - module_finder = ModuleFinder(context.modules_paths, context.add_module) - sys.meta_path.append(module_finder) - # Reload configuration file for path in args.files: if os.path.isfile(path): diff --git a/nemubot/bot.py b/nemubot/bot.py index 8caf0ed..125189b 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -564,22 +564,3 @@ class Bot(threading.Thread): del store[hook.name] elif isinstance(store, list): store.remove(hook) - - -def hotswap(bak): - bak.stop = True - if bak.event_timer is not None: - bak.event_timer.cancel() - - # Unload modules - for mod in [k for k in bak.modules.keys()]: - bak.unload_module(mod) - - # Save datastore - bak.datastore.close() - - new = Bot(str(bak.ip), bak.modules_paths, bak.datastore) - new.servers = bak.servers - - new._update_event_timer() - return new diff --git a/nemubot/config/__init__.py b/nemubot/config/__init__.py index 7e0b74a..6bbc1b2 100644 --- a/nemubot/config/__init__.py +++ b/nemubot/config/__init__.py @@ -24,24 +24,3 @@ from nemubot.config.include import Include from nemubot.config.module import Module from nemubot.config.nemubot import Nemubot from nemubot.config.server import Server - -def reload(): - global Include, Module, Nemubot, Server - - import imp - - import nemubot.config.include - imp.reload(nemubot.config.include) - Include = nemubot.config.include.Include - - import nemubot.config.module - imp.reload(nemubot.config.module) - Module = nemubot.config.module.Module - - import nemubot.config.nemubot - imp.reload(nemubot.config.nemubot) - Nemubot = nemubot.config.nemubot.Nemubot - - import nemubot.config.server - imp.reload(nemubot.config.server) - Server = nemubot.config.server.Server diff --git a/nemubot/datastore/__init__.py b/nemubot/datastore/__init__.py index 411eab1..3e38ad2 100644 --- a/nemubot/datastore/__init__.py +++ b/nemubot/datastore/__init__.py @@ -16,16 +16,3 @@ from nemubot.datastore.abstract import Abstract from nemubot.datastore.xml import XML - - -def reload(): - global Abstract, XML - import imp - - import nemubot.datastore.abstract - imp.reload(nemubot.datastore.abstract) - Abstract = nemubot.datastore.abstract.Abstract - - import nemubot.datastore.xml - imp.reload(nemubot.datastore.xml) - XML = nemubot.datastore.xml.XML diff --git a/nemubot/exception/__init__.py b/nemubot/exception/__init__.py index 1e34923..84464a0 100644 --- a/nemubot/exception/__init__.py +++ b/nemubot/exception/__init__.py @@ -32,10 +32,3 @@ class IMException(Exception): from nemubot.message import Text return Text(*self.args, server=msg.server, to=msg.to_response) - - -def reload(): - import imp - - import nemubot.exception.Keyword - imp.reload(nemubot.exception.printer.IRC) diff --git a/nemubot/hooks/__init__.py b/nemubot/hooks/__init__.py index e9113eb..9024494 100644 --- a/nemubot/hooks/__init__.py +++ b/nemubot/hooks/__init__.py @@ -49,23 +49,3 @@ class hook: def pre(*args, store=["pre"], **kwargs): return hook._add(store, Abstract, *args, **kwargs) - - -def reload(): - import imp - - import nemubot.hooks.abstract - imp.reload(nemubot.hooks.abstract) - - import nemubot.hooks.command - imp.reload(nemubot.hooks.command) - - import nemubot.hooks.message - imp.reload(nemubot.hooks.message) - - import nemubot.hooks.keywords - imp.reload(nemubot.hooks.keywords) - nemubot.hooks.keywords.reload() - - import nemubot.hooks.manager - imp.reload(nemubot.hooks.manager) diff --git a/nemubot/hooks/keywords/__init__.py b/nemubot/hooks/keywords/__init__.py index 95984e8..598b04f 100644 --- a/nemubot/hooks/keywords/__init__.py +++ b/nemubot/hooks/keywords/__init__.py @@ -45,13 +45,3 @@ class AnyKeyword(Abstract): def help(self): return self.h - - -def reload(): - import imp - - import nemubot.hooks.keywords.abstract - imp.reload(nemubot.hooks.keywords.abstract) - - import nemubot.hooks.keywords.dict - imp.reload(nemubot.hooks.keywords.dict) diff --git a/nemubot/message/__init__.py b/nemubot/message/__init__.py index 31d7313..4d69dbb 100644 --- a/nemubot/message/__init__.py +++ b/nemubot/message/__init__.py @@ -19,27 +19,3 @@ from nemubot.message.text import Text from nemubot.message.directask import DirectAsk from nemubot.message.command import Command from nemubot.message.command import OwnerCommand - - -def reload(): - global Abstract, Text, DirectAsk, Command, OwnerCommand - import imp - - import nemubot.message.abstract - imp.reload(nemubot.message.abstract) - Abstract = nemubot.message.abstract.Abstract - imp.reload(nemubot.message.text) - Text = nemubot.message.text.Text - imp.reload(nemubot.message.directask) - DirectAsk = nemubot.message.directask.DirectAsk - imp.reload(nemubot.message.command) - Command = nemubot.message.command.Command - OwnerCommand = nemubot.message.command.OwnerCommand - - import nemubot.message.visitor - imp.reload(nemubot.message.visitor) - - import nemubot.message.printer - imp.reload(nemubot.message.printer) - - nemubot.message.printer.reload() diff --git a/nemubot/message/printer/__init__.py b/nemubot/message/printer/__init__.py index 060118b..e0fbeef 100644 --- a/nemubot/message/printer/__init__.py +++ b/nemubot/message/printer/__init__.py @@ -13,12 +13,3 @@ # # 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/>. - -def reload(): - import imp - - import nemubot.message.printer.IRC - imp.reload(nemubot.message.printer.IRC) - - import nemubot.message.printer.socket - imp.reload(nemubot.message.printer.socket) diff --git a/nemubot/server/__init__.py b/nemubot/server/__init__.py index b9a8fe4..3c88138 100644 --- a/nemubot/server/__init__.py +++ b/nemubot/server/__init__.py @@ -68,21 +68,3 @@ def factory(uri, **init_args): return IRCServer(**args) else: return None - - -def reload(): - import imp - - import nemubot.server.abstract - imp.reload(nemubot.server.abstract) - - import nemubot.server.socket - imp.reload(nemubot.server.socket) - - import nemubot.server.IRC - imp.reload(nemubot.server.IRC) - - import nemubot.server.message - imp.reload(nemubot.server.message) - - nemubot.server.message.reload() diff --git a/nemubot/tools/__init__.py b/nemubot/tools/__init__.py index 127154c..57f3468 100644 --- a/nemubot/tools/__init__.py +++ b/nemubot/tools/__init__.py @@ -13,29 +13,3 @@ # # 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/>. - -def reload(): - import imp - - import nemubot.tools.config - imp.reload(nemubot.tools.config) - - import nemubot.tools.countdown - imp.reload(nemubot.tools.countdown) - - import nemubot.tools.feed - imp.reload(nemubot.tools.feed) - - import nemubot.tools.date - imp.reload(nemubot.tools.date) - - import nemubot.tools.human - imp.reload(nemubot.tools.human) - - import nemubot.tools.web - imp.reload(nemubot.tools.web) - - import nemubot.tools.xmlparser - imp.reload(nemubot.tools.xmlparser) - import nemubot.tools.xmlparser.node - imp.reload(nemubot.tools.xmlparser.node) From a0c3f6d2b3461fe5d60893c8dd5ac46f83323861 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 6 Mar 2016 21:43:08 +0100 Subject: [PATCH 126/271] Review consumer errors --- nemubot/consumer.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/nemubot/consumer.py b/nemubot/consumer.py index 886c4cf..431db82 100644 --- a/nemubot/consumer.py +++ b/nemubot/consumer.py @@ -44,7 +44,7 @@ class MessageConsumer: msgs.append(msg) except: logger.exception("Error occurred during the processing of the %s: " - "%s", type(self.msgs[0]).__name__, self.msgs[0]) + "%s", type(self.orig).__name__, self.orig) if len(msgs) <= 0: return @@ -55,6 +55,8 @@ class MessageConsumer: if hasattr(msg, "frm_owner"): msg.frm_owner = (not hasattr(self.srv, "owner") or self.srv.owner == msg.frm) + from nemubot.server.abstract import AbstractServer + # Treat the message for msg in msgs: for res in context.treater.treat_msg(msg): @@ -62,15 +64,19 @@ class MessageConsumer: to_server = None if isinstance(res, str): to_server = self.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.id elif isinstance(res.server, str) and res.server in context.servers: to_server = context.servers[res.server] + else: + to_server = res.server - if to_server is None: - logger.error("The server defined in this response doesn't " - "exist: %s", res.server) + if to_server is None or not hasattr(to_server, "send_response") or not callable(to_server.send_response): + logger.error("The server defined in this response doesn't exist: %s", res.server) continue # Sent the message only if treat_post authorize it From 4c11c5e215adb5ebcea42e2e109a2d34079afdaf Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 6 Mar 2016 21:45:13 +0100 Subject: [PATCH 127/271] Handle case where frm and to have not been filled --- nemubot/message/printer/socket.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nemubot/message/printer/socket.py b/nemubot/message/printer/socket.py index cb9bc4c..6884c88 100644 --- a/nemubot/message/printer/socket.py +++ b/nemubot/message/printer/socket.py @@ -35,7 +35,7 @@ class Socket(AbstractVisitor): others = [to for to in msg.to if to != msg.designated] # Avoid nick starting message when discussing on user channel - if len(others) != len(msg.to): + if len(others) == 0 or len(others) != len(msg.to): res = Text(msg.message, server=msg.server, date=msg.date, to=msg.to, frm=msg.frm) From b5d5a67b2d50f5d97fd65eb459d73e4f027ea332 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 8 Jul 2016 22:40:49 +0200 Subject: [PATCH 128/271] In debug mode, display running thread at exit --- nemubot/__main__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 64e4c74..5a236f4 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -191,6 +191,8 @@ def main(): # Wait for consumers logger.info("Waiting for other threads shuts down...") + if args.debug: + sigusr1handler(0, None) sys.exit(0) if __name__ == "__main__": From 2a3cd07c63ab7718bdcff28aacc1ed89189a0fdf Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 16 May 2016 16:28:19 +0200 Subject: [PATCH 129/271] Documentation --- nemubot/bot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nemubot/bot.py b/nemubot/bot.py index 125189b..0cac3ab 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -37,8 +37,9 @@ class Bot(threading.Thread): Keyword arguments: ip -- The external IP of the bot (default: 127.0.0.1) - modules_paths -- Paths to all directories where looking for module + 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 """ threading.Thread.__init__(self) From 1c21231f3121e7628b3cb82074f77731ef7c0ec9 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 18 Apr 2016 19:58:10 +0200 Subject: [PATCH 130/271] Use super() instead of parent class name --- nemubot/message/command.py | 2 +- nemubot/message/directask.py | 2 +- nemubot/message/printer/IRC.py | 2 +- nemubot/message/response.py | 34 +++++++++++++++++++++ nemubot/message/text.py | 2 +- nemubot/server/DCC.py | 2 +- nemubot/server/IRC.py | 15 ++++----- nemubot/server/abstract.py | 2 ++ nemubot/server/socket.py | 56 +++++++++++++++++++--------------- 9 files changed, 80 insertions(+), 37 deletions(-) create mode 100644 nemubot/message/response.py diff --git a/nemubot/message/command.py b/nemubot/message/command.py index 895d16e..6c208b2 100644 --- a/nemubot/message/command.py +++ b/nemubot/message/command.py @@ -22,7 +22,7 @@ class Command(Abstract): """This class represents a specialized TextMessage""" def __init__(self, cmd, args=None, kwargs=None, *nargs, **kargs): - Abstract.__init__(self, *nargs, **kargs) + super().__init__(*nargs, **kargs) self.cmd = cmd self.args = args if args is not None else list() diff --git a/nemubot/message/directask.py b/nemubot/message/directask.py index 03c7902..3b1fabb 100644 --- a/nemubot/message/directask.py +++ b/nemubot/message/directask.py @@ -28,7 +28,7 @@ class DirectAsk(Text): designated -- the user designated by the message """ - Text.__init__(self, *args, **kargs) + super().__init__(*args, **kargs) self.designated = designated diff --git a/nemubot/message/printer/IRC.py b/nemubot/message/printer/IRC.py index 320366c..df9cb9f 100644 --- a/nemubot/message/printer/IRC.py +++ b/nemubot/message/printer/IRC.py @@ -22,4 +22,4 @@ class IRC(SocketPrinter): def visit_Text(self, msg): self.pp += "PRIVMSG %s :" % ",".join(msg.to) - SocketPrinter.visit_Text(self, msg) + super().visit_Text(msg) diff --git a/nemubot/message/response.py b/nemubot/message/response.py new file mode 100644 index 0000000..fba864b --- /dev/null +++ b/nemubot/message/response.py @@ -0,0 +1,34 @@ +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2015 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 nemubot.message.abstract import Abstract + + +class Response(Abstract): + + def __init__(self, cmd, args=None, *nargs, **kargs): + super().__init__(*nargs, **kargs) + + self.cmd = cmd + self.args = args if args is not None else list() + + def __str__(self): + return self.cmd + " @" + ",@".join(self.args) + + @property + def cmds(self): + # TODO: this is for legacy modules + return [self.cmd] + self.args diff --git a/nemubot/message/text.py b/nemubot/message/text.py index ec90a36..f691a04 100644 --- a/nemubot/message/text.py +++ b/nemubot/message/text.py @@ -28,7 +28,7 @@ class Text(Abstract): message -- the parsed message """ - Abstract.__init__(self, *args, **kargs) + super().__init__(*args, **kargs) self.message = message diff --git a/nemubot/server/DCC.py b/nemubot/server/DCC.py index 6655d52..644a8cb 100644 --- a/nemubot/server/DCC.py +++ b/nemubot/server/DCC.py @@ -31,7 +31,7 @@ PORTS = list() class DCC(server.AbstractServer): def __init__(self, srv, dest, socket=None): - server.Server.__init__(self) + super().__init__(self) self.error = False # An error has occur, closing the connection? self.messages = list() # Message queued before connexion diff --git a/nemubot/server/IRC.py b/nemubot/server/IRC.py index e433176..e09c77e 100644 --- a/nemubot/server/IRC.py +++ b/nemubot/server/IRC.py @@ -55,7 +55,7 @@ class IRC(SocketServer): self.realname = realname self.id = self.username + "@" + host + ":" + str(port) - SocketServer.__init__(self, host=host, port=port, ssl=ssl) + super().__init__(host=host, port=port, ssl=ssl) self.printer = IRCPrinter self.encoding = encoding @@ -232,8 +232,8 @@ class IRC(SocketServer): # Open/close - def _open(self): - if SocketServer._open(self): + def open(self): + if super().open(): if self.password is not None: self.write("PASS :" + self.password) if self.capabilities is not None: @@ -244,9 +244,10 @@ class IRC(SocketServer): return False - def _close(self): - if self.connected: self.write("QUIT") - return SocketServer._close(self) + def close(self): + if not self.closed: + self.write("QUIT") + return super().close() # Writes: as inherited @@ -254,7 +255,7 @@ class IRC(SocketServer): # Read def read(self): - for line in SocketServer.read(self): + for line in super().read(): # PING should be handled here, so start parsing here :/ msg = IRCMessage(line, self.encoding) diff --git a/nemubot/server/abstract.py b/nemubot/server/abstract.py index 8e3dc3b..518d7d6 100644 --- a/nemubot/server/abstract.py +++ b/nemubot/server/abstract.py @@ -32,6 +32,8 @@ class AbstractServer(io.IOBase): send_callback -- Callback when developper want to send a message """ + super().__init__() + if not hasattr(self, "id"): raise Exception("No id defined for this server. Please set one!") diff --git a/nemubot/server/socket.py b/nemubot/server/socket.py index 907b3c3..6876d2f 100644 --- a/nemubot/server/socket.py +++ b/nemubot/server/socket.py @@ -26,7 +26,7 @@ class SocketServer(AbstractServer): def __init__(self, sock_location=None, host=None, port=None, ssl=False, socket=None, id=None): if id is not None: self.id = id - AbstractServer.__init__(self) + super().__init__() if sock_location is not None: self.filename = sock_location elif host is not None: @@ -44,18 +44,17 @@ class SocketServer(AbstractServer): @property - def connected(self): + def closed(self): """Indicator of the connection aliveness""" - return self.socket is not None + return self.socket is None # Open/close - def _open(self): - import os + def open(self): import socket - if self.connected: + if not self.closed: return True try: @@ -66,11 +65,14 @@ class SocketServer(AbstractServer): else: self.socket = socket.create_connection((self.host, self.port)) self.logger.info("Connected to %s:%d", self.host, self.port) - except socket.error as e: + except: self.socket = None - self.logger.critical("Unable to connect to %s:%d: %s", - self.host, self.port, - os.strerror(e.errno)) + if hasattr(self, "filename"): + self.logger.exception("Unable to connect to %s", + self.filename) + else: + self.logger.exception("Unable to connect to %s:%d", + self.host, self.port) return False # Wrap the socket for SSL @@ -79,17 +81,17 @@ class SocketServer(AbstractServer): ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1) self.socket = ctx.wrap_socket(self.socket) - return True + return super().open() - def _close(self): + def close(self): import socket from nemubot.server import _lock _lock.release() self._sending_queue.join() _lock.acquire() - if self.connected: + if not self.closed: try: self.socket.shutdown(socket.SHUT_RDWR) self.socket.close() @@ -98,16 +100,16 @@ class SocketServer(AbstractServer): self.socket = None - return True + return super().close() # Write def _write(self, cnt): - if not self.connected: + if self.closed: return - self.socket.send(cnt) + self.socket.sendall(cnt) def format(self, txt): @@ -120,7 +122,7 @@ class SocketServer(AbstractServer): # Read def read(self): - if not self.connected: + if self.closed: return [] raw = self.socket.recv(1024) @@ -147,7 +149,7 @@ class SocketListener(AbstractServer): def __init__(self, new_server_cb, id, sock_location=None, host=None, port=None, ssl=None): self.id = id - AbstractServer.__init__(self) + super().__init__() self.new_server_cb = new_server_cb self.sock_location = sock_location self.host = host @@ -161,30 +163,31 @@ class SocketListener(AbstractServer): @property - def connected(self): + def closed(self): """Indicator of the connection aliveness""" - return self.socket is not None + return self.socket is None - def _open(self): + def open(self): import os import socket - self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) if self.sock_location is not None: + self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) try: os.remove(self.sock_location) except FileNotFoundError: pass self.socket.bind(self.sock_location) elif self.host is not None and self.port is not None: + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket.bind((self.host, self.port)) self.socket.listen(5) - return True + return super().open() - def _close(self): + def close(self): import os import socket @@ -196,10 +199,13 @@ class SocketListener(AbstractServer): except socket.error: pass + return super().close() + + # Read def read(self): - if not self.connected: + if self.closed: return [] conn, addr = self.socket.accept() From 6d8dca211dad30e02e07422febbe3869315c4e96 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 16 May 2016 17:35:24 +0200 Subject: [PATCH 131/271] Use fileno instead of name to index existing servers --- nemubot/bot.py | 12 ++--- nemubot/consumer.py | 27 ++++-------- nemubot/server/IRC.py | 3 +- nemubot/server/abstract.py | 19 +++++--- nemubot/server/message/IRC.py | 2 +- nemubot/server/socket.py | 82 ++++++++++++++++++++--------------- 6 files changed, 78 insertions(+), 67 deletions(-) diff --git a/nemubot/bot.py b/nemubot/bot.py index 0cac3ab..2657d52 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -255,9 +255,9 @@ class Bot(threading.Thread): srv = server.server(config) # Add the server in the context if self.add_server(srv, server.autoconnect): - logger.info("Server '%s' successfully added." % srv.id) + logger.info("Server '%s' successfully added." % srv.name) else: - logger.error("Can't add server '%s'." % srv.id) + logger.error("Can't add server '%s'." % srv.name) # Load module and their configuration for mod in config.modules: @@ -306,7 +306,7 @@ class Bot(threading.Thread): if type(eid) is uuid.UUID: evt.id = str(eid) else: - # Ok, this is quite useless... + # Ok, this is quiet useless... try: evt.id = str(uuid.UUID(eid)) except ValueError: @@ -414,8 +414,10 @@ class Bot(threading.Thread): autoconnect -- connect after add? """ - if srv.id not in self.servers: - self.servers[srv.id] = srv + 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.open() return True diff --git a/nemubot/consumer.py b/nemubot/consumer.py index 431db82..2765aff 100644 --- a/nemubot/consumer.py +++ b/nemubot/consumer.py @@ -1,5 +1,5 @@ # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 Mercier Pierre-Olivier +# Copyright (C) 2012-2016 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 @@ -38,7 +38,7 @@ class MessageConsumer: msgs = [] - # Parse the message + # Parse message try: for msg in self.srv.parse(self.orig): msgs.append(msg) @@ -46,21 +46,10 @@ class MessageConsumer: logger.exception("Error occurred during the processing of the %s: " "%s", type(self.orig).__name__, self.orig) - if len(msgs) <= 0: - return - - # Qualify the message - if not hasattr(msg, "server") or msg.server is None: - msg.server = self.srv.id - if hasattr(msg, "frm_owner"): - msg.frm_owner = (not hasattr(self.srv, "owner") or self.srv.owner == msg.frm) - - from nemubot.server.abstract import AbstractServer - - # Treat the message + # Treat message for msg in msgs: for res in context.treater.treat_msg(msg): - # Identify the destination + # Identify destination to_server = None if isinstance(res, str): to_server = self.srv @@ -69,8 +58,8 @@ class MessageConsumer: continue elif res.server is None: to_server = self.srv - res.server = self.srv.id - elif isinstance(res.server, str) and res.server in context.servers: + res.server = self.srv.fileno() + elif res.server in context.servers: to_server = context.servers[res.server] else: to_server = res.server @@ -79,7 +68,7 @@ class MessageConsumer: logger.error("The server defined in this response doesn't exist: %s", res.server) continue - # Sent the message only if treat_post authorize it + # Sent message to_server.send_response(res) @@ -116,7 +105,7 @@ class Consumer(threading.Thread): def __init__(self, context): self.context = context self.stop = False - threading.Thread.__init__(self) + super().__init__(name="Nemubot consumer") def run(self): diff --git a/nemubot/server/IRC.py b/nemubot/server/IRC.py index e09c77e..08e2bc5 100644 --- a/nemubot/server/IRC.py +++ b/nemubot/server/IRC.py @@ -54,8 +54,7 @@ class IRC(SocketServer): self.owner = owner self.realname = realname - self.id = self.username + "@" + host + ":" + str(port) - super().__init__(host=host, port=port, ssl=ssl) + super().__init__(host=host, port=port, ssl=ssl, name=self.username + "@" + host + ":" + str(port)) self.printer = IRCPrinter self.encoding = encoding diff --git a/nemubot/server/abstract.py b/nemubot/server/abstract.py index 518d7d6..dc2081d 100644 --- a/nemubot/server/abstract.py +++ b/nemubot/server/abstract.py @@ -25,19 +25,18 @@ class AbstractServer(io.IOBase): """An abstract server: handle communication with an IM server""" - def __init__(self, send_callback=None): + def __init__(self, name=None, send_callback=None): """Initialize an abstract server Keyword argument: send_callback -- Callback when developper want to send a message """ + self._name = name + super().__init__() - if not hasattr(self, "id"): - raise Exception("No id defined for this server. Please set one!") - - self.logger = logging.getLogger("nemubot.server." + self.id) + self.logger = logging.getLogger("nemubot.server." + self.name) self._sending_queue = queue.Queue() if send_callback is not None: self._send_callback = send_callback @@ -45,6 +44,14 @@ class AbstractServer(io.IOBase): self._send_callback = self._write_select + @property + def name(self): + if self._name is not None: + return self._name + else: + return self.fileno() + + # Open/close def __enter__(self): @@ -151,4 +158,4 @@ class AbstractServer(io.IOBase): def exception(self): """Exception occurs in fd""" self.logger.warning("Unhandle file descriptor exception on server %s", - self.id) + self.name) diff --git a/nemubot/server/message/IRC.py b/nemubot/server/message/IRC.py index f6d562f..4c9e280 100644 --- a/nemubot/server/message/IRC.py +++ b/nemubot/server/message/IRC.py @@ -146,7 +146,7 @@ class IRC(Abstract): receivers = self.decode(self.params[0]).split(',') common_args = { - "server": srv.id, + "server": srv.name, "date": self.tags["time"], "to": receivers, "to_response": [r if r != srv.nick else self.nick for r in receivers], diff --git a/nemubot/server/socket.py b/nemubot/server/socket.py index 6876d2f..13ac9bd 100644 --- a/nemubot/server/socket.py +++ b/nemubot/server/socket.py @@ -23,18 +23,43 @@ class SocketServer(AbstractServer): """Concrete implementation of a socket connexion (can be wrapped with TLS)""" - def __init__(self, sock_location=None, host=None, port=None, ssl=False, socket=None, id=None): - if id is not None: - self.id = id - super().__init__() - if sock_location is not None: - self.filename = sock_location - elif host is not None: - self.host = host - self.port = int(port) + def __init__(self, sock_location=None, + host=None, port=None, + sock=None, + ssl=False, + name=None): + """Create a server socket + + Keyword arguments: + sock_location -- Path to the UNIX socket + host -- Hostname of the INET socket + port -- Port of the INET socket + sock -- Already connected socket + ssl -- Should TLS connection enabled + name -- Convinience name + """ + + import socket + + assert(sock is None or isinstance(sock, socket.SocketType)) + assert(port is None or isinstance(port, int)) + + super().__init__(name=name) + + if sock is None: + if sock_location is not None: + self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.connect_to = sock_location + elif host is not None: + for af, socktype, proto, canonname, sa in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM): + self.socket = socket.socket(af, socktype, proto) + self.connect_to = sa + break + else: + self.socket = sock + self.ssl = ssl - self.socket = socket self.readbuffer = b'' self.printer = SocketPrinter @@ -46,33 +71,22 @@ class SocketServer(AbstractServer): @property def closed(self): """Indicator of the connection aliveness""" - return self.socket is None + return self.socket._closed # Open/close def open(self): - import socket - if not self.closed: return True try: - if hasattr(self, "filename"): - self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - self.socket.connect(self.filename) - self.logger.info("Connected to %s", self.filename) - else: - self.socket = socket.create_connection((self.host, self.port)) - self.logger.info("Connected to %s:%d", self.host, self.port) + self.socket.connect(self.connect_to) + self.logger.info("Connected to %s", self.connect_to) except: - self.socket = None - if hasattr(self, "filename"): - self.logger.exception("Unable to connect to %s", - self.filename) - else: - self.logger.exception("Unable to connect to %s:%d", - self.host, self.port) + self.socket.close() + self.logger.exception("Unable to connect to %s", + self.connect_to) return False # Wrap the socket for SSL @@ -87,18 +101,19 @@ class SocketServer(AbstractServer): def close(self): import socket + # Flush the sending queue before close from nemubot.server import _lock _lock.release() self._sending_queue.join() _lock.acquire() + if not self.closed: try: self.socket.shutdown(socket.SHUT_RDWR) - self.socket.close() except socket.error: pass - self.socket = None + self.socket.close() return super().close() @@ -142,14 +157,13 @@ class SocketServer(AbstractServer): except ValueError: args = line.split(' ') - yield message.Command(cmd=args[0], args=args[1:], server=self.id, to=["you"], frm="you") + yield message.Command(cmd=args[0], args=args[1:], server=self.name, to=["you"], frm="you") class SocketListener(AbstractServer): - def __init__(self, new_server_cb, id, sock_location=None, host=None, port=None, ssl=None): - self.id = id - super().__init__() + def __init__(self, new_server_cb, name, sock_location=None, host=None, port=None, ssl=None): + super().__init__(name=name) self.new_server_cb = new_server_cb self.sock_location = sock_location self.host = host @@ -210,7 +224,7 @@ class SocketListener(AbstractServer): conn, addr = self.socket.accept() self.nb_son += 1 - ss = SocketServer(id=self.id + "#" + str(self.nb_son), socket=conn) + ss = SocketServer(name=self.name + "#" + str(self.nb_son), socket=conn) self.new_server_cb(ss) return [] From 764e6f070b9f4e6a5ff0fc401aa15b8448ed78de Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 30 May 2016 22:06:35 +0200 Subject: [PATCH 132/271] Refactor file/socket management (use poll instead of select) --- README.md | 2 + nemubot/__main__.py | 26 +-- nemubot/bot.py | 145 +++++++++-------- nemubot/server/DCC.py | 2 +- nemubot/server/IRC.py | 43 +++-- nemubot/server/__init__.py | 45 ++--- nemubot/server/abstract.py | 126 +++++++------- nemubot/server/factory_test.py | 10 +- nemubot/server/message/__init__.py | 15 ++ nemubot/server/socket.py | 253 ++++++++++++----------------- 10 files changed, 328 insertions(+), 339 deletions(-) create mode 100644 nemubot/server/message/__init__.py diff --git a/README.md b/README.md index aa3b141..1d40faf 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ Requirements *nemubot* requires at least Python 3.3 to work. +Connecting to SSL server requires [this patch](http://bugs.python.org/issue27629). + Some modules (like `cve`, `nextstop` or `laposte`) require the [BeautifulSoup module](http://www.crummy.com/software/BeautifulSoup/), but the core and framework has no dependency. diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 5a236f4..c39dd2f 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -1,5 +1,5 @@ # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 Mercier Pierre-Olivier +# Copyright (C) 2012-2016 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 @@ -125,7 +125,7 @@ def main(): # Create bot context from nemubot import datastore - from nemubot.bot import Bot + from nemubot.bot import Bot, sync_act context = Bot(modules_paths=modules_paths, data_store=datastore.XML(args.data_path), verbosity=args.verbose) @@ -141,7 +141,7 @@ def main(): # Load requested configuration files for path in args.files: if os.path.isfile(path): - context.sync_queue.put_nowait(["loadconf", path]) + sync_act("loadconf", path) else: logger.error("%s is not a readable file", path) @@ -165,22 +165,28 @@ def main(): # Reload configuration file for path in args.files: if os.path.isfile(path): - context.sync_queue.put_nowait(["loadconf", path]) + sync_act("loadconf", path) signal.signal(signal.SIGHUP, sighuphandler) def sigusr1handler(signum, frame): """On SIGHUSR1, display stacktraces""" - import traceback + import threading, traceback for threadId, stack in sys._current_frames().items(): - logger.debug("########### Thread %d:\n%s", - threadId, + thName = "#%d" % threadId + for th in threading.enumerate(): + if th.ident == threadId: + thName = th.name + break + logger.debug("########### Thread %s:\n%s", + thName, "".join(traceback.format_stack(stack))) signal.signal(signal.SIGUSR1, sigusr1handler) if args.socketfile: - from nemubot.server.socket import SocketListener - context.add_server(SocketListener(context.add_server, "master_socket", - sock_location=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 diff --git a/nemubot/bot.py b/nemubot/bot.py index 2657d52..c8ede40 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -16,7 +16,9 @@ from datetime import datetime, timezone import logging +from multiprocessing import JoinableQueue import threading +import select import sys from nemubot import __version__ @@ -26,6 +28,11 @@ import nemubot.hooks logger = logging.getLogger("nemubot") +sync_queue = JoinableQueue() + +def sync_act(*args): + sync_queue.put(list(args)) + class Bot(threading.Thread): @@ -42,7 +49,7 @@ class Bot(threading.Thread): verbosity -- verbosity level """ - threading.Thread.__init__(self) + super().__init__(name="Nemubot main") logger.info("Initiate nemubot v%s (running on Python %s.%s.%s)", __version__, @@ -61,6 +68,7 @@ class Bot(threading.Thread): self.datastore.open() # Keep global context: servers and modules + self._poll = select.poll() self.servers = dict() self.modules = dict() self.modules_configuration = dict() @@ -138,60 +146,72 @@ class Bot(threading.Thread): self.cnsr_queue = Queue() self.cnsr_thrd = list() self.cnsr_thrd_size = -1 - # Synchrone actions to be treated by main thread - self.sync_queue = Queue() def run(self): - from select import select - from nemubot.server import _lock, _rlist, _wlist, _xlist + self._poll.register(sync_queue._reader, select.POLLIN | select.POLLPRI) logger.info("Starting main loop") self.stop = False while not self.stop: - with _lock: - 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) or r.fileno() < 0: - _rlist.remove(r) - logger.error("Found invalid object in _rlist: " + str(r)) - fnd_smth = True - for w in _wlist: - if not hasattr(w, "fileno") or not isinstance(w.fileno(), int) or w.fileno() < 0: - _wlist.remove(w) - logger.error("Found invalid object in _wlist: " + str(w)) - fnd_smth = True - for x in _xlist: - if not hasattr(x, "fileno") or not isinstance(x.fileno(), int) or x.fileno() < 0: - _xlist.remove(x) - logger.error("Found invalid object in _xlist: " + str(x)) - fnd_smth = True - if not fnd_smth: - logger.exception("Can't continue, sorry") - self.quit() - continue + 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] - 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(): + if flag & (select.POLLERR | select.POLLHUP | select.POLLNVAL): try: - self.receive_message(r, i) + srv.exception(flag) except: - logger.exception("Uncatched exception on server read") + 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 @@ -202,17 +222,6 @@ class Bot(threading.Thread): c = Consumer(self) self.cnsr_thrd.append(c) c.start() - - while self.sync_queue.qsize() > 0: - action = self.sync_queue.get_nowait() - if action[0] == "exit": - self.quit() - elif action[0] == "loadconf": - for path in action[1:]: - logger.debug("Load configuration from %s", path) - self.load_file(path) - logger.info("Configurations successfully loaded") - self.sync_queue.task_done() logger.info("Ending main loop") @@ -419,7 +428,7 @@ class Bot(threading.Thread): self.servers[fileno] = srv self.servers[srv.name] = srv if autoconnect and not hasattr(self, "noautoconnect"): - srv.open() + srv.connect() return True else: @@ -532,28 +541,28 @@ class Bot(threading.Thread): def quit(self): """Save and unload modules and disconnect servers""" - self.datastore.close() - 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 - logger.info("Save and unload all modules...") - k = list(self.modules.keys()) - for mod in k: - self.unload_module(mod) - - logger.info("Close all servers connection...") - k = list(self.servers.keys()) - for srv in k: - self.servers[srv].close() + self.datastore.close() self.stop = True + sync_act("end") + sync_queue.join() # Treatment diff --git a/nemubot/server/DCC.py b/nemubot/server/DCC.py index 644a8cb..c1a6852 100644 --- a/nemubot/server/DCC.py +++ b/nemubot/server/DCC.py @@ -31,7 +31,7 @@ PORTS = list() class DCC(server.AbstractServer): def __init__(self, srv, dest, socket=None): - super().__init__(self) + super().__init__(name="Nemubot DCC server") self.error = False # An error has occur, closing the connection? self.messages = list() # Message queued before connexion diff --git a/nemubot/server/IRC.py b/nemubot/server/IRC.py index 08e2bc5..89eeab5 100644 --- a/nemubot/server/IRC.py +++ b/nemubot/server/IRC.py @@ -20,17 +20,17 @@ import re from nemubot.channel import Channel from nemubot.message.printer.IRC import IRC as IRCPrinter from nemubot.server.message.IRC import IRC as IRCMessage -from nemubot.server.socket import SocketServer +from nemubot.server.socket import SocketServer, SecureSocketServer -class IRC(SocketServer): +class _IRC: """Concrete implementation of a connexion to an IRC server""" - def __init__(self, host="localhost", port=6667, ssl=False, owner=None, + def __init__(self, host="localhost", port=6667, owner=None, nick="nemubot", username=None, password=None, realname="Nemubot", encoding="utf-8", caps=None, - channels=list(), on_connect=None): + channels=list(), on_connect=None, **kwargs): """Prepare a connection with an IRC server Keyword arguments: @@ -54,7 +54,8 @@ class IRC(SocketServer): self.owner = owner self.realname = realname - super().__init__(host=host, port=port, ssl=ssl, name=self.username + "@" + host + ":" + str(port)) + super().__init__(name=self.username + "@" + host + ":" + str(port), + host=host, port=port, **kwargs) self.printer = IRCPrinter self.encoding = encoding @@ -231,20 +232,19 @@ class IRC(SocketServer): # Open/close - def open(self): - if super().open(): - if self.password is not None: - self.write("PASS :" + self.password) - if self.capabilities is not None: - self.write("CAP LS") - self.write("NICK :" + self.nick) - self.write("USER %s %s bla :%s" % (self.username, self.host, self.realname)) - return True - return False + def connect(self): + super().connect() + + if self.password is not None: + self.write("PASS :" + self.password) + if self.capabilities is not None: + self.write("CAP LS") + self.write("NICK :" + self.nick) + self.write("USER %s %s bla :%s" % (self.username, self.host, self.realname)) def close(self): - if not self.closed: + if not self._closed: self.write("QUIT") return super().close() @@ -253,8 +253,8 @@ class IRC(SocketServer): # Read - def read(self): - for line in super().read(): + def async_read(self): + for line in super().async_read(): # PING should be handled here, so start parsing here :/ msg = IRCMessage(line, self.encoding) @@ -273,3 +273,10 @@ class IRC(SocketServer): 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 3c88138..6b583b7 100644 --- a/nemubot/server/__init__.py +++ b/nemubot/server/__init__.py @@ -1,5 +1,5 @@ # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 Mercier Pierre-Olivier +# Copyright (C) 2012-2016 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 @@ -14,34 +14,37 @@ # 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/>. -import threading -_lock = threading.Lock() - -# Lists for select -_rlist = [] -_wlist = [] -_xlist = [] - - -def factory(uri, **init_args): +def factory(uri, ssl=False, **init_args): from urllib.parse import urlparse, unquote o = urlparse(uri) + srv = None + if o.scheme == "irc" or o.scheme == "ircs": # http://www.w3.org/Addressing/draft-mirashi-url-irc-01.txt # http://www-archive.mozilla.org/projects/rt-messaging/chatzilla/irc-urls.html args = init_args - modifiers = o.path.split(",") - target = unquote(modifiers.pop(0)[1:]) - - if o.scheme == "ircs": args["ssl"] = True + if o.scheme == "ircs": ssl = True if o.hostname is not None: args["host"] = o.hostname if o.port is not None: args["port"] = o.port if o.username is not None: args["username"] = o.username if o.password is not None: args["password"] = o.password + if ssl: + try: + from ssl import create_default_context + args["_context"] = create_default_context() + except ImportError: + # Python 3.3 compat + from ssl import SSLContext, PROTOCOL_TLSv1 + args["_context"] = SSLContext(PROTOCOL_TLSv1) + args["server_hostname"] = o.hostname + + modifiers = o.path.split(",") + target = unquote(modifiers.pop(0)[1:]) + queries = o.query.split("&") for q in queries: if "=" in q: @@ -64,7 +67,11 @@ def factory(uri, **init_args): if "channels" not in args and "isnick" not in modifiers: args["channels"] = [ target ] - from nemubot.server.IRC import IRC as IRCServer - return IRCServer(**args) - else: - return None + if ssl: + from nemubot.server.IRC import IRC_secure as SecureIRCServer + srv = SecureIRCServer(**args) + else: + from nemubot.server.IRC import IRC as IRCServer + srv = IRCServer(**args) + + return srv diff --git a/nemubot/server/abstract.py b/nemubot/server/abstract.py index dc2081d..fd25c2d 100644 --- a/nemubot/server/abstract.py +++ b/nemubot/server/abstract.py @@ -1,5 +1,5 @@ # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 Mercier Pierre-Olivier +# Copyright (C) 2012-2016 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 @@ -14,34 +14,30 @@ # 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/>. -import io import logging import queue -from nemubot.server import _lock, _rlist, _wlist, _xlist +from nemubot.bot import sync_act -# Extends from IOBase in order to be compatible with select function -class AbstractServer(io.IOBase): + +class AbstractServer: """An abstract server: handle communication with an IM server""" - def __init__(self, name=None, send_callback=None): + def __init__(self, name=None, **kwargs): """Initialize an abstract server Keyword argument: - send_callback -- Callback when developper want to send a message + name -- Identifier of the socket, for convinience """ self._name = name - super().__init__() + super().__init__(**kwargs) - self.logger = logging.getLogger("nemubot.server." + self.name) + self.logger = logging.getLogger("nemubot.server." + str(self.name)) + self._readbuffer = b'' self._sending_queue = queue.Queue() - if send_callback is not None: - self._send_callback = send_callback - else: - self._send_callback = self._write_select @property @@ -54,40 +50,28 @@ class AbstractServer(io.IOBase): # Open/close - def __enter__(self): - self.open() - return self + def connect(self, *args, **kwargs): + """Register the server in _poll""" + + self.logger.info("Opening connection") + + super().connect(*args, **kwargs) + + self._on_connect() + + def _on_connect(self): + sync_act("sckt", "register", self.fileno()) - def __exit__(self, type, value, traceback): - self.close() + def close(self, *args, **kwargs): + """Unregister the server from _poll""" + self.logger.info("Closing connection") - def open(self): - """Generic open function that register the server un _rlist in case - of successful _open""" - self.logger.info("Opening connection to %s", self.id) - if not hasattr(self, "_open") or self._open(): - _rlist.append(self) - _xlist.append(self) - return True - return False + if self.fileno() > 0: + sync_act("sckt", "unregister", self.fileno()) - - def close(self): - """Generic close function that register the server un _{r,w,x}list in - case of successful _close""" - self.logger.info("Closing connection to %s", self.id) - with _lock: - if not hasattr(self, "_close") or self._close(): - if self in _rlist: - _rlist.remove(self) - if self in _wlist: - _wlist.remove(self) - if self in _xlist: - _xlist.remove(self) - return True - return False + super().close(*args, **kwargs) # Writes @@ -99,13 +83,16 @@ class AbstractServer(io.IOBase): message -- message to send """ - self._send_callback(message) + self._sending_queue.put(self.format(message)) + self.logger.debug("Message '%s' appended to write queue", message) + sync_act("sckt", "write", self.fileno()) - def write_select(self): - """Internal function used by the select function""" + def async_write(self): + """Internal function used when the file descriptor is writable""" + try: - _wlist.remove(self) + sync_act("sckt", "unwrite", self.fileno()) while not self._sending_queue.empty(): self._write(self._sending_queue.get_nowait()) self._sending_queue.task_done() @@ -114,19 +101,6 @@ class AbstractServer(io.IOBase): pass - def _write_select(self, message): - """Send a message to the server safely through select - - Argument: - message -- message to send - """ - - self._sending_queue.put(self.format(message)) - self.logger.debug("Message '%s' appended to write queue", message) - if self not in _wlist: - _wlist.append(self) - - def send_response(self, response): """Send a formated Message class @@ -149,13 +123,39 @@ class AbstractServer(io.IOBase): # Read + def async_read(self): + """Internal function used when the file descriptor is readable + + Returns: + A list of fully received messages + """ + + ret, self._readbuffer = self.lex(self._readbuffer + self.read()) + + for r in ret: + yield r + + + def lex(self, buf): + """Assume lexing in default case is per line + + Argument: + buf -- buffer to lex + """ + + msgs = buf.split(b'\r\n') + partial = msgs.pop() + + return msgs, partial + + def parse(self, msg): raise NotImplemented # Exceptions - def exception(self): - """Exception occurs in fd""" - self.logger.warning("Unhandle file descriptor exception on server %s", - self.name) + def exception(self, flags): + """Exception occurs on fd""" + + self.close() diff --git a/nemubot/server/factory_test.py b/nemubot/server/factory_test.py index cc7d35b..358591e 100644 --- a/nemubot/server/factory_test.py +++ b/nemubot/server/factory_test.py @@ -22,34 +22,30 @@ class TestFactory(unittest.TestCase): def test_IRC1(self): from nemubot.server.IRC import IRC as IRCServer + from nemubot.server.IRC import IRC_secure as IRCSServer # <host>: If omitted, the client must connect to a prespecified default IRC server. server = factory("irc:///") self.assertIsInstance(server, IRCServer) self.assertEqual(server.host, "localhost") - self.assertFalse(server.ssl) server = factory("ircs:///") - self.assertIsInstance(server, IRCServer) + self.assertIsInstance(server, IRCSServer) self.assertEqual(server.host, "localhost") - self.assertTrue(server.ssl) server = factory("irc://host1") self.assertIsInstance(server, IRCServer) self.assertEqual(server.host, "host1") - self.assertFalse(server.ssl) server = factory("irc://host2:6667") self.assertIsInstance(server, IRCServer) self.assertEqual(server.host, "host2") self.assertEqual(server.port, 6667) - self.assertFalse(server.ssl) server = factory("ircs://host3:194/") - self.assertIsInstance(server, IRCServer) + self.assertIsInstance(server, IRCSServer) self.assertEqual(server.host, "host3") self.assertEqual(server.port, 194) - self.assertTrue(server.ssl) if __name__ == '__main__': diff --git a/nemubot/server/message/__init__.py b/nemubot/server/message/__init__.py new file mode 100644 index 0000000..57f3468 --- /dev/null +++ b/nemubot/server/message/__init__.py @@ -0,0 +1,15 @@ +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2015 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/>. diff --git a/nemubot/server/socket.py b/nemubot/server/socket.py index 13ac9bd..1137e36 100644 --- a/nemubot/server/socket.py +++ b/nemubot/server/socket.py @@ -1,5 +1,5 @@ # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 Mercier Pierre-Olivier +# Copyright (C) 2012-2016 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 @@ -14,117 +14,33 @@ # 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/>. +import os +import socket +import ssl + import nemubot.message as message from nemubot.message.printer.socket import Socket as SocketPrinter from nemubot.server.abstract import AbstractServer -class SocketServer(AbstractServer): +class _Socket(AbstractServer): - """Concrete implementation of a socket connexion (can be wrapped with TLS)""" + """Concrete implementation of a socket connection""" - def __init__(self, sock_location=None, - host=None, port=None, - sock=None, - ssl=False, - name=None): + def __init__(self, printer=SocketPrinter, **kwargs): """Create a server socket - - Keyword arguments: - sock_location -- Path to the UNIX socket - host -- Hostname of the INET socket - port -- Port of the INET socket - sock -- Already connected socket - ssl -- Should TLS connection enabled - name -- Convinience name """ - import socket - - assert(sock is None or isinstance(sock, socket.SocketType)) - assert(port is None or isinstance(port, int)) - - super().__init__(name=name) - - if sock is None: - if sock_location is not None: - self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - self.connect_to = sock_location - elif host is not None: - for af, socktype, proto, canonname, sa in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM): - self.socket = socket.socket(af, socktype, proto) - self.connect_to = sa - break - else: - self.socket = sock - - self.ssl = ssl + super().__init__(**kwargs) self.readbuffer = b'' - self.printer = SocketPrinter - - - def fileno(self): - return self.socket.fileno() if self.socket else None - - - @property - def closed(self): - """Indicator of the connection aliveness""" - return self.socket._closed - - - # Open/close - - def open(self): - if not self.closed: - return True - - try: - self.socket.connect(self.connect_to) - self.logger.info("Connected to %s", self.connect_to) - except: - self.socket.close() - self.logger.exception("Unable to connect to %s", - self.connect_to) - return False - - # Wrap the socket for SSL - if self.ssl: - import ssl - ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1) - self.socket = ctx.wrap_socket(self.socket) - - return super().open() - - - def close(self): - import socket - - # Flush the sending queue before close - from nemubot.server import _lock - _lock.release() - self._sending_queue.join() - _lock.acquire() - - if not self.closed: - try: - self.socket.shutdown(socket.SHUT_RDWR) - except socket.error: - pass - - self.socket.close() - - return super().close() + self.printer = printer # Write def _write(self, cnt): - if self.closed: - return - - self.socket.sendall(cnt) + self.sendall(cnt) def format(self, txt): @@ -136,19 +52,12 @@ class SocketServer(AbstractServer): # Read - def read(self): - if self.closed: - return [] - - raw = self.socket.recv(1024) - temp = (self.readbuffer + raw).split(b'\r\n') - self.readbuffer = temp.pop() - - for line in temp: - yield line + def recv(self, n=1024): + return super().recv(n) def parse(self, line): + """Implement a default behaviour for socket""" import shlex line = line.strip().decode() @@ -157,48 +66,97 @@ class SocketServer(AbstractServer): except ValueError: args = line.split(' ') - yield message.Command(cmd=args[0], args=args[1:], server=self.name, to=["you"], frm="you") + if len(args): + yield message.Command(cmd=args[0], args=args[1:], server=self.fileno(), to=["you"], frm="you") -class SocketListener(AbstractServer): +class _SocketServer(_Socket): - def __init__(self, new_server_cb, name, sock_location=None, host=None, port=None, ssl=None): - super().__init__(name=name) - self.new_server_cb = new_server_cb - self.sock_location = sock_location - self.host = host - self.port = port - self.ssl = ssl - self.nb_son = 0 + def __init__(self, host, port, bind=None, **kwargs): + super().__init__(family=socket.AF_INET, **kwargs) + assert(host is not None) + assert(isinstance(port, int)) - def fileno(self): - return self.socket.fileno() if self.socket else None + self._host = host + self._port = port + self._bind = bind @property - def closed(self): - """Indicator of the connection aliveness""" - return self.socket is None + def host(self): + return self._host - def open(self): - import os - import socket + def connect(self): + self.logger.info("Connection to %s:%d", self._host, self._port) + super().connect((self._host, self._port)) - if self.sock_location is not None: - self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - try: - os.remove(self.sock_location) - except FileNotFoundError: - pass - self.socket.bind(self.sock_location) - elif self.host is not None and self.port is not None: - self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.socket.bind((self.host, self.port)) - self.socket.listen(5) + if self._bind: + super().bind(self._bind) - return super().open() + +class SocketServer(_SocketServer, socket.socket): + pass + + +class SecureSocketServer(_SocketServer, ssl.SSLSocket): + pass + + +class UnixSocket: + + def __init__(self, location, **kwargs): + super().__init__(family=socket.AF_UNIX, **kwargs) + + self._socket_path = location + + + def connect(self): + self.logger.info("Connection to unix://%s", self._socket_path) + super().connect(self._socket_path) + + +class _Listener: + + def __init__(self, new_server_cb, instanciate=_Socket, **kwargs): + super().__init__(**kwargs) + + self._instanciate = instanciate + self._new_server_cb = new_server_cb + + + def read(self): + conn, addr = self.accept() + fileno = conn.fileno() + self.logger.info("Accept new connection from %s (fd=%d)", addr, fileno) + + ss = self._instanciate(name=self.name + "#" + str(fileno), fileno=conn.detach()) + ss.connect = ss._on_connect + self._new_server_cb(ss, autoconnect=True) + + return b'' + + +class UnixSocketListener(_Listener, UnixSocket, _Socket, socket.socket): + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + + def connect(self): + self.logger.info("Creating Unix socket at unix://%s", self._socket_path) + + try: + os.remove(self._socket_path) + except FileNotFoundError: + pass + + self.bind(self._socket_path) + self.listen(5) + self.logger.info("Socket ready for accepting new connections") + + self._on_connect() def close(self): @@ -206,25 +164,14 @@ class SocketListener(AbstractServer): import socket try: - self.socket.shutdown(socket.SHUT_RDWR) - self.socket.close() - if self.sock_location is not None: - os.remove(self.sock_location) + self.shutdown(socket.SHUT_RDWR) except socket.error: pass - return super().close() + super().close() - - # Read - - def read(self): - if self.closed: - return [] - - conn, addr = self.socket.accept() - self.nb_son += 1 - ss = SocketServer(name=self.name + "#" + str(self.nb_son), socket=conn) - self.new_server_cb(ss) - - return [] + try: + if self._socket_path is not None: + os.remove(self._socket_path) + except: + pass From 97a1385903ce4768dd10d124146c92b58702cef8 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 8 Jul 2016 22:38:25 +0200 Subject: [PATCH 133/271] Implement socket server subparse --- nemubot/server/socket.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/nemubot/server/socket.py b/nemubot/server/socket.py index 1137e36..72c0c7b 100644 --- a/nemubot/server/socket.py +++ b/nemubot/server/socket.py @@ -70,6 +70,14 @@ class _Socket(AbstractServer): yield message.Command(cmd=args[0], args=args[1:], server=self.fileno(), to=["you"], frm="you") + def subparse(self, orig, cnt): + for m in self.parse(cnt): + m.to = orig.to + m.frm = orig.frm + m.date = orig.date + yield m + + class _SocketServer(_Socket): def __init__(self, host, port, bind=None, **kwargs): From cf8e1cffc5bf1891f0d608c48ab3627ac89b6afd Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 23 Oct 2016 21:33:58 +0200 Subject: [PATCH 134/271] Format and typo --- nemubot/__main__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/nemubot/__main__.py b/nemubot/__main__.py index c39dd2f..2eda441 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -71,8 +71,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 +96,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) @@ -201,5 +201,6 @@ def main(): sigusr1handler(0, None) sys.exit(0) + if __name__ == "__main__": main() From f4216af7c72e131cd3f8b1af1e15d47e1684c33f Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 23 Oct 2016 21:35:37 +0200 Subject: [PATCH 135/271] Parse server urls using parse_qs --- nemubot/server/__init__.py | 39 +++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) 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 ] From 8de31d784b7f6957a10634540ba321509010aae4 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Tue, 18 Oct 2016 01:34:46 +0200 Subject: [PATCH 136/271] Allow module function to be generators --- nemubot/treatment.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/nemubot/treatment.py b/nemubot/treatment.py index 2c1955d..884de4a 100644 --- a/nemubot/treatment.py +++ b/nemubot/treatment.py @@ -15,6 +15,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import logging +import types logger = logging.getLogger("nemubot.treatment") @@ -116,10 +117,18 @@ class MessageTreater: yield r elif res is not None: - if not hasattr(res, "server") or res.server is None: - res.server = msg.server + if isinstance(res, types.GeneratorType): + for r in res: + if not hasattr(r, "server") or r.server is None: + r.server = msg.server - yield res + yield r + + else: + if not hasattr(res, "server") or res.server is None: + res.server = msg.server + + yield res hook = next(hook_gen, None) From dbcc7c664f5073058075cd3931d3654f95b9dd69 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Tue, 18 Oct 2016 01:21:02 +0200 Subject: [PATCH 137/271] [nextstop] Use as system wide module --- .gitmodules | 3 -- modules/nextstop.py | 74 ++++++++++++++++++++++++++++++++++++ modules/nextstop/__init__.py | 55 --------------------------- modules/nextstop/external | 1 - 4 files changed, 74 insertions(+), 59 deletions(-) delete mode 100644 .gitmodules create mode 100644 modules/nextstop.py delete mode 100644 modules/nextstop/__init__.py delete mode 160000 modules/nextstop/external diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 23cf4a0..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "modules/nextstop/external"] - path = modules/nextstop/external - url = git://github.com/nbr23/NextStop.git diff --git a/modules/nextstop.py b/modules/nextstop.py new file mode 100644 index 0000000..7f4b211 --- /dev/null +++ b/modules/nextstop.py @@ -0,0 +1,74 @@ +"""Informe les usagers des prochains passages des transports en communs de la RATP""" + +# PYTHON STUFFS ####################################################### + +from nemubot.exception import IMException +from nemubot.hooks import hook +from more import Response + +from nextstop import ratp + +@hook.command("ratp", + help="Affiche les prochains horaires de passage", + help_usage={ + "TRANSPORT": "Affiche les lignes du moyen de transport donné", + "TRANSPORT LINE": "Affiche les stations sur la ligne de transport donnée", + "TRANSPORT LINE STATION": "Affiche les prochains horaires de passage à l'arrêt donné", + "TRANSPORT LINE STATION DESTINATION": "Affiche les prochains horaires de passage dans la direction donnée", + }) +def ask_ratp(msg): + l = len(msg.args) + + transport = msg.args[0] if l > 0 else None + line = msg.args[1] if l > 1 else None + station = msg.args[2] if l > 2 else None + direction = msg.args[3] if l > 3 else None + + if station is not None: + times = sorted(ratp.getNextStopsAtStation(transport, line, station, direction), key=lambda i: i[0]) + + if len(times) == 0: + raise IMException("la station %s n'existe pas sur le %s ligne %s." % (station, transport, line)) + + (time, direction, stationname) = times[0] + return Response(message=["\x03\x02%s\x03\x02 direction %s" % (time, direction) for time, direction, stationname in times], + title="Prochains passages du %s ligne %s à l'arrêt %s" % (transport, line, stationname), + channel=msg.channel) + + elif line is not None: + stations = ratp.getAllStations(transport, line) + + if len(stations) == 0: + raise IMException("aucune station trouvée.") + return Response(stations, title="Stations", channel=msg.channel) + + elif transport is not None: + lines = ratp.getTransportLines(transport) + if len(lines) == 0: + raise IMException("aucune ligne trouvée.") + return Response(lines, title="Lignes", channel=msg.channel) + + else: + raise IMException("précise au moins un moyen de transport.") + + +@hook.command("ratp_alert", + help="Affiche les perturbations en cours sur le réseau") +def ratp_alert(msg): + if len(msg.args) == 0: + raise IMException("précise au moins un moyen de transport.") + + l = len(msg.args) + transport = msg.args[0] if l > 0 else None + line = msg.args[1] if l > 1 else None + + if line is not None: + d = ratp.getDisturbanceFromLine(transport, line) + if "date" in d and d["date"] is not None: + incidents = "Au {date[date]}, {title}: {message}".format(**d) + else: + incidents = "{title}: {message}".format(**d) + else: + incidents = ratp.getDisturbance(None, transport) + + return Response(incidents, channel=msg.channel, nomore="No more incidents", count=" (%d more incidents)") diff --git a/modules/nextstop/__init__.py b/modules/nextstop/__init__.py deleted file mode 100644 index 9530ab8..0000000 --- a/modules/nextstop/__init__.py +++ /dev/null @@ -1,55 +0,0 @@ -# coding=utf-8 - -"""Informe les usagers des prochains passages des transports en communs de la RATP""" - -from nemubot.exception import IMException -from nemubot.hooks import hook -from more import Response - -nemubotversion = 3.4 - -from .external.src import ratp - -def help_full (): - return "!ratp transport line [station]: Donne des informations sur les prochains passages du transport en commun séléctionné à l'arrêt désiré. Si aucune station n'est précisée, les liste toutes." - - -@hook.command("ratp") -def ask_ratp(msg): - """Hook entry from !ratp""" - if len(msg.args) >= 3: - transport = msg.args[0] - line = msg.args[1] - station = msg.args[2] - if len(msg.args) == 4: - times = ratp.getNextStopsAtStation(transport, line, station, msg.args[3]) - else: - times = ratp.getNextStopsAtStation(transport, line, station) - - if len(times) == 0: - raise IMException("la station %s n'existe pas sur le %s ligne %s." % (station, transport, line)) - - (time, direction, stationname) = times[0] - return Response(message=["\x03\x02%s\x03\x02 direction %s" % (time, direction) for time, direction, stationname in times], - title="Prochains passages du %s ligne %s à l'arrêt %s" % (transport, line, stationname), - channel=msg.channel) - - elif len(msg.args) == 2: - stations = ratp.getAllStations(msg.args[0], msg.args[1]) - - if len(stations) == 0: - raise IMException("aucune station trouvée.") - return Response([s for s in stations], title="Stations", channel=msg.channel) - - else: - raise IMException("Mauvais usage, merci de spécifier un type de transport et une ligne, ou de consulter l'aide du module.") - -@hook.command("ratp_alert") -def ratp_alert(msg): - if len(msg.args) == 2: - transport = msg.args[0] - cause = msg.args[1] - incidents = ratp.getDisturbance(cause, transport) - return Response(incidents, channel=msg.channel, nomore="No more incidents", count=" (%d more incidents)") - else: - raise IMException("Mauvais usage, merci de spécifier un type de transport et un type d'alerte (alerte, manif, travaux), ou de consulter l'aide du module.") diff --git a/modules/nextstop/external b/modules/nextstop/external deleted file mode 160000 index 3d5c9b2..0000000 --- a/modules/nextstop/external +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3d5c9b2d52fbd214f5aaad00e5f3952de919b3e5 From b809451be29d4c734045181098907c466e100249 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Tue, 18 Oct 2016 01:45:39 +0200 Subject: [PATCH 138/271] Avoid stack-trace and DOS if event is not well formed --- nemubot/bot.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/nemubot/bot.py b/nemubot/bot.py index c8ede40..42f9aa7 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -394,7 +394,13 @@ class Bot(threading.Thread): self.event_timer.cancel() if len(self.events): - remaining = self.events[0].time_left.total_seconds() + try: + remaining = self.events[0].time_left.total_seconds() + except: + logger.exception("An error occurs during event time calculation:") + self.events.pop(0) + return self._update_event_timer() + 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() From 7791f24423f45a225ff9267b7c2a79a803bdc664 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Mon, 3 Jul 2017 07:19:01 +0200 Subject: [PATCH 139/271] modulecontext: use inheritance instead of conditional init --- nemubot/__init__.py | 4 +- nemubot/bot.py | 4 +- nemubot/modulecontext.py | 183 +++++++++++++++++++++------------------ 3 files changed, 101 insertions(+), 90 deletions(-) diff --git a/nemubot/__init__.py b/nemubot/__init__.py index a56c472..42a2fba 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -17,9 +17,9 @@ __version__ = '4.0.dev3' __author__ = 'nemunaire' -from nemubot.modulecontext import ModuleContext +from nemubot.modulecontext import _ModuleContext -context = ModuleContext(None, None) +context = _ModuleContext() def requires_version(min=None, max=None): diff --git a/nemubot/bot.py b/nemubot/bot.py index 42f9aa7..3553ecd 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -478,7 +478,7 @@ class Bot(threading.Thread): module.print = prnt # Create module context - from nemubot.modulecontext import ModuleContext + from nemubot.modulecontext import _ModuleContext, ModuleContext module.__nemubot_context__ = ModuleContext(self, module) if not hasattr(module, "logger"): @@ -486,7 +486,7 @@ class Bot(threading.Thread): # Replace imported context by real one for attr in module.__dict__: - if attr != "__nemubot_context__" and type(module.__dict__[attr]) == ModuleContext: + if attr != "__nemubot_context__" and type(module.__dict__[attr]) == _ModuleContext: module.__dict__[attr] = module.__nemubot_context__ # Register decorated functions diff --git a/nemubot/modulecontext.py b/nemubot/modulecontext.py index 1d1b3d0..877b8de 100644 --- a/nemubot/modulecontext.py +++ b/nemubot/modulecontext.py @@ -1,5 +1,5 @@ # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 Mercier Pierre-Olivier +# Copyright (C) 2012-2017 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 @@ -14,105 +14,61 @@ # 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/>. -class ModuleContext: +class _ModuleContext: - def __init__(self, context, module): - """Initialize the module context - - arguments: - context -- the bot context - module -- the module - """ + def __init__(self, module=None): + self.module = module if module is not None: - module_name = module.__spec__.name if hasattr(module, "__spec__") else module.__name__ + self.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): - self.config = context.modules_configuration[module_name] - else: - from nemubot.config.module import Module - self.config = Module(module_name) + self.module_name = "" self.hooks = list() self.events = list() - self.debug = context.verbosity > 0 if context is not None else False + self.debug = False + from nemubot.config.module import Module + self.config = Module(self.module_name) + + + def load_data(self): + from nemubot.tools.xmlparser import module_state + return module_state.ModuleState("nemubotstate") + + def add_hook(self, hook, *triggers): from nemubot.hooks import Abstract as AbstractHook + assert isinstance(hook, AbstractHook), hook + self.hooks.append((triggers, hook)) - # Define some callbacks - if context is not None: - def load_data(): - return context.datastore.load(module_name) + def del_hook(self, hook, *triggers): + from nemubot.hooks import Abstract as AbstractHook + assert isinstance(hook, AbstractHook), hook + self.hooks.remove((triggers, hook)) - def add_hook(hook, *triggers): - assert isinstance(hook, AbstractHook), hook - self.hooks.append((triggers, hook)) - return context.treater.hm.add_hook(hook, *triggers) + def subtreat(self, msg): + return None - def del_hook(hook, *triggers): - assert isinstance(hook, AbstractHook), hook - self.hooks.remove((triggers, hook)) - return context.treater.hm.del_hooks(*triggers, hook=hook) + def add_event(self, evt, eid=None): + return self.events.append((evt, eid)) - 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 del_event(evt): - return context.del_event(evt, module_src=module) + def del_event(self, evt): + for i in self.events: + e, eid = i + if e == evt: + self.events.remove(i) + return True + return False - def send_response(server, res): - if server in context.servers: - if res.server is not None: - return context.servers[res.server].send_response(res) - else: - return context.servers[server].send_response(res) - else: - module.logger.error("Try to send a message to the unknown server: %s", server) - return False + def send_response(self, server, res): + self.module.logger.info("Send response: %s", res) - else: # Used when using outside of nemubot - def load_data(): - from nemubot.tools.xmlparser import module_state - return module_state.ModuleState("nemubotstate") - - def add_hook(hook, *triggers): - assert isinstance(hook, AbstractHook), hook - self.hooks.append((triggers, hook)) - def del_hook(hook, *triggers): - assert isinstance(hook, AbstractHook), hook - 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 del_event(evt): - return context.del_event(evt, module_src=module) - - def send_response(server, res): - module.logger.info("Send response: %s", res) - - def save(): - context.datastore.save(module_name, self.data) - - def subparse(orig, cnt): - if orig.server in context.servers: - return context.servers[orig.server].subparse(orig, cnt) - - self.load_data = load_data - self.add_hook = add_hook - self.del_hook = del_hook - self.add_event = add_event - self.del_event = del_event - self.save = save - self.send_response = send_response - self.subtreat = subtreat - self.subparse = subparse + def save(self): + self.context.datastore.save(self.module_name, self.data) + def subparse(self, orig, cnt): + if orig.server in self.context.servers: + return self.context.servers[orig.server].subparse(orig, cnt) @property def data(self): @@ -129,7 +85,62 @@ class ModuleContext: self.del_hook(h, *s) # Remove registered events - for e in self.events: - self.del_event(e) + for evt, eid, module_src in self.events: + self.del_event(evt) self.save() + + +class ModuleContext(_ModuleContext): + + def __init__(self, context, *args, **kwargs): + """Initialize the module context + + arguments: + context -- the bot context + module -- the module + """ + + super().__init__(*args, **kwargs) + + # Load module configuration if exists + if self.module_name in context.modules_configuration: + self.config = context.modules_configuration[self.module_name] + + self.context = context + self.debug = context.verbosity > 0 + + + def load_data(self): + return self.context.datastore.load(self.module_name) + + def add_hook(self, hook, *triggers): + from nemubot.hooks import Abstract as AbstractHook + assert isinstance(hook, AbstractHook), hook + self.hooks.append((triggers, hook)) + return self.context.treater.hm.add_hook(hook, *triggers) + + def del_hook(self, hook, *triggers): + from nemubot.hooks import Abstract as AbstractHook + assert isinstance(hook, AbstractHook), hook + self.hooks.remove((triggers, hook)) + return self.context.treater.hm.del_hooks(*triggers, hook=hook) + + def subtreat(self, msg): + yield from self.context.treater.treat_msg(msg) + + def add_event(self, evt, eid=None): + return self.context.add_event(evt, eid, module_src=self.module) + + def del_event(self, evt): + return self.context.del_event(evt, module_src=self.module) + + def send_response(self, server, res): + if server in self.context.servers: + if res.server is not None: + return self.context.servers[res.server].send_response(res) + else: + return self.context.servers[server].send_response(res) + else: + self.module.logger.error("Try to send a message to the unknown server: %s", server) + return False From 8a96f7bee978f7bb95f3202d1695ba03bd2247c1 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Thu, 10 Nov 2016 18:32:50 +0100 Subject: [PATCH 140/271] Update weather module: refleting forcastAPI changes --- modules/weather.py | 78 +++++++++++++++++----------------------------- 1 file changed, 28 insertions(+), 50 deletions(-) diff --git a/modules/weather.py b/modules/weather.py index 1fadc71..8b3540e 100644 --- a/modules/weather.py +++ b/modules/weather.py @@ -1,6 +1,6 @@ # coding=utf-8 -"""The weather module""" +"""The weather module. Powered by Dark Sky <https://darksky.net/poweredby/>""" import datetime import re @@ -17,7 +17,7 @@ nemubotversion = 4.0 from more import Response -URL_DSAPI = "https://api.forecast.io/forecast/%s/%%s,%%s" +URL_DSAPI = "https://api.darksky.net/forecast/%s/%%s,%%s?lang=%%s&units=%%s" def load(context): if not context.config or "darkskyapikey" not in context.config: @@ -30,52 +30,14 @@ def load(context): URL_DSAPI = URL_DSAPI % context.config["darkskyapikey"] -def help_full (): - return "!weather /city/: Display the current weather in /city/." - - -def fahrenheit2celsius(temp): - return int((temp - 32) * 50/9)/10 - - -def mph2kmph(speed): - return int(speed * 160.9344)/100 - - -def inh2mmh(size): - return int(size * 254)/10 - - def format_wth(wth): - return ("%s °C %s; precipitation (%s %% chance) intensity: %s mm/h; relative humidity: %s %%; wind speed: %s km/h %s°; cloud coverage: %s %%; pressure: %s hPa; ozone: %s DU" % - ( - fahrenheit2celsius(wth["temperature"]), - wth["summary"], - int(wth["precipProbability"] * 100), - inh2mmh(wth["precipIntensity"]), - int(wth["humidity"] * 100), - mph2kmph(wth["windSpeed"]), - wth["windBearing"], - int(wth["cloudCover"] * 100), - int(wth["pressure"]), - int(wth["ozone"]) - )) + return ("{temperature} °C {summary}; precipitation ({precipProbability:.0%} chance) intensity: {precipIntensity} mm/h; relative humidity: {humidity:.0%}; wind speed: {windSpeed} km/s {windBearing}°; cloud coverage: {cloudCover:.0%}; pressure: {pressure} hPa; ozone: {ozone} DU" + .format(**wth) + ) def format_forecast_daily(wth): - return ("%s; between %s-%s °C; precipitation (%s %% chance) intensity: maximum %s mm/h; relative humidity: %s %%; wind speed: %s km/h %s°; cloud coverage: %s %%; pressure: %s hPa; ozone: %s DU" % - ( - wth["summary"], - fahrenheit2celsius(wth["temperatureMin"]), fahrenheit2celsius(wth["temperatureMax"]), - int(wth["precipProbability"] * 100), - inh2mmh(wth["precipIntensityMax"]), - int(wth["humidity"] * 100), - mph2kmph(wth["windSpeed"]), - wth["windBearing"], - int(wth["cloudCover"] * 100), - int(wth["pressure"]), - int(wth["ozone"]) - )) + return ("{summary}; between {temperatureMin}-{temperatureMax} °C; precipitation ({precipProbability:.0%} chance) intensity: maximum {precipIntensity} mm/h; relative humidity: {humidity:.0%}; wind speed: {windSpeed} km/h {windBearing}°; cloud coverage: {cloudCover:.0%}; pressure: {pressure} hPa; ozone: {ozone} DU".format(**wth)) def format_timestamp(timestamp, tzname, tzoffset, format="%c"): @@ -126,8 +88,8 @@ def treat_coord(msg): raise IMException("indique-moi un nom de ville ou des coordonnées.") -def get_json_weather(coords): - wth = web.getJSON(URL_DSAPI % (float(coords[0]), float(coords[1]))) +def get_json_weather(coords, lang="en", units="auto"): + wth = web.getJSON(URL_DSAPI % (float(coords[0]), float(coords[1]), lang, units)) # First read flags if wth is None or "darksky-unavailable" in wth["flags"]: @@ -149,10 +111,16 @@ def cmd_coordinates(msg): return Response("Les coordonnées de %s sont %s,%s" % (msg.args[0], coords["lat"], coords["long"]), channel=msg.channel) -@hook.command("alert") +@hook.command("alert", + keywords={ + "lang=LANG": "change the output language of weather sumarry; default: en", + "units=UNITS": "return weather conditions in the requested units; default: auto", + }) def cmd_alert(msg): loc, coords, specific = treat_coord(msg) - wth = get_json_weather(coords) + wth = get_json_weather(coords, + lang=msg.kwargs["lang"] if "lang" in msg.kwargs else "en", + units=msg.kwargs["units"] if "units" in msg.kwargs else "auto") res = Response(channel=msg.channel, nomore="No more weather alert", count=" (%d more alerts)") @@ -166,10 +134,20 @@ def cmd_alert(msg): return res -@hook.command("météo") +@hook.command("météo", + help="Display current weather and previsions", + help_usage={ + "CITY": "Display the current weather and previsions in CITY", + }, + keywords={ + "lang=LANG": "change the output language of weather sumarry; default: en", + "units=UNITS": "return weather conditions in the requested units; default: auto", + }) def cmd_weather(msg): loc, coords, specific = treat_coord(msg) - wth = get_json_weather(coords) + wth = get_json_weather(coords, + lang=msg.kwargs["lang"] if "lang" in msg.kwargs else "en", + units=msg.kwargs["units"] if "units" in msg.kwargs else "auto") res = Response(channel=msg.channel, nomore="No more weather information") From 6ac9fc48572d9705a1a5a0400e6d8b279dc12454 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Tue, 4 Jul 2017 06:53:34 +0200 Subject: [PATCH 141/271] tools/web: forward all arguments passed to getJSON and getXML to getURLContent --- nemubot/tools/web.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/nemubot/tools/web.py b/nemubot/tools/web.py index d35740c..dc967be 100644 --- a/nemubot/tools/web.py +++ b/nemubot/tools/web.py @@ -162,15 +162,13 @@ def getURLContent(url, body=None, timeout=7, header=None): (res.status, http.client.responses[res.status])) -def getXML(url, timeout=7): +def getXML(*args, **kwargs): """Get content page and return XML parsed content - Arguments: - url -- the URL to get - timeout -- maximum number of seconds to wait before returning an exception + Arguments: same as getURLContent """ - cnt = getURLContent(url, timeout=timeout) + cnt = getURLContent(*args, **kwargs) if cnt is None: return None else: @@ -178,15 +176,13 @@ def getXML(url, timeout=7): return parseString(cnt) -def getJSON(url, timeout=7): +def getJSON(*args, **kwargs): """Get content page and return JSON content - Arguments: - url -- the URL to get - timeout -- maximum number of seconds to wait before returning an exception + Arguments: same as getURLContent """ - cnt = getURLContent(url, timeout=timeout) + cnt = getURLContent(*args, **kwargs) if cnt is None: return None else: From b4218478bdb632fe9536425b3cca9060ed33ff44 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Tue, 4 Jul 2017 07:26:37 +0200 Subject: [PATCH 142/271] tools/web: improve redirection reliability --- nemubot/tools/web.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/nemubot/tools/web.py b/nemubot/tools/web.py index dc967be..fc37391 100644 --- a/nemubot/tools/web.py +++ b/nemubot/tools/web.py @@ -14,7 +14,7 @@ # 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 urllib.parse import urlparse, urlsplit, urlunsplit +from urllib.parse import urljoin, urlparse, urlsplit, urlunsplit from nemubot.exception import IMException @@ -156,7 +156,11 @@ def getURLContent(url, body=None, timeout=7, header=None): elif ((res.status == http.client.FOUND or res.status == http.client.MOVED_PERMANENTLY) and res.getheader("Location") != url): - return getURLContent(res.getheader("Location"), timeout=timeout) + return getURLContent( + urljoin(url, res.getheader("Location")), + body=body, + timeout=timeout, + header=header) else: raise IMException("A HTTP error occurs: %d - %s" % (res.status, http.client.responses[res.status])) From 0be6ebcd4bfda88eaab4fe899a1216d4038cc79c Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Tue, 4 Jul 2017 07:27:44 +0200 Subject: [PATCH 143/271] tools/web: fill a default Content-Type in case of POST --- nemubot/tools/web.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nemubot/tools/web.py b/nemubot/tools/web.py index fc37391..0852664 100644 --- a/nemubot/tools/web.py +++ b/nemubot/tools/web.py @@ -108,6 +108,9 @@ def getURLContent(url, body=None, timeout=7, header=None): elif "User-agent" not in header: header["User-agent"] = "Nemubot v%s" % __version__ + if body is not None and "Content-Type" not in header: + header["Content-Type"] = "application/x-www-form-urlencoded" + import socket try: if o.query != '': From bcd57e61ea070ecc29fa81e1acc18ee949fdc26c Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Fri, 7 Jul 2017 06:38:00 +0200 Subject: [PATCH 144/271] suivi: use getURLContent instead of call to urllib --- modules/suivi.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/modules/suivi.py b/modules/suivi.py index 79910d4..f62bd84 100644 --- a/modules/suivi.py +++ b/modules/suivi.py @@ -17,8 +17,7 @@ from more import Response def get_tnt_info(track_id): values = [] - data = getURLContent('www.tnt.fr/public/suivi_colis/recherche/' - 'visubontransport.do?bonTransport=%s' % track_id) + data = getURLContent('www.tnt.fr/public/suivi_colis/recherche/visubontransport.do?bonTransport=%s' % track_id) soup = BeautifulSoup(data) status_list = soup.find('div', class_='result__content') if not status_list: @@ -32,8 +31,7 @@ def get_tnt_info(track_id): def get_colissimo_info(colissimo_id): - colissimo_data = getURLContent("http://www.colissimo.fr/portail_colissimo/" - "suivre.do?colispart=%s" % colissimo_id) + colissimo_data = getURLContent("http://www.colissimo.fr/portail_colissimo/suivre.do?colispart=%s" % colissimo_id) soup = BeautifulSoup(colissimo_data) dataArray = soup.find(class_='dataArray') @@ -47,9 +45,8 @@ def get_colissimo_info(colissimo_id): def get_chronopost_info(track_id): data = urllib.parse.urlencode({'listeNumeros': track_id}) - track_baseurl = "http://www.chronopost.fr/expedier/" \ - "inputLTNumbersNoJahia.do?lang=fr_FR" - track_data = urllib.request.urlopen(track_baseurl, data.encode('utf-8')) + track_baseurl = "http://www.chronopost.fr/expedier/inputLTNumbersNoJahia.do?lang=fr_FR" + track_data = getURLContent(track_baseurl, data.encode('utf-8')) soup = BeautifulSoup(track_data) infoClass = soup.find(class_='numeroColi2') @@ -65,9 +62,8 @@ def get_chronopost_info(track_id): def get_colisprive_info(track_id): data = urllib.parse.urlencode({'numColis': track_id}) - track_baseurl = "https://www.colisprive.com/moncolis/pages/" \ - "detailColis.aspx" - track_data = urllib.request.urlopen(track_baseurl, data.encode('utf-8')) + track_baseurl = "https://www.colisprive.com/moncolis/pages/detailColis.aspx" + track_data = getURLContent(track_baseurl, data.encode('utf-8')) soup = BeautifulSoup(track_data) dataArray = soup.find(class_='BandeauInfoColis') @@ -82,8 +78,7 @@ def get_laposte_info(laposte_id): data = urllib.parse.urlencode({'id': laposte_id}) laposte_baseurl = "http://www.part.csuivi.courrier.laposte.fr/suivi/index" - laposte_data = urllib.request.urlopen(laposte_baseurl, - data.encode('utf-8')) + laposte_data = getURLContent(laposte_baseurl, data.encode('utf-8')) soup = BeautifulSoup(laposte_data) search_res = soup.find(class_='resultat_rech_simple_table').tbody.tr if (soup.find(class_='resultat_rech_simple_table').thead @@ -112,8 +107,7 @@ def get_postnl_info(postnl_id): data = urllib.parse.urlencode({'barcodes': postnl_id}) postnl_baseurl = "http://www.postnl.post/details/" - postnl_data = urllib.request.urlopen(postnl_baseurl, - data.encode('utf-8')) + postnl_data = getURLContent(postnl_baseurl, data.encode('utf-8')) soup = BeautifulSoup(postnl_data) if (soup.find(id='datatables') and soup.find(id='datatables').tbody From 58c349eb2c8f4a3bdeeccc7b8de7b615b535bfe0 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Sat, 8 Jul 2017 14:38:24 +0200 Subject: [PATCH 145/271] suivi: add fedex --- modules/suivi.py | 51 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/modules/suivi.py b/modules/suivi.py index f62bd84..a6f6ab4 100644 --- a/modules/suivi.py +++ b/modules/suivi.py @@ -2,14 +2,14 @@ # PYTHON STUFF ############################################ -import urllib.request +import json import urllib.parse from bs4 import BeautifulSoup import re from nemubot.hooks import hook from nemubot.exception import IMException -from nemubot.tools.web import getURLContent +from nemubot.tools.web import getURLContent, getJSON from more import Response @@ -126,6 +126,41 @@ def get_postnl_info(postnl_id): return (post_status.lower(), post_destination, post_date) +def get_fedex_info(fedex_id, lang="en_US"): + data = urllib.parse.urlencode({ + 'data': json.dumps({ + "TrackPackagesRequest": { + "appType": "WTRK", + "appDeviceType": "DESKTOP", + "uniqueKey": "", + "processingParameters": {}, + "trackingInfoList": [ + { + "trackNumberInfo": { + "trackingNumber": str(fedex_id), + "trackingQualifier": "", + "trackingCarrier": "" + } + } + ] + } + }), + 'action': "trackpackages", + 'locale': lang, + 'version': 1, + 'format': "json" + }) + fedex_baseurl = "https://www.fedex.com/trackingCal/track" + + fedex_data = getJSON(fedex_baseurl, data.encode('utf-8')) + + if ("TrackPackagesResponse" in fedex_data and + "packageList" in fedex_data["TrackPackagesResponse"] and + len(fedex_data["TrackPackagesResponse"]["packageList"]) + ): + return fedex_data["TrackPackagesResponse"]["packageList"][0] + + # TRACKING HANDLERS ################################################### def handle_tnt(tracknum): @@ -183,6 +218,17 @@ def handle_coliprive(tracknum): return ("Colis Privé: \x02%s\x0F : \x02%s\x0F." % (tracknum, info)) +def handle_fedex(tracknum): + info = get_fedex_info(tracknum) + if info: + if info["displayActDeliveryDateTime"] != "": + return ("{trackingCarrierDesc}: \x02{statusWithDetails}\x0F: in \x02{statusLocationCity}, {statusLocationCntryCD}\x0F, delivered on: {displayActDeliveryDateTime}.".format(**info)) + elif info["statusLocationCity"] != "": + return ("{trackingCarrierDesc}: \x02{statusWithDetails}\x0F: estimated delivery: {displayEstDeliveryDateTime}.".format(**info)) + else: + return ("{trackingCarrierDesc}: \x02{statusWithDetails}\x0F: in \x02{statusLocationCity}, {statusLocationCntryCD}\x0F, estimated delivery: {displayEstDeliveryDateTime}.".format(**info)) + + TRACKING_HANDLERS = { 'laposte': handle_laposte, 'postnl': handle_postnl, @@ -190,6 +236,7 @@ TRACKING_HANDLERS = { 'chronopost': handle_chronopost, 'coliprive': handle_coliprive, 'tnt': handle_tnt, + 'fedex': handle_fedex, } From 35e0890563d1034653f3720eb1027075cc799968 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 23 Jun 2017 20:07:22 +0200 Subject: [PATCH 146/271] Handle multiple SIGTERM --- nemubot/bot.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/nemubot/bot.py b/nemubot/bot.py index 3553ecd..7ec3b30 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -149,6 +149,8 @@ class Bot(threading.Thread): def run(self): + global sync_queue + self._poll.register(sync_queue._reader, select.POLLIN | select.POLLPRI) logger.info("Starting main loop") @@ -222,6 +224,7 @@ class Bot(threading.Thread): c = Consumer(self) self.cnsr_thrd.append(c) c.start() + sync_queue = None logger.info("Ending main loop") @@ -566,9 +569,10 @@ class Bot(threading.Thread): self.datastore.close() - self.stop = True - sync_act("end") - sync_queue.join() + if self.stop is False or sync_queue is not None: + self.stop = True + sync_act("end") + sync_queue.join() # Treatment From ac0cf729f135cea48f9ddc843391c8f09f5c5d87 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 23 Jun 2017 20:41:57 +0200 Subject: [PATCH 147/271] Fix communication over unix socket --- nemubot/server/socket.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/nemubot/server/socket.py b/nemubot/server/socket.py index 72c0c7b..84b1f4f 100644 --- a/nemubot/server/socket.py +++ b/nemubot/server/socket.py @@ -125,9 +125,15 @@ class UnixSocket: super().connect(self._socket_path) +class SocketClient(_Socket, socket.socket): + + def read(self): + return self.recv() + + class _Listener: - def __init__(self, new_server_cb, instanciate=_Socket, **kwargs): + def __init__(self, new_server_cb, instanciate=SocketClient, **kwargs): super().__init__(**kwargs) self._instanciate = instanciate From cde4ee05f7474223638fc98d83f043c8a0e6e866 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 23 Jun 2017 21:20:32 +0200 Subject: [PATCH 148/271] Local client now detects when server close the connection --- nemubot/__init__.py | 63 ++++++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/nemubot/__init__.py b/nemubot/__init__.py index 42a2fba..82be366 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -53,41 +53,50 @@ def attach(pid, socketfile): sys.stderr.write("\n") return 1 - from select import select + import select + mypoll = select.poll() + + mypoll.register(sys.stdin.fileno(), select.POLLIN | select.POLLPRI) + mypoll.register(sock.fileno(), select.POLLIN | select.POLLPRI) try: while True: - rl, wl, xl = select([sys.stdin, sock], [], []) + for fd, flag in mypoll.poll(): + if flag & (select.POLLERR | select.POLLHUP | select.POLLNVAL): + sock.close() + print("Connection closed.") + return 1 - if sys.stdin in rl: - line = sys.stdin.readline().strip() - if line == "exit" or line == "quit": - return 0 - elif line == "reload": - import os, signal - os.kill(pid, signal.SIGHUP) - print("Reload signal sent. Please wait...") + if fd == sys.stdin.fileno(): + line = sys.stdin.readline().strip() + if line == "exit" or line == "quit": + return 0 + elif line == "reload": + import os, signal + os.kill(pid, signal.SIGHUP) + print("Reload signal sent. Please wait...") - elif line == "shutdown": - import os, signal - os.kill(pid, signal.SIGTERM) - print("Shutdown signal sent. Please wait...") + elif line == "shutdown": + import os, signal + os.kill(pid, signal.SIGTERM) + print("Shutdown signal sent. Please wait...") - elif line == "kill": - import os, signal - os.kill(pid, signal.SIGKILL) - print("Signal sent...") - return 0 + elif line == "kill": + import os, signal + os.kill(pid, signal.SIGKILL) + print("Signal sent...") + return 0 - elif line == "stack" or line == "stacks": - import os, signal - os.kill(pid, signal.SIGUSR1) - print("Debug signal sent. Consult logs.") + elif line == "stack" or line == "stacks": + import os, signal + os.kill(pid, signal.SIGUSR1) + print("Debug signal sent. Consult logs.") - else: - sock.send(line.encode() + b'\r\n') + else: + sock.send(line.encode() + b'\r\n') + + if fd == sock.fileno(): + sys.stdout.write(sock.recv(2048).decode()) - if sock in rl: - sys.stdout.write(sock.recv(2048).decode()) except KeyboardInterrupt: pass except: From 9d446cbd14863125a209459eabd02e9ef3aadad4 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 23 Jun 2017 21:22:12 +0200 Subject: [PATCH 149/271] Deamonize later --- nemubot/__main__.py | 56 ++++++++++++++++++++++----------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 2eda441..9dea209 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -1,5 +1,5 @@ # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2016 Mercier Pierre-Olivier +# Copyright (C) 2012-2017 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 @@ -74,28 +74,6 @@ def main(): 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): - with open(args.pidfile, "r") as f: - pid = int(f.readline()) - try: - os.kill(pid, 0) - except OSError: - pass - else: - from nemubot import attach - sys.exit(attach(pid, args.socketfile)) - - # Daemonize - if not args.debug: - from nemubot import daemonize - daemonize() - - # Store PID to pidfile - if args.pidfile is not None: - with open(args.pidfile, "w+") as f: - f.write(str(os.getpid())) - # Setup logging interface import logging logger = logging.getLogger("nemubot") @@ -115,6 +93,18 @@ def main(): fh.setFormatter(formatter) logger.addHandler(fh) + # Check if an instance is already launched + if args.pidfile is not None and os.path.isfile(args.pidfile): + with open(args.pidfile, "r") as f: + pid = int(f.readline()) + try: + os.kill(pid, 0) + except OSError: + pass + else: + from nemubot import attach + sys.exit(attach(pid, args.socketfile)) + # Add modules dir paths modules_paths = list() for path in args.modules_path: @@ -149,6 +139,17 @@ def main(): for module in args.module: __import__(module) + 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")) + + # Daemonize + if not args.debug: + from nemubot import daemonize + daemonize() + # Signals handling def sigtermhandler(signum, frame): """On SIGTERM and SIGINT, quit nicely""" @@ -182,11 +183,10 @@ def main(): "".join(traceback.format_stack(stack))) signal.signal(signal.SIGUSR1, sigusr1handler) - 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")) + # Store PID to pidfile + if args.pidfile is not None: + with open(args.pidfile, "w+") as f: + f.write(str(os.getpid())) # context can change when performing an hotswap, always join the latest context oldcontext = None From e8809b77d29129389a12b561085cef2b7b930f49 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 23 Jun 2017 22:15:26 +0200 Subject: [PATCH 150/271] When launched in daemon mode, attach to the socket --- nemubot/__init__.py | 14 +++++++++++++- nemubot/__main__.py | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/nemubot/__init__.py b/nemubot/__init__.py index 82be366..4b14c07 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -106,13 +106,25 @@ def attach(pid, socketfile): return 0 -def daemonize(): +def daemonize(socketfile=None): """Detach the running process to run as a daemon """ import os import sys + if socketfile is not None: + try: + pid = os.fork() + if pid > 0: + import time + os.waitpid(pid, 0) + time.sleep(1) + sys.exit(attach(pid, socketfile)) + except OSError as err: + sys.stderr.write("Unable to fork: %s\n" % err) + sys.exit(1) + try: pid = os.fork() if pid > 0: diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 9dea209..fa9d3ba 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -148,7 +148,7 @@ def main(): # Daemonize if not args.debug: from nemubot import daemonize - daemonize() + daemonize(args.socketfile) # Signals handling def sigtermhandler(signum, frame): From b6945cf81c2343f6d50241d716c6ed7fb262c188 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Thu, 10 Nov 2016 18:36:10 +0100 Subject: [PATCH 151/271] Try to restaure frm_owner flag --- nemubot/message/abstract.py | 7 ++++--- nemubot/server/message/IRC.py | 3 ++- nemubot/treatment.py | 3 +++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/nemubot/message/abstract.py b/nemubot/message/abstract.py index 5d74549..6ee43d5 100644 --- a/nemubot/message/abstract.py +++ b/nemubot/message/abstract.py @@ -21,7 +21,7 @@ class Abstract: """This class represents an abstract message""" - def __init__(self, server=None, date=None, to=None, to_response=None, frm=None): + def __init__(self, server=None, date=None, to=None, to_response=None, frm=None, frm_owner=False): """Initialize an abstract message Arguments: @@ -40,7 +40,7 @@ class Abstract: else [ to_response ]) self.frm = frm # None allowed when it designate this bot - self.frm_owner = False # Filled later, in consumer + self.frm_owner = frm_owner @property @@ -78,7 +78,8 @@ class Abstract: "date": self.date, "to": self.to, "to_response": self._to_response, - "frm": self.frm + "frm": self.frm, + "frm_owner": self.frm_owner, } for w in without: diff --git a/nemubot/server/message/IRC.py b/nemubot/server/message/IRC.py index 4c9e280..5ccd735 100644 --- a/nemubot/server/message/IRC.py +++ b/nemubot/server/message/IRC.py @@ -150,7 +150,8 @@ class IRC(Abstract): "date": self.tags["time"], "to": receivers, "to_response": [r if r != srv.nick else self.nick for r in receivers], - "frm": self.nick + "frm": self.nick, + "frm_owner": self.nick == srv.owner } # If CTCP, remove 0x01 diff --git a/nemubot/treatment.py b/nemubot/treatment.py index 884de4a..4f629e0 100644 --- a/nemubot/treatment.py +++ b/nemubot/treatment.py @@ -109,6 +109,9 @@ class MessageTreater: msg -- message to treat """ + if hasattr(msg, "frm_owner"): + msg.frm_owner = (not hasattr(msg.server, "owner") or msg.server.owner == msg.frm) + while hook is not None: res = hook.run(msg) From 2265e1a09657e84d95d44ee78c516660f191ed10 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 10 Aug 2016 23:56:50 +0200 Subject: [PATCH 152/271] Use getaddrinfo to create the right socket --- nemubot/server/IRC.py | 3 ++- nemubot/server/socket.py | 17 +++++------------ 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/nemubot/server/IRC.py b/nemubot/server/IRC.py index 89eeab5..7469abc 100644 --- a/nemubot/server/IRC.py +++ b/nemubot/server/IRC.py @@ -16,6 +16,7 @@ from datetime import datetime import re +import socket from nemubot.channel import Channel from nemubot.message.printer.IRC import IRC as IRCPrinter @@ -240,7 +241,7 @@ class _IRC: if self.capabilities is not None: self.write("CAP LS") self.write("NICK :" + self.nick) - self.write("USER %s %s bla :%s" % (self.username, self.host, self.realname)) + self.write("USER %s %s bla :%s" % (self.username, socket.getfqdn(), self.realname)) def close(self): diff --git a/nemubot/server/socket.py b/nemubot/server/socket.py index 84b1f4f..2510833 100644 --- a/nemubot/server/socket.py +++ b/nemubot/server/socket.py @@ -81,24 +81,17 @@ class _Socket(AbstractServer): class _SocketServer(_Socket): def __init__(self, host, port, bind=None, **kwargs): - super().__init__(family=socket.AF_INET, **kwargs) + (family, type, proto, canonname, sockaddr) = socket.getaddrinfo(host, port)[0] - assert(host is not None) - assert(isinstance(port, int)) + super().__init__(family=family, type=type, proto=proto, **kwargs) - self._host = host - self._port = port + self._sockaddr = sockaddr self._bind = bind - @property - def host(self): - return self._host - - def connect(self): - self.logger.info("Connection to %s:%d", self._host, self._port) - super().connect((self._host, self._port)) + self.logger.info("Connection to %s:%d", *self._sockaddr[:2]) + super().connect(self._sockaddr) if self._bind: super().bind(self._bind) From 67cb3caa95af528f424237cb75fca1bcf8625ea8 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 15 Jul 2017 10:53:30 +0200 Subject: [PATCH 153/271] main: new option -A to run as daemon --- nemubot/__init__.py | 13 ++++++++----- nemubot/__main__.py | 5 ++++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/nemubot/__init__.py b/nemubot/__init__.py index 4b14c07..48de6ea 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -106,7 +106,7 @@ def attach(pid, socketfile): return 0 -def daemonize(socketfile=None): +def daemonize(socketfile=None, autoattach=True): """Detach the running process to run as a daemon """ @@ -117,10 +117,13 @@ def daemonize(socketfile=None): try: pid = os.fork() if pid > 0: - import time - os.waitpid(pid, 0) - time.sleep(1) - sys.exit(attach(pid, socketfile)) + if autoattach: + import time + os.waitpid(pid, 0) + time.sleep(1) + sys.exit(attach(pid, socketfile)) + else: + sys.exit(0) except OSError as err: sys.stderr.write("Unable to fork: %s\n" % err) sys.exit(1) diff --git a/nemubot/__main__.py b/nemubot/__main__.py index fa9d3ba..e1576fb 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -37,6 +37,9 @@ def main(): default=["./modules/"], help="directory to use as modules store") + parser.add_argument("-A", "--no-attach", action="store_true", + help="don't attach after fork") + parser.add_argument("-d", "--debug", action="store_true", help="don't deamonize, keep in foreground") @@ -148,7 +151,7 @@ def main(): # Daemonize if not args.debug: from nemubot import daemonize - daemonize(args.socketfile) + daemonize(args.socketfile, not args.no_attach) # Signals handling def sigtermhandler(signum, frame): From 0a3744577d401d316e54bf8318bc615397024a64 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 16 Jul 2017 18:17:15 +0200 Subject: [PATCH 154/271] rename module nextstop: ratp to avoid import loop with the inderlying Python module --- modules/nextstop.xml | 4 ---- modules/{nextstop.py => ratp.py} | 0 2 files changed, 4 deletions(-) delete mode 100644 modules/nextstop.xml rename modules/{nextstop.py => ratp.py} (100%) diff --git a/modules/nextstop.xml b/modules/nextstop.xml deleted file mode 100644 index d34e8ae..0000000 --- a/modules/nextstop.xml +++ /dev/null @@ -1,4 +0,0 @@ -<?xml version="1.0" ?> -<nemubotmodule name="nextstop"> - <message type="cmd" name="ratp" call="ask_ratp" /> -</nemubotmodule> diff --git a/modules/nextstop.py b/modules/ratp.py similarity index 100% rename from modules/nextstop.py rename to modules/ratp.py From a5479d7b0d1ca458b39c25f3615e6822a6600264 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 16 Jul 2017 18:39:56 +0200 Subject: [PATCH 155/271] event: ensure that enough consumers are launched at the end of an event --- nemubot/bot.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nemubot/bot.py b/nemubot/bot.py index 7ec3b30..ed46d48 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -207,6 +207,9 @@ class Bot(threading.Thread): elif action == "exit": self.quit() + elif action == "launch_consumer": + pass # This is treated after the loop + elif action == "loadconf": for path in args: logger.debug("Load configuration from %s", path) @@ -418,6 +421,7 @@ class Bot(threading.Thread): 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)) + sync_act("launch_consumer") self._update_event_timer() From 94ff951b2e95b8c3cffb5b1555b3beb1b2e96212 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 16 Jul 2017 21:15:10 +0200 Subject: [PATCH 156/271] run: recreate the sync_queue on run, it seems to have strange behaviour when created before the fork --- nemubot/bot.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/nemubot/bot.py b/nemubot/bot.py index ed46d48..b0d3915 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -151,6 +151,11 @@ class Bot(threading.Thread): def run(self): global sync_queue + # Rewrite the sync_queue, as the daemonization process tend to disturb it + old_sync_queue, sync_queue = sync_queue, JoinableQueue() + while not old_sync_queue.empty(): + sync_queue.put_nowait(old_sync_queue.get()) + self._poll.register(sync_queue._reader, select.POLLIN | select.POLLPRI) logger.info("Starting main loop") @@ -190,6 +195,8 @@ class Bot(threading.Thread): args = sync_queue.get() action = args.pop(0) + logger.debug("Executing sync_queue action %s%s", action, args) + if action == "sckt" and len(args) >= 2: try: if args[0] == "write": From bbfecdfced2c640112f300cb3774181cf77b47a8 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 15 Jul 2017 23:30:50 +0200 Subject: [PATCH 157/271] events: fix help when no event is defined --- modules/events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/events.py b/modules/events.py index 2887514..a35c28b 100644 --- a/modules/events.py +++ b/modules/events.py @@ -16,7 +16,7 @@ from more import Response def help_full (): - return "This module store a lot of events: ny, we, " + (", ".join(context.datas.index.keys())) + "\n!eventslist: gets list of timer\n!start /something/: launch a timer" + return "This module store a lot of events: ny, we, " + (", ".join(context.datas.index.keys() if hasattr(context, "datas") else [])) + "\n!eventslist: gets list of timer\n!start /something/: launch a timer" def load(context): From f633a3effed4add27feeac78732d2a672f5bdf70 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Mon, 17 Jul 2017 07:53:36 +0200 Subject: [PATCH 158/271] socket: limit getaddrinfo to TCP connections --- nemubot/server/socket.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nemubot/server/socket.py b/nemubot/server/socket.py index 2510833..8a0950c 100644 --- a/nemubot/server/socket.py +++ b/nemubot/server/socket.py @@ -81,7 +81,7 @@ class _Socket(AbstractServer): class _SocketServer(_Socket): def __init__(self, host, port, bind=None, **kwargs): - (family, type, proto, canonname, sockaddr) = socket.getaddrinfo(host, port)[0] + (family, type, proto, canonname, sockaddr) = socket.getaddrinfo(host, port, proto=socket.IPPROTO_TCP)[0] super().__init__(family=family, type=type, proto=proto, **kwargs) From aad777058ee8600b12f1e835e4b8db30dc2f7d52 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Fri, 21 Jul 2017 07:26:00 +0200 Subject: [PATCH 159/271] cve: update and clean module, following NIST website changes --- modules/cve.py | 47 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/modules/cve.py b/modules/cve.py index 23a0302..c470e29 100644 --- a/modules/cve.py +++ b/modules/cve.py @@ -10,7 +10,7 @@ from nemubot.tools.web import getURLContent, striphtml from more import Response -BASEURL_NIST = 'https://web.nvd.nist.gov/view/vuln/detail?vulnId=' +BASEURL_NIST = 'https://nvd.nist.gov/vuln/detail/' # MODULE CORE ######################################################### @@ -19,15 +19,40 @@ def get_cve(cve_id): search_url = BASEURL_NIST + quote(cve_id.upper()) soup = BeautifulSoup(getURLContent(search_url)) - vuln = soup.body.find(class_="vuln-detail") - cvss = vuln.findAll('div')[4] - return [ - "Base score: " + cvss.findAll('div')[0].findAll('a')[0].text.strip(), - vuln.findAll('p')[0].text, # description - striphtml(vuln.findAll('div')[0].text).strip(), # publication date - striphtml(vuln.findAll('div')[1].text).strip(), # last revised - ] + return { + "description": soup.body.find(attrs={"data-testid":"vuln-description"}).text.strip(), + "published": soup.body.find(attrs={"data-testid":"vuln-published-on"}).text.strip(), + "last_modified": soup.body.find(attrs={"data-testid":"vuln-last-modified-on"}).text.strip(), + "source": soup.body.find(attrs={"data-testid":"vuln-source"}).text.strip(), + + "base_score": float(soup.body.find(attrs={"data-testid":"vuln-cvssv3-base-score-link"}).text.strip()), + "severity": soup.body.find(attrs={"data-testid":"vuln-cvssv3-base-score-severity"}).text.strip(), + "impact_score": float(soup.body.find(attrs={"data-testid":"vuln-cvssv3-impact-score"}).text.strip()), + "exploitability_score": float(soup.body.find(attrs={"data-testid":"vuln-cvssv3-exploitability-score"}).text.strip()), + + "av": soup.body.find(attrs={"data-testid":"vuln-cvssv3-av"}).text.strip(), + "ac": soup.body.find(attrs={"data-testid":"vuln-cvssv3-ac"}).text.strip(), + "pr": soup.body.find(attrs={"data-testid":"vuln-cvssv3-pr"}).text.strip(), + "ui": soup.body.find(attrs={"data-testid":"vuln-cvssv3-ui"}).text.strip(), + "s": soup.body.find(attrs={"data-testid":"vuln-cvssv3-s"}).text.strip(), + "c": soup.body.find(attrs={"data-testid":"vuln-cvssv3-c"}).text.strip(), + "i": soup.body.find(attrs={"data-testid":"vuln-cvssv3-i"}).text.strip(), + "a": soup.body.find(attrs={"data-testid":"vuln-cvssv3-a"}).text.strip(), + } + + +def display_metrics(av, ac, pr, ui, s, c, i, a, **kwargs): + ret = [] + if av != "None": ret.append("Attack Vector: \x02%s\x0F" % av) + if ac != "None": ret.append("Attack Complexity: \x02%s\x0F" % ac) + if pr != "None": ret.append("Privileges Required: \x02%s\x0F" % pr) + if ui != "None": ret.append("User Interaction: \x02%s\x0F" % ui) + if s != "Unchanged": ret.append("Scope: \x02%s\x0F" % s) + if c != "None": ret.append("Confidentiality: \x02%s\x0F" % c) + if i != "None": ret.append("Integrity: \x02%s\x0F" % i) + if a != "None": ret.append("Availability: \x02%s\x0F" % a) + return ', '.join(ret) # MODULE INTERFACE #################################################### @@ -42,6 +67,8 @@ def get_cve_desc(msg): if cve_id[:3].lower() != 'cve': cve_id = 'cve-' + cve_id - res.append_message(get_cve(cve_id)) + cve = get_cve(cve_id) + metrics = display_metrics(**cve) + res.append_message("{cveid}: Base score: \x02{base_score} {severity}\x0F (impact: \x02{impact_score}\x0F, exploitability: \x02{exploitability_score}\x0F; {metrics}), from \x02{source}\x0F, last modified on \x02{last_modified}\x0F. {description}".format(cveid=cve_id, metrics=metrics, **cve)) return res From 3267c3e2e15df50bceb14390bd198cb7fc4f9fcd Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Sat, 22 Jul 2017 10:49:38 +0200 Subject: [PATCH 160/271] tools/web: display socket timeout --- nemubot/tools/web.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nemubot/tools/web.py b/nemubot/tools/web.py index 0852664..9ced693 100644 --- a/nemubot/tools/web.py +++ b/nemubot/tools/web.py @@ -15,6 +15,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. from urllib.parse import urljoin, urlparse, urlsplit, urlunsplit +import socket from nemubot.exception import IMException @@ -123,6 +124,8 @@ def getURLContent(url, body=None, timeout=7, header=None): o.path, body, header) + except socket.timeout as e: + raise IMException(e) except OSError as e: raise IMException(e.strerror) From 171297b5810a863ed02754850afd2d8a76cd2c04 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Sat, 22 Jul 2017 10:53:08 +0200 Subject: [PATCH 161/271] tools/web: new option decode_error to decode non-200 page content (useful on REST API) --- nemubot/tools/web.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/nemubot/tools/web.py b/nemubot/tools/web.py index 9ced693..0394aac 100644 --- a/nemubot/tools/web.py +++ b/nemubot/tools/web.py @@ -68,13 +68,14 @@ def getPassword(url): # Get real pages -def getURLContent(url, body=None, timeout=7, header=None): +def getURLContent(url, body=None, timeout=7, header=None, decode_error=False): """Return page content corresponding to URL or None if any error occurs Arguments: url -- the URL to get body -- Data to send as POST content timeout -- maximum number of seconds to wait before returning an exception + decode_error -- raise exception on non-200 pages or ignore it """ o = urlparse(_getNormalizedURL(url), "http") @@ -166,7 +167,10 @@ def getURLContent(url, body=None, timeout=7, header=None): urljoin(url, res.getheader("Location")), body=body, timeout=timeout, - header=header) + header=header, + decode_error=decode_error) + elif decode_error: + return data.decode(charset).strip() else: raise IMException("A HTTP error occurs: %d - %s" % (res.status, http.client.responses[res.status])) From f16dedb320b12634bbadc202a191727d90a5c30c Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Wed, 26 Jul 2017 07:51:35 +0200 Subject: [PATCH 162/271] openroute: new module providing geocode and direction instructions Closing issue #46 --- modules/openroute.py | 158 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 modules/openroute.py diff --git a/modules/openroute.py b/modules/openroute.py new file mode 100644 index 0000000..440b05a --- /dev/null +++ b/modules/openroute.py @@ -0,0 +1,158 @@ +"""Lost? use our commands to find your way!""" + +# PYTHON STUFFS ####################################################### + +import re +import urllib.parse + +from nemubot.exception import IMException +from nemubot.hooks import hook +from nemubot.tools import web + +from more import Response + +# GLOBALS ############################################################# + +URL_DIRECTIONS_API = "https://api.openrouteservice.org/directions?api_key=%s&" +URL_GEOCODE_API = "https://api.openrouteservice.org/geocoding?api_key=%s&" + +waytype = [ + "unknown", + "state road", + "road", + "street", + "path", + "track", + "cycleway", + "footway", + "steps", + "ferry", + "construction", +] + + +# LOADING ############################################################# + +def load(context): + if not context.config or "apikey" not in context.config: + raise ImportError("You need an OpenRouteService API key in order to use this " + "module. Add it to the module configuration file:\n" + "<module name=\"ors\" apikey=\"XXXXXXXXXXXXXXXX\" " + "/>\nRegister at https://developers.openrouteservice.org") + global URL_DIRECTIONS_API + URL_DIRECTIONS_API = URL_DIRECTIONS_API % context.config["apikey"] + global URL_GEOCODE_API + URL_GEOCODE_API = URL_GEOCODE_API % context.config["apikey"] + + +# MODULE CORE ######################################################### + +def approx_distance(lng): + if lng > 1111: + return "%f km" % (lng / 1000) + else: + return "%f m" % lng + + +def approx_duration(sec): + days = int(sec / 86400) + if days > 0: + return "%d days %f hours" % (days, (sec % 86400) / 3600) + hours = int((sec % 86400) / 3600) + if hours > 0: + return "%d hours %f minutes" % (hours, (sec % 3600) / 60) + minutes = (sec % 3600) / 60 + if minutes > 0: + return "%d minutes" % minutes + else: + return "%d seconds" % sec + + +def geocode(query, limit=7): + obj = web.getJSON(URL_GEOCODE_API + urllib.parse.urlencode({ + 'query': query, + 'limit': limit, + })) + + for f in obj["features"]: + yield f["geometry"]["coordinates"], f["properties"] + + +def firstgeocode(query): + for g in geocode(query, limit=1): + return g + + +def where(loc): + return "{name} {city} {state} {county} {country}".format(**loc) + + +def directions(coordinates, **kwargs): + kwargs['coordinates'] = '|'.join(coordinates) + + print(URL_DIRECTIONS_API + urllib.parse.urlencode(kwargs)) + return web.getJSON(URL_DIRECTIONS_API + urllib.parse.urlencode(kwargs), decode_error=True) + + +# MODULE INTERFACE #################################################### + +@hook.command("geocode", + help="Get GPS coordinates of a place", + help_usage={ + "PLACE": "Get GPS coordinates of PLACE" + }) +def cmd_geocode(msg): + res = Response(channel=msg.channel, nick=msg.frm, + nomore="No more geocode", count=" (%s more geocode)") + + for loc in geocode(' '.join(msg.args)): + res.append_message("%s is at %s,%s" % ( + where(loc[1]), + loc[0][1], loc[0][0], + )) + + return res + + +@hook.command("directions", + help="Get routing instructions", + help_usage={ + "POINT1 POINT2 ...": "Get routing instructions to go from POINT1 to the last POINTX via intermediates POINTX" + }, + keywords={ + "profile=PROF": "One of driving-car, driving-hgv, cycling-regular, cycling-road, cycling-safe, cycling-mountain, cycling-tour, cycling-electric, foot-walking, foot-hiking, wheelchair. Default: foot-walking", + "preference=PREF": "One of fastest, shortest, recommended. Default: recommended", + "lang=LANG": "default: en", + }) +def cmd_directions(msg): + drcts = directions(["{0},{1}".format(*firstgeocode(g)[0]) for g in msg.args], + profile=msg.kwargs["profile"] if "profile" in msg.kwargs else "foot-walking", + preference=msg.kwargs["preference"] if "preference" in msg.kwargs else "recommended", + units="m", + language=msg.kwargs["lang"] if "lang" in msg.kwargs else "en", + geometry=False, + instructions=True, + instruction_format="text") + if "error" in drcts and "message" in drcts["error"] and drcts["error"]["message"]: + raise IMException(drcts["error"]["message"]) + + if "routes" not in drcts or not drcts["routes"]: + raise IMException("No route available for this trip") + + myway = drcts["routes"][0] + myway["summary"]["strduration"] = approx_duration(myway["summary"]["duration"]) + myway["summary"]["strdistance"] = approx_distance(myway["summary"]["distance"]) + res = Response("Trip summary: {strdistance} in approximate {strduration}; elevation +{ascent} m -{descent} m".format(**myway["summary"]), channel=msg.channel, count=" (%d more steps)", nomore="You have arrived!") + + def formatSegments(segments): + for segment in segments: + for step in segment["steps"]: + step["strtype"] = waytype[step["type"]] + step["strduration"] = approx_duration(step["duration"]) + step["strdistance"] = approx_distance(step["distance"]) + yield "{instruction} for {strdistance} on {strtype} (approximate time: {strduration})".format(**step) + + if "segments" in myway: + res.append_message([m for m in formatSegments(myway["segments"])]) + + return res From 39056cf3584cc352a8dcbd472d30bac4e76d673c Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Thu, 27 Jul 2017 20:44:26 +0200 Subject: [PATCH 163/271] tools/xmlparser: implement writer --- nemubot/tools/test_xmlparser.py | 36 +++++++++++++++++++++++--- nemubot/tools/xmlparser/__init__.py | 15 +++++++++++ nemubot/tools/xmlparser/basic.py | 20 ++++++++++++++ nemubot/tools/xmlparser/genericnode.py | 8 ++++++ 4 files changed, 76 insertions(+), 3 deletions(-) diff --git a/nemubot/tools/test_xmlparser.py b/nemubot/tools/test_xmlparser.py index d7f5a9a..0feda73 100644 --- a/nemubot/tools/test_xmlparser.py +++ b/nemubot/tools/test_xmlparser.py @@ -1,5 +1,6 @@ import unittest +import io import xml.parsers.expat from nemubot.tools.xmlparser import XMLParser @@ -12,6 +13,11 @@ class StringNode(): def characters(self, content): self.string += content + def saveElement(self, store, tag="string"): + store.startElement(tag, {}) + store.characters(self.string) + store.endElement(tag) + class TestNode(): def __init__(self, option=None): @@ -22,6 +28,15 @@ class TestNode(): self.mystr = child.string return True + def saveElement(self, store, tag="test"): + store.startElement(tag, {"option": self.option}) + + strNode = StringNode() + strNode.string = self.mystr + strNode.saveElement(store) + + store.endElement(tag) + class Test2Node(): def __init__(self, option=None): @@ -33,6 +48,15 @@ class Test2Node(): self.mystrs.append(attrs["value"]) return True + def saveElement(self, store, tag="test"): + store.startElement(tag, {"option": self.option} if self.option is not None else {}) + + for mystr in self.mystrs: + store.startElement("string", {"value": mystr}) + store.endElement("string") + + store.endElement(tag) + class TestXMLParser(unittest.TestCase): @@ -44,9 +68,11 @@ class TestXMLParser(unittest.TestCase): p.CharacterDataHandler = mod.characters p.EndElementHandler = mod.endElement - p.Parse("<string>toto</string>", 1) + inputstr = "<string>toto</string>" + p.Parse(inputstr, 1) self.assertEqual(mod.root.string, "toto") + self.assertEqual(mod.saveDocument(header=False).getvalue(), inputstr) def test_parser2(self): @@ -57,10 +83,12 @@ class TestXMLParser(unittest.TestCase): p.CharacterDataHandler = mod.characters p.EndElementHandler = mod.endElement - p.Parse("<test option='123'><string>toto</string></test>", 1) + inputstr = '<test option="123"><string>toto</string></test>' + p.Parse(inputstr, 1) self.assertEqual(mod.root.option, "123") self.assertEqual(mod.root.mystr, "toto") + self.assertEqual(mod.saveDocument(header=False).getvalue(), inputstr) def test_parser3(self): @@ -71,12 +99,14 @@ class TestXMLParser(unittest.TestCase): p.CharacterDataHandler = mod.characters p.EndElementHandler = mod.endElement - p.Parse("<test><string value='toto' /><string value='toto2' /></test>", 1) + inputstr = '<test><string value="toto"/><string value="toto2"/></test>' + p.Parse(inputstr, 1) self.assertEqual(mod.root.option, None) self.assertEqual(len(mod.root.mystrs), 2) self.assertEqual(mod.root.mystrs[0], "toto") self.assertEqual(mod.root.mystrs[1], "toto2") + self.assertEqual(mod.saveDocument(header=False, short_empty_elements=True).getvalue(), inputstr) if __name__ == '__main__': diff --git a/nemubot/tools/xmlparser/__init__.py b/nemubot/tools/xmlparser/__init__.py index abc5bb9..c8d393a 100644 --- a/nemubot/tools/xmlparser/__init__.py +++ b/nemubot/tools/xmlparser/__init__.py @@ -134,6 +134,21 @@ class XMLParser: return raise TypeError(name + " tag not expected in " + self.display_stack()) + def saveDocument(self, f=None, header=True, short_empty_elements=False): + if f is None: + import io + f = io.StringIO() + + import xml.sax.saxutils + gen = xml.sax.saxutils.XMLGenerator(f, "utf-8", short_empty_elements=short_empty_elements) + if header: + gen.startDocument() + self.root.saveElement(gen) + if header: + gen.endDocument() + + return f + def parse_file(filename): p = xml.parsers.expat.ParserCreate() diff --git a/nemubot/tools/xmlparser/basic.py b/nemubot/tools/xmlparser/basic.py index 8456629..86eac3c 100644 --- a/nemubot/tools/xmlparser/basic.py +++ b/nemubot/tools/xmlparser/basic.py @@ -44,6 +44,13 @@ class ListNode: return self.items.__repr__() + def saveElement(self, store, tag="list"): + store.startElement(tag, {}) + for i in self.items: + i.saveElement(store) + store.endElement(tag) + + class DictNode: """XML node representing a Python dictionnnary @@ -106,3 +113,16 @@ class DictNode: def __repr__(self): return self.items.__repr__() + + + def saveElement(self, store, tag="dict"): + store.startElement(tag, {}) + for k, v in self.items.items(): + store.startElement("item", {"key": k}) + if isinstance(v, str): + store.characters(v) + else: + for i in v: + i.saveElement(store) + store.endElement("item") + store.endElement(tag) diff --git a/nemubot/tools/xmlparser/genericnode.py b/nemubot/tools/xmlparser/genericnode.py index 9c29a23..425934c 100644 --- a/nemubot/tools/xmlparser/genericnode.py +++ b/nemubot/tools/xmlparser/genericnode.py @@ -53,6 +53,14 @@ class ParsingNode: return item in self.attrs + def saveElement(self, store, tag=None): + store.startElement(tag if tag is not None else self.tag, self.attrs) + for child in self.children: + child.saveElement(store) + store.characters(self.content) + store.endElement(tag if tag is not None else self.tag) + + class GenericNode(ParsingNode): """Consider all subtags as dictionnary From e3b6c3b85ea552051b0ba839145dcfd67e8bafde Mon Sep 17 00:00:00 2001 From: Max <max@23.tf> Date: Sun, 30 Jul 2017 23:22:14 +0200 Subject: [PATCH 164/271] Set urlreducer to use https --- modules/urlreducer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/urlreducer.py b/modules/urlreducer.py index bd5dc9a..36fcb3c 100644 --- a/modules/urlreducer.py +++ b/modules/urlreducer.py @@ -21,7 +21,7 @@ def default_reducer(url, data): def ycc_reducer(url, data): - return "http://ycc.fr/%s" % default_reducer(url, data) + return "https://ycc.fr/%s" % default_reducer(url, data) def lstu_reducer(url, data): json_data = json.loads(web.getURLContent(url, "lsturl=" + quote(data), @@ -36,8 +36,8 @@ def lstu_reducer(url, data): # MODULE VARIABLES #################################################### PROVIDERS = { - "tinyurl": (default_reducer, "http://tinyurl.com/api-create.php?url="), - "ycc": (ycc_reducer, "http://ycc.fr/redirection/create/"), + "tinyurl": (default_reducer, "https://tinyurl.com/api-create.php?url="), + "ycc": (ycc_reducer, "https://ycc.fr/redirection/create/"), "framalink": (lstu_reducer, "https://frama.link/a?format=json"), "huitre": (lstu_reducer, "https://huit.re/a?format=json"), "lstu": (lstu_reducer, "https://lstu.fr/a?format=json"), From ce012b70170412c710ae00615e5a7d93d9812268 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Fri, 28 Jul 2017 06:55:17 +0200 Subject: [PATCH 165/271] datastore/xml: handle entire file save and be closer with new nemubot XML API --- nemubot/datastore/xml.py | 13 ++++++++++++- nemubot/tools/xmlparser/node.py | 24 ++---------------------- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/nemubot/datastore/xml.py b/nemubot/datastore/xml.py index 46dca70..025c0c5 100644 --- a/nemubot/datastore/xml.py +++ b/nemubot/datastore/xml.py @@ -143,4 +143,15 @@ class XML(Abstract): if self.rotate: self._rotate(path) - return data.save(path) + import tempfile + _, tmpath = tempfile.mkstemp() + with open(tmpath, "w") as f: + import xml.sax.saxutils + gen = xml.sax.saxutils.XMLGenerator(f, "utf-8") + gen.startDocument() + data.saveElement(gen) + gen.endDocument() + + # Atomic save + import shutil + shutil.move(tmpath, path) diff --git a/nemubot/tools/xmlparser/node.py b/nemubot/tools/xmlparser/node.py index 965a475..7df255e 100644 --- a/nemubot/tools/xmlparser/node.py +++ b/nemubot/tools/xmlparser/node.py @@ -196,7 +196,7 @@ class ModuleState: if self.index_fieldname is not None: self.setIndex(self.index_fieldname, self.index_tagname) - def save_node(self, gen): + def saveElement(self, gen): """Serialize this node as a XML node""" from datetime import datetime attribs = {} @@ -215,29 +215,9 @@ class ModuleState: gen.startElement(self.name, attrs) for child in self.childs: - child.save_node(gen) + child.saveElement(gen) gen.endElement(self.name) except: logger.exception("Error occured when saving the following " "XML node: %s with %s", self.name, attrs) - - def save(self, filename): - """Save the current node as root node in a XML file - - Argument: - filename -- location of the file to create/erase - """ - - import tempfile - _, tmpath = tempfile.mkstemp() - with open(tmpath, "w") as f: - import xml.sax.saxutils - gen = xml.sax.saxutils.XMLGenerator(f, "utf-8") - gen.startDocument() - self.save_node(gen) - gen.endDocument() - - # Atomic save - import shutil - shutil.move(tmpath, filename) From f81349bbfd48f8fd37b18b80274f8bac59bf9418 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Sat, 29 Jul 2017 15:22:57 +0200 Subject: [PATCH 166/271] Store module into weakref --- nemubot/bot.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/nemubot/bot.py b/nemubot/bot.py index b0d3915..febe7d6 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -20,6 +20,7 @@ from multiprocessing import JoinableQueue import threading import select import sys +import weakref from nemubot import __version__ from nemubot.consumer import Consumer, EventConsumer, MessageConsumer @@ -99,15 +100,15 @@ class Bot(threading.Thread): 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 msg.args[0] in self.modules and self.modules[msg.args[0]]() is not None: + 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]) + 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:])): @@ -137,7 +138,7 @@ class Bot(threading.Thread): 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__]) + message=["\x03\x02%s\x03\x02 (%s)" % (im, self.modules[im]().__doc__) for im in self.modules if self.modules[im]() is not None and self.modules[im]().__doc__]) return res self.treater.hm.add_hook(nemubot.hooks.Command(_help_msg, "help"), "in", "Command") @@ -518,18 +519,20 @@ class Bot(threading.Thread): raise # Save a reference to the module - self.modules[module_name] = module + self.modules[module_name] = weakref.ref(module) + logger.info("Module '%s' successfully loaded.", module_name) def unload_module(self, name): """Unload a module""" - if name in self.modules: - self.modules[name].print("Unloading module %s" % name) + if name in self.modules and self.modules[name]() is not None: + module = self.modules[name]() + module.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() + if hasattr(module, "unload"): + module.unload(self) + module.__nemubot_context__.unload() # Remove from the nemubot dict del self.modules[name] From b517cac4cfde69e81f8c7228089c56a8c3505348 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Sat, 29 Jul 2017 15:25:44 +0200 Subject: [PATCH 167/271] Fix module unloading --- nemubot/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nemubot/bot.py b/nemubot/bot.py index febe7d6..aa1cb3e 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -569,7 +569,7 @@ class Bot(threading.Thread): self.event_timer.cancel() logger.info("Save and unload all modules...") - for mod in self.modules.items(): + for mod in [m for m in self.modules.keys()]: self.unload_module(mod) logger.info("Close all servers connection...") From 29817ba1c1fd5f8b5618877e0023a834f145e65b Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Tue, 8 Aug 2017 23:24:37 +0200 Subject: [PATCH 168/271] pkgs: new module to display quick information about common softwares --- modules/pkgs.py | 68 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 modules/pkgs.py diff --git a/modules/pkgs.py b/modules/pkgs.py new file mode 100644 index 0000000..5a7b0a9 --- /dev/null +++ b/modules/pkgs.py @@ -0,0 +1,68 @@ +"""Get information about common software""" + +# PYTHON STUFFS ####################################################### + +import portage + +from nemubot import context +from nemubot.exception import IMException +from nemubot.hooks import hook + +from more import Response + +DB = None + +# MODULE CORE ######################################################### + +def get_db(): + global DB + if DB is None: + DB = portage.db[portage.root]["porttree"].dbapi + return DB + + +def package_info(pkgname): + pv = get_db().xmatch("match-all", pkgname) + if not pv: + raise IMException("No package named '%s' found" % pkgname) + + bv = get_db().xmatch("bestmatch-visible", pkgname) + pvsplit = portage.catpkgsplit(bv if bv else pv[-1]) + info = get_db().aux_get(bv if bv else pv[-1], ["DESCRIPTION", "HOMEPAGE", "LICENSE", "IUSE", "KEYWORDS"]) + + return { + "pkgname": '/'.join(pvsplit[:2]), + "category": pvsplit[0], + "shortname": pvsplit[1], + "lastvers": '-'.join(pvsplit[2:]) if pvsplit[3] != "r0" else pvsplit[2], + "othersvers": ['-'.join(portage.catpkgsplit(p)[2:]) for p in pv if p != bv], + "description": info[0], + "homepage": info[1], + "license": info[2], + "uses": info[3], + "keywords": info[4], + } + + +# MODULE INTERFACE #################################################### + +@hook.command("eix", + help="Get information about a package", + help_usage={ + "NAME": "Get information about a software NAME" + }) +def cmd_eix(msg): + if not len(msg.args): + raise IMException("please give me a package to search") + + def srch(term): + try: + yield package_info(term) + except portage.exception.AmbiguousPackageName as e: + for i in e.args[0]: + yield package_info(i) + + res = Response(channel=msg.channel, count=" (%d more packages)", nomore="No more package '%s'" % msg.args[0]) + for pi in srch(msg.args[0]): + res.append_message("\x03\x02{pkgname}:\x03\x02 {description} - {homepage} - {license} - last revisions: \x03\x02{lastvers}\x03\x02{ov}".format(ov=(", " + ', '.join(pi["othersvers"])) if pi["othersvers"] else "", **pi)) + return res From 281d81acc4c1b167dbf89b75408a3a427ea67576 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Wed, 9 Aug 2017 22:53:35 +0200 Subject: [PATCH 169/271] suivi: fix error handling of fedex parcel --- modules/suivi.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/suivi.py b/modules/suivi.py index a6f6ab4..24f5bf9 100644 --- a/modules/suivi.py +++ b/modules/suivi.py @@ -156,7 +156,9 @@ def get_fedex_info(fedex_id, lang="en_US"): if ("TrackPackagesResponse" in fedex_data and "packageList" in fedex_data["TrackPackagesResponse"] and - len(fedex_data["TrackPackagesResponse"]["packageList"]) + len(fedex_data["TrackPackagesResponse"]["packageList"]) and + not fedex_data["TrackPackagesResponse"]["errorList"][0]["code"] and + not fedex_data["TrackPackagesResponse"]["packageList"][0]["errorList"][0]["code"] ): return fedex_data["TrackPackagesResponse"]["packageList"][0] From b8f4560780a7570a71d4e17cf0b6f688eb7a2ead Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Thu, 10 Aug 2017 00:55:13 +0200 Subject: [PATCH 170/271] suivi: support DHL --- modules/suivi.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/modules/suivi.py b/modules/suivi.py index 24f5bf9..75a065b 100644 --- a/modules/suivi.py +++ b/modules/suivi.py @@ -163,6 +163,15 @@ def get_fedex_info(fedex_id, lang="en_US"): return fedex_data["TrackPackagesResponse"]["packageList"][0] +def get_dhl_info(dhl_id, lang="en"): + dhl_parcelurl = "http://www.dhl.com/shipmentTracking?" + urllib.parse.urlencode({'AWB': dhl_id}) + + dhl_data = getJSON(dhl_parcelurl) + + if "results" in dhl_data and dhl_data["results"]: + return dhl_data["results"][0] + + # TRACKING HANDLERS ################################################### def handle_tnt(tracknum): @@ -231,6 +240,12 @@ def handle_fedex(tracknum): return ("{trackingCarrierDesc}: \x02{statusWithDetails}\x0F: in \x02{statusLocationCity}, {statusLocationCntryCD}\x0F, estimated delivery: {displayEstDeliveryDateTime}.".format(**info)) +def handle_dhl(tracknum): + info = get_dhl_info(tracknum) + if info: + return "DHL {label} {id}: \x02{description}\x0F".format(**info) + + TRACKING_HANDLERS = { 'laposte': handle_laposte, 'postnl': handle_postnl, @@ -239,6 +254,7 @@ TRACKING_HANDLERS = { 'coliprive': handle_coliprive, 'tnt': handle_tnt, 'fedex': handle_fedex, + 'dhl': handle_dhl, } From 09462d0d90b6cd9d2241e6dd18cc77e5424d6de7 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Thu, 10 Aug 2017 06:48:48 +0200 Subject: [PATCH 171/271] suivi: support USPS --- modules/suivi.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/modules/suivi.py b/modules/suivi.py index 75a065b..6ad13e9 100644 --- a/modules/suivi.py +++ b/modules/suivi.py @@ -126,6 +126,24 @@ def get_postnl_info(postnl_id): return (post_status.lower(), post_destination, post_date) +def get_usps_info(usps_id): + usps_parcelurl = "https://tools.usps.com/go/TrackConfirmAction_input?" + urllib.parse.urlencode({'qtc_tLabels1': usps_id}) + + usps_data = getURLContent(usps_parcelurl) + soup = BeautifulSoup(usps_data) + if (soup.find(class_="tracking_history") + and soup.find(class_="tracking_history").find(class_="row_notification") + and soup.find(class_="tracking_history").find(class_="row_top").find_all("td")): + notification = soup.find(class_="tracking_history").find(class_="row_notification").text.strip() + date = re.sub(r"\s+", " ", soup.find(class_="tracking_history").find(class_="row_top").find_all("td")[0].text.strip()) + status = soup.find(class_="tracking_history").find(class_="row_top").find_all("td")[1].text.strip() + last_location = soup.find(class_="tracking_history").find(class_="row_top").find_all("td")[2].text.strip() + + print(notification) + + return (notification, date, status, last_location) + + def get_fedex_info(fedex_id, lang="en_US"): data = urllib.parse.urlencode({ 'data': json.dumps({ @@ -206,6 +224,13 @@ def handle_postnl(tracknum): ")." % (tracknum, post_status, post_destination, post_date)) +def handle_usps(tracknum): + info = get_usps_info(tracknum) + if info: + notif, last_date, last_status, last_location = info + return ("USPS \x02{tracknum}\x0F is {last_status} in \x02{last_location}\x0F as of {last_date}: {notif}".format(tracknum=tracknum, notif=notif, last_date=last_date, last_status=last_status.lower(), last_location=last_location)) + + def handle_colissimo(tracknum): info = get_colissimo_info(tracknum) if info: @@ -255,6 +280,7 @@ TRACKING_HANDLERS = { 'tnt': handle_tnt, 'fedex': handle_fedex, 'dhl': handle_dhl, + 'usps': handle_usps, } From 39b7b1ae2fc88db91eb8ddd890082fa77c0fd0fc Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Fri, 4 Aug 2017 01:22:24 +0200 Subject: [PATCH 172/271] freetarifs: new module --- modules/freetarifs.py | 64 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 modules/freetarifs.py diff --git a/modules/freetarifs.py b/modules/freetarifs.py new file mode 100644 index 0000000..b96a30f --- /dev/null +++ b/modules/freetarifs.py @@ -0,0 +1,64 @@ +"""Inform about Free Mobile tarifs""" + +# PYTHON STUFFS ####################################################### + +import urllib.parse +from bs4 import BeautifulSoup + +from nemubot.exception import IMException +from nemubot.hooks import hook +from nemubot.tools import web + +from more import Response + + +# MODULE CORE ######################################################### + +ACT = { + "ff_toFixe": "Appel vers les fixes", + "ff_toMobile": "Appel vers les mobiles", + "ff_smsSendedToCountry": "SMS vers le pays", + "ff_mmsSendedToCountry": "MMS vers le pays", + "fc_callToFrance": "Appel vers la France", + "fc_smsToFrance": "SMS vers la france", + "fc_mmsSended": "MMS vers la france", + "fc_callToSameCountry": "Réception des appels", + "fc_callReceived": "Appel dans le pays", + "fc_smsReceived": "SMS (Réception)", + "fc_mmsReceived": "MMS (Réception)", + "fc_moDataFromCountry": "Data", +} + +def get_land_tarif(country, forfait="pkgFREE"): + url = "http://mobile.international.free.fr/?" + urllib.parse.urlencode({'pays': country}) + page = web.getURLContent(url) + soup = BeautifulSoup(page) + + fact = soup.find(class_=forfait) + + if fact is None: + raise IMException("Country or forfait not found.") + + res = {} + for s in ACT.keys(): + try: + res[s] = fact.find(attrs={"data-bind": "text: " + s}).text + " " + fact.find(attrs={"data-bind": "html: " + s + "Unit"}).text + except AttributeError: + res[s] = "inclus" + + return res + +@hook.command("freetarifs", + help="Show Free Mobile tarifs for given contries", + help_usage={"COUNTRY": "Show Free Mobile tarifs for given CONTRY"}, + keywords={ + "forfait=FORFAIT": "Related forfait between Free (default) and 2euro" + }) +def get_freetarif(msg): + res = Response(channel=msg.channel) + + for country in msg.args: + t = get_land_tarif(country.lower().capitalize(), "pkg" + (msg.kwargs["forfait"] if "forfait" in msg.kwargs else "FREE").upper()) + res.append_message(["\x02%s\x0F : %s" % (ACT[k], t[k]) for k in sorted(ACT.keys(), reverse=True)], title=country) + + return res From 128afb5914059c9e88df878576738d768529601b Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Sun, 6 Aug 2017 12:27:19 +0200 Subject: [PATCH 173/271] disas: new module, aim to disassemble binary code. Closing #67 --- modules/disas.py | 89 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 modules/disas.py diff --git a/modules/disas.py b/modules/disas.py new file mode 100644 index 0000000..7c17907 --- /dev/null +++ b/modules/disas.py @@ -0,0 +1,89 @@ +"""The Ultimate Disassembler Module""" + +# PYTHON STUFFS ####################################################### + +import capstone + +from nemubot.exception import IMException +from nemubot.hooks import hook + +from more import Response + + +# MODULE CORE ######################################################### + +ARCHITECTURES = { + "arm": capstone.CS_ARCH_ARM, + "arm64": capstone.CS_ARCH_ARM64, + "mips": capstone.CS_ARCH_MIPS, + "ppc": capstone.CS_ARCH_PPC, + "sparc": capstone.CS_ARCH_SPARC, + "sysz": capstone.CS_ARCH_SYSZ, + "x86": capstone.CS_ARCH_X86, + "xcore": capstone.CS_ARCH_XCORE, +} + +MODES = { + "arm": capstone.CS_MODE_ARM, + "thumb": capstone.CS_MODE_THUMB, + "mips32": capstone.CS_MODE_MIPS32, + "mips64": capstone.CS_MODE_MIPS64, + "mips32r6": capstone.CS_MODE_MIPS32R6, + "16": capstone.CS_MODE_16, + "32": capstone.CS_MODE_32, + "64": capstone.CS_MODE_64, + "le": capstone.CS_MODE_LITTLE_ENDIAN, + "be": capstone.CS_MODE_BIG_ENDIAN, + "micro": capstone.CS_MODE_MICRO, + "mclass": capstone.CS_MODE_MCLASS, + "v8": capstone.CS_MODE_V8, + "v9": capstone.CS_MODE_V9, +} + +# MODULE INTERFACE #################################################### + +@hook.command("disas", + help="Display assembly code", + help_usage={"CODE": "Display assembly code corresponding to the given CODE"}, + keywords={ + "arch=ARCH": "Specify the architecture of the code to disassemble (default: x86, choose between: %s)" % ', '.join(ARCHITECTURES.keys()), + "modes=MODE[,MODE]": "Specify hardware mode of the code to disassemble (default: 32, between: %s)" % ', '.join(MODES.keys()), + }) +def cmd_disas(msg): + if not len(msg.args): + raise IMException("please give me some code") + + # Determine the architecture + if "arch" in msg.kwargs: + if msg.kwargs["arch"] not in ARCHITECTURES: + raise IMException("unknown architectures '%s'" % msg.kwargs["arch"]) + architecture = ARCHITECTURES[msg.kwargs["arch"]] + else: + architecture = capstone.CS_ARCH_X86 + + # Determine hardware modes + modes = 0 + if "modes" in msg.kwargs: + for mode in msg.kwargs["modes"].split(','): + if mode not in MODES: + raise IMException("unknown mode '%s'" % mode) + modes += MODES[mode] + elif architecture == capstone.CS_ARCH_X86 or architecture == capstone.CS_ARCH_PPC: + modes = capstone.CS_MODE_32 + elif architecture == capstone.CS_ARCH_ARM or architecture == capstone.CS_ARCH_ARM64: + modes = capstone.CS_MODE_ARM + elif architecture == capstone.CS_ARCH_MIPS: + modes = capstone.CS_MODE_MIPS32 + + # Get the code + code = bytearray.fromhex(''.join([a.replace("0x", "") for a in msg.args])) + + # Setup capstone + md = capstone.Cs(architecture, modes) + + res = Response(channel=msg.channel, nomore="No more instruction") + + for isn in md.disasm(code, 0x1000): + res.append_message("%s %s" %(isn.mnemonic, isn.op_str), title="0x%x" % isn.address) + + return res From 0a576410c72951fabc1b0284d98078bbe19acf4f Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Thu, 3 Aug 2017 21:28:56 +0200 Subject: [PATCH 174/271] cve: improve read of partial and inexistant CVE --- modules/cve.py | 66 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 46 insertions(+), 20 deletions(-) diff --git a/modules/cve.py b/modules/cve.py index c470e29..6cdb339 100644 --- a/modules/cve.py +++ b/modules/cve.py @@ -5,6 +5,7 @@ from bs4 import BeautifulSoup from urllib.parse import quote +from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools.web import getURLContent, striphtml @@ -15,31 +16,44 @@ BASEURL_NIST = 'https://nvd.nist.gov/vuln/detail/' # MODULE CORE ######################################################### +VULN_DATAS = { + "alert-title": "vuln-warning-status-name", + "alert-content": "vuln-warning-banner-content", + + "description": "vuln-description", + "published": "vuln-published-on", + "last_modified": "vuln-last-modified-on", + "source": "vuln-source", + + "base_score": "vuln-cvssv3-base-score-link", + "severity": "vuln-cvssv3-base-score-severity", + "impact_score": "vuln-cvssv3-impact-score", + "exploitability_score": "vuln-cvssv3-exploitability-score", + + "av": "vuln-cvssv3-av", + "ac": "vuln-cvssv3-ac", + "pr": "vuln-cvssv3-pr", + "ui": "vuln-cvssv3-ui", + "s": "vuln-cvssv3-s", + "c": "vuln-cvssv3-c", + "i": "vuln-cvssv3-i", + "a": "vuln-cvssv3-a", +} + + def get_cve(cve_id): search_url = BASEURL_NIST + quote(cve_id.upper()) soup = BeautifulSoup(getURLContent(search_url)) - return { - "description": soup.body.find(attrs={"data-testid":"vuln-description"}).text.strip(), - "published": soup.body.find(attrs={"data-testid":"vuln-published-on"}).text.strip(), - "last_modified": soup.body.find(attrs={"data-testid":"vuln-last-modified-on"}).text.strip(), - "source": soup.body.find(attrs={"data-testid":"vuln-source"}).text.strip(), + vuln = {} - "base_score": float(soup.body.find(attrs={"data-testid":"vuln-cvssv3-base-score-link"}).text.strip()), - "severity": soup.body.find(attrs={"data-testid":"vuln-cvssv3-base-score-severity"}).text.strip(), - "impact_score": float(soup.body.find(attrs={"data-testid":"vuln-cvssv3-impact-score"}).text.strip()), - "exploitability_score": float(soup.body.find(attrs={"data-testid":"vuln-cvssv3-exploitability-score"}).text.strip()), + for vd in VULN_DATAS: + r = soup.body.find(attrs={"data-testid": VULN_DATAS[vd]}) + if r: + vuln[vd] = r.text.strip() - "av": soup.body.find(attrs={"data-testid":"vuln-cvssv3-av"}).text.strip(), - "ac": soup.body.find(attrs={"data-testid":"vuln-cvssv3-ac"}).text.strip(), - "pr": soup.body.find(attrs={"data-testid":"vuln-cvssv3-pr"}).text.strip(), - "ui": soup.body.find(attrs={"data-testid":"vuln-cvssv3-ui"}).text.strip(), - "s": soup.body.find(attrs={"data-testid":"vuln-cvssv3-s"}).text.strip(), - "c": soup.body.find(attrs={"data-testid":"vuln-cvssv3-c"}).text.strip(), - "i": soup.body.find(attrs={"data-testid":"vuln-cvssv3-i"}).text.strip(), - "a": soup.body.find(attrs={"data-testid":"vuln-cvssv3-a"}).text.strip(), - } + return vuln def display_metrics(av, ac, pr, ui, s, c, i, a, **kwargs): @@ -68,7 +82,19 @@ def get_cve_desc(msg): cve_id = 'cve-' + cve_id cve = get_cve(cve_id) - metrics = display_metrics(**cve) - res.append_message("{cveid}: Base score: \x02{base_score} {severity}\x0F (impact: \x02{impact_score}\x0F, exploitability: \x02{exploitability_score}\x0F; {metrics}), from \x02{source}\x0F, last modified on \x02{last_modified}\x0F. {description}".format(cveid=cve_id, metrics=metrics, **cve)) + if not cve: + raise IMException("CVE %s doesn't exists." % cve_id) + + if "alert-title" in cve or "alert-content" in cve: + alert = "\x02%s:\x0F %s " % (cve["alert-title"] if "alert-title" in cve else "", + cve["alert-content"] if "alert-content" in cve else "") + else: + alert = "" + + if "base_score" not in cve and "description" in cve: + res.append_message("{alert}From \x02{source}\x0F, last modified on \x02{last_modified}\x0F. {description}".format(alert=alert, **cve), title=cve_id) + else: + metrics = display_metrics(**cve) + res.append_message("{alert}Base score: \x02{base_score} {severity}\x0F (impact: \x02{impact_score}\x0F, exploitability: \x02{exploitability_score}\x0F; {metrics}), from \x02{source}\x0F, last modified on \x02{last_modified}\x0F. {description}".format(alert=alert, metrics=metrics, **cve), title=cve_id) return res From dcb44ca3f24b111de4718db7db5fd652ef9abdc1 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Wed, 2 Aug 2017 19:58:49 +0200 Subject: [PATCH 175/271] tools/web: new parameter to choose max content size to retrieve --- nemubot/tools/web.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/nemubot/tools/web.py b/nemubot/tools/web.py index 0394aac..164f5da 100644 --- a/nemubot/tools/web.py +++ b/nemubot/tools/web.py @@ -68,7 +68,8 @@ def getPassword(url): # Get real pages -def getURLContent(url, body=None, timeout=7, header=None, decode_error=False): +def getURLContent(url, body=None, timeout=7, header=None, decode_error=False, + max_size=524288): """Return page content corresponding to URL or None if any error occurs Arguments: @@ -76,6 +77,7 @@ def getURLContent(url, body=None, timeout=7, header=None, decode_error=False): body -- Data to send as POST content timeout -- maximum number of seconds to wait before returning an exception decode_error -- raise exception on non-200 pages or ignore it + max_size -- maximal size allow for the content """ o = urlparse(_getNormalizedURL(url), "http") @@ -135,7 +137,7 @@ def getURLContent(url, body=None, timeout=7, header=None, decode_error=False): size = int(res.getheader("Content-Length", 524288)) cntype = res.getheader("Content-Type") - if size > 524288 or (cntype is not None and cntype[:4] != "text" and cntype[:4] != "appl"): + if size > max_size or (cntype is not None and cntype[:4] != "text" and cntype[:4] != "appl"): raise IMException("Content too large to be retrieved") data = res.read(size) @@ -168,7 +170,8 @@ def getURLContent(url, body=None, timeout=7, header=None, decode_error=False): body=body, timeout=timeout, header=header, - decode_error=decode_error) + decode_error=decode_error, + max_size=max_size) elif decode_error: return data.decode(charset).strip() else: From 6dda14218855c7cabd613001ec46ea1f77401c44 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Sun, 30 Jul 2017 11:49:21 +0200 Subject: [PATCH 176/271] shodan: introducing new module to search on shodan --- modules/shodan.py | 104 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 modules/shodan.py diff --git a/modules/shodan.py b/modules/shodan.py new file mode 100644 index 0000000..4b2edae --- /dev/null +++ b/modules/shodan.py @@ -0,0 +1,104 @@ +"""Search engine for IoT""" + +# PYTHON STUFFS ####################################################### + +from datetime import datetime +import ipaddress +import urllib.parse + +from nemubot import context +from nemubot.exception import IMException +from nemubot.hooks import hook +from nemubot.tools import web + +from more import Response + + +# GLOBALS ############################################################# + +BASEURL = "https://api.shodan.io/shodan/" + + +# LOADING ############################################################# + +def load(context): + if not context.config or "apikey" not in context.config: + raise ImportError("You need a Shodan API key in order to use this " + "module. Add it to the module configuration file:\n" + "<module name=\"shodan\" apikey=\"XXXXXXXXXXXXXXXX\" " + "/>\nRegister at https://account.shodan.io/register") + + +# MODULE CORE ######################################################### + +def host_lookup(ip): + url = BASEURL + "host/" + urllib.parse.quote(ip) + "?" + urllib.parse.urlencode({'key': context.config["apikey"]}) + return web.getJSON(url) + + +def search_hosts(query): + url = BASEURL + "host/search?" + urllib.parse.urlencode({'query': query, 'key': context.config["apikey"]}) + return web.getJSON(url, max_size=4194304) + + +def print_ssl(ssl): + return ( + "SSL: " + + " ".join([v for v in ssl["versions"] if v[0] != "-"]) + + "; cipher used: " + ssl["cipher"]["name"] + + ("; certificate: " + ssl["cert"]["sig_alg"] + + " issued by: " + ssl["cert"]["issuer"]["CN"] + + " expires on: " + str(datetime.strptime(ssl["cert"]["expires"], "%Y%m%d%H%M%SZ")) if "cert" in ssl else "") + ) + +def print_service(svc): + ip = ipaddress.ip_address(svc["ip_str"]) + return ((svc["ip_str"] if ip.version == 4 else "[%s]" % svc["ip_str"]) + + ":{port}/{transport} ({module}):" + + (" {os}" if svc["os"] else "") + + (" {product}" if "product" in svc else "") + + (" {version}" if "version" in svc else "") + + (" {info}" if "info" in svc else "") + + (" Vulns: " + ", ".join(svc["opts"]["vulns"]) if "opts" in svc and "vulns" in svc["opts"] else "") + + (" " + print_ssl(svc["ssl"]) if "ssl" in svc else "") + + (" \x03\x1D" + svc["data"].replace("\r\n", "\n").split("\n")[0] + "\x03\x1D" if "data" in svc else "") + + (" " + svc["title"] if "title" in svc else "") + ).format(module=svc["_shodan"]["module"], **svc) + + +# MODULE INTERFACE #################################################### + +@hook.command("shodan", + help="Use shodan.io to get information on machines connected to Internet", + help_usage={ + "IP": "retrieve information about the given IP (can be v4 or v6)", + "TERM": "retrieve all hosts matching TERM somewhere in their exposed stuff" + }) +def shodan(msg): + if not msg.args: + raise IMException("indicate an IP or a term to search!") + + terms = " ".join(msg.args) + + try: + ip = ipaddress.ip_address(terms) + except ValueError: + ip = None + + if ip: + h = host_lookup(terms) + res = Response(channel=msg.channel, + title="%s" % ((h["ip_str"] if ip.version == 4 else "[%s]" % h["ip_str"]) + (" (" + ", ".join(h["hostnames"]) + ")") if h["hostnames"] else "")) + res.append_message("{isp} ({asn}) -> {city} ({country_code}), running {os}. Vulns: {vulns_str}. Open ports: {open_ports}. Last update: {last_update}".format( + open_ports=", ".join(map(lambda a: str(a), h["ports"])), vulns_str=", ".join(h["vulns"]) if "vulns" in h else None, **h).strip()) + for d in h["data"]: + res.append_message(print_service(d)) + + else: + q = search_hosts(terms) + res = Response(channel=msg.channel, + count=" (%%s/%s results)" % q["total"]) + for r in q["matches"]: + res.append_message(print_service(r)) + + return res From 3c7ed176c09b8ebb0072cc88a802b8da3856865b Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Sat, 26 Aug 2017 10:38:52 +0200 Subject: [PATCH 177/271] dig: new module --- modules/dig.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 modules/dig.py diff --git a/modules/dig.py b/modules/dig.py new file mode 100644 index 0000000..3db5581 --- /dev/null +++ b/modules/dig.py @@ -0,0 +1,36 @@ +"""DNS resolver""" + +# PYTHON STUFFS ####################################################### + +import dns.rdtypes.ANY +import dns.rdtypes.IN +import dns.resolver + +from nemubot.exception import IMException +from nemubot.hooks import hook + +from more import Response + + +# MODULE INTERFACE #################################################### + +@hook.command("dig") +def dig(msg): + ltype = "A" + ldomain = None + for a in msg.args: + if a in dns.rdtypes.IN.__all__ or a in dns.rdtypes.ANY.__all__: + ltype = a + else: + ldomain = a + + if not ldomain: + raise IMException("indicate a domain to resolve") + + answers = dns.resolver.query(ldomain, ltype) + + res = Response(channel=msg.channel, title=ldomain, count=" (%s others records)") + for rdata in answers: + res.append_message(type(rdata).__name__ + " " + rdata.to_text()) + + return res From aa81aa4e96041ec45b5fb3bcd59ed110a5ebad1b Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Sat, 26 Aug 2017 12:14:29 +0200 Subject: [PATCH 178/271] dig: better parse dig syntax @ and some + --- modules/dig.py | 72 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 65 insertions(+), 7 deletions(-) diff --git a/modules/dig.py b/modules/dig.py index 3db5581..de7b2a3 100644 --- a/modules/dig.py +++ b/modules/dig.py @@ -2,8 +2,13 @@ # PYTHON STUFFS ####################################################### -import dns.rdtypes.ANY -import dns.rdtypes.IN +import ipaddress +import socket + +import dns.exception +import dns.name +import dns.rdataclass +import dns.rdatatype import dns.resolver from nemubot.exception import IMException @@ -14,23 +19,76 @@ from more import Response # MODULE INTERFACE #################################################### -@hook.command("dig") +@hook.command("dig", + help="Resolve domain name with a basic syntax similar to dig(1)") def dig(msg): + lclass = "IN" ltype = "A" + ledns = None + ltimeout = 6.0 ldomain = None + lnameservers = [] + lsearchlist = [] + loptions = [] for a in msg.args: - if a in dns.rdtypes.IN.__all__ or a in dns.rdtypes.ANY.__all__: + if a in dns.rdatatype._by_text: ltype = a + elif a in dns.rdataclass._by_text: + lclass = a + elif a[0] == "@": + try: + lnameservers.append(str(ipaddress.ip_address(a[1:]))) + except ValueError: + for r in socket.getaddrinfo(a[1:], 53, proto=socket.IPPROTO_UDP): + lnameservers.append(r[4][0]) + + elif a[0:8] == "+domain=": + lsearchlist.append(dns.name.from_unicode(a[8:])) + elif a[0:6] == "+edns=": + ledns = int(a[6:]) + elif a[0:6] == "+time=": + ltimeout = float(a[6:]) + elif a[0] == "+": + loptions.append(a[1:]) else: ldomain = a if not ldomain: raise IMException("indicate a domain to resolve") - answers = dns.resolver.query(ldomain, ltype) + resolv = dns.resolver.Resolver() + if ledns: + resolv.edns = ledns + resolv.lifetime = ltimeout + resolv.timeout = ltimeout + resolv.flags = ( + dns.flags.QR | dns.flags.RA | + dns.flags.AA if "aaonly" in loptions or "aaflag" in loptions else 0 | + dns.flags.AD if "adflag" in loptions else 0 | + dns.flags.CD if "cdflag" in loptions else 0 | + dns.flags.RD if "norecurse" not in loptions else 0 + ) + if lsearchlist: + resolv.search = lsearchlist + else: + resolv.search = [dns.name.from_text(".")] - res = Response(channel=msg.channel, title=ldomain, count=" (%s others records)") + if lnameservers: + resolv.nameservers = lnameservers + + try: + answers = resolv.query(ldomain, ltype, lclass, tcp="tcp" in loptions) + except dns.exception.DNSException as e: + raise IMException(str(e)) + + res = Response(channel=msg.channel, count=" (%s others entries)") for rdata in answers: - res.append_message(type(rdata).__name__ + " " + rdata.to_text()) + res.append_message("%s %s %s %s %s" % ( + answers.qname.to_text(), + answers.ttl if not "nottlid" in loptions else "", + dns.rdataclass.to_text(answers.rdclass) if not "nocl" in loptions else "", + dns.rdatatype.to_text(answers.rdtype), + rdata.to_text()) + ) return res From 89772ebce0893eb3b7ec20a1a7a960114a39686f Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Sat, 26 Aug 2017 16:56:05 +0200 Subject: [PATCH 179/271] whois: now able to use a CRI API dump --- modules/whois.py | 64 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 16 deletions(-) diff --git a/modules/whois.py b/modules/whois.py index 52344d1..fb6d250 100644 --- a/modules/whois.py +++ b/modules/whois.py @@ -1,5 +1,6 @@ # coding=utf-8 +import json import re from nemubot import context @@ -13,13 +14,26 @@ from more import Response from networking.page import headers PASSWD_FILE = None +# You can get one with: curl -b "sessionid=YOURSESSIONID" 'https://accounts.cri.epita.net/api/users/' > users.json +APIEXTRACT_FILE = None def load(context): global PASSWD_FILE if not context.config or "passwd" not in context.config: print("No passwd file given") + else: + PASSWD_FILE = context.config["passwd"] + print("passwd file loaded:", PASSWD_FILE) + + global APIEXTRACT_FILE + if not context.config or "apiextract" not in context.config: + print("No passwd file given") + else: + APIEXTRACT_FILE = context.config["apiextract"] + print("JSON users file loaded:", APIEXTRACT_FILE) + + if PASSWD_FILE is None and APIEXTRACT_FILE is None: return None - PASSWD_FILE = context.config["passwd"] if not context.data.hasNode("aliases"): context.data.addChild(ModuleState("aliases")) @@ -35,16 +49,26 @@ def load(context): class Login: - def __init__(self, line): - s = line.split(":") - self.login = s[0] - self.uid = s[2] - self.gid = s[3] - self.cn = s[4] - self.home = s[5] + def __init__(self, line=None, login=None, uidNumber=None, cn=None, promo=None, **kwargs): + if line is not None: + s = line.split(":") + self.login = s[0] + self.uid = s[2] + self.gid = s[3] + self.cn = s[4] + self.home = s[5] + else: + self.login = login + self.uid = uidNumber + self.promo = promo + self.cn = cn + self.gid = "epita" + promo def get_promo(self): - return self.home.split("/")[2].replace("_", " ") + if hasattr(self, "promo"): + return self.promo + if hasattr(self, "home"): + return self.home.split("/")[2].replace("_", " ") def get_photo(self): if self.login in context.data.getNode("pics").index: @@ -60,17 +84,25 @@ class Login: return None -def found_login(login, search=False): +def login_lookup(login, search=False): if login in context.data.getNode("aliases").index: login = context.data.getNode("aliases").index[login]["to"] + if APIEXTRACT_FILE: + with open(APIEXTRACT_FILE, encoding="utf-8") as f: + api = json.load(f) + for l in api: + if (not search and l["login"] == login) or (search and (("login" in l and l["login"].find(login) != -1) or ("cn" in l and l["cn"].find(login) != -1) or ("uid" in l and str(l["uid"]) == login))): + yield Login(**l) + login_ = login + (":" if not search else "") lsize = len(login_) - with open(PASSWD_FILE, encoding="iso-8859-15") as f: - for l in f.readlines(): - if l[:lsize] == login_: - yield Login(l.strip()) + if PASSWD_FILE: + with open(PASSWD_FILE, encoding="iso-8859-15") as f: + for l in f.readlines(): + if l[:lsize] == login_: + yield Login(l.strip()) def cmd_whois(msg): if len(msg.args) < 1: @@ -87,7 +119,7 @@ def cmd_whois(msg): res = Response(channel=msg.channel, count=" (%d more logins)", line_treat=format_response) for srch in msg.args: found = False - for l in found_login(srch, "lookup" in msg.kwargs): + for l in login_lookup(srch, "lookup" in msg.kwargs): found = True res.append_message((srch, l)) if not found: @@ -98,7 +130,7 @@ def cmd_whois(msg): def cmd_nicks(msg): if len(msg.args) < 1: raise IMException("Provide a login") - nick = found_login(msg.args[0]) + nick = login_lookup(msg.args[0]) if nick is None: nick = msg.args[0] else: From 1dae3c713a4095320f637bca89264926571b276c Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Fri, 25 Aug 2017 23:53:10 +0200 Subject: [PATCH 180/271] tools/web: new option to remove callback from JSON files --- nemubot/tools/web.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nemubot/tools/web.py b/nemubot/tools/web.py index 164f5da..c3ba42a 100644 --- a/nemubot/tools/web.py +++ b/nemubot/tools/web.py @@ -193,7 +193,7 @@ def getXML(*args, **kwargs): return parseString(cnt) -def getJSON(*args, **kwargs): +def getJSON(*args, remove_callback=False, **kwargs): """Get content page and return JSON content Arguments: same as getURLContent @@ -204,6 +204,9 @@ def getJSON(*args, **kwargs): return None else: import json + if remove_callback: + import re + cnt = re.sub(r"^[^(]+\((.*)\)$", r"\1", cnt) return json.loads(cnt) From 694c54a6bc30841bb040b270d8c288a624b3707a Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Sat, 26 Aug 2017 00:14:14 +0200 Subject: [PATCH 181/271] imdb: switch to ugly IMDB HTML parsing --- modules/imdb.py | 87 ++++++++++++++++++++++++------------------------- 1 file changed, 43 insertions(+), 44 deletions(-) diff --git a/modules/imdb.py b/modules/imdb.py index 2434a3c..bd1cadf 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -5,6 +5,8 @@ import re import urllib.parse +from bs4 import BeautifulSoup + from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web @@ -14,54 +16,46 @@ from more import Response # MODULE CORE ######################################################### -def get_movie(title=None, year=None, imdbid=None, fullplot=True, tomatoes=False): +def get_movie_by_id(imdbid): """Returns the information about the matching movie""" - # Built URL - url = "http://www.omdbapi.com/?" - if title is not None: - url += "t=%s&" % urllib.parse.quote(title) - if year is not None: - url += "y=%s&" % urllib.parse.quote(year) - if imdbid is not None: - url += "i=%s&" % urllib.parse.quote(imdbid) - if fullplot: - url += "plot=full&" - if tomatoes: - url += "tomatoes=true&" + url = "http://www.imdb.com/title/" + urllib.parse.quote(imdbid) + soup = BeautifulSoup(web.getURLContent(url)) - # Make the request - data = web.getJSON(url) + return { + "imdbID": imdbid, + "Title": soup.body.find(attrs={"itemprop": "name"}).next_element.strip(), + "Year": soup.body.find(id="titleYear").find("a").text.strip() if soup.body.find(id="titleYear") else ", ".join([y.text.strip() for y in soup.body.find(attrs={"class": "seasons-and-year-nav"}).find_all("div")[3].find_all("a")[:-1]]), + "Duration": soup.body.find_all(attrs={"itemprop": "duration"})[-1].text.strip(), + "imdbRating": soup.body.find(attrs={"itemprop": "ratingValue"}).text.strip(), + "imdbVotes": soup.body.find(attrs={"itemprop": "ratingCount"}).text.strip(), + "Plot": re.sub(r"\s+", " ", soup.body.find(id="titleStoryLine").find(attrs={"itemprop": "description"}).text).strip(), - # Return data - if "Error" in data: - raise IMException(data["Error"]) - - elif "Response" in data and data["Response"] == "True": - return data - - else: - raise IMException("An error occurs during movie search") + "Type": "TV Series" if soup.find(attrs={"class": "np_episode_guide"}) else "Movie", + "Country": ", ".join([c.find("a").text.strip() for c in soup.body.find(id="titleDetails").find_all(attrs={"class": "txt-block"}) if c.text.find("Country") != -1]), + "Released": soup.body.find(attrs={"itemprop": "datePublished"}).attrs["content"] if "content" in soup.body.find(attrs={"itemprop": "datePublished"}).attrs else "N\A", + "Genre": ", ".join([g.text.strip() for g in soup.body.find_all(attrs={"itemprop": "genre"})[:-1]]), + "Director": ", ".join([d.find(attrs={"itemprop": "name"}).text.strip() for d in soup.body.find_all(attrs={"itemprop": "director"})]), + "Writer": ", ".join([d.find(attrs={"itemprop": "name"}).text.strip() for d in soup.body.find_all(attrs={"itemprop": "creator"})]), + "Actors": ", ".join([d.find(attrs={"itemprop": "name"}).text.strip() for d in soup.body.find_all(attrs={"itemprop": "actors"})]), + } -def find_movies(title): +def find_movies(title, year=None): """Find existing movies matching a approximate title""" + title = title.lower() + # Built URL - url = "http://www.omdbapi.com/?s=%s" % urllib.parse.quote(title) + url = "https://v2.sg.media-imdb.com/suggests/%s/%s.json" % (urllib.parse.quote(title[0]), urllib.parse.quote(title.replace(" ", "_"))) # Make the request - data = web.getJSON(url) - - # Return data - if "Error" in data: - raise IMException(data["Error"]) - - elif "Search" in data: - return data + data = web.getJSON(url, remove_callback=True) + if year is None: + return data["d"] else: - raise IMException("An error occurs during movie search") + return [d for d in data["d"] if "y" in d and str(d["y"]) == year] # MODULE INTERFACE #################################################### @@ -79,23 +73,28 @@ def cmd_imdb(msg): title = ' '.join(msg.args) if re.match("^tt[0-9]{7}$", title) is not None: - data = get_movie(imdbid=title) + data = get_movie_by_id(imdbid=title) else: rm = re.match(r"^(.+)\s\(([0-9]{4})\)$", title) if rm is not None: - data = get_movie(title=rm.group(1), year=rm.group(2)) + data = find_movies(rm.group(1), year=rm.group(2)) else: - data = get_movie(title=title) + data = find_movies(title) + + if not data: + raise IMException("Movie/series not found") + + data = get_movie_by_id(data[0]["id"]) res = Response(channel=msg.channel, title="%s (%s)" % (data['Title'], data['Year']), nomore="No more information, more at http://www.imdb.com/title/%s" % data['imdbID']) - res.append_message("\x02rating\x0F: %s (%s votes); \x02plot\x0F: %s" % - (data['imdbRating'], data['imdbVotes'], data['Plot'])) + res.append_message("%s \x02genre:\x0F %s; \x02rating\x0F: %s (%s votes); \x02plot\x0F: %s" % + (data['Type'], data['Genre'], data['imdbRating'], data['imdbVotes'], data['Plot'])) - res.append_message("%s \x02from\x0F %s \x02released on\x0F %s; \x02genre:\x0F %s; \x02directed by:\x0F %s; \x02written by:\x0F %s; \x02main actors:\x0F %s" - % (data['Type'], data['Country'], data['Released'], data['Genre'], data['Director'], data['Writer'], data['Actors'])) + res.append_message("%s \x02from\x0F %s \x02released on\x0F %s; \x02directed by:\x0F %s; \x02written by:\x0F %s; \x02main actors:\x0F %s" + % (data['Type'], data['Country'], data['Released'], data['Director'], data['Writer'], data['Actors'])) return res @@ -111,7 +110,7 @@ def cmd_search(msg): data = find_movies(' '.join(msg.args)) movies = list() - for m in data['Search']: - movies.append("\x02%s\x0F (%s of %s)" % (m['Title'], m['Type'], m['Year'])) + for m in data: + movies.append("\x02%s\x0F%s with %s" % (m['l'], (" (" + str(m['y']) + ")") if "y" in m else "", m['s'])) return Response(movies, title="Titles found", channel=msg.channel) From fde459c3fff26bb1f1f982bd5966ec448b13ca29 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Tue, 18 Jul 2017 06:32:48 +0200 Subject: [PATCH 182/271] Remove legacy msg.nick --- modules/alias.py | 6 +++--- modules/birthday.py | 12 ++++++------ modules/events.py | 14 +++++++------- modules/mapquest.py | 2 +- modules/reddit.py | 2 +- modules/rnd.py | 2 +- modules/sms.py | 16 ++++++++-------- modules/spell/__init__.py | 6 +++--- modules/virtualradar.py | 2 +- modules/weather.py | 2 +- modules/whois.py | 4 ++-- modules/worldcup.py | 2 +- nemubot/bot.py | 2 +- nemubot/channel.py | 18 +++++++++--------- nemubot/message/abstract.py | 6 ------ nemubot/server/IRC.py | 8 ++++---- 16 files changed, 49 insertions(+), 55 deletions(-) diff --git a/modules/alias.py b/modules/alias.py index 5053783..5aae6bb 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -185,7 +185,7 @@ def cmd_listvars(msg): def cmd_set(msg): if len(msg.args) < 2: raise IMException("!set take two args: the key and the value.") - set_variable(msg.args[0], " ".join(msg.args[1:]), msg.nick) + set_variable(msg.args[0], " ".join(msg.args[1:]), msg.frm) return Response("Variable $%s successfully defined." % msg.args[0], channel=msg.channel) @@ -222,13 +222,13 @@ def cmd_alias(msg): if alias.cmd in context.data.getNode("aliases").index: return Response("%s corresponds to %s" % (alias.cmd, context.data.getNode("aliases").index[alias.cmd]["origin"]), - channel=msg.channel, nick=msg.nick) + channel=msg.channel, nick=msg.frm) elif len(msg.args) > 1: create_alias(alias.cmd, " ".join(msg.args[1:]), channel=msg.channel, - creator=msg.nick) + creator=msg.frm) return Response("New alias %s successfully registered." % alias.cmd, channel=msg.channel) diff --git a/modules/birthday.py b/modules/birthday.py index cb850ac..7a9cdaa 100644 --- a/modules/birthday.py +++ b/modules/birthday.py @@ -27,7 +27,7 @@ def load(context): def findName(msg): if (not len(msg.args) or msg.args[0].lower() == "moi" or msg.args[0].lower() == "me"): - name = msg.nick.lower() + name = msg.frm.lower() else: name = msg.args[0].lower() @@ -77,7 +77,7 @@ def cmd_anniv(msg): else: return Response("désolé, je ne connais pas la date d'anniversaire" " de %s. Quand est-il né ?" % name, - msg.channel, msg.nick) + msg.channel, msg.frm) @hook.command("age", @@ -98,7 +98,7 @@ def cmd_age(msg): msg.channel) else: return Response("désolé, je ne connais pas l'âge de %s." - " Quand est-il né ?" % name, msg.channel, msg.nick) + " Quand est-il né ?" % name, msg.channel, msg.frm) return True @@ -113,11 +113,11 @@ def parseask(msg): if extDate is None or extDate.year > datetime.now().year: return Response("la date de naissance ne paraît pas valide...", msg.channel, - msg.nick) + msg.frm) else: nick = res.group(1) if nick == "my" or nick == "I" or nick == "i" or nick == "je" or nick == "mon" or nick == "ma": - nick = msg.nick + nick = msg.frm if nick.lower() in context.data.index: context.data.index[nick.lower()]["born"] = extDate else: @@ -129,6 +129,6 @@ def parseask(msg): return Response("ok, c'est noté, %s est né le %s" % (nick, extDate.strftime("%A %d %B %Y à %H:%M")), msg.channel, - msg.nick) + msg.frm) except: raise IMException("la date de naissance ne paraît pas valide.") diff --git a/modules/events.py b/modules/events.py index a35c28b..0cc5a44 100644 --- a/modules/events.py +++ b/modules/events.py @@ -69,7 +69,7 @@ def start_countdown(msg): strnd = ModuleState("strend") strnd["server"] = msg.server strnd["channel"] = msg.channel - strnd["proprio"] = msg.nick + strnd["proprio"] = msg.frm strnd["start"] = msg.date strnd["name"] = msg.args[0] context.data.addChild(strnd) @@ -145,17 +145,17 @@ def end_countdown(msg): raise IMException("quel événement terminer ?") if msg.args[0] in context.data.index: - if context.data.index[msg.args[0]]["proprio"] == msg.nick or (msg.cmd == "forceend" and msg.frm_owner): + if context.data.index[msg.args[0]]["proprio"] == msg.frm or (msg.cmd == "forceend" and msg.frm_owner): duration = countdown(msg.date - context.data.index[msg.args[0]].getDate("start")) context.del_event(context.data.index[msg.args[0]]["_id"]) context.data.delChild(context.data.index[msg.args[0]]) context.save() return Response("%s a duré %s." % (msg.args[0], duration), - channel=msg.channel, nick=msg.nick) + channel=msg.channel, nick=msg.frm) else: raise IMException("Vous ne pouvez pas terminer le compteur %s, créé par %s." % (msg.args[0], context.data.index[msg.args[0]]["proprio"])) else: - return Response("%s n'est pas un compteur connu."% (msg.args[0]), channel=msg.channel, nick=msg.nick) + return Response("%s n'est pas un compteur connu."% (msg.args[0]), channel=msg.channel, nick=msg.frm) @hook.command("eventslist") @@ -180,7 +180,7 @@ def parseanswer(msg): # Avoid message starting by ! which can be interpreted as command by other bots if msg.cmd[0] == "!": - res.nick = msg.nick + res.nick = msg.frm if context.data.index[msg.cmd].name == "strend": if context.data.index[msg.cmd].hasAttribute("end"): @@ -223,7 +223,7 @@ def parseask(msg): evt = ModuleState("event") evt["server"] = msg.server evt["channel"] = msg.channel - evt["proprio"] = msg.nick + evt["proprio"] = msg.frm evt["name"] = name.group(1) evt["start"] = extDate evt["msg_after"] = msg_after @@ -237,7 +237,7 @@ def parseask(msg): evt = ModuleState("event") evt["server"] = msg.server evt["channel"] = msg.channel - evt["proprio"] = msg.nick + evt["proprio"] = msg.frm evt["name"] = name.group(1) evt["msg_before"] = texts.group (2) context.data.addChild(evt) diff --git a/modules/mapquest.py b/modules/mapquest.py index 55b87c0..1caa41c 100644 --- a/modules/mapquest.py +++ b/modules/mapquest.py @@ -55,7 +55,7 @@ def cmd_geocode(msg): if not len(msg.args): raise IMException("indicate a name") - res = Response(channel=msg.channel, nick=msg.nick, + res = Response(channel=msg.channel, nick=msg.frm, nomore="No more geocode", count=" (%s more geocode)") for loc in geocode(' '.join(msg.args)): diff --git a/modules/reddit.py b/modules/reddit.py index 7d481b7..31f566c 100644 --- a/modules/reddit.py +++ b/modules/reddit.py @@ -64,7 +64,7 @@ def cmd_subreddit(msg): channel=msg.channel)) else: all_res.append(Response("%s is not a valid subreddit" % osub, - channel=msg.channel, nick=msg.nick)) + channel=msg.channel, nick=msg.frm)) return all_res diff --git a/modules/rnd.py b/modules/rnd.py index 5329b06..6044bd4 100644 --- a/modules/rnd.py +++ b/modules/rnd.py @@ -21,7 +21,7 @@ def cmd_choice(msg): return Response(random.choice(msg.args), channel=msg.channel, - nick=msg.nick) + nick=msg.frm) @hook.command("choicecmd") diff --git a/modules/sms.py b/modules/sms.py index 3a9727f..61e63d6 100644 --- a/modules/sms.py +++ b/modules/sms.py @@ -73,9 +73,9 @@ def cmd_sms(msg): fails.append( "%s: %s" % (u, test) ) if len(fails) > 0: - return Response("quelque chose ne s'est pas bien passé durant l'envoi du SMS : " + ", ".join(fails), msg.channel, msg.nick) + return Response("quelque chose ne s'est pas bien passé durant l'envoi du SMS : " + ", ".join(fails), msg.channel, msg.frm) else: - return Response("le SMS a bien été envoyé", msg.channel, msg.nick) + return Response("le SMS a bien été envoyé", msg.channel, msg.frm) apiuser_ask = re.compile(r"(utilisateur|user|numéro|numero|compte|abonne|abone|abonné|account)\s+(est|is)\s+(?P<user>[0-9]{7,})", re.IGNORECASE) apikey_ask = re.compile(r"(clef|key|password|mot de passe?)\s+(?:est|is)?\s+(?P<key>[a-zA-Z0-9]{10,})", re.IGNORECASE) @@ -94,18 +94,18 @@ def parseask(msg): test = send_sms("nemubot", apiuser, apikey, "Vous avez enregistré vos codes d'authentification dans nemubot, félicitation !") if test is not None: - return Response("je n'ai pas pu enregistrer tes identifiants : %s" % test, msg.channel, msg.nick) + return Response("je n'ai pas pu enregistrer tes identifiants : %s" % test, msg.channel, msg.frm) - if msg.nick in context.data.index: - context.data.index[msg.nick]["user"] = apiuser - context.data.index[msg.nick]["key"] = apikey + if msg.frm in context.data.index: + context.data.index[msg.frm]["user"] = apiuser + context.data.index[msg.frm]["key"] = apikey else: ms = ModuleState("phone") - ms.setAttribute("name", msg.nick) + ms.setAttribute("name", msg.frm) ms.setAttribute("user", apiuser) ms.setAttribute("key", apikey) ms.setAttribute("lastuse", 0) context.data.addChild(ms) context.save() return Response("ok, c'est noté. Je t'ai envoyé un SMS pour tester ;)", - msg.channel, msg.nick) + msg.channel, msg.frm) diff --git a/modules/spell/__init__.py b/modules/spell/__init__.py index a70b016..c15f5fc 100644 --- a/modules/spell/__init__.py +++ b/modules/spell/__init__.py @@ -64,15 +64,15 @@ def cmd_spell(msg): raise IMException("Je n'ai pas le dictionnaire `%s' :(" % lang) if r == True: - add_score(msg.nick, "correct") + add_score(msg.frm, "correct") res.append_message("l'orthographe de `%s' est correcte" % word) elif len(r) > 0: - add_score(msg.nick, "bad") + add_score(msg.frm, "bad") res.append_message(r, title="suggestions pour `%s'" % word) else: - add_score(msg.nick, "bad") + add_score(msg.frm, "bad") res.append_message("aucune suggestion pour `%s'" % word) return res diff --git a/modules/virtualradar.py b/modules/virtualradar.py index ffd5a67..d7448ce 100644 --- a/modules/virtualradar.py +++ b/modules/virtualradar.py @@ -80,7 +80,7 @@ def cmd_flight(msg): if not len(msg.args): raise IMException("please indicate a flight") - res = Response(channel=msg.channel, nick=msg.nick, + res = Response(channel=msg.channel, nick=msg.frm, nomore="No more flights", count=" (%s more flights)") for param in msg.args: diff --git a/modules/weather.py b/modules/weather.py index 8b3540e..8c9ca0e 100644 --- a/modules/weather.py +++ b/modules/weather.py @@ -220,4 +220,4 @@ def parseask(msg): context.data.addChild(ms) context.save() return Response("ok, j'ai bien noté les coordonnées de %s" % res.group("city"), - msg.channel, msg.nick) + msg.channel, msg.frm) diff --git a/modules/whois.py b/modules/whois.py index fb6d250..ae27ccc 100644 --- a/modules/whois.py +++ b/modules/whois.py @@ -152,7 +152,7 @@ def parseask(msg): nick = res.group(1) login = res.group(3) if nick == "my" or nick == "I" or nick == "i" or nick == "je" or nick == "mon" or nick == "ma": - nick = msg.nick + nick = msg.frm if nick in context.data.getNode("aliases").index: context.data.getNode("aliases").index[nick]["to"] = login else: @@ -164,4 +164,4 @@ def parseask(msg): return Response("ok, c'est noté, %s est %s" % (nick, login), channel=msg.channel, - nick=msg.nick) + nick=msg.frm) diff --git a/modules/worldcup.py b/modules/worldcup.py index 7b4f53d..ff3e0c4 100644 --- a/modules/worldcup.py +++ b/modules/worldcup.py @@ -32,7 +32,7 @@ def start_watch(msg): w = ModuleState("watch") w["server"] = msg.server w["channel"] = msg.channel - w["proprio"] = msg.nick + w["proprio"] = msg.frm w["start"] = datetime.now(timezone.utc) context.data.addChild(w) context.save() diff --git a/nemubot/bot.py b/nemubot/bot.py index aa1cb3e..6327afe 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -92,7 +92,7 @@ class Bot(threading.Thread): def in_echo(msg): from nemubot.message import Text - return Text(msg.nick + ": " + " ".join(msg.args), to=msg.to_response) + return Text(msg.frm + ": " + " ".join(msg.args), to=msg.to_response) self.treater.hm.add_hook(nemubot.hooks.Command(in_echo, "echo"), "in", "Command") def _help_msg(msg): diff --git a/nemubot/channel.py b/nemubot/channel.py index a070131..835c22f 100644 --- a/nemubot/channel.py +++ b/nemubot/channel.py @@ -52,11 +52,11 @@ class Channel: elif cmd == "MODE": self.mode(msg) elif cmd == "JOIN": - self.join(msg.nick) + self.join(msg.frm) elif cmd == "NICK": - self.nick(msg.nick, msg.text) + self.nick(msg.frm, msg.text) elif cmd == "PART" or cmd == "QUIT": - self.part(msg.nick) + self.part(msg.frm) elif cmd == "TOPIC": self.topic = self.text @@ -120,17 +120,17 @@ class Channel: else: self.password = msg.text[1] elif msg.text[0] == "+o": - self.people[msg.nick] |= 4 + self.people[msg.frm] |= 4 elif msg.text[0] == "-o": - self.people[msg.nick] &= ~4 + self.people[msg.frm] &= ~4 elif msg.text[0] == "+h": - self.people[msg.nick] |= 2 + self.people[msg.frm] |= 2 elif msg.text[0] == "-h": - self.people[msg.nick] &= ~2 + self.people[msg.frm] &= ~2 elif msg.text[0] == "+v": - self.people[msg.nick] |= 1 + self.people[msg.frm] |= 1 elif msg.text[0] == "-v": - self.people[msg.nick] &= ~1 + self.people[msg.frm] &= ~1 def parse332(self, msg): """Parse RPL_TOPIC message diff --git a/nemubot/message/abstract.py b/nemubot/message/abstract.py index 6ee43d5..3af0511 100644 --- a/nemubot/message/abstract.py +++ b/nemubot/message/abstract.py @@ -59,12 +59,6 @@ class Abstract: else: return None - @property - def nick(self): - # TODO: this is for legacy modules - return self.frm - - def accept(self, visitor): visitor.visit(self) diff --git a/nemubot/server/IRC.py b/nemubot/server/IRC.py index 7469abc..7adc484 100644 --- a/nemubot/server/IRC.py +++ b/nemubot/server/IRC.py @@ -174,10 +174,10 @@ class _IRC: for chname in msg.params[0].split(b","): if chname in self.channels: - if msg.nick == self.nick: + if msg.frm == self.nick: del self.channels[chname] - elif msg.nick in self.channels[chname].people: - del self.channels[chname].people[msg.nick] + elif msg.frm in self.channels[chname].people: + del self.channels[chname].people[msg.frm] self.hookscmd["PART"] = _on_part # Respond to 331/RPL_NOTOPIC,332/RPL_TOPIC,TOPIC def _on_topic(msg): @@ -227,7 +227,7 @@ class _IRC: else: res = "ERRMSG Unknown or unimplemented CTCP request" if res is not None: - self.write("NOTICE %s :\x01%s\x01" % (msg.nick, res)) + self.write("NOTICE %s :\x01%s\x01" % (msg.frm, res)) self.hookscmd["PRIVMSG"] = _on_ctcp From a11ccb2e39b5a1402f82393446aacc0a4c0e51f0 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Tue, 18 Jul 2017 06:39:17 +0200 Subject: [PATCH 183/271] Remove legacy msg.cmds --- nemubot/message/command.py | 5 ----- nemubot/message/response.py | 5 ----- 2 files changed, 10 deletions(-) diff --git a/nemubot/message/command.py b/nemubot/message/command.py index 6c208b2..ca87e4c 100644 --- a/nemubot/message/command.py +++ b/nemubot/message/command.py @@ -31,11 +31,6 @@ class Command(Abstract): def __str__(self): return self.cmd + " @" + ",@".join(self.args) - @property - def cmds(self): - # TODO: this is for legacy modules - return [self.cmd] + self.args - class OwnerCommand(Command): diff --git a/nemubot/message/response.py b/nemubot/message/response.py index fba864b..f9353ad 100644 --- a/nemubot/message/response.py +++ b/nemubot/message/response.py @@ -27,8 +27,3 @@ class Response(Abstract): def __str__(self): return self.cmd + " @" + ",@".join(self.args) - - @property - def cmds(self): - # TODO: this is for legacy modules - return [self.cmd] + self.args From e49312e63e3c9b91cf77c2392705a6aabe4316af Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Tue, 18 Jul 2017 06:48:15 +0200 Subject: [PATCH 184/271] Remove legacy msg.text --- modules/birthday.py | 4 ++-- modules/events.py | 8 ++++---- modules/reddit.py | 11 +++++++++-- modules/sms.py | 10 +++++----- modules/urlreducer.py | 18 ++++++++++++++++-- modules/weather.py | 2 +- modules/whois.py | 2 +- 7 files changed, 38 insertions(+), 17 deletions(-) diff --git a/modules/birthday.py b/modules/birthday.py index 7a9cdaa..d8093b8 100644 --- a/modules/birthday.py +++ b/modules/birthday.py @@ -106,10 +106,10 @@ def cmd_age(msg): @hook.ask() def parseask(msg): - res = re.match(r"^(\S+)\s*('s|suis|est|is|was|were)?\s+(birthday|geburtstag|née? |nee? le|born on).*$", msg.text, re.I) + res = re.match(r"^(\S+)\s*('s|suis|est|is|was|were)?\s+(birthday|geburtstag|née? |nee? le|born on).*$", msg.message, re.I) if res is not None: try: - extDate = extractDate(msg.text) + extDate = extractDate(msg.message) if extDate is None or extDate.year > datetime.now().year: return Response("la date de naissance ne paraît pas valide...", msg.channel, diff --git a/modules/events.py b/modules/events.py index 0cc5a44..f6c6621 100644 --- a/modules/events.py +++ b/modules/events.py @@ -194,17 +194,17 @@ def parseanswer(msg): RGXP_ask = re.compile(r"^.*((create|new)\s+(a|an|a\s*new|an\s*other)?\s*(events?|commande?)|(nouvel(le)?|ajoute|cr[ée]{1,3})\s+(un)?\s*([eé]v[ée]nements?|commande?)).*$", re.I) -@hook.ask(match=lambda msg: RGXP_ask.match(msg.text)) +@hook.ask(match=lambda msg: RGXP_ask.match(msg.message)) def parseask(msg): - name = re.match("^.*!([^ \"'@!]+).*$", msg.text) + name = re.match("^.*!([^ \"'@!]+).*$", msg.message) if name is None: raise IMException("il faut que tu attribues une commande à l'événement.") if name.group(1) in context.data.index: raise IMException("un événement portant ce nom existe déjà.") - texts = re.match("^[^\"]*(avant|après|apres|before|after)?[^\"]*\"([^\"]+)\"[^\"]*((avant|après|apres|before|after)?.*\"([^\"]+)\".*)?$", msg.text, re.I) + texts = re.match("^[^\"]*(avant|après|apres|before|after)?[^\"]*\"([^\"]+)\"[^\"]*((avant|après|apres|before|after)?.*\"([^\"]+)\".*)?$", msg.message, re.I) if texts is not None and texts.group(3) is not None: - extDate = extractDate(msg.text) + extDate = extractDate(msg.message) if extDate is None or extDate == "": raise IMException("la date de l'événement est invalide !") diff --git a/modules/reddit.py b/modules/reddit.py index 31f566c..ae28999 100644 --- a/modules/reddit.py +++ b/modules/reddit.py @@ -71,8 +71,15 @@ def cmd_subreddit(msg): @hook.message() def parselisten(msg): - parseresponse(msg) - return None + global LAST_SUBS + + if hasattr(msg, "message") and msg.message and type(msg.message) == str: + urls = re.findall("www.reddit.com(/\w/\w+/?)", msg.message) + for url in urls: + for recv in msg.to: + if recv not in LAST_SUBS: + LAST_SUBS[recv] = list() + LAST_SUBS[recv].append(url) @hook.post() diff --git a/modules/sms.py b/modules/sms.py index 61e63d6..ca7e9f0 100644 --- a/modules/sms.py +++ b/modules/sms.py @@ -82,11 +82,11 @@ apikey_ask = re.compile(r"(clef|key|password|mot de passe?)\s+(?:est|is)?\s+(?P< @hook.ask() def parseask(msg): - if msg.text.find("Free") >= 0 and ( - msg.text.find("API") >= 0 or msg.text.find("api") >= 0) and ( - msg.text.find("SMS") >= 0 or msg.text.find("sms") >= 0): - resuser = apiuser_ask.search(msg.text) - reskey = apikey_ask.search(msg.text) + if msg.message.find("Free") >= 0 and ( + msg.message.find("API") >= 0 or msg.message.find("api") >= 0) and ( + msg.message.find("SMS") >= 0 or msg.message.find("sms") >= 0): + resuser = apiuser_ask.search(msg.message) + reskey = apikey_ask.search(msg.message) if resuser is not None and reskey is not None: apiuser = resuser.group("user") apikey = reskey.group("key") diff --git a/modules/urlreducer.py b/modules/urlreducer.py index 36fcb3c..bd7646b 100644 --- a/modules/urlreducer.py +++ b/modules/urlreducer.py @@ -84,8 +84,22 @@ LAST_URLS = dict() @hook.message() def parselisten(msg): - parseresponse(msg) - return None + global LAST_URLS + if hasattr(msg, "message") and isinstance(msg.message, str): + urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?(?:[^ :/]+:[0-9]+)?[^ :]+)", + msg.message) + for url in urls: + o = urlparse(web._getNormalizedURL(url), "http") + + # Skip short URLs + if (o.netloc == "" or o.netloc in PROVIDERS or + len(o.netloc) + len(o.path) < 17): + continue + + for recv in msg.to: + if recv not in LAST_URLS: + LAST_URLS[recv] = list() + LAST_URLS[recv].append(url) @hook.post() diff --git a/modules/weather.py b/modules/weather.py index 8c9ca0e..3f74b8e 100644 --- a/modules/weather.py +++ b/modules/weather.py @@ -203,7 +203,7 @@ gps_ask = re.compile(r"^\s*(?P<city>.*\w)\s*(?:(?:se|est)\s+(?:trouve|situ[ée]* @hook.ask() def parseask(msg): - res = gps_ask.match(msg.text) + res = gps_ask.match(msg.message) if res is not None: city_name = res.group("city").lower() gps_lat = res.group("lat").replace(",", ".") diff --git a/modules/whois.py b/modules/whois.py index ae27ccc..00eb940 100644 --- a/modules/whois.py +++ b/modules/whois.py @@ -147,7 +147,7 @@ def cmd_nicks(msg): @hook.ask() def parseask(msg): - res = re.match(r"^(\S+)\s*('s|suis|est|is|was|were)\s+([a-zA-Z0-9_-]{3,8})$", msg.text, re.I) + res = re.match(r"^(\S+)\s*('s|suis|est|is|was|were)\s+([a-zA-Z0-9_-]{3,8})$", msg.message, re.I) if res is not None: nick = res.group(1) login = res.group(3) From 45fe5b21561e664cac526dd6e8b0f02b8e27545e Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Tue, 18 Jul 2017 07:16:54 +0200 Subject: [PATCH 185/271] Refactor configuration loading --- nemubot/__main__.py | 60 ++++++++++++++++++++++++++++--- nemubot/bot.py | 76 ++++------------------------------------ nemubot/modulecontext.py | 2 +- 3 files changed, 63 insertions(+), 75 deletions(-) diff --git a/nemubot/__main__.py b/nemubot/__main__.py index e1576fb..8d51249 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -118,10 +118,10 @@ def main(): # Create bot context from nemubot import datastore - from nemubot.bot import Bot, sync_act + from nemubot.bot import Bot context = Bot(modules_paths=modules_paths, data_store=datastore.XML(args.data_path), - verbosity=args.verbose) + debug=args.verbose > 0) if args.no_connect: context.noautoconnect = True @@ -133,10 +133,34 @@ def main(): # Load requested configuration files for path in args.files: - if os.path.isfile(path): - sync_act("loadconf", path) - else: + if not os.path.isfile(path): logger.error("%s is not a readable file", path) + continue + + config = load_config(path) + + # Preset each server in this file + for server in config.servers: + srv = server.server(config) + # Add the server in the context + if context.add_server(srv): + 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: + context.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 + args.files += config.includes + if args.module: for module in args.module: @@ -205,5 +229,31 @@ def main(): sys.exit(0) +def load_config(filename): + """Load a configuration file + + Arguments: + filename -- the path to the file to load + """ + + 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, + }) + return p.parse_file(filename) + except: + logger.exception("Can't load `%s'; this is not a valid nemubot " + "configuration file.", filename) + return None + + if __name__ == "__main__": main() diff --git a/nemubot/bot.py b/nemubot/bot.py index 6327afe..7975958 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -40,14 +40,14 @@ class Bot(threading.Thread): """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): + data_store=datastore.Abstract(), debug=False): """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 modules data_store -- An instance of the nemubot datastore for bot's modules - verbosity -- verbosity level + debug -- enable debug """ super().__init__(name="Nemubot main") @@ -56,7 +56,7 @@ class Bot(threading.Thread): __version__, sys.version_info.major, sys.version_info.minor, sys.version_info.micro) - self.verbosity = verbosity + self.debug = debug self.stop = None # External IP for accessing this bot @@ -149,6 +149,10 @@ class Bot(threading.Thread): self.cnsr_thrd_size = -1 + def __del__(self): + self.datastore.close() + + def run(self): global sync_queue @@ -218,12 +222,6 @@ class Bot(threading.Thread): elif action == "launch_consumer": pass # This is treated after the loop - 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() @@ -240,64 +238,6 @@ class Bot(threading.Thread): - # 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): @@ -581,8 +521,6 @@ class Bot(threading.Thread): for cnsr in k: cnsr.stop = True - self.datastore.close() - if self.stop is False or sync_queue is not None: self.stop = True sync_act("end") diff --git a/nemubot/modulecontext.py b/nemubot/modulecontext.py index 877b8de..70e4b6f 100644 --- a/nemubot/modulecontext.py +++ b/nemubot/modulecontext.py @@ -108,7 +108,7 @@ class ModuleContext(_ModuleContext): self.config = context.modules_configuration[self.module_name] self.context = context - self.debug = context.verbosity > 0 + self.debug = context.debug def load_data(self): From f60de818f275f71e932d05d8056daf7f0f13cf2b Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Sun, 27 Aug 2017 18:22:53 +0200 Subject: [PATCH 186/271] Virtualy move all nemubot modules into nemubot.module.* hierarchy, to avoid conflict with system/vendor modules --- modules/alias.py | 2 +- modules/birthday.py | 2 +- modules/bonneannee.py | 3 +-- modules/books.py | 2 +- modules/cat.py | 2 +- modules/conjugaison.py | 2 +- modules/ctfs.py | 2 +- modules/cve.py | 2 +- modules/ddg.py | 2 +- modules/dig.py | 2 +- modules/disas.py | 2 +- modules/events.py | 2 +- modules/freetarifs.py | 2 +- modules/github.py | 2 +- modules/grep.py | 2 +- modules/imdb.py | 2 +- modules/jsonbot.py | 2 +- modules/man.py | 2 +- modules/mapquest.py | 2 +- modules/mediawiki.py | 2 +- modules/networking/__init__.py | 2 +- modules/networking/watchWebsite.py | 2 +- modules/networking/whois.py | 2 +- modules/news.py | 2 +- modules/openroute.py | 2 +- modules/pkgs.py | 2 +- modules/ratp.py | 2 +- modules/reddit.py | 2 +- modules/rnd.py | 2 +- modules/sap.py | 2 +- modules/shodan.py | 2 +- modules/sleepytime.py | 2 +- modules/sms.py | 2 +- modules/spell/__init__.py | 2 +- modules/suivi.py | 2 +- modules/syno.py | 2 +- modules/tpb.py | 2 +- modules/translate.py | 2 +- modules/urbandict.py | 2 +- modules/velib.py | 2 +- modules/virtualradar.py | 4 ++-- modules/weather.py | 4 ++-- modules/whois.py | 4 ++-- modules/wolframalpha.py | 2 +- modules/worldcup.py | 2 +- modules/youtube-title.py | 2 +- nemubot/__main__.py | 4 ++-- nemubot/bot.py | 11 ++++++----- nemubot/importer.py | 18 +++++++++--------- nemubot/module/__init__.py | 7 +++++++ {modules => nemubot/module}/more.py | 0 nemubot/modulecontext.py | 2 +- setup.py | 1 + 53 files changed, 75 insertions(+), 67 deletions(-) create mode 100644 nemubot/module/__init__.py rename {modules => nemubot/module}/more.py (100%) diff --git a/modules/alias.py b/modules/alias.py index 5aae6bb..a246d2c 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -12,7 +12,7 @@ from nemubot.message import Command from nemubot.tools.human import guess from nemubot.tools.xmlparser.node import ModuleState -from more import Response +from nemubot.module.more import Response # LOADING ############################################################# diff --git a/modules/birthday.py b/modules/birthday.py index d8093b8..e1406d4 100644 --- a/modules/birthday.py +++ b/modules/birthday.py @@ -13,7 +13,7 @@ from nemubot.tools.countdown import countdown_format from nemubot.tools.date import extractDate from nemubot.tools.xmlparser.node import ModuleState -from more import Response +from nemubot.module.more import Response # LOADING ############################################################# diff --git a/modules/bonneannee.py b/modules/bonneannee.py index b3b3934..1829bce 100644 --- a/modules/bonneannee.py +++ b/modules/bonneannee.py @@ -4,12 +4,11 @@ from datetime import datetime, timezone -from nemubot import context from nemubot.event import ModuleEvent from nemubot.hooks import hook from nemubot.tools.countdown import countdown_format -from more import Response +from nemubot.module.more import Response # GLOBALS ############################################################# diff --git a/modules/books.py b/modules/books.py index df48056..5ab404b 100644 --- a/modules/books.py +++ b/modules/books.py @@ -9,7 +9,7 @@ from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web -from more import Response +from nemubot.module.more import Response # LOADING ############################################################# diff --git a/modules/cat.py b/modules/cat.py index 0619cee..5eb3e19 100644 --- a/modules/cat.py +++ b/modules/cat.py @@ -7,7 +7,7 @@ from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.message import Command, DirectAsk, Text -from more import Response +from nemubot.module.more import Response # MODULE CORE ######################################################### diff --git a/modules/conjugaison.py b/modules/conjugaison.py index 25fe242..42d78c6 100644 --- a/modules/conjugaison.py +++ b/modules/conjugaison.py @@ -11,7 +11,7 @@ from nemubot.hooks import hook from nemubot.tools import web from nemubot.tools.web import striphtml -from more import Response +from nemubot.module.more import Response # GLOBALS ############################################################# diff --git a/modules/ctfs.py b/modules/ctfs.py index 1526cbc..169ee46 100644 --- a/modules/ctfs.py +++ b/modules/ctfs.py @@ -6,7 +6,7 @@ from bs4 import BeautifulSoup from nemubot.hooks import hook from nemubot.tools.web import getURLContent, striphtml -from more import Response +from nemubot.module.more import Response # GLOBALS ############################################################# diff --git a/modules/cve.py b/modules/cve.py index 6cdb339..b9cf1c3 100644 --- a/modules/cve.py +++ b/modules/cve.py @@ -9,7 +9,7 @@ from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools.web import getURLContent, striphtml -from more import Response +from nemubot.module.more import Response BASEURL_NIST = 'https://nvd.nist.gov/vuln/detail/' diff --git a/modules/ddg.py b/modules/ddg.py index d94bd61..089409b 100644 --- a/modules/ddg.py +++ b/modules/ddg.py @@ -8,7 +8,7 @@ from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web -from more import Response +from nemubot.module.more import Response # MODULE CORE ######################################################### diff --git a/modules/dig.py b/modules/dig.py index de7b2a3..bec0a87 100644 --- a/modules/dig.py +++ b/modules/dig.py @@ -14,7 +14,7 @@ import dns.resolver from nemubot.exception import IMException from nemubot.hooks import hook -from more import Response +from nemubot.module.more import Response # MODULE INTERFACE #################################################### diff --git a/modules/disas.py b/modules/disas.py index 7c17907..cb80ef3 100644 --- a/modules/disas.py +++ b/modules/disas.py @@ -7,7 +7,7 @@ import capstone from nemubot.exception import IMException from nemubot.hooks import hook -from more import Response +from nemubot.module.more import Response # MODULE CORE ######################################################### diff --git a/modules/events.py b/modules/events.py index f6c6621..9814aa2 100644 --- a/modules/events.py +++ b/modules/events.py @@ -12,7 +12,7 @@ from nemubot.tools.countdown import countdown_format, countdown from nemubot.tools.date import extractDate from nemubot.tools.xmlparser.node import ModuleState -from more import Response +from nemubot.module.more import Response def help_full (): diff --git a/modules/freetarifs.py b/modules/freetarifs.py index b96a30f..49ad8a6 100644 --- a/modules/freetarifs.py +++ b/modules/freetarifs.py @@ -9,7 +9,7 @@ from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web -from more import Response +from nemubot.module.more import Response # MODULE CORE ######################################################### diff --git a/modules/github.py b/modules/github.py index ddd0851..5f9a7d9 100644 --- a/modules/github.py +++ b/modules/github.py @@ -9,7 +9,7 @@ from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web -from more import Response +from nemubot.module.more import Response # MODULE CORE ######################################################### diff --git a/modules/grep.py b/modules/grep.py index 6a26c02..5c25c7d 100644 --- a/modules/grep.py +++ b/modules/grep.py @@ -9,7 +9,7 @@ from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.message import Command, Text -from more import Response +from nemubot.module.more import Response # MODULE CORE ######################################################### diff --git a/modules/imdb.py b/modules/imdb.py index bd1cadf..d5ff158 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -11,7 +11,7 @@ from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web -from more import Response +from nemubot.module.more import Response # MODULE CORE ######################################################### diff --git a/modules/jsonbot.py b/modules/jsonbot.py index fe25187..3126dc1 100644 --- a/modules/jsonbot.py +++ b/modules/jsonbot.py @@ -1,7 +1,7 @@ from nemubot.hooks import hook from nemubot.exception import IMException from nemubot.tools import web -from more import Response +from nemubot.module.more import Response import json nemubotversion = 3.4 diff --git a/modules/man.py b/modules/man.py index f45e30d..f60e0cf 100644 --- a/modules/man.py +++ b/modules/man.py @@ -8,7 +8,7 @@ import os from nemubot.hooks import hook -from more import Response +from nemubot.module.more import Response # GLOBALS ############################################################# diff --git a/modules/mapquest.py b/modules/mapquest.py index 1caa41c..5662a49 100644 --- a/modules/mapquest.py +++ b/modules/mapquest.py @@ -9,7 +9,7 @@ from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web -from more import Response +from nemubot.module.more import Response # GLOBALS ############################################################# diff --git a/modules/mediawiki.py b/modules/mediawiki.py index cb3d1da..be608ca 100644 --- a/modules/mediawiki.py +++ b/modules/mediawiki.py @@ -11,7 +11,7 @@ from nemubot.tools import web nemubotversion = 3.4 -from more import Response +from nemubot.module.more import Response # MEDIAWIKI REQUESTS ################################################## diff --git a/modules/networking/__init__.py b/modules/networking/__init__.py index f0df094..3b939ab 100644 --- a/modules/networking/__init__.py +++ b/modules/networking/__init__.py @@ -8,7 +8,7 @@ import re from nemubot.exception import IMException from nemubot.hooks import hook -from more import Response +from nemubot.module.more import Response from . import isup from . import page diff --git a/modules/networking/watchWebsite.py b/modules/networking/watchWebsite.py index 4945981..adedbee 100644 --- a/modules/networking/watchWebsite.py +++ b/modules/networking/watchWebsite.py @@ -12,7 +12,7 @@ from nemubot.tools.xmlparser.node import ModuleState logger = logging.getLogger("nemubot.module.networking.watchWebsite") -from more import Response +from nemubot.module.more import Response from . import page diff --git a/modules/networking/whois.py b/modules/networking/whois.py index d3d30b1..787cd17 100644 --- a/modules/networking/whois.py +++ b/modules/networking/whois.py @@ -6,7 +6,7 @@ import urllib from nemubot.exception import IMException from nemubot.tools.web import getJSON -from more import Response +from nemubot.module.more import Response URL_AVAIL = "https://www.whoisxmlapi.com/whoisserver/WhoisService?cmd=GET_DN_AVAILABILITY&domainName=%%s&outputFormat=json&username=%s&password=%s" URL_WHOIS = "http://www.whoisxmlapi.com/whoisserver/WhoisService?da=2&domainName=%%s&outputFormat=json&userName=%s&password=%s" diff --git a/modules/news.py b/modules/news.py index a8fb8de..40daa92 100644 --- a/modules/news.py +++ b/modules/news.py @@ -12,7 +12,7 @@ from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web -from more import Response +from nemubot.module.more import Response from nemubot.tools.feed import Feed, AtomEntry diff --git a/modules/openroute.py b/modules/openroute.py index 440b05a..c280dec 100644 --- a/modules/openroute.py +++ b/modules/openroute.py @@ -9,7 +9,7 @@ from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web -from more import Response +from nemubot.module.more import Response # GLOBALS ############################################################# diff --git a/modules/pkgs.py b/modules/pkgs.py index 5a7b0a9..386946f 100644 --- a/modules/pkgs.py +++ b/modules/pkgs.py @@ -8,7 +8,7 @@ from nemubot import context from nemubot.exception import IMException from nemubot.hooks import hook -from more import Response +from nemubot.module.more import Response DB = None diff --git a/modules/ratp.py b/modules/ratp.py index 7f4b211..06f5f1d 100644 --- a/modules/ratp.py +++ b/modules/ratp.py @@ -4,7 +4,7 @@ from nemubot.exception import IMException from nemubot.hooks import hook -from more import Response +from nemubot.module.more import Response from nextstop import ratp diff --git a/modules/reddit.py b/modules/reddit.py index ae28999..2de7612 100644 --- a/modules/reddit.py +++ b/modules/reddit.py @@ -10,7 +10,7 @@ from nemubot.tools import web nemubotversion = 3.4 -from more import Response +from nemubot.module.more import Response def help_full(): diff --git a/modules/rnd.py b/modules/rnd.py index 6044bd4..d1c6fe7 100644 --- a/modules/rnd.py +++ b/modules/rnd.py @@ -9,7 +9,7 @@ from nemubot import context from nemubot.exception import IMException from nemubot.hooks import hook -from more import Response +from nemubot.module.more import Response # MODULE INTERFACE #################################################### diff --git a/modules/sap.py b/modules/sap.py index 8691d6a..a6168a2 100644 --- a/modules/sap.py +++ b/modules/sap.py @@ -12,7 +12,7 @@ from nemubot.tools import web nemubotversion = 4.0 -from more import Response +from nemubot.module.more import Response def help_full(): diff --git a/modules/shodan.py b/modules/shodan.py index 4b2edae..9c158c6 100644 --- a/modules/shodan.py +++ b/modules/shodan.py @@ -11,7 +11,7 @@ from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web -from more import Response +from nemubot.module.more import Response # GLOBALS ############################################################# diff --git a/modules/sleepytime.py b/modules/sleepytime.py index 715b3b9..f7fb626 100644 --- a/modules/sleepytime.py +++ b/modules/sleepytime.py @@ -10,7 +10,7 @@ from nemubot.hooks import hook nemubotversion = 3.4 -from more import Response +from nemubot.module.more import Response def help_full(): diff --git a/modules/sms.py b/modules/sms.py index ca7e9f0..7db172b 100644 --- a/modules/sms.py +++ b/modules/sms.py @@ -16,7 +16,7 @@ from nemubot.tools.xmlparser.node import ModuleState nemubotversion = 3.4 -from more import Response +from nemubot.module.more import Response def load(context): context.data.setIndex("name", "phone") diff --git a/modules/spell/__init__.py b/modules/spell/__init__.py index c15f5fc..da16a80 100644 --- a/modules/spell/__init__.py +++ b/modules/spell/__init__.py @@ -10,7 +10,7 @@ from nemubot.tools.xmlparser.node import ModuleState from .pyaspell import Aspell from .pyaspell import AspellError -from more import Response +from nemubot.module.more import Response # LOADING ############################################################# diff --git a/modules/suivi.py b/modules/suivi.py index 6ad13e9..4bc079e 100644 --- a/modules/suivi.py +++ b/modules/suivi.py @@ -10,7 +10,7 @@ import re from nemubot.hooks import hook from nemubot.exception import IMException from nemubot.tools.web import getURLContent, getJSON -from more import Response +from nemubot.module.more import Response # POSTAGE SERVICE PARSERS ############################################ diff --git a/modules/syno.py b/modules/syno.py index 4bdc990..bda0456 100644 --- a/modules/syno.py +++ b/modules/syno.py @@ -9,7 +9,7 @@ from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web -from more import Response +from nemubot.module.more import Response # LOADING ############################################################# diff --git a/modules/tpb.py b/modules/tpb.py index ce98b04..a752324 100644 --- a/modules/tpb.py +++ b/modules/tpb.py @@ -8,7 +8,7 @@ from nemubot.tools.web import getJSON nemubotversion = 4.0 -from more import Response +from nemubot.module.more import Response URL_TPBAPI = None diff --git a/modules/translate.py b/modules/translate.py index 9d50966..906ba93 100644 --- a/modules/translate.py +++ b/modules/translate.py @@ -8,7 +8,7 @@ from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web -from more import Response +from nemubot.module.more import Response # GLOBALS ############################################################# diff --git a/modules/urbandict.py b/modules/urbandict.py index e90c096..a897fad 100644 --- a/modules/urbandict.py +++ b/modules/urbandict.py @@ -8,7 +8,7 @@ from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web -from more import Response +from nemubot.module.more import Response # MODULE CORE ######################################################### diff --git a/modules/velib.py b/modules/velib.py index 8ef6833..71c472c 100644 --- a/modules/velib.py +++ b/modules/velib.py @@ -9,7 +9,7 @@ from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web -from more import Response +from nemubot.module.more import Response # LOADING ############################################################# diff --git a/modules/virtualradar.py b/modules/virtualradar.py index d7448ce..9382d3b 100644 --- a/modules/virtualradar.py +++ b/modules/virtualradar.py @@ -10,8 +10,8 @@ from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web -from more import Response -import mapquest +from nemubot.module.more import Response +from nemubot.module import mapquest # GLOBALS ############################################################# diff --git a/modules/weather.py b/modules/weather.py index 3f74b8e..bee0d20 100644 --- a/modules/weather.py +++ b/modules/weather.py @@ -11,11 +11,11 @@ from nemubot.hooks import hook from nemubot.tools import web from nemubot.tools.xmlparser.node import ModuleState -import mapquest +from nemubot.module import mapquest nemubotversion = 4.0 -from more import Response +from nemubot.module.more import Response URL_DSAPI = "https://api.darksky.net/forecast/%s/%%s,%%s?lang=%%s&units=%%s" diff --git a/modules/whois.py b/modules/whois.py index 00eb940..d6106dd 100644 --- a/modules/whois.py +++ b/modules/whois.py @@ -10,8 +10,8 @@ from nemubot.tools.xmlparser.node import ModuleState nemubotversion = 3.4 -from more import Response -from networking.page import headers +from nemubot.module.more import Response +from nemubot.module.networking.page import headers PASSWD_FILE = None # You can get one with: curl -b "sessionid=YOURSESSIONID" 'https://accounts.cri.epita.net/api/users/' > users.json diff --git a/modules/wolframalpha.py b/modules/wolframalpha.py index e6bf86c..b7cc7fb 100644 --- a/modules/wolframalpha.py +++ b/modules/wolframalpha.py @@ -10,7 +10,7 @@ from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools import web -from more import Response +from nemubot.module.more import Response # LOADING ############################################################# diff --git a/modules/worldcup.py b/modules/worldcup.py index ff3e0c4..b12ca30 100644 --- a/modules/worldcup.py +++ b/modules/worldcup.py @@ -15,7 +15,7 @@ from nemubot.tools.xmlparser.node import ModuleState nemubotversion = 3.4 -from more import Response +from nemubot.module.more import Response API_URL="http://worldcup.sfg.io/%s" diff --git a/modules/youtube-title.py b/modules/youtube-title.py index fe62cda..41b613a 100644 --- a/modules/youtube-title.py +++ b/modules/youtube-title.py @@ -4,7 +4,7 @@ import re, json, subprocess from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools.web import _getNormalizedURL, getURLContent -from more import Response +from nemubot.module.more import Response """Get information of youtube videos""" diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 8d51249..b79d90e 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -153,7 +153,7 @@ def main(): context.modules_configuration[mod.name] = mod if mod.autoload: try: - __import__(mod.name) + __import__("nemubot.module." + mod.name) except: logger.exception("Exception occurs when loading module" " '%s'", mod.name) @@ -164,7 +164,7 @@ def main(): if args.module: for module in args.module: - __import__(module) + __import__("nemubot.module." + module) if args.socketfile: from nemubot.server.socket import UnixSocketListener diff --git a/nemubot/bot.py b/nemubot/bot.py index 7975958..d2e042c 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -97,18 +97,19 @@ class Bot(threading.Thread): def _help_msg(msg): """Parse and response to help messages""" - from more import Response + from nemubot.module.more import Response res = Response(channel=msg.to_response) if len(msg.args) >= 1: - if msg.args[0] in self.modules and self.modules[msg.args[0]]() is not None: - if hasattr(self.modules[msg.args[0]](), "help_full"): - hlp = self.modules[msg.args[0]]().help_full() + if "nemubot.module." + msg.args[0] in self.modules and self.modules["nemubot.module." + msg.args[0]]() is not None: + mname = "nemubot.module." + msg.args[0] + if hasattr(self.modules[mname](), "help_full"): + hlp = self.modules[mname]().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]) + res.append_message([str(h) for s,h in self.modules[mname]().__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:])): diff --git a/nemubot/importer.py b/nemubot/importer.py index eaf1535..674ab40 100644 --- a/nemubot/importer.py +++ b/nemubot/importer.py @@ -29,16 +29,16 @@ class ModuleFinder(Finder): self.add_module = add_module def find_module(self, fullname, path=None): - # Search only for new nemubot modules (packages init) - if path is None: + if path is not None and fullname.startswith("nemubot.module."): + module_name = fullname.split(".", 2)[2] for mpath in self.modules_paths: - if os.path.isfile(os.path.join(mpath, fullname + ".py")): + if os.path.isfile(os.path.join(mpath, module_name + ".py")): return ModuleLoader(self.add_module, fullname, - os.path.join(mpath, fullname + ".py")) - elif os.path.isfile(os.path.join(os.path.join(mpath, fullname), "__init__.py")): + os.path.join(mpath, module_name + ".py")) + elif os.path.isfile(os.path.join(os.path.join(mpath, module_name), "__init__.py")): return ModuleLoader(self.add_module, fullname, os.path.join( - os.path.join(mpath, fullname), + os.path.join(mpath, module_name), "__init__.py")) return None @@ -53,17 +53,17 @@ class ModuleLoader(SourceFileLoader): def _load(self, module, name): # Add the module to the global modules list self.add_module(module) - logger.info("Module '%s' successfully loaded.", name) + logger.info("Module '%s' successfully imported from %s.", name.split(".", 2)[2], self.path) return module # Python 3.4 def exec_module(self, module): - super(ModuleLoader, self).exec_module(module) + super().exec_module(module) self._load(module, module.__spec__.name) # Python 3.3 def load_module(self, fullname): - module = super(ModuleLoader, self).load_module(fullname) + module = super().load_module(fullname) return self._load(module, module.__name__) diff --git a/nemubot/module/__init__.py b/nemubot/module/__init__.py new file mode 100644 index 0000000..33f0e41 --- /dev/null +++ b/nemubot/module/__init__.py @@ -0,0 +1,7 @@ +# +# This directory aims to store nemubot core modules. +# +# Custom modules should be placed into a separate directory. +# By default, this is the directory modules in your current directory. +# Use the --modules-path argument to define a custom directory for your modules. +# diff --git a/modules/more.py b/nemubot/module/more.py similarity index 100% rename from modules/more.py rename to nemubot/module/more.py diff --git a/nemubot/modulecontext.py b/nemubot/modulecontext.py index 70e4b6f..d6291c4 100644 --- a/nemubot/modulecontext.py +++ b/nemubot/modulecontext.py @@ -20,7 +20,7 @@ class _ModuleContext: self.module = module if module is not None: - self.module_name = module.__spec__.name if hasattr(module, "__spec__") else module.__name__ + self.module_name = (module.__spec__.name if hasattr(module, "__spec__") else module.__name__).replace("nemubot.module.", "") else: self.module_name = "" diff --git a/setup.py b/setup.py index 36dddb4..94c1274 100755 --- a/setup.py +++ b/setup.py @@ -69,6 +69,7 @@ setup( 'nemubot.hooks.keywords', 'nemubot.message', 'nemubot.message.printer', + 'nemubot.module', 'nemubot.server', 'nemubot.server.message', 'nemubot.tools', From 05d20ed6ee70177da1df3286eaa708211c72d10a Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Mon, 4 Sep 2017 23:54:40 +0200 Subject: [PATCH 187/271] weather: handle units --- modules/weather.py | 68 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 53 insertions(+), 15 deletions(-) diff --git a/modules/weather.py b/modules/weather.py index bee0d20..96fd718 100644 --- a/modules/weather.py +++ b/modules/weather.py @@ -19,6 +19,41 @@ from nemubot.module.more import Response URL_DSAPI = "https://api.darksky.net/forecast/%s/%%s,%%s?lang=%%s&units=%%s" +UNITS = { + "ca": { + "temperature": "°C", + "distance": "km", + "precipIntensity": "mm/h", + "precip": "cm", + "speed": "km/h", + "pressure": "hPa", + }, + "uk2": { + "temperature": "°C", + "distance": "mi", + "precipIntensity": "mm/h", + "precip": "cm", + "speed": "mi/h", + "pressure": "hPa", + }, + "us": { + "temperature": "°F", + "distance": "mi", + "precipIntensity": "in/h", + "precip": "in", + "speed": "mi/h", + "pressure": "mbar", + }, + "si": { + "temperature": "°C", + "distance": "km", + "precipIntensity": "mm/h", + "precip": "cm", + "speed": "m/s", + "pressure": "hPa", + }, +} + def load(context): if not context.config or "darkskyapikey" not in context.config: raise ImportError("You need a Dark-Sky API key in order to use this " @@ -30,14 +65,17 @@ def load(context): URL_DSAPI = URL_DSAPI % context.config["darkskyapikey"] -def format_wth(wth): - return ("{temperature} °C {summary}; precipitation ({precipProbability:.0%} chance) intensity: {precipIntensity} mm/h; relative humidity: {humidity:.0%}; wind speed: {windSpeed} km/s {windBearing}°; cloud coverage: {cloudCover:.0%}; pressure: {pressure} hPa; ozone: {ozone} DU" - .format(**wth) +def format_wth(wth, flags): + units = UNITS[flags["units"] if "units" in flags and flags["units"] in UNITS else "si"] + return ("{temperature} {units[temperature]} {summary}; precipitation ({precipProbability:.0%} chance) intensity: {precipIntensity} {units[precipIntensity]}; relative humidity: {humidity:.0%}; wind speed: {windSpeed} {units[speed]} {windBearing}°; cloud coverage: {cloudCover:.0%}; pressure: {pressure} {units[pressure]}; ozone: {ozone} DU" + .format(units=units, **wth) ) -def format_forecast_daily(wth): - return ("{summary}; between {temperatureMin}-{temperatureMax} °C; precipitation ({precipProbability:.0%} chance) intensity: maximum {precipIntensity} mm/h; relative humidity: {humidity:.0%}; wind speed: {windSpeed} km/h {windBearing}°; cloud coverage: {cloudCover:.0%}; pressure: {pressure} hPa; ozone: {ozone} DU".format(**wth)) +def format_forecast_daily(wth, flags): + units = UNITS[flags["units"] if "units" in flags and flags["units"] in UNITS else "si"] + print(units) + return ("{summary}; between {temperatureMin}-{temperatureMax} {units[temperature]}; precipitation ({precipProbability:.0%} chance) intensity: maximum {precipIntensity} {units[precipIntensity]}; relative humidity: {humidity:.0%}; wind speed: {windSpeed} {units[speed]} {windBearing}°; cloud coverage: {cloudCover:.0%}; pressure: {pressure} {units[pressure]}; ozone: {ozone} DU".format(units=units, **wth)) def format_timestamp(timestamp, tzname, tzoffset, format="%c"): @@ -88,7 +126,7 @@ def treat_coord(msg): raise IMException("indique-moi un nom de ville ou des coordonnées.") -def get_json_weather(coords, lang="en", units="auto"): +def get_json_weather(coords, lang="en", units="ca"): wth = web.getJSON(URL_DSAPI % (float(coords[0]), float(coords[1]), lang, units)) # First read flags @@ -114,13 +152,13 @@ def cmd_coordinates(msg): @hook.command("alert", keywords={ "lang=LANG": "change the output language of weather sumarry; default: en", - "units=UNITS": "return weather conditions in the requested units; default: auto", + "units=UNITS": "return weather conditions in the requested units; default: ca", }) def cmd_alert(msg): loc, coords, specific = treat_coord(msg) wth = get_json_weather(coords, lang=msg.kwargs["lang"] if "lang" in msg.kwargs else "en", - units=msg.kwargs["units"] if "units" in msg.kwargs else "auto") + units=msg.kwargs["units"] if "units" in msg.kwargs else "ca") res = Response(channel=msg.channel, nomore="No more weather alert", count=" (%d more alerts)") @@ -141,13 +179,13 @@ def cmd_alert(msg): }, keywords={ "lang=LANG": "change the output language of weather sumarry; default: en", - "units=UNITS": "return weather conditions in the requested units; default: auto", + "units=UNITS": "return weather conditions in the requested units; default: ca", }) def cmd_weather(msg): loc, coords, specific = treat_coord(msg) wth = get_json_weather(coords, lang=msg.kwargs["lang"] if "lang" in msg.kwargs else "en", - units=msg.kwargs["units"] if "units" in msg.kwargs else "auto") + units=msg.kwargs["units"] if "units" in msg.kwargs else "ca") res = Response(channel=msg.channel, nomore="No more weather information") @@ -169,17 +207,17 @@ def cmd_weather(msg): if gr.group(2).lower() == "h" and gr1 < len(wth["hourly"]["data"]): hour = wth["hourly"]["data"][gr1] - res.append_message("\x03\x02At %sh:\x03\x02 %s" % (format_timestamp(int(hour["time"]), wth["timezone"], wth["offset"], '%H'), format_wth(hour))) + res.append_message("\x03\x02At %sh:\x03\x02 %s" % (format_timestamp(int(hour["time"]), wth["timezone"], wth["offset"], '%H'), format_wth(hour, wth["flags"]))) elif gr.group(2).lower() == "d" and gr1 < len(wth["daily"]["data"]): day = wth["daily"]["data"][gr1] - res.append_message("\x03\x02On %s:\x03\x02 %s" % (format_timestamp(int(day["time"]), wth["timezone"], wth["offset"], '%A'), format_forecast_daily(day))) + res.append_message("\x03\x02On %s:\x03\x02 %s" % (format_timestamp(int(day["time"]), wth["timezone"], wth["offset"], '%A'), format_forecast_daily(day, wth["flags"]))) else: res.append_message("I don't understand %s or information is not available" % specific) else: - res.append_message("\x03\x02Currently:\x03\x02 " + format_wth(wth["currently"])) + res.append_message("\x03\x02Currently:\x03\x02 " + format_wth(wth["currently"], wth["flags"])) nextres = "\x03\x02Today:\x03\x02 %s " % wth["daily"]["data"][0]["summary"] if "minutely" in wth: @@ -189,11 +227,11 @@ def cmd_weather(msg): for hour in wth["hourly"]["data"][1:4]: res.append_message("\x03\x02At %sh:\x03\x02 %s" % (format_timestamp(int(hour["time"]), wth["timezone"], wth["offset"], '%H'), - format_wth(hour))) + format_wth(hour, wth["flags"]))) for day in wth["daily"]["data"][1:]: res.append_message("\x03\x02On %s:\x03\x02 %s" % (format_timestamp(int(day["time"]), wth["timezone"], wth["offset"], '%A'), - format_forecast_daily(day))) + format_forecast_daily(day, wth["flags"]))) return res From 7a4b27510c701be8bc633f075d57e94cca01d8e8 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 13 Sep 2017 08:03:47 +0200 Subject: [PATCH 188/271] Replace logger by _logger in servers --- nemubot/server/DCC.py | 12 ++++++------ nemubot/server/IRC.py | 6 +++--- nemubot/server/abstract.py | 9 +++++---- nemubot/server/socket.py | 10 +++++----- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/nemubot/server/DCC.py b/nemubot/server/DCC.py index c1a6852..f5d4b8f 100644 --- a/nemubot/server/DCC.py +++ b/nemubot/server/DCC.py @@ -53,7 +53,7 @@ class DCC(server.AbstractServer): self.port = self.foundPort() if self.port is None: - self.logger.critical("No more available slot for DCC connection") + self._logger.critical("No more available slot for DCC connection") self.setError("Il n'y a plus de place disponible sur le serveur" " pour initialiser une session DCC.") @@ -79,7 +79,7 @@ class DCC(server.AbstractServer): self.s = socket.socket() try: self.s.connect((host, port)) - self.logger.info("Accepted user from %s:%d for %s", host, port, self.sender) + self._logger.info("Accepted user from %s:%d for %s", host, port, self.sender) self.connected = True self.stop = False except: @@ -104,7 +104,7 @@ class DCC(server.AbstractServer): self.setError("Une erreur s'est produite durant la tentative" " d'ouverture d'une session DCC.") return False - self.logger.info("Listening on %d for %s", self.port, self.sender) + self._logger.info("Listening on %d for %s", self.port, self.sender) #Send CTCP request for DCC self.srv.send_ctcp(self.sender, @@ -115,7 +115,7 @@ class DCC(server.AbstractServer): s.listen(1) #Waiting for the client (self.s, addr) = s.accept() - self.logger.info("Connected by %d", addr) + self._logger.info("Connected by %d", addr) self.connected = True return True @@ -149,7 +149,7 @@ class DCC(server.AbstractServer): except RuntimeError: pass else: - self.logger.error("File not found `%s'", filename) + self._logger.error("File not found `%s'", filename) def run(self): self.stopping.clear() @@ -202,7 +202,7 @@ class DCC(server.AbstractServer): if self.realname in self.srv.dcc_clients: del self.srv.dcc_clients[self.realname] - self.logger.info("Closing connection with %s", self.nick) + self._logger.info("Closing connection with %s", self.nick) self.stopping.set() if self.closing_event is not None: self.closing_event() diff --git a/nemubot/server/IRC.py b/nemubot/server/IRC.py index 7adc484..6b90bbb 100644 --- a/nemubot/server/IRC.py +++ b/nemubot/server/IRC.py @@ -84,13 +84,13 @@ class _IRC: except: return "ERRMSG invalid parameters provided as DCC CTCP request" - self.logger.info("Receive DCC connection request from %s to %s:%d", conn.sender, ip, port) + self._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: - self.logger.error("DCC: unable to connect to %s:%d", ip, port) + self._logger.error("DCC: unable to connect to %s:%d", ip, port) return "ERRMSG unable to connect to %s:%d" % (ip, port) import nemubot @@ -109,7 +109,7 @@ class _IRC: # TODO: Temporary fix, waiting for hook based CTCP management self.ctcp_capabilities["TYPING"] = lambda msg, cmds: None - self.logger.debug("CTCP capabilities setup: %s", ", ".join(self.ctcp_capabilities)) + self._logger.debug("CTCP capabilities setup: %s", ", ".join(self.ctcp_capabilities)) # Register hooks on some IRC CMD diff --git a/nemubot/server/abstract.py b/nemubot/server/abstract.py index fd25c2d..814461c 100644 --- a/nemubot/server/abstract.py +++ b/nemubot/server/abstract.py @@ -32,10 +32,11 @@ class AbstractServer: """ self._name = name + self._socket = socket super().__init__(**kwargs) - self.logger = logging.getLogger("nemubot.server." + str(self.name)) + self._logger = logging.getLogger("nemubot.server." + str(self.name)) self._readbuffer = b'' self._sending_queue = queue.Queue() @@ -53,7 +54,7 @@ class AbstractServer: def connect(self, *args, **kwargs): """Register the server in _poll""" - self.logger.info("Opening connection") + self._logger.info("Opening connection") super().connect(*args, **kwargs) @@ -66,7 +67,7 @@ class AbstractServer: def close(self, *args, **kwargs): """Unregister the server from _poll""" - self.logger.info("Closing connection") + self._logger.info("Closing connection") if self.fileno() > 0: sync_act("sckt", "unregister", self.fileno()) @@ -84,7 +85,7 @@ class AbstractServer: """ self._sending_queue.put(self.format(message)) - self.logger.debug("Message '%s' appended to write queue", message) + self._logger.debug("Message '%s' appended to write queue", message) sync_act("sckt", "write", self.fileno()) diff --git a/nemubot/server/socket.py b/nemubot/server/socket.py index 8a0950c..a803bb2 100644 --- a/nemubot/server/socket.py +++ b/nemubot/server/socket.py @@ -90,7 +90,7 @@ class _SocketServer(_Socket): def connect(self): - self.logger.info("Connection to %s:%d", *self._sockaddr[:2]) + self._logger.info("Connection to %s:%d", *self._sockaddr[:2]) super().connect(self._sockaddr) if self._bind: @@ -114,7 +114,7 @@ class UnixSocket: def connect(self): - self.logger.info("Connection to unix://%s", self._socket_path) + self._logger.info("Connection to unix://%s", self._socket_path) super().connect(self._socket_path) @@ -136,7 +136,7 @@ class _Listener: def read(self): conn, addr = self.accept() fileno = conn.fileno() - self.logger.info("Accept new connection from %s (fd=%d)", addr, fileno) + self._logger.info("Accept new connection from %s (fd=%d)", addr, fileno) ss = self._instanciate(name=self.name + "#" + str(fileno), fileno=conn.detach()) ss.connect = ss._on_connect @@ -152,7 +152,7 @@ class UnixSocketListener(_Listener, UnixSocket, _Socket, socket.socket): def connect(self): - self.logger.info("Creating Unix socket at unix://%s", self._socket_path) + self._logger.info("Creating Unix socket at unix://%s", self._socket_path) try: os.remove(self._socket_path) @@ -161,7 +161,7 @@ class UnixSocketListener(_Listener, UnixSocket, _Socket, socket.socket): self.bind(self._socket_path) self.listen(5) - self.logger.info("Socket ready for accepting new connections") + self._logger.info("Socket ready for accepting new connections") self._on_connect() From 12ddf40ef4e05b8b20e4f9826ac127af921734d4 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 16 Sep 2017 19:32:58 +0200 Subject: [PATCH 189/271] servers: use proxy design pattern instead of inheritance, because Python ssl patch has benn refused --- README.md | 2 -- nemubot/server/IRC.py | 13 +++------ nemubot/server/__init__.py | 28 +++++++++---------- nemubot/server/abstract.py | 32 ++++++++++++---------- nemubot/server/socket.py | 55 ++++++++++++++++---------------------- 5 files changed, 57 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index 1d40faf..aa3b141 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,6 @@ Requirements *nemubot* requires at least Python 3.3 to work. -Connecting to SSL server requires [this patch](http://bugs.python.org/issue27629). - Some modules (like `cve`, `nextstop` or `laposte`) require the [BeautifulSoup module](http://www.crummy.com/software/BeautifulSoup/), but the core and framework has no dependency. diff --git a/nemubot/server/IRC.py b/nemubot/server/IRC.py index 6b90bbb..2096a63 100644 --- a/nemubot/server/IRC.py +++ b/nemubot/server/IRC.py @@ -21,10 +21,10 @@ import socket from nemubot.channel import Channel from nemubot.message.printer.IRC import IRC as IRCPrinter from nemubot.server.message.IRC import IRC as IRCMessage -from nemubot.server.socket import SocketServer, SecureSocketServer +from nemubot.server.socket import SocketServer -class _IRC: +class IRC(SocketServer): """Concrete implementation of a connexion to an IRC server""" @@ -245,7 +245,7 @@ class _IRC: def close(self): - if not self._closed: + if not self._fd._closed: self.write("QUIT") return super().close() @@ -274,10 +274,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 a533491..068d152 100644 --- a/nemubot/server/__init__.py +++ b/nemubot/server/__init__.py @@ -32,16 +32,6 @@ def factory(uri, ssl=False, **init_args): if o.username is not None: args["username"] = o.username if o.password is not None: args["password"] = o.password - if ssl: - try: - from ssl import create_default_context - args["_context"] = create_default_context() - except ImportError: - # Python 3.3 compat - from ssl import SSLContext, PROTOCOL_TLSv1 - args["_context"] = SSLContext(PROTOCOL_TLSv1) - args["server_hostname"] = o.hostname - modifiers = o.path.split(",") target = unquote(modifiers.pop(0)[1:]) @@ -68,11 +58,19 @@ def factory(uri, ssl=False, **init_args): if "channels" not in args and "isnick" not in modifiers: args["channels"] = [ target ] + from nemubot.server.IRC import IRC as IRCServer + srv = IRCServer(**args) + if ssl: - from nemubot.server.IRC import IRC_secure as SecureIRCServer - srv = SecureIRCServer(**args) - else: - from nemubot.server.IRC import IRC as IRCServer - srv = IRCServer(**args) + try: + from ssl import create_default_context + context = create_default_context() + except ImportError: + # Python 3.3 compat + from ssl import SSLContext, PROTOCOL_TLSv1 + context = SSLContext(PROTOCOL_TLSv1) + from ssl import wrap_socket + srv._fd = context.wrap_socket(srv._fd, server_hostname=o.hostname) + return srv diff --git a/nemubot/server/abstract.py b/nemubot/server/abstract.py index 814461c..433068d 100644 --- a/nemubot/server/abstract.py +++ b/nemubot/server/abstract.py @@ -24,17 +24,16 @@ class AbstractServer: """An abstract server: handle communication with an IM server""" - def __init__(self, name=None, **kwargs): + def __init__(self, name, fdClass, **kwargs): """Initialize an abstract server Keyword argument: name -- Identifier of the socket, for convinience + fdClass -- Class to instantiate as support file """ self._name = name - self._socket = socket - - super().__init__(**kwargs) + self._fd = fdClass(**kwargs) self._logger = logging.getLogger("nemubot.server." + str(self.name)) self._readbuffer = b'' @@ -46,7 +45,7 @@ class AbstractServer: if self._name is not None: return self._name else: - return self.fileno() + return self._fd.fileno() # Open/close @@ -56,12 +55,12 @@ class AbstractServer: self._logger.info("Opening connection") - super().connect(*args, **kwargs) + self._fd.connect(*args, **kwargs) self._on_connect() def _on_connect(self): - sync_act("sckt", "register", self.fileno()) + sync_act("sckt", "register", self._fd.fileno()) def close(self, *args, **kwargs): @@ -69,10 +68,10 @@ class AbstractServer: self._logger.info("Closing connection") - if self.fileno() > 0: - sync_act("sckt", "unregister", self.fileno()) + if self._fd.fileno() > 0: + sync_act("sckt", "unregister", self._fd.fileno()) - super().close(*args, **kwargs) + self._fd.close(*args, **kwargs) # Writes @@ -86,14 +85,14 @@ class AbstractServer: self._sending_queue.put(self.format(message)) self._logger.debug("Message '%s' appended to write queue", message) - sync_act("sckt", "write", self.fileno()) + sync_act("sckt", "write", self._fd.fileno()) def async_write(self): """Internal function used when the file descriptor is writable""" try: - sync_act("sckt", "unwrite", self.fileno()) + sync_act("sckt", "unwrite", self._fd.fileno()) while not self._sending_queue.empty(): self._write(self._sending_queue.get_nowait()) self._sending_queue.task_done() @@ -131,7 +130,7 @@ class AbstractServer: A list of fully received messages """ - ret, self._readbuffer = self.lex(self._readbuffer + self.read()) + ret, self._readbuffer = self.lex(self._readbuffer + self._fd.read()) for r in ret: yield r @@ -159,4 +158,9 @@ class AbstractServer: def exception(self, flags): """Exception occurs on fd""" - self.close() + self._fd.close() + + # Proxy + + def fileno(self): + return self._fd.fileno() diff --git a/nemubot/server/socket.py b/nemubot/server/socket.py index a803bb2..9cf7c18 100644 --- a/nemubot/server/socket.py +++ b/nemubot/server/socket.py @@ -16,7 +16,6 @@ import os import socket -import ssl import nemubot.message as message from nemubot.message.printer.socket import Socket as SocketPrinter @@ -40,7 +39,7 @@ class _Socket(AbstractServer): # Write def _write(self, cnt): - self.sendall(cnt) + self._fd.sendall(cnt) def format(self, txt): @@ -52,8 +51,8 @@ class _Socket(AbstractServer): # Read - def recv(self, n=1024): - return super().recv(n) + def recv(self, *args, **kwargs): + return self._fd.recv(*args, **kwargs) def parse(self, line): @@ -67,7 +66,7 @@ class _Socket(AbstractServer): args = line.split(' ') if len(args): - yield message.Command(cmd=args[0], args=args[1:], server=self.fileno(), to=["you"], frm="you") + yield message.Command(cmd=args[0], args=args[1:], server=self._fd.fileno(), to=["you"], frm="you") def subparse(self, orig, cnt): @@ -78,50 +77,46 @@ class _Socket(AbstractServer): yield m -class _SocketServer(_Socket): +class SocketServer(_Socket): def __init__(self, host, port, bind=None, **kwargs): - (family, type, proto, canonname, sockaddr) = socket.getaddrinfo(host, port, proto=socket.IPPROTO_TCP)[0] + (family, type, proto, canonname, self._sockaddr) = socket.getaddrinfo(host, port, proto=socket.IPPROTO_TCP)[0] - super().__init__(family=family, type=type, proto=proto, **kwargs) + super().__init__(fdClass=socket.socket, family=family, type=type, proto=proto, **kwargs) - self._sockaddr = sockaddr self._bind = bind def connect(self): - self._logger.info("Connection to %s:%d", *self._sockaddr[:2]) + self._logger.info("Connecting to %s:%d", *self._sockaddr[:2]) super().connect(self._sockaddr) + self._logger.info("Connected to %s:%d", *self._sockaddr[:2]) if self._bind: - super().bind(self._bind) - - -class SocketServer(_SocketServer, socket.socket): - pass - - -class SecureSocketServer(_SocketServer, ssl.SSLSocket): - pass + self._fd.bind(self._bind) class UnixSocket: def __init__(self, location, **kwargs): - super().__init__(family=socket.AF_UNIX, **kwargs) + super().__init__(fdClass=socket.socket, family=socket.AF_UNIX, **kwargs) self._socket_path = location def connect(self): self._logger.info("Connection to unix://%s", self._socket_path) - super().connect(self._socket_path) + self.connect(self._socket_path) -class SocketClient(_Socket, socket.socket): +class SocketClient(_Socket): + + def __init__(self, **kwargs): + super().__init__(fdClass=socket.socket, **kwargs) + def read(self): - return self.recv() + return self._fd.recv() class _Listener: @@ -134,7 +129,7 @@ class _Listener: def read(self): - conn, addr = self.accept() + conn, addr = self._fd.accept() fileno = conn.fileno() self._logger.info("Accept new connection from %s (fd=%d)", addr, fileno) @@ -145,11 +140,7 @@ class _Listener: return b'' -class UnixSocketListener(_Listener, UnixSocket, _Socket, socket.socket): - - def __init__(self, **kwargs): - super().__init__(**kwargs) - +class UnixSocketListener(_Listener, UnixSocket, _Socket): def connect(self): self._logger.info("Creating Unix socket at unix://%s", self._socket_path) @@ -159,8 +150,8 @@ class UnixSocketListener(_Listener, UnixSocket, _Socket, socket.socket): except FileNotFoundError: pass - self.bind(self._socket_path) - self.listen(5) + self._fd.bind(self._socket_path) + self._fd.listen(5) self._logger.info("Socket ready for accepting new connections") self._on_connect() @@ -171,7 +162,7 @@ class UnixSocketListener(_Listener, UnixSocket, _Socket, socket.socket): import socket try: - self.shutdown(socket.SHUT_RDWR) + self._fd.shutdown(socket.SHUT_RDWR) except socket.error: pass From 62cd92e1cbea9b8876634799afc14bb15995703c Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Sun, 17 Sep 2017 18:09:35 +0200 Subject: [PATCH 190/271] server: Rework factory tests --- nemubot/server/factory_test.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/nemubot/server/factory_test.py b/nemubot/server/factory_test.py index 358591e..efc3130 100644 --- a/nemubot/server/factory_test.py +++ b/nemubot/server/factory_test.py @@ -22,30 +22,32 @@ class TestFactory(unittest.TestCase): def test_IRC1(self): from nemubot.server.IRC import IRC as IRCServer - from nemubot.server.IRC import IRC_secure as IRCSServer + import socket + import ssl # <host>: If omitted, the client must connect to a prespecified default IRC server. server = factory("irc:///") self.assertIsInstance(server, IRCServer) - self.assertEqual(server.host, "localhost") + self.assertIsInstance(server._fd, socket.socket) + self.assertIn(server._sockaddr[0], ["127.0.0.1", "::1"]) - server = factory("ircs:///") - self.assertIsInstance(server, IRCSServer) - self.assertEqual(server.host, "localhost") - - server = factory("irc://host1") + server = factory("irc://2.2.2.2") self.assertIsInstance(server, IRCServer) - self.assertEqual(server.host, "host1") + self.assertEqual(server._sockaddr[0], "2.2.2.2") - server = factory("irc://host2:6667") + server = factory("ircs://1.2.1.2") self.assertIsInstance(server, IRCServer) - self.assertEqual(server.host, "host2") - self.assertEqual(server.port, 6667) + self.assertIsInstance(server._fd, ssl.SSLSocket) - server = factory("ircs://host3:194/") - self.assertIsInstance(server, IRCSServer) - self.assertEqual(server.host, "host3") - self.assertEqual(server.port, 194) + server = factory("irc://1.2.3.4:6667") + self.assertIsInstance(server, IRCServer) + self.assertEqual(server._sockaddr[0], "1.2.3.4") + self.assertEqual(server._sockaddr[1], 6667) + + server = factory("ircs://4.3.2.1:194/") + self.assertIsInstance(server, IRCServer) + self.assertEqual(server._sockaddr[0], "4.3.2.1") + self.assertEqual(server._sockaddr[1], 194) if __name__ == '__main__': From 28d4e507eb9e53654e4516fb298e2110cd9d69e2 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 25 Sep 2017 23:56:28 +0200 Subject: [PATCH 191/271] servers: call recv late --- nemubot/server/abstract.py | 2 +- nemubot/server/socket.py | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/nemubot/server/abstract.py b/nemubot/server/abstract.py index 433068d..510a319 100644 --- a/nemubot/server/abstract.py +++ b/nemubot/server/abstract.py @@ -130,7 +130,7 @@ class AbstractServer: A list of fully received messages """ - ret, self._readbuffer = self.lex(self._readbuffer + self._fd.read()) + ret, self._readbuffer = self.lex(self._readbuffer + self.read()) for r in ret: yield r diff --git a/nemubot/server/socket.py b/nemubot/server/socket.py index 9cf7c18..a6be620 100644 --- a/nemubot/server/socket.py +++ b/nemubot/server/socket.py @@ -51,8 +51,8 @@ class _Socket(AbstractServer): # Read - def recv(self, *args, **kwargs): - return self._fd.recv(*args, **kwargs) + def read(self, bufsize=1024, *args, **kwargs): + return self._fd.recv(bufsize, *args, **kwargs) def parse(self, line): @@ -115,10 +115,6 @@ class SocketClient(_Socket): super().__init__(fdClass=socket.socket, **kwargs) - def read(self): - return self._fd.recv() - - class _Listener: def __init__(self, new_server_cb, instanciate=SocketClient, **kwargs): From 30ec9121628950c9b2be4f60ef1e374a27362724 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Tue, 26 Sep 2017 19:35:31 +0200 Subject: [PATCH 192/271] daemonize: fork client before loading context --- nemubot/__init__.py | 23 ++++++----------------- nemubot/__main__.py | 18 ++++++++++++++++-- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/nemubot/__init__.py b/nemubot/__init__.py index 48de6ea..62807c6 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -39,10 +39,14 @@ def requires_version(min=None, max=None): "but this is nemubot v%s." % (str(max), __version__)) -def attach(pid, socketfile): +def attach(pidfile, socketfile): import socket import sys + # Read PID from pidfile + with open(pidfile, "r") as f: + pid = int(f.readline()) + print("nemubot is launched with PID %d. Attaching to Unix socket at: %s" % (pid, socketfile)) sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) @@ -106,28 +110,13 @@ def attach(pid, socketfile): return 0 -def daemonize(socketfile=None, autoattach=True): +def daemonize(socketfile=None): """Detach the running process to run as a daemon """ import os import sys - if socketfile is not None: - try: - pid = os.fork() - if pid > 0: - if autoattach: - import time - os.waitpid(pid, 0) - time.sleep(1) - sys.exit(attach(pid, socketfile)) - else: - sys.exit(0) - except OSError as err: - sys.stderr.write("Unable to fork: %s\n" % err) - sys.exit(1) - try: pid = os.fork() if pid > 0: diff --git a/nemubot/__main__.py b/nemubot/__main__.py index b79d90e..abb290b 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -77,6 +77,20 @@ def main(): 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)] + # Prepare the attached client, before setting other stuff + if not args.debug and not args.no_attach and args.socketfile is not None and args.pidfile is not None: + try: + pid = os.fork() + if pid > 0: + import time + os.waitpid(pid, 0) + time.sleep(1) + from nemubot import attach + sys.exit(attach(args.pidfile, args.socketfile)) + except OSError as err: + sys.stderr.write("Unable to fork: %s\n" % err) + sys.exit(1) + # Setup logging interface import logging logger = logging.getLogger("nemubot") @@ -106,7 +120,7 @@ def main(): pass else: from nemubot import attach - sys.exit(attach(pid, args.socketfile)) + sys.exit(attach(args.pidfile, args.socketfile)) # Add modules dir paths modules_paths = list() @@ -175,7 +189,7 @@ def main(): # Daemonize if not args.debug: from nemubot import daemonize - daemonize(args.socketfile, not args.no_attach) + daemonize(args.socketfile) # Signals handling def sigtermhandler(signum, frame): From 5646850df11be31a9b0e98e2a7bc20348a0e922d Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Tue, 26 Sep 2017 23:51:48 +0200 Subject: [PATCH 193/271] Don't launch timer thread before bot launch --- nemubot/bot.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/nemubot/bot.py b/nemubot/bot.py index d2e042c..c9c0e66 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -57,7 +57,7 @@ class Bot(threading.Thread): sys.version_info.major, sys.version_info.minor, sys.version_info.micro) self.debug = debug - self.stop = None + self.stop = True # External IP for accessing this bot import ipaddress @@ -164,8 +164,13 @@ class Bot(threading.Thread): self._poll.register(sync_queue._reader, select.POLLIN | select.POLLPRI) - logger.info("Starting main loop") + self.stop = False + + # Relaunch events + self._update_event_timer() + + logger.info("Starting main loop") while not self.stop: for fd, flag in self._poll.poll(): # Handle internal socket passing orders @@ -256,10 +261,6 @@ class Bot(threading.Thread): 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 @@ -286,7 +287,7 @@ class Bot(threading.Thread): break self.events.insert(i, evt) - if i == 0: + if i == 0 and not self.stop: # First event changed, reset timer self._update_event_timer() if len(self.events) <= 0 or self.events[i] != evt: @@ -417,10 +418,6 @@ class Bot(threading.Thread): 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) From b15d18b3a5c7b0c42e1a8d382c60fdc371a3419a Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Wed, 27 Sep 2017 00:13:26 +0200 Subject: [PATCH 194/271] events: fix event removal --- nemubot/bot.py | 8 ++++---- nemubot/consumer.py | 2 +- nemubot/modulecontext.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/nemubot/bot.py b/nemubot/bot.py index c9c0e66..28df8ce 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -296,7 +296,7 @@ class Bot(threading.Thread): # Register the event in the source module if module_src is not None: - module_src.__nemubot_context__.events.append(evt.id) + module_src.__nemubot_context__.events.append((evt, evt.id)) evt.module_src = module_src logger.info("New event registered in %d position: %s", i, t) @@ -326,10 +326,10 @@ class Bot(threading.Thread): id = evt if len(self.events) > 0 and id == self.events[0].id: + if module_src is not None: + module_src.__nemubot_context__.events.remove((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: @@ -337,7 +337,7 @@ class Bot(threading.Thread): self.events.remove(evt) if module_src is not None: - module_src.__nemubot_context__.events.remove(evt.id) + module_src.__nemubot_context__.events.remove((evt, evt.id)) return True return False diff --git a/nemubot/consumer.py b/nemubot/consumer.py index 2765aff..3a58219 100644 --- a/nemubot/consumer.py +++ b/nemubot/consumer.py @@ -94,7 +94,7 @@ class EventConsumer: # 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) + self.evt.module_src.__nemubot_context__.events.remove((self.evt, self.evt.id)) diff --git a/nemubot/modulecontext.py b/nemubot/modulecontext.py index d6291c4..f39934c 100644 --- a/nemubot/modulecontext.py +++ b/nemubot/modulecontext.py @@ -85,7 +85,7 @@ class _ModuleContext: self.del_hook(h, *s) # Remove registered events - for evt, eid, module_src in self.events: + for evt, eid in self.events: self.del_event(evt) self.save() From bb0e958118f917907553075426c7d004dac2306c Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Tue, 3 Oct 2017 07:00:08 +0200 Subject: [PATCH 195/271] grep: allow the pattern to be empty --- modules/grep.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/grep.py b/modules/grep.py index 5c25c7d..fde8ecb 100644 --- a/modules/grep.py +++ b/modules/grep.py @@ -73,7 +73,7 @@ def cmd_grep(msg): only = "only" in msg.kwargs - l = [m for m in grep(msg.args[0] if msg.args[0][0] == "^" else ".*?(" + msg.args[0] + ").*?", + l = [m for m in grep(msg.args[0] if len(msg.args[0]) and msg.args[0][0] == "^" else ".*?(" + msg.args[0] + ").*?", " ".join(msg.args[1:]), msg, icase="nocase" in msg.kwargs, From c23dc22ce234fc9c04eac3376fded293e0db97e0 Mon Sep 17 00:00:00 2001 From: Max <max@23.tf> Date: Tue, 26 Sep 2017 02:21:14 +0200 Subject: [PATCH 196/271] [suivi] Fix colissimo tracking --- modules/suivi.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/modules/suivi.py b/modules/suivi.py index 4bc079e..1d04333 100644 --- a/modules/suivi.py +++ b/modules/suivi.py @@ -31,16 +31,17 @@ def get_tnt_info(track_id): def get_colissimo_info(colissimo_id): - colissimo_data = getURLContent("http://www.colissimo.fr/portail_colissimo/suivre.do?colispart=%s" % colissimo_id) + colissimo_data = getURLContent("https://www.laposte.fr/particulier/outils/suivre-vos-envois?code=%s" % colissimo_id) soup = BeautifulSoup(colissimo_data) - dataArray = soup.find(class_='dataArray') - if dataArray and dataArray.tbody and dataArray.tbody.tr: - date = dataArray.tbody.tr.find(headers="Date").get_text() - libelle = re.sub(r'[\n\t\r]', '', - dataArray.tbody.tr.find(headers="Libelle").get_text()) - site = dataArray.tbody.tr.find(headers="site").get_text().strip() - return (date, libelle, site.strip()) + dataArray = soup.find(class_='results-suivi') + if dataArray and dataArray.table and dataArray.table.tbody and dataArray.table.tbody.tr: + td = dataArray.table.tbody.tr.find_all('td') + if len(td) > 2: + date = td[0].get_text() + libelle = re.sub(r'[\n\t\r]', '', td[1].get_text()) + site = td[2].get_text().strip() + return (date, libelle, site.strip()) def get_chronopost_info(track_id): From a7f4ccc959565701f3f0a46aab70980fa69176d0 Mon Sep 17 00:00:00 2001 From: Max <max@23.tf> Date: Tue, 26 Sep 2017 02:25:14 +0200 Subject: [PATCH 197/271] [suivi] Fix Fedex tracking --- modules/suivi.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/suivi.py b/modules/suivi.py index 1d04333..100f98b 100644 --- a/modules/suivi.py +++ b/modules/suivi.py @@ -176,7 +176,8 @@ def get_fedex_info(fedex_id, lang="en_US"): if ("TrackPackagesResponse" in fedex_data and "packageList" in fedex_data["TrackPackagesResponse"] and len(fedex_data["TrackPackagesResponse"]["packageList"]) and - not fedex_data["TrackPackagesResponse"]["errorList"][0]["code"] and + (not fedex_data["TrackPackagesResponse"]["errorList"][0]["code"] or + fedex_data["TrackPackagesResponse"]["errorList"][0]["code"] == '0') and not fedex_data["TrackPackagesResponse"]["packageList"][0]["errorList"][0]["code"] ): return fedex_data["TrackPackagesResponse"]["packageList"][0] From ef4c119f1f4aea6ebe3eeb2f7339fa6619c3f7e6 Mon Sep 17 00:00:00 2001 From: Max <max@23.tf> Date: Tue, 26 Sep 2017 02:30:14 +0200 Subject: [PATCH 198/271] [suivi] Add https when supported by service --- modules/suivi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/suivi.py b/modules/suivi.py index 100f98b..27a939a 100644 --- a/modules/suivi.py +++ b/modules/suivi.py @@ -17,7 +17,7 @@ from nemubot.module.more import Response def get_tnt_info(track_id): values = [] - data = getURLContent('www.tnt.fr/public/suivi_colis/recherche/visubontransport.do?bonTransport=%s' % track_id) + data = getURLContent('https://www.tnt.fr/public/suivi_colis/recherche/visubontransport.do?bonTransport=%s' % track_id) soup = BeautifulSoup(data) status_list = soup.find('div', class_='result__content') if not status_list: @@ -46,7 +46,7 @@ def get_colissimo_info(colissimo_id): def get_chronopost_info(track_id): data = urllib.parse.urlencode({'listeNumeros': track_id}) - track_baseurl = "http://www.chronopost.fr/expedier/inputLTNumbersNoJahia.do?lang=fr_FR" + track_baseurl = "https://www.chronopost.fr/expedier/inputLTNumbersNoJahia.do?lang=fr_FR" track_data = getURLContent(track_baseurl, data.encode('utf-8')) soup = BeautifulSoup(track_data) From 99384ad6f7b4de313d130b297f86635080a93ad8 Mon Sep 17 00:00:00 2001 From: Max <max@23.tf> Date: Thu, 28 Sep 2017 02:30:14 +0200 Subject: [PATCH 199/271] [suivi] Fix awkward USPS message --- modules/suivi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/suivi.py b/modules/suivi.py index 27a939a..637f64f 100644 --- a/modules/suivi.py +++ b/modules/suivi.py @@ -230,7 +230,7 @@ def handle_usps(tracknum): info = get_usps_info(tracknum) if info: notif, last_date, last_status, last_location = info - return ("USPS \x02{tracknum}\x0F is {last_status} in \x02{last_location}\x0F as of {last_date}: {notif}".format(tracknum=tracknum, notif=notif, last_date=last_date, last_status=last_status.lower(), last_location=last_location)) + return ("USPS \x02{tracknum}\x0F: {last_status} in \x02{last_location}\x0F as of {last_date}: {notif}".format(tracknum=tracknum, notif=notif, last_date=last_date, last_status=last_status.lower(), last_location=last_location)) def handle_colissimo(tracknum): From e0d7ef13146b78edc4c8a3f7cc2e7d343f8515b5 Mon Sep 17 00:00:00 2001 From: Max <max@23.tf> Date: Fri, 6 Oct 2017 02:54:37 +0200 Subject: [PATCH 200/271] Fix https links when available, everywhere --- README.md | 2 +- modules/conjugaison.py | 2 +- modules/mapquest.py | 4 ++-- modules/networking/isup.py | 2 +- modules/networking/w3c.py | 2 +- modules/networking/whois.py | 4 ++-- modules/reddit.py | 2 +- modules/sap.py | 2 +- modules/syno.py | 2 +- modules/urbandict.py | 2 +- modules/virtualradar.py | 2 +- modules/weather.py | 2 +- modules/wolframalpha.py | 4 ++-- nemubot/bot.py | 2 +- nemubot/server/__init__.py | 4 ++-- 15 files changed, 19 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index aa3b141..6977c9f 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Requirements *nemubot* requires at least Python 3.3 to work. Some modules (like `cve`, `nextstop` or `laposte`) require the -[BeautifulSoup module](http://www.crummy.com/software/BeautifulSoup/), +[BeautifulSoup module](https://www.crummy.com/software/BeautifulSoup/), but the core and framework has no dependency. diff --git a/modules/conjugaison.py b/modules/conjugaison.py index 42d78c6..c953da3 100644 --- a/modules/conjugaison.py +++ b/modules/conjugaison.py @@ -36,7 +36,7 @@ for k, v in s: # MODULE CORE ######################################################### def get_conjug(verb, stringTens): - url = ("http://leconjugueur.lefigaro.fr/conjugaison/verbe/%s.html" % + url = ("https://leconjugueur.lefigaro.fr/conjugaison/verbe/%s.html" % quote(verb.encode("ISO-8859-1"))) page = web.getURLContent(url) diff --git a/modules/mapquest.py b/modules/mapquest.py index 5662a49..f328e1d 100644 --- a/modules/mapquest.py +++ b/modules/mapquest.py @@ -13,7 +13,7 @@ from nemubot.module.more import Response # GLOBALS ############################################################# -URL_API = "http://open.mapquestapi.com/geocoding/v1/address?key=%s&location=%%s" +URL_API = "https://open.mapquestapi.com/geocoding/v1/address?key=%s&location=%%s" # LOADING ############################################################# @@ -23,7 +23,7 @@ def load(context): raise ImportError("You need a MapQuest API key in order to use this " "module. Add it to the module configuration file:\n" "<module name=\"mapquest\" key=\"XXXXXXXXXXXXXXXX\" " - "/>\nRegister at http://developer.mapquest.com/") + "/>\nRegister at https://developer.mapquest.com/") global URL_API URL_API = URL_API % context.config["apikey"].replace("%", "%%") diff --git a/modules/networking/isup.py b/modules/networking/isup.py index c518900..99e2664 100644 --- a/modules/networking/isup.py +++ b/modules/networking/isup.py @@ -11,7 +11,7 @@ def isup(url): o = urllib.parse.urlparse(getNormalizedURL(url), "http") if o.netloc != "": - isup = getJSON("http://isitup.org/%s.json" % o.netloc) + isup = getJSON("https://isitup.org/%s.json" % o.netloc) if isup is not None and "status_code" in isup and isup["status_code"] == 1: return isup["response_time"] diff --git a/modules/networking/w3c.py b/modules/networking/w3c.py index 83056dd..3c8084f 100644 --- a/modules/networking/w3c.py +++ b/modules/networking/w3c.py @@ -17,7 +17,7 @@ def validator(url): raise IMException("Indicate a valid URL!") try: - req = urllib.request.Request("http://validator.w3.org/check?uri=%s&output=json" % (urllib.parse.quote(o.geturl())), headers={ 'User-Agent' : "Nemubot v%s" % __version__}) + req = urllib.request.Request("https://validator.w3.org/check?uri=%s&output=json" % (urllib.parse.quote(o.geturl())), headers={ 'User-Agent' : "Nemubot v%s" % __version__}) raw = urllib.request.urlopen(req, timeout=10) except urllib.error.HTTPError as e: raise IMException("HTTP error occurs: %s %s" % (e.code, e.reason)) diff --git a/modules/networking/whois.py b/modules/networking/whois.py index 787cd17..999dc01 100644 --- a/modules/networking/whois.py +++ b/modules/networking/whois.py @@ -9,7 +9,7 @@ from nemubot.tools.web import getJSON from nemubot.module.more import Response URL_AVAIL = "https://www.whoisxmlapi.com/whoisserver/WhoisService?cmd=GET_DN_AVAILABILITY&domainName=%%s&outputFormat=json&username=%s&password=%s" -URL_WHOIS = "http://www.whoisxmlapi.com/whoisserver/WhoisService?da=2&domainName=%%s&outputFormat=json&userName=%s&password=%s" +URL_WHOIS = "https://www.whoisxmlapi.com/whoisserver/WhoisService?da=2&domainName=%%s&outputFormat=json&userName=%s&password=%s" # LOADING ############################################################# @@ -22,7 +22,7 @@ def load(CONF, add_hook): "the !netwhois feature. Add it to the module " "configuration file:\n<whoisxmlapi username=\"XX\" " "password=\"XXX\" />\nRegister at " - "http://www.whoisxmlapi.com/newaccount.php") + "https://www.whoisxmlapi.com/newaccount.php") URL_AVAIL = URL_AVAIL % (urllib.parse.quote(CONF.getNode("whoisxmlapi")["username"]), urllib.parse.quote(CONF.getNode("whoisxmlapi")["password"])) URL_WHOIS = URL_WHOIS % (urllib.parse.quote(CONF.getNode("whoisxmlapi")["username"]), urllib.parse.quote(CONF.getNode("whoisxmlapi")["password"])) diff --git a/modules/reddit.py b/modules/reddit.py index 2de7612..d4def85 100644 --- a/modules/reddit.py +++ b/modules/reddit.py @@ -40,7 +40,7 @@ def cmd_subreddit(msg): else: where = "r" - sbr = web.getJSON("http://www.reddit.com/%s/%s/about.json" % + sbr = web.getJSON("https://www.reddit.com/%s/%s/about.json" % (where, sub.group(2))) if sbr is None: diff --git a/modules/sap.py b/modules/sap.py index a6168a2..0b9017f 100644 --- a/modules/sap.py +++ b/modules/sap.py @@ -25,7 +25,7 @@ def cmd_tcode(msg): raise IMException("indicate a transaction code or " "a keyword to search!") - url = ("http://www.tcodesearch.com/tcodes/search?q=%s" % + url = ("https://www.tcodesearch.com/tcodes/search?q=%s" % urllib.parse.quote(msg.args[0])) page = web.getURLContent(url) diff --git a/modules/syno.py b/modules/syno.py index bda0456..6f6a625 100644 --- a/modules/syno.py +++ b/modules/syno.py @@ -53,7 +53,7 @@ def get_french_synos(word): def get_english_synos(key, word): - cnt = web.getJSON("http://words.bighugelabs.com/api/2/%s/%s/json" % + cnt = web.getJSON("https://words.bighugelabs.com/api/2/%s/%s/json" % (quote(key), quote(word.encode("ISO-8859-1")))) best = list(); synos = list(); anton = list() diff --git a/modules/urbandict.py b/modules/urbandict.py index a897fad..b561e89 100644 --- a/modules/urbandict.py +++ b/modules/urbandict.py @@ -14,7 +14,7 @@ from nemubot.module.more import Response def search(terms): return web.getJSON( - "http://api.urbandictionary.com/v0/define?term=%s" + "https://api.urbandictionary.com/v0/define?term=%s" % quote(' '.join(terms))) diff --git a/modules/virtualradar.py b/modules/virtualradar.py index 9382d3b..2c87e79 100644 --- a/modules/virtualradar.py +++ b/modules/virtualradar.py @@ -15,7 +15,7 @@ from nemubot.module import mapquest # GLOBALS ############################################################# -URL_API = "http://public-api.adsbexchange.com/VirtualRadar/AircraftList.json?fcallC=%s" +URL_API = "https://public-api.adsbexchange.com/VirtualRadar/AircraftList.json?fcallC=%s" SPEED_TYPES = { 0: 'Ground speed', diff --git a/modules/weather.py b/modules/weather.py index 96fd718..9b36470 100644 --- a/modules/weather.py +++ b/modules/weather.py @@ -59,7 +59,7 @@ def load(context): raise ImportError("You need a Dark-Sky API key in order to use this " "module. Add it to the module configuration file:\n" "<module name=\"weather\" darkskyapikey=\"XXX\" />\n" - "Register at http://developer.forecast.io/") + "Register at https://developer.forecast.io/") context.data.setIndex("name", "city") global URL_DSAPI URL_DSAPI = URL_DSAPI % context.config["darkskyapikey"] diff --git a/modules/wolframalpha.py b/modules/wolframalpha.py index b7cc7fb..fc83815 100644 --- a/modules/wolframalpha.py +++ b/modules/wolframalpha.py @@ -15,7 +15,7 @@ from nemubot.module.more import Response # LOADING ############################################################# -URL_API = "http://api.wolframalpha.com/v2/query?input=%%s&format=plaintext&appid=%s" +URL_API = "https://api.wolframalpha.com/v2/query?input=%%s&format=plaintext&appid=%s" def load(context): global URL_API @@ -24,7 +24,7 @@ def load(context): "this module. Add it to the module configuration: " "\n<module name=\"wolframalpha\" " "apikey=\"XXXXXX-XXXXXXXXXX\" />\n" - "Register at http://products.wolframalpha.com/api/") + "Register at https://products.wolframalpha.com/api/") URL_API = URL_API % quote(context.config["apikey"]).replace("%", "%%") diff --git a/nemubot/bot.py b/nemubot/bot.py index 28df8ce..6aa5ed6 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -135,7 +135,7 @@ class Bot(threading.Thread): "Vous pouvez le consulter, le dupliquer, " "envoyer des rapports de bogues ou bien " "contribuer au projet sur GitHub : " - "http://github.com/nemunaire/nemubot/") + "https://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", diff --git a/nemubot/server/__init__.py b/nemubot/server/__init__.py index 068d152..a39c491 100644 --- a/nemubot/server/__init__.py +++ b/nemubot/server/__init__.py @@ -22,8 +22,8 @@ def factory(uri, ssl=False, **init_args): srv = None if o.scheme == "irc" or o.scheme == "ircs": - # http://www.w3.org/Addressing/draft-mirashi-url-irc-01.txt - # http://www-archive.mozilla.org/projects/rt-messaging/chatzilla/irc-urls.html + # https://www.w3.org/Addressing/draft-mirashi-url-irc-01.txt + # https://www-archive.mozilla.org/projects/rt-messaging/chatzilla/irc-urls.html args = init_args if o.scheme == "ircs": ssl = True From 226ee4e34eece827a5fc91389fb144b0b46c63fc Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Wed, 11 Oct 2017 08:03:05 +0200 Subject: [PATCH 201/271] ctfs: update module reflecting site changes --- modules/ctfs.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/modules/ctfs.py b/modules/ctfs.py index 169ee46..ac27c4a 100644 --- a/modules/ctfs.py +++ b/modules/ctfs.py @@ -25,10 +25,8 @@ def get_info_yt(msg): for line in soup.body.find_all('tr'): n = line.find_all('td') - if len(n) == 5: - try: - res.append_message("\x02%s:\x0F from %s type %s at %s. %s" % - tuple([striphtml(x.text) for x in n])) - except: - pass + if len(n) == 7: + res.append_message("\x02%s:\x0F from %s type %s at %s. Weight: %s. %s%s" % + tuple([striphtml(x.text).strip() for x in n])) + return res From 5cbad964920ea36ab2442331e5abf7d0fbfbce3c Mon Sep 17 00:00:00 2001 From: Max <max@23.tf> Date: Tue, 31 Oct 2017 00:13:14 -0500 Subject: [PATCH 202/271] [module] RMS upcoming locations --- modules/rms.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 modules/rms.py diff --git a/modules/rms.py b/modules/rms.py new file mode 100644 index 0000000..e7b89ce --- /dev/null +++ b/modules/rms.py @@ -0,0 +1,35 @@ +"""Finding RMS""" + +# PYTHON STUFFS ####################################################### + +from bs4 import BeautifulSoup + +from nemubot.hooks import hook +from nemubot.tools.web import getURLContent, striphtml +from nemubot.module.more import Response + + +# GLOBALS ############################################################# + +URL = 'https://www.fsf.org/events/rms-speeches.html' + + +# MODULE INTERFACE #################################################### + +@hook.command("rms", + help="Lists upcoming RMS events.") +def cmd_rms(msg): + soup = BeautifulSoup(getURLContent(URL), "lxml") + + res = Response(channel=msg.channel, + nomore="", + count=" (%d more event(s))") + + search_res = soup.find("table", {'class':'listing'}) + for item in search_res.tbody.find_all('tr'): + columns = item.find_all('td') + res.append_message("RMS will be in \x02%s\x0F for \x02%s\x0F on \x02%s\x0F." % ( + columns[1].get_text(), + columns[2].get_text().replace('\n', ''), + columns[0].get_text().replace('\n', ''))) + return res From d528746cb51557ca70c61c5c2d714c9172e39e82 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 20 Aug 2017 21:17:08 +0200 Subject: [PATCH 203/271] datastore: support custom knodes instead of nemubotstate --- nemubot/datastore/abstract.py | 6 +++++- nemubot/datastore/xml.py | 24 +++++++++++++++++++----- nemubot/modulecontext.py | 8 ++++++-- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/nemubot/datastore/abstract.py b/nemubot/datastore/abstract.py index 96e2c0d..aeaecc6 100644 --- a/nemubot/datastore/abstract.py +++ b/nemubot/datastore/abstract.py @@ -32,16 +32,20 @@ class Abstract: def close(self): return - def load(self, module): + def load(self, module, knodes): """Load data for the given module Argument: module -- the module name of data to load + knodes -- the schema to use to load the datas Return: The loaded data """ + if knodes is not None: + return None + return self.new() def save(self, module, data): diff --git a/nemubot/datastore/xml.py b/nemubot/datastore/xml.py index 025c0c5..aa6cbd0 100644 --- a/nemubot/datastore/xml.py +++ b/nemubot/datastore/xml.py @@ -83,27 +83,38 @@ class XML(Abstract): return os.path.join(self.basedir, module + ".xml") - def load(self, module): + def load(self, module, knodes): """Load data for the given module Argument: module -- the module name of data to load + knodes -- the schema to use to load the datas """ data_file = self._get_data_file_path(module) + if knodes is None: + from nemubot.tools.xmlparser import parse_file + def _true_load(path): + return parse_file(path) + + else: + from nemubot.tools.xmlparser import XMLParser + p = XMLParser(knodes) + def _true_load(path): + return p.parse_file(path) + # Try to load original file if os.path.isfile(data_file): - from nemubot.tools.xmlparser import parse_file try: - return parse_file(data_file) + return _true_load(data_file) except xml.parsers.expat.ExpatError: # Try to load from backup for i in range(10): path = data_file + "." + str(i) if os.path.isfile(path): try: - cnt = parse_file(path) + cnt = _true_load(path) logger.warn("Restoring from backup: %s", path) @@ -112,7 +123,7 @@ class XML(Abstract): continue # Default case: initialize a new empty datastore - return Abstract.load(self, module) + return super().load(module, knodes) def _rotate(self, path): """Backup given path @@ -143,6 +154,9 @@ class XML(Abstract): if self.rotate: self._rotate(path) + if data is None: + return + import tempfile _, tmpath = tempfile.mkstemp() with open(tmpath, "w") as f: diff --git a/nemubot/modulecontext.py b/nemubot/modulecontext.py index f39934c..c7fa3d4 100644 --- a/nemubot/modulecontext.py +++ b/nemubot/modulecontext.py @@ -16,7 +16,7 @@ class _ModuleContext: - def __init__(self, module=None): + def __init__(self, module=None, knodes=None): self.module = module if module is not None: @@ -30,12 +30,16 @@ class _ModuleContext: from nemubot.config.module import Module self.config = Module(self.module_name) + self._knodes = knodes def load_data(self): from nemubot.tools.xmlparser import module_state return module_state.ModuleState("nemubotstate") + def set_knodes(self, knodes): + self._knodes = knodes + def add_hook(self, hook, *triggers): from nemubot.hooks import Abstract as AbstractHook assert isinstance(hook, AbstractHook), hook @@ -112,7 +116,7 @@ class ModuleContext(_ModuleContext): def load_data(self): - return self.context.datastore.load(self.module_name) + return self.context.datastore.load(self.module_name, self._knodes) def add_hook(self, hook, *triggers): from nemubot.hooks import Abstract as AbstractHook From 4cd099e087ac04838de323c6b9d05d513387106f Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 1 Sep 2017 20:45:58 +0200 Subject: [PATCH 204/271] xmlparser: make DictNode more usable --- nemubot/tools/xmlparser/basic.py | 33 ++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/nemubot/tools/xmlparser/basic.py b/nemubot/tools/xmlparser/basic.py index 86eac3c..dadff23 100644 --- a/nemubot/tools/xmlparser/basic.py +++ b/nemubot/tools/xmlparser/basic.py @@ -77,12 +77,12 @@ class DictNode: def endElement(self, name): - if name is None or self._cur is None: + if name is not None or self._cur is None: return key, cnt = self._cur if isinstance(cnt, list) and len(cnt) == 1: - self.items[key] = cnt + self.items[key] = cnt[0] else: self.items[key] = cnt @@ -122,7 +122,32 @@ class DictNode: if isinstance(v, str): store.characters(v) else: - for i in v: - i.saveElement(store) + if hasattr(v, "__iter__"): + for i in v: + i.saveElement(store) + else: + v.saveElement(store) store.endElement("item") store.endElement(tag) + + + def __contain__(self, i): + return i in self.items + + def __getitem__(self, i): + return self.items[i] + + def __setitem__(self, i, c): + self.items[i] = c + + def __delitem__(self, k): + del self.items[k] + + def __iter__(self): + return self.items.__iter__() + + def keys(self): + return self.items.keys() + + def items(self): + return self.items.items() From c3c74847923aae6d470e4989dc880c04c96ad7d6 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Wed, 19 Jul 2017 07:51:19 +0200 Subject: [PATCH 205/271] In debug mode, display the last stack element to be able to trace --- nemubot/server/abstract.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nemubot/server/abstract.py b/nemubot/server/abstract.py index 510a319..8fbb923 100644 --- a/nemubot/server/abstract.py +++ b/nemubot/server/abstract.py @@ -16,6 +16,7 @@ import logging import queue +import traceback from nemubot.bot import sync_act @@ -84,7 +85,7 @@ class AbstractServer: """ self._sending_queue.put(self.format(message)) - self._logger.debug("Message '%s' appended to write queue", message) + self._logger.debug("Message '%s' appended to write queue coming from %s:%d in %s", message, *traceback.extract_stack(limit=3)[0][:3]) sync_act("sckt", "write", self._fd.fileno()) From f520c67c8927ef4608f399a0cc221d188d90f8af Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Thu, 4 Jan 2018 18:12:31 +0100 Subject: [PATCH 206/271] context: new function to define default data, instead of None --- nemubot/modulecontext.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/nemubot/modulecontext.py b/nemubot/modulecontext.py index c7fa3d4..4af3731 100644 --- a/nemubot/modulecontext.py +++ b/nemubot/modulecontext.py @@ -40,6 +40,11 @@ class _ModuleContext: def set_knodes(self, knodes): self._knodes = knodes + def set_default(self, default): + # Access to data will trigger the load of data + if self.data is None: + self._data = default + def add_hook(self, hook, *triggers): from nemubot.hooks import Abstract as AbstractHook assert isinstance(hook, AbstractHook), hook From 4275009dead4cac6c2b39219b03bbad79ea434bd Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Thu, 4 Jan 2018 18:16:47 +0100 Subject: [PATCH 207/271] events: now support timedelta instead of int/float --- nemubot/event/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/nemubot/event/__init__.py b/nemubot/event/__init__.py index 981cf4b..2b3ed6b 100644 --- a/nemubot/event/__init__.py +++ b/nemubot/event/__init__.py @@ -63,8 +63,14 @@ class ModuleEvent: self.call_data = func_data # Store times - self.offset = timedelta(seconds=offset) # Time to wait before the first check - self.interval = timedelta(seconds=interval) + if isinstance(offset, timedelta): + self.offset = offset # Time to wait before the first check + else: + self.offset = timedelta(seconds=offset) # Time to wait before the first check + if isinstance(interval, timedelta): + self.interval = interval + else: + self.interval = timedelta(seconds=interval) self._end = None # Cache # How many times do this event? From 2af56e606a79b82b728b6d07f2a2f1c774ee2114 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 1 Sep 2017 20:47:38 +0200 Subject: [PATCH 208/271] events: Use the new data parser, knodes based --- modules/events.py | 176 +++++++++++++++++++++++++++++----------------- 1 file changed, 110 insertions(+), 66 deletions(-) diff --git a/modules/events.py b/modules/events.py index 9814aa2..2b6bfb1 100644 --- a/modules/events.py +++ b/modules/events.py @@ -1,7 +1,9 @@ """Create countdowns and reminders""" -import re +import calendar from datetime import datetime, timedelta, timezone +from functools import partial +import re from nemubot import context from nemubot.exception import IMException @@ -10,31 +12,84 @@ from nemubot.hooks import hook from nemubot.message import Command from nemubot.tools.countdown import countdown_format, countdown from nemubot.tools.date import extractDate -from nemubot.tools.xmlparser.node import ModuleState +from nemubot.tools.xmlparser.basic import DictNode from nemubot.module.more import Response +class Event: + + def __init__(self, server, channel, creator, start_time, end_time=None): + self._server = server + self._channel = channel + self._creator = creator + self._start = datetime.utcfromtimestamp(float(start_time)).replace(tzinfo=timezone.utc) if not isinstance(start_time, datetime) else start_time + self._end = datetime.utcfromtimestamp(float(end_time)).replace(tzinfo=timezone.utc) if end_time else None + self._evt = None + + + def __del__(self): + if self._evt is not None: + context.del_event(self._evt) + self._evt = None + + + def saveElement(self, store, tag="event"): + attrs = { + "server": str(self._server), + "channel": str(self._channel), + "creator": str(self._creator), + "start_time": str(calendar.timegm(self._start.timetuple())), + } + if self._end: + attrs["end_time"] = str(calendar.timegm(self._end.timetuple())) + store.startElement(tag, attrs) + store.endElement(tag) + + @property + def creator(self): + return self._creator + + @property + def start(self): + return self._start + + @property + def end(self): + return self._end + + @end.setter + def end(self, c): + self._end = c + + @end.deleter + def end(self): + self._end = None + + def help_full (): - return "This module store a lot of events: ny, we, " + (", ".join(context.datas.index.keys() if hasattr(context, "datas") else [])) + "\n!eventslist: gets list of timer\n!start /something/: launch a timer" + return "This module store a lot of events: ny, we, " + (", ".join(context.datas.keys()) if hasattr(context, "datas") else "") + "\n!eventslist: gets list of timer\n!start /something/: launch a timer" def load(context): - #Define the index - context.data.setIndex("name") + context.set_knodes({ + "dict": DictNode, + "event": Event, + }) - for evt in context.data.index.keys(): - if context.data.index[evt].hasAttribute("end"): - event = ModuleEvent(call=fini, call_data=dict(strend=context.data.index[evt])) - event._end = context.data.index[evt].getDate("end") - idt = context.add_event(event) - if idt is not None: - context.data.index[evt]["_id"] = idt + if context.data is None: + context.set_default(DictNode()) + + # Relaunch all timers + for kevt in context.data: + if context.data[kevt].end: + context.data[kevt]._evt = context.add_event(ModuleEvent(partial(fini, kevt, context.data[kevt]), offset=context.data[kevt].end - datetime.now(timezone.utc), interval=0)) -def fini(d, strend): - context.send_response(strend["server"], Response("%s arrivé à échéance." % strend["name"], channel=strend["channel"], nick=strend["proprio"])) - context.data.delChild(context.data.index[strend["name"]]) +def fini(name, evt): + context.send_response(evt._server, Response("%s arrivé à échéance." % name, channel=evt._channel, nick=evt.creator)) + evt._evt = None + del context.data[name] context.save() @@ -63,18 +118,10 @@ def start_countdown(msg): """!start /something/: launch a timer""" if len(msg.args) < 1: raise IMException("indique le nom d'un événement à chronométrer") - if msg.args[0] in context.data.index: + if msg.args[0] in context.data: raise IMException("%s existe déjà." % msg.args[0]) - strnd = ModuleState("strend") - strnd["server"] = msg.server - strnd["channel"] = msg.channel - strnd["proprio"] = msg.frm - strnd["start"] = msg.date - strnd["name"] = msg.args[0] - context.data.addChild(strnd) - - evt = ModuleEvent(call=fini, call_data=dict(strend=strnd)) + evt = Event(server=msg.server, channel=msg.channel, creator=msg.frm, start_time=msg.date) if len(msg.args) > 1: result1 = re.findall("([0-9]+)([smhdjwyaSMHDJWYA])?", msg.args[1]) @@ -92,50 +139,48 @@ def start_countdown(msg): if result2 is None or result2.group(4) is None: yea = now.year else: yea = int(result2.group(4)) if result2 is not None and result3 is not None: - strnd["end"] = datetime(yea, int(result2.group(3)), int(result2.group(2)), hou, minu, sec, timezone.utc) + evt.end = datetime(yea, int(result2.group(3)), int(result2.group(2)), hou, minu, sec, timezone.utc) elif result2 is not None: - strnd["end"] = datetime(int(result2.group(4)), int(result2.group(3)), int(result2.group(2)), 0, 0, 0, timezone.utc) + evt.end = datetime(int(result2.group(4)), int(result2.group(3)), int(result2.group(2)), 0, 0, 0, timezone.utc) elif result3 is not None: if hou * 3600 + minu * 60 + sec > now.hour * 3600 + now.minute * 60 + now.second: - strnd["end"] = datetime(now.year, now.month, now.day, hou, minu, sec, timezone.utc) + evt.end = datetime(now.year, now.month, now.day, hou, minu, sec, timezone.utc) else: - strnd["end"] = datetime(now.year, now.month, now.day + 1, hou, minu, sec, timezone.utc) - evt._end = strnd.getDate("end") - strnd["_id"] = context.add_event(evt) + evt.end = datetime(now.year, now.month, now.day + 1, hou, minu, sec, timezone.utc) except: - context.data.delChild(strnd) raise IMException("Mauvais format de date pour l'événement %s. Il n'a pas été créé." % msg.args[0]) elif result1 is not None and len(result1) > 0: - strnd["end"] = msg.date + evt.end = msg.date for (t, g) in result1: if g is None or g == "" or g == "m" or g == "M": - strnd["end"] += timedelta(minutes=int(t)) + evt.end += timedelta(minutes=int(t)) elif g == "h" or g == "H": - strnd["end"] += timedelta(hours=int(t)) + evt.end += timedelta(hours=int(t)) elif g == "d" or g == "D" or g == "j" or g == "J": - strnd["end"] += timedelta(days=int(t)) + evt.end += timedelta(days=int(t)) elif g == "w" or g == "W": - strnd["end"] += timedelta(days=int(t)*7) + evt.end += timedelta(days=int(t)*7) elif g == "y" or g == "Y" or g == "a" or g == "A": - strnd["end"] += timedelta(days=int(t)*365) + evt.end += timedelta(days=int(t)*365) else: - strnd["end"] += timedelta(seconds=int(t)) - evt._end = strnd.getDate("end") - eid = context.add_event(evt) - if eid is not None: - strnd["_id"] = eid + evt.end += timedelta(seconds=int(t)) + context.data[msg.args[0]] = evt context.save() - if "end" in strnd: + + if evt.end is not None: + context.add_event(ModuleEvent(partial(fini, msg.args[0], evt), + offset=evt.end - datetime.now(timezone.utc), + interval=0)) return Response("%s commencé le %s et se terminera le %s." % (msg.args[0], msg.date.strftime("%A %d %B %Y à %H:%M:%S"), - strnd.getDate("end").strftime("%A %d %B %Y à %H:%M:%S")), - nick=msg.frm) + evt.end.strftime("%A %d %B %Y à %H:%M:%S")), + channel=msg.channel) else: return Response("%s commencé le %s"% (msg.args[0], msg.date.strftime("%A %d %B %Y à %H:%M:%S")), - nick=msg.frm) + channel=msg.channel) @hook.command("end") @@ -144,16 +189,15 @@ def end_countdown(msg): if len(msg.args) < 1: raise IMException("quel événement terminer ?") - if msg.args[0] in context.data.index: - if context.data.index[msg.args[0]]["proprio"] == msg.frm or (msg.cmd == "forceend" and msg.frm_owner): - duration = countdown(msg.date - context.data.index[msg.args[0]].getDate("start")) - context.del_event(context.data.index[msg.args[0]]["_id"]) - context.data.delChild(context.data.index[msg.args[0]]) + if msg.args[0] in context.data: + if context.data[msg.args[0]].creator == msg.frm or (msg.cmd == "forceend" and msg.frm_owner): + duration = countdown(msg.date - context.data[msg.args[0]].start) + del context.data[msg.args[0]] context.save() return Response("%s a duré %s." % (msg.args[0], duration), channel=msg.channel, nick=msg.frm) else: - raise IMException("Vous ne pouvez pas terminer le compteur %s, créé par %s." % (msg.args[0], context.data.index[msg.args[0]]["proprio"])) + raise IMException("Vous ne pouvez pas terminer le compteur %s, créé par %s." % (msg.args[0], context.data[msg.args[0]].creator)) else: return Response("%s n'est pas un compteur connu."% (msg.args[0]), channel=msg.channel, nick=msg.frm) @@ -162,19 +206,19 @@ def end_countdown(msg): def liste(msg): """!eventslist: gets list of timer""" if len(msg.args): - res = list() + res = Response(channel=msg.channel) for user in msg.args: - cmptr = [x["name"] for x in context.data.index.values() if x["proprio"] == user] + cmptr = [k for k in context.data if context.data[k].creator == user] if len(cmptr) > 0: - res.append("Compteurs créés par %s : %s" % (user, ", ".join(cmptr))) + res.append_message(cmptr, title="Events created by %s" % user) else: - res.append("%s n'a pas créé de compteur" % user) - return Response(" ; ".join(res), channel=msg.channel) + res.append_message("%s doesn't have any counting events" % user) + return res else: - return Response("Compteurs connus : %s." % ", ".join(context.data.index.keys()), channel=msg.channel) + return Response(list(context.data.keys()), channel=msg.channel, title="Known events") -@hook.command(match=lambda msg: isinstance(msg, Command) and msg.cmd in context.data.index) +@hook.command(match=lambda msg: isinstance(msg, Command) and msg.cmd in context.data) def parseanswer(msg): res = Response(channel=msg.channel) @@ -182,13 +226,13 @@ def parseanswer(msg): if msg.cmd[0] == "!": res.nick = msg.frm - if context.data.index[msg.cmd].name == "strend": - if context.data.index[msg.cmd].hasAttribute("end"): - res.append_message("%s commencé il y a %s et se terminera dans %s." % (msg.cmd, countdown(msg.date - context.data.index[msg.cmd].getDate("start")), countdown(context.data.index[msg.cmd].getDate("end") - msg.date))) + if msg.cmd in context.data: + if context.data[msg.cmd].end: + res.append_message("%s commencé il y a %s et se terminera dans %s." % (msg.cmd, countdown(msg.date - context.data[msg.cmd].start), countdown(context.data[msg.cmd].end - msg.date))) else: - res.append_message("%s commencé il y a %s." % (msg.cmd, countdown(msg.date - context.data.index[msg.cmd].getDate("start")))) + res.append_message("%s commencé il y a %s." % (msg.cmd, countdown(msg.date - context.data[msg.cmd].start))) else: - res.append_message(countdown_format(context.data.index[msg.cmd].getDate("start"), context.data.index[msg.cmd]["msg_before"], context.data.index[msg.cmd]["msg_after"])) + res.append_message(countdown_format(context.data[msg.cmd].start, context.data[msg.cmd]["msg_before"], context.data[msg.cmd]["msg_after"])) return res @@ -199,7 +243,7 @@ def parseask(msg): name = re.match("^.*!([^ \"'@!]+).*$", msg.message) if name is None: raise IMException("il faut que tu attribues une commande à l'événement.") - if name.group(1) in context.data.index: + if name.group(1) in context.data: raise IMException("un événement portant ce nom existe déjà.") texts = re.match("^[^\"]*(avant|après|apres|before|after)?[^\"]*\"([^\"]+)\"[^\"]*((avant|après|apres|before|after)?.*\"([^\"]+)\".*)?$", msg.message, re.I) From 342bb9acdc88c779e44adf2e9f4a2b4e675d134c Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Sat, 10 Feb 2018 09:51:51 +0100 Subject: [PATCH 209/271] Refactor in treatment analysis --- modules/alias.py | 3 +- nemubot/hooks/abstract.py | 12 ++++++- nemubot/treatment.py | 66 +++++++++++++-------------------------- 3 files changed, 34 insertions(+), 47 deletions(-) diff --git a/modules/alias.py b/modules/alias.py index a246d2c..c432a85 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -272,7 +272,6 @@ def treat_alias(msg): # Avoid infinite recursion if not isinstance(rpl_msg, Command) or msg.cmd != rpl_msg.cmd: - # Also return origin message, if it can be treated as well - return [msg, rpl_msg] + return rpl_msg return msg diff --git a/nemubot/hooks/abstract.py b/nemubot/hooks/abstract.py index eac4b20..ffe79fb 100644 --- a/nemubot/hooks/abstract.py +++ b/nemubot/hooks/abstract.py @@ -14,6 +14,8 @@ # 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/>. +import types + def call_game(call, *args, **kargs): """With given args, try to determine the right call to make @@ -119,10 +121,18 @@ class Abstract: try: if self.check(data1): ret = call_game(self.call, data1, self.data, *args) + if isinstance(ret, types.GeneratorType): + for r in ret: + yield r + ret = None except IMException as e: ret = e.fill_response(data1) finally: if self.times == 0: self.call_end(ret) - return ret + if isinstance(ret, list): + for r in ret: + yield ret + elif ret is not None: + yield ret diff --git a/nemubot/treatment.py b/nemubot/treatment.py index 4f629e0..ed7cacb 100644 --- a/nemubot/treatment.py +++ b/nemubot/treatment.py @@ -15,7 +15,6 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import logging -import types logger = logging.getLogger("nemubot.treatment") @@ -79,19 +78,12 @@ class MessageTreater: for h in self.hm.get_hooks("pre", type(msg).__name__): if h.can_read(msg.to, msg.server) and h.match(msg): - res = h.run(msg) + for res in flatify(h.run(msg)): + if res is not None and res != msg: + yield from self._pre_treat(res) - if isinstance(res, list): - for i in range(len(res)): - # Avoid infinite loop - if res[i] != msg: - yield from self._pre_treat(res[i]) - - elif res is not None and res != msg: - yield from self._pre_treat(res) - - elif res is None or res is False: - break + elif res is None or res is False: + break else: yield msg @@ -113,25 +105,10 @@ class MessageTreater: msg.frm_owner = (not hasattr(msg.server, "owner") or msg.server.owner == msg.frm) while hook is not None: - res = hook.run(msg) - - if isinstance(res, list): - for r in res: - yield r - - elif res is not None: - if isinstance(res, types.GeneratorType): - for r in res: - if not hasattr(r, "server") or r.server is None: - r.server = msg.server - - yield r - - else: - if not hasattr(res, "server") or res.server is None: - res.server = msg.server - - yield res + for res in flatify(hook.run(msg)): + if not hasattr(res, "server") or res.server is None: + res.server = msg.server + yield res hook = next(hook_gen, None) @@ -165,19 +142,20 @@ class MessageTreater: for h in self.hm.get_hooks("post", type(msg).__name__): if h.can_write(msg.to, msg.server) and h.match(msg): - res = h.run(msg) + for res in flatify(h.run(msg)): + if res is not None and res != msg: + yield from self._post_treat(res) - if isinstance(res, list): - for i in range(len(res)): - # Avoid infinite loop - if res[i] != msg: - yield from self._post_treat(res[i]) - - elif res is not None and res != msg: - yield from self._post_treat(res) - - elif res is None or res is False: - break + elif res is None or res is False: + break else: yield msg + + +def flatify(g): + if hasattr(g, "__iter__"): + for i in g: + yield from flatify(i) + else: + yield g From 1887e481d2978949a52710ee45926bcdd1bacc8b Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Tue, 3 Apr 2018 08:02:41 +0200 Subject: [PATCH 210/271] sms: send result of command by SMS --- modules/sms.py | 74 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 58 insertions(+), 16 deletions(-) diff --git a/modules/sms.py b/modules/sms.py index 7db172b..57ab3ae 100644 --- a/modules/sms.py +++ b/modules/sms.py @@ -46,29 +46,22 @@ def send_sms(frm, api_usr, api_key, content): return None - -@hook.command("sms") -def cmd_sms(msg): - if not len(msg.args): - raise IMException("À qui veux-tu envoyer ce SMS ?") - - # Check dests - cur_epoch = time.mktime(time.localtime()); - for u in msg.args[0].split(","): +def check_sms_dests(dests, cur_epoch): + """Raise exception if one of the dest is not known or has already receive a SMS recently + """ + for u in dests: if u not in context.data.index: raise IMException("Désolé, je sais pas comment envoyer de SMS à %s." % u) elif cur_epoch - float(context.data.index[u]["lastuse"]) < 42: raise IMException("Un peu de calme, %s a déjà reçu un SMS il n'y a pas si longtemps." % u) + return True - # Go! + +def send_sms_to_list(msg, frm, dests, content, cur_epoch): fails = list() - for u in msg.args[0].split(","): + for u in dests: context.data.index[u]["lastuse"] = cur_epoch - if msg.to_response[0] == msg.frm: - frm = msg.frm - else: - frm = msg.frm + "@" + msg.to[0] - test = send_sms(frm, context.data.index[u]["user"], context.data.index[u]["key"], " ".join(msg.args[1:])) + test = send_sms(frm, context.data.index[u]["user"], context.data.index[u]["key"], content) if test is not None: fails.append( "%s: %s" % (u, test) ) @@ -77,6 +70,55 @@ def cmd_sms(msg): else: return Response("le SMS a bien été envoyé", msg.channel, msg.frm) + +@hook.command("sms") +def cmd_sms(msg): + if not len(msg.args): + raise IMException("À qui veux-tu envoyer ce SMS ?") + + cur_epoch = time.mktime(time.localtime()) + dests = msg.args[0].split(",") + frm = msg.frm if msg.to_response[0] == msg.frm else msg.frm + "@" + msg.to[0] + content = " ".join(msg.args[1:]) + + check_sms_dests(dests, cur_epoch) + return send_sms_to_list(msg, frm, dests, content, cur_epoch) + + +@hook.command("smscmd") +def cmd_smscmd(msg): + if not len(msg.args): + raise IMException("À qui veux-tu envoyer ce SMS ?") + + cur_epoch = time.mktime(time.localtime()) + dests = msg.args[0].split(",") + frm = msg.frm if msg.to_response[0] == msg.frm else msg.frm + "@" + msg.to[0] + cmd = " ".join(msg.args[1:]) + + content = None + for r in context.subtreat(context.subparse(msg, cmd)): + if isinstance(r, Response): + for m in r.messages: + if isinstance(m, list): + for n in m: + content = n + break + if content is not None: + break + elif isinstance(m, str): + content = m + break + + elif isinstance(r, Text): + content = r.message + + if content is None: + raise IMException("Aucun SMS envoyé : le résultat de la commande n'a pas retourné de contenu.") + + check_sms_dests(dests, cur_epoch) + return send_sms_to_list(msg, frm, dests, content, cur_epoch) + + apiuser_ask = re.compile(r"(utilisateur|user|numéro|numero|compte|abonne|abone|abonné|account)\s+(est|is)\s+(?P<user>[0-9]{7,})", re.IGNORECASE) apikey_ask = re.compile(r"(clef|key|password|mot de passe?)\s+(?:est|is)?\s+(?P<key>[a-zA-Z0-9]{10,})", re.IGNORECASE) From 8a25ebb45b81f92d9bb1fe9702f5d52318487a56 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 19 Apr 2018 23:52:35 +0200 Subject: [PATCH 211/271] xmlparser: fix parsing of subchild --- nemubot/tools/xmlparser/__init__.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/nemubot/tools/xmlparser/__init__.py b/nemubot/tools/xmlparser/__init__.py index c8d393a..1bf60a8 100644 --- a/nemubot/tools/xmlparser/__init__.py +++ b/nemubot/tools/xmlparser/__init__.py @@ -83,7 +83,7 @@ class XMLParser: @property def root(self): if len(self.stack): - return self.stack[0] + return self.stack[0][0] else: return None @@ -91,13 +91,13 @@ class XMLParser: @property def current(self): if len(self.stack): - return self.stack[-1] + return self.stack[-1][0] else: return None def display_stack(self): - return " in ".join([str(type(s).__name__) for s in reversed(self.stack)]) + return " in ".join([str(type(s).__name__) for s,c in reversed(self.stack)]) def startElement(self, name, attrs): @@ -105,7 +105,8 @@ class XMLParser: if name not in self.knodes: raise TypeError(name + " is not a known type to decode") else: - self.stack.append(self.knodes[name](**attrs)) + self.stack.append((self.knodes[name](**attrs), self.child)) + self.child = 0 else: self.child += 1 @@ -116,19 +117,15 @@ class XMLParser: def endElement(self, name): - if self.child: - self.child -= 1 - - if hasattr(self.current, "endElement"): - self.current.endElement(name) - return - if hasattr(self.current, "endElement"): self.current.endElement(None) + if self.child: + self.child -= 1 + # Don't remove root - if len(self.stack) > 1: - last = self.stack.pop() + elif len(self.stack) > 1: + last, self.child = self.stack.pop() if hasattr(self.current, "addChild"): if self.current.addChild(name, last): return From 015fb47d90344847b94e3c86d55ac5944909c2d9 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Sun, 3 Jun 2018 09:58:38 +0200 Subject: [PATCH 212/271] events: alert on malformed start command --- modules/events.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/events.py b/modules/events.py index 2b6bfb1..acac196 100644 --- a/modules/events.py +++ b/modules/events.py @@ -166,6 +166,9 @@ def start_countdown(msg): else: evt.end += timedelta(seconds=int(t)) + else: + raise IMException("Mauvais format de date pour l'événement %s. Il n'a pas été créé." % msg.args[0]) + context.data[msg.args[0]] = evt context.save() From 125ae6ad0b288e8e2f931794ded32655c40f6fcb Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Sat, 2 Jun 2018 14:17:31 +0200 Subject: [PATCH 213/271] feed: fix RSS parsing --- nemubot/tools/feed.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nemubot/tools/feed.py b/nemubot/tools/feed.py index 7e63cd2..0404fff 100644 --- a/nemubot/tools/feed.py +++ b/nemubot/tools/feed.py @@ -105,13 +105,13 @@ class Feed: self.updated = None self.entries = list() - if self.feed.tagName == "rss": + if self.feed.tagName == "rdf:RDF": self._parse_rss_feed() elif self.feed.tagName == "feed": self._parse_atom_feed() else: from nemubot.exception import IMException - raise IMException("This is not a valid Atom or RSS feed") + raise IMException("This is not a valid Atom or RSS feed.") def _parse_atom_feed(self): From b8741bb1f71ecf4d91f6a4d0547e292eeb8a4774 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Wed, 20 Jun 2018 07:46:46 +0200 Subject: [PATCH 214/271] imdb: fix exception when no movie found --- modules/imdb.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/imdb.py b/modules/imdb.py index d5ff158..c5fdf76 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -52,7 +52,9 @@ def find_movies(title, year=None): # Make the request data = web.getJSON(url, remove_callback=True) - if year is None: + if "d" not in data: + return None + elif year is None: return data["d"] else: return [d for d in data["d"] if "y" in d and str(d["y"]) == year] From cd6750154c9270fbf3a9324bbd024f9956107b9b Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Fri, 15 Jun 2018 00:55:08 +0200 Subject: [PATCH 215/271] worldcup: update module to 2018 worldcup --- modules/worldcup.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/modules/worldcup.py b/modules/worldcup.py index b12ca30..764991d 100644 --- a/modules/worldcup.py +++ b/modules/worldcup.py @@ -1,6 +1,6 @@ # coding=utf-8 -"""The 2014 football worldcup module""" +"""The 2014,2018 football worldcup module""" from datetime import datetime, timezone import json @@ -9,6 +9,7 @@ from urllib.parse import quote from urllib.request import urlopen from nemubot import context +from nemubot.event import ModuleEvent from nemubot.exception import IMException from nemubot.hooks import hook from nemubot.tools.xmlparser.node import ModuleState @@ -20,7 +21,6 @@ from nemubot.module.more import Response API_URL="http://worldcup.sfg.io/%s" def load(context): - from nemubot.event import ModuleEvent context.add_event(ModuleEvent(func=lambda url: urlopen(url, timeout=10).read().decode(), func_data=API_URL % "matches/current?by_date=DESC", call=current_match_new_action, interval=30)) @@ -65,10 +65,10 @@ def cmd_watch(msg): context.save() raise IMException("This channel will not anymore receives world cup events.") -def current_match_new_action(match_str, osef): - context.add_event(ModuleEvent(func=lambda url: urlopen(url).read().decode(), func_data=API_URL % "matches/current?by_date=DESC", call=current_match_new_action, interval=30)) - - matches = json.loads(match_str) +def current_match_new_action(matches, osef): + def cmp(om, nm): + return len(nm) and (len(om) == 0 or len(nm[0]["home_team_events"]) != len(om[0]["home_team_events"]) or len(nm[0]["away_team_events"]) != len(om[0]["away_team_events"])) + context.add_event(ModuleEvent(func=lambda url: json.loads(urlopen(url).read().decode()), func_data=API_URL % "matches/current?by_date=DESC", cmp=cmp, call=current_match_new_action, interval=30)) for match in matches: if is_valid(match): @@ -120,20 +120,19 @@ def detail_event(evt): return evt + " par" def txt_event(e): - return "%se minutes : %s %s (%s)" % (e["time"], detail_event(e["type_of_event"]), e["player"], e["team"]["code"]) + return "%s minute : %s %s (%s)" % (e["time"], detail_event(e["type_of_event"]), e["player"], e["team"]["code"]) def prettify(match): - matchdate_local = datetime.strptime(match["datetime"].replace(':', ''), "%Y-%m-%dT%H%M%S.%f%z") - matchdate = matchdate_local - (matchdate_local.utcoffset() - datetime.timedelta(hours=2)) + matchdate = datetime.strptime(match["datetime"].replace(':', ''), "%Y-%m-%dT%H%M%SZ").replace(tzinfo=timezone.utc) if match["status"] == "future": - return ["Match à venir (%s) le %s : %s vs. %s" % (match["match_number"], matchdate.strftime("%A %d à %H:%M"), match["home_team"]["country"], match["away_team"]["country"])] + return ["Match à venir (%s) le %s : %s vs. %s" % (match["fifa_id"], matchdate.strftime("%A %d à %H:%M"), match["home_team"]["country"], match["away_team"]["country"])] else: msgs = list() msg = "" if match["status"] == "completed": - msg += "Match (%s) du %s terminé : " % (match["match_number"], matchdate.strftime("%A %d à %H:%M")) + msg += "Match (%s) du %s terminé : " % (match["fifa_id"], matchdate.strftime("%A %d à %H:%M")) else: - msg += "Match en cours (%s) depuis %d minutes : " % (match["match_number"], (datetime.now(matchdate.tzinfo) - matchdate_local).total_seconds() / 60) + msg += "Match en cours (%s) depuis %d minutes : " % (match["fifa_id"], (datetime.now(tz=timezone.utc) - matchdate).total_seconds() / 60) msg += "%s %d - %d %s" % (match["home_team"]["country"], match["home_team"]["goals"], match["away_team"]["goals"], match["away_team"]["country"]) @@ -163,7 +162,7 @@ def is_valid(match): def get_match(url, matchid): allm = get_matches(url) for m in allm: - if int(m["match_number"]) == matchid: + if int(m["fifa_id"]) == matchid: return [ m ] def get_matches(url): @@ -192,7 +191,7 @@ def cmd_worldcup(msg): elif len(msg.args[0]) == 3: url = "matches/country?fifa_code=%s&by_date=DESC" % msg.args[0] elif is_int(msg.args[0]): - url = int(msg.arg[0]) + url = int(msg.args[0]) else: raise IMException("unrecognized request; choose between 'today', 'tomorrow', a FIFA country code or a match identifier") From 3b99099b528859db95106eb0309c6e9cd7f09809 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Thu, 30 Aug 2018 07:20:00 +0200 Subject: [PATCH 216/271] imdb: fix compatibility with new IMDB version --- modules/imdb.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/modules/imdb.py b/modules/imdb.py index c5fdf76..a938c7b 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -24,20 +24,17 @@ def get_movie_by_id(imdbid): return { "imdbID": imdbid, - "Title": soup.body.find(attrs={"itemprop": "name"}).next_element.strip(), + "Title": soup.body.find('h1').next_element.strip(), "Year": soup.body.find(id="titleYear").find("a").text.strip() if soup.body.find(id="titleYear") else ", ".join([y.text.strip() for y in soup.body.find(attrs={"class": "seasons-and-year-nav"}).find_all("div")[3].find_all("a")[:-1]]), - "Duration": soup.body.find_all(attrs={"itemprop": "duration"})[-1].text.strip(), - "imdbRating": soup.body.find(attrs={"itemprop": "ratingValue"}).text.strip(), - "imdbVotes": soup.body.find(attrs={"itemprop": "ratingCount"}).text.strip(), - "Plot": re.sub(r"\s+", " ", soup.body.find(id="titleStoryLine").find(attrs={"itemprop": "description"}).text).strip(), + "Duration": soup.body.find(attrs={"class": "subtext"}).find("time").text.strip(), + "imdbRating": soup.body.find(attrs={"class": "ratingValue"}).find("strong").text.strip(), + "imdbVotes": soup.body.find(attrs={"class": "imdbRating"}).find("a").text.strip(), + "Plot": re.sub(r"\s+", " ", soup.body.find(attrs={"class": "summary_text"}).text).strip(), - "Type": "TV Series" if soup.find(attrs={"class": "np_episode_guide"}) else "Movie", - "Country": ", ".join([c.find("a").text.strip() for c in soup.body.find(id="titleDetails").find_all(attrs={"class": "txt-block"}) if c.text.find("Country") != -1]), - "Released": soup.body.find(attrs={"itemprop": "datePublished"}).attrs["content"] if "content" in soup.body.find(attrs={"itemprop": "datePublished"}).attrs else "N\A", - "Genre": ", ".join([g.text.strip() for g in soup.body.find_all(attrs={"itemprop": "genre"})[:-1]]), - "Director": ", ".join([d.find(attrs={"itemprop": "name"}).text.strip() for d in soup.body.find_all(attrs={"itemprop": "director"})]), - "Writer": ", ".join([d.find(attrs={"itemprop": "name"}).text.strip() for d in soup.body.find_all(attrs={"itemprop": "creator"})]), - "Actors": ", ".join([d.find(attrs={"itemprop": "name"}).text.strip() for d in soup.body.find_all(attrs={"itemprop": "actors"})]), + "Type": "TV Series" if soup.find(id="title-episode-widget") else "Movie", + "Genre": ", ".join([x.text.strip() for x in soup.body.find(id="titleStoryLine").find_all("a") if x.get("href") is not None and x.get("href")[:7] == "/genre/"]), + "Country": ", ".join([x.text.strip() for x in soup.body.find(id="titleDetails").find_all("a") if x.get("href") is not None and x.get("href")[:32] == "/search/title?country_of_origin="]), + "Credits": " ; ".join([x.find("h4").text.strip() + " " + (", ".join([y.text.strip() for y in x.find_all("a") if y.get("href") is not None and y.get("href")[:6] == "/name/"])) for x in soup.body.find_all(attrs={"class": "credit_summary_item"})]), } @@ -94,9 +91,9 @@ def cmd_imdb(msg): res.append_message("%s \x02genre:\x0F %s; \x02rating\x0F: %s (%s votes); \x02plot\x0F: %s" % (data['Type'], data['Genre'], data['imdbRating'], data['imdbVotes'], data['Plot'])) + res.append_message("%s \x02from\x0F %s; %s" + % (data['Type'], data['Country'], data['Credits'])) - res.append_message("%s \x02from\x0F %s \x02released on\x0F %s; \x02directed by:\x0F %s; \x02written by:\x0F %s; \x02main actors:\x0F %s" - % (data['Type'], data['Country'], data['Released'], data['Director'], data['Writer'], data['Actors'])) return res From 5578e8b86e7bd3f707ee7b2f0bfe81dd14d19be7 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Thu, 30 Aug 2018 07:32:15 +0200 Subject: [PATCH 217/271] tools/web: split getURLContent function --- nemubot/tools/web.py | 73 ++++++++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 33 deletions(-) diff --git a/nemubot/tools/web.py b/nemubot/tools/web.py index c3ba42a..9428dd5 100644 --- a/nemubot/tools/web.py +++ b/nemubot/tools/web.py @@ -68,18 +68,7 @@ def getPassword(url): # Get real pages -def getURLContent(url, body=None, timeout=7, header=None, decode_error=False, - max_size=524288): - """Return page content corresponding to URL or None if any error occurs - - Arguments: - url -- the URL to get - body -- Data to send as POST content - timeout -- maximum number of seconds to wait before returning an exception - decode_error -- raise exception on non-200 pages or ignore it - max_size -- maximal size allow for the content - """ - +def _URLConn(cb, url, body=None, timeout=7, header=None): o = urlparse(_getNormalizedURL(url), "http") import http.client @@ -134,6 +123,27 @@ def getURLContent(url, body=None, timeout=7, header=None, decode_error=False, try: res = conn.getresponse() + return cb(res) + except http.client.BadStatusLine: + raise IMException("Invalid HTTP response") + finally: + conn.close() + +def getURLContent(url, body=None, timeout=7, header=None, decode_error=False, + max_size=524288): + """Return page content corresponding to URL or None if any error occurs + + Arguments: + url -- the URL to get + body -- Data to send as POST content + timeout -- maximum number of seconds to wait before returning an exception + decode_error -- raise exception on non-200 pages or ignore it + max_size -- maximal size allow for the content + """ + + import http.client + + def next(res): size = int(res.getheader("Content-Length", 524288)) cntype = res.getheader("Content-Type") @@ -155,28 +165,25 @@ def getURLContent(url, body=None, timeout=7, header=None, decode_error=False, charset = cha[1] else: charset = cha[0] - except http.client.BadStatusLine: - raise IMException("Invalid HTTP response") - finally: - conn.close() - if res.status == http.client.OK or res.status == http.client.SEE_OTHER: - return data.decode(charset).strip() - elif ((res.status == http.client.FOUND or - res.status == http.client.MOVED_PERMANENTLY) and - res.getheader("Location") != url): - return getURLContent( - urljoin(url, res.getheader("Location")), - body=body, - timeout=timeout, - header=header, - decode_error=decode_error, - max_size=max_size) - elif decode_error: - return data.decode(charset).strip() - else: - raise IMException("A HTTP error occurs: %d - %s" % - (res.status, http.client.responses[res.status])) + if res.status == http.client.OK or res.status == http.client.SEE_OTHER: + return data.decode(charset).strip() + elif ((res.status == http.client.FOUND or + res.status == http.client.MOVED_PERMANENTLY) and + res.getheader("Location") != url): + return getURLContent( + urljoin(url, res.getheader("Location")), + body=body, + timeout=timeout, + header=header, + decode_error=decode_error, + max_size=max_size) + elif decode_error: + return data.decode(charset).strip() + else: + raise IMException("A HTTP error occurs: %d - %s" % + (res.status, http.client.responses[res.status])) + return _URLConn(next, url=url, body=body, timeout=timeout) def getXML(*args, **kwargs): From 72bc8d3839a6b51394d23ccc4a385086d679d9ca Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Thu, 30 Aug 2018 07:53:38 +0200 Subject: [PATCH 218/271] feed: accept RSS that begins with <rss> tag --- nemubot/tools/feed.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nemubot/tools/feed.py b/nemubot/tools/feed.py index 0404fff..2873a65 100644 --- a/nemubot/tools/feed.py +++ b/nemubot/tools/feed.py @@ -105,7 +105,7 @@ class Feed: self.updated = None self.entries = list() - if self.feed.tagName == "rdf:RDF": + if self.feed.tagName == "rdf:RDF" or self.feed.tagName == "rss": self._parse_rss_feed() elif self.feed.tagName == "feed": self._parse_atom_feed() From 4a636b2b119cb52dbe4c8db16412695b199dce00 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Thu, 30 Aug 2018 07:59:26 +0200 Subject: [PATCH 219/271] tools/web: follow redirection in URLConn --- nemubot/tools/web.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/nemubot/tools/web.py b/nemubot/tools/web.py index 9428dd5..73a3807 100644 --- a/nemubot/tools/web.py +++ b/nemubot/tools/web.py @@ -68,7 +68,7 @@ def getPassword(url): # Get real pages -def _URLConn(cb, url, body=None, timeout=7, header=None): +def _URLConn(cb, url, body=None, timeout=7, header=None, follow_redir=True): o = urlparse(_getNormalizedURL(url), "http") import http.client @@ -123,6 +123,15 @@ def _URLConn(cb, url, body=None, timeout=7, header=None): try: res = conn.getresponse() + if follow_redir and ((res.status == http.client.FOUND or + res.status == http.client.MOVED_PERMANENTLY) and + res.getheader("Location") != url): + return _URLConn(cb, + url=urljoin(url, res.getheader("Location")), + body=body, + timeout=timeout, + header=header, + follow_redir=follow_redir) return cb(res) except http.client.BadStatusLine: raise IMException("Invalid HTTP response") @@ -141,13 +150,11 @@ def getURLContent(url, body=None, timeout=7, header=None, decode_error=False, max_size -- maximal size allow for the content """ - import http.client - - def next(res): + def _nextURLContent(res): size = int(res.getheader("Content-Length", 524288)) cntype = res.getheader("Content-Type") - if size > max_size or (cntype is not None and cntype[:4] != "text" and cntype[:4] != "appl"): + if max_size >= 0 and (size > max_size or (cntype is not None and cntype[:4] != "text" and cntype[:4] != "appl")): raise IMException("Content too large to be retrieved") data = res.read(size) @@ -166,24 +173,17 @@ def getURLContent(url, body=None, timeout=7, header=None, decode_error=False, else: charset = cha[0] + import http.client + if res.status == http.client.OK or res.status == http.client.SEE_OTHER: return data.decode(charset).strip() - elif ((res.status == http.client.FOUND or - res.status == http.client.MOVED_PERMANENTLY) and - res.getheader("Location") != url): - return getURLContent( - urljoin(url, res.getheader("Location")), - body=body, - timeout=timeout, - header=header, - decode_error=decode_error, - max_size=max_size) elif decode_error: return data.decode(charset).strip() else: raise IMException("A HTTP error occurs: %d - %s" % (res.status, http.client.responses[res.status])) - return _URLConn(next, url=url, body=body, timeout=timeout) + + return _URLConn(_nextURLContent, url=url, body=body, timeout=timeout, header=header) def getXML(*args, **kwargs): From 53fe00ed58f3401e7bb1da5964d397e8966288a8 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Thu, 30 Aug 2018 07:59:41 +0200 Subject: [PATCH 220/271] tools/web: new function to retrieve only headers --- nemubot/tools/web.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/nemubot/tools/web.py b/nemubot/tools/web.py index 73a3807..1a4fbd7 100644 --- a/nemubot/tools/web.py +++ b/nemubot/tools/web.py @@ -138,6 +138,21 @@ def _URLConn(cb, url, body=None, timeout=7, header=None, follow_redir=True): finally: conn.close() + +def getURLHeaders(url, body=None, timeout=7, header=None, follow_redir=True): + """Return page headers corresponding to URL or None if any error occurs + + Arguments: + url -- the URL to get + body -- Data to send as POST content + timeout -- maximum number of seconds to wait before returning an exception + """ + + def next(res): + return res.status, res.getheaders() + return _URLConn(next, url=url, body=body, timeout=timeout, header=header, follow_redir=follow_redir) + + def getURLContent(url, body=None, timeout=7, header=None, decode_error=False, max_size=524288): """Return page content corresponding to URL or None if any error occurs From 31abcc97cfd54c223e1cbe5271d3a8655ab66d1d Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 9 Sep 2018 19:17:56 +0200 Subject: [PATCH 221/271] event: extract dict before call without init data --- nemubot/event/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/nemubot/event/__init__.py b/nemubot/event/__init__.py index 2b3ed6b..c471b2e 100644 --- a/nemubot/event/__init__.py +++ b/nemubot/event/__init__.py @@ -138,9 +138,12 @@ class ModuleEvent: self.call() else: self.call(d_init) + elif d_init is None: + if isinstance(self.call_data, dict): + self.call(**self.call_data) + else: + self.call(self.call_data) elif isinstance(self.call_data, dict): self.call(d_init, **self.call_data) - elif d_init is None: - self.call(self.call_data) else: self.call(d_init, self.call_data) From 2fd20d900242325b535c020c6e32f0a8d5af5fa6 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 9 Sep 2018 19:33:42 +0200 Subject: [PATCH 222/271] nntp: Here it is! --- modules/nntp.py | 209 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 modules/nntp.py diff --git a/modules/nntp.py b/modules/nntp.py new file mode 100644 index 0000000..67757d1 --- /dev/null +++ b/modules/nntp.py @@ -0,0 +1,209 @@ +"""The NNTP module""" + +# PYTHON STUFFS ####################################################### + +import email +from email.utils import mktime_tz, parseaddr, parsedate_tz +from nntplib import NNTP, decode_header +import re +import time +from datetime import datetime +from zlib import adler32 + +from nemubot import context +from nemubot.event import ModuleEvent +from nemubot.exception import IMException +from nemubot.hooks import hook +from nemubot.tools.xmlparser.node import ModuleState + +from nemubot.module.more import Response + + +# LOADING ############################################################# + +def load(context): + for wn in context.data.getNodes("watched_newsgroup"): + watch(**wn.attributes) + + +# MODULE CORE ######################################################### + +def list_groups(group_pattern="*", **server): + with NNTP(**server) as srv: + response, l = srv.list(group_pattern) + for i in l: + yield i.group, srv.description(i.group), i.flag + +def read_group(group, **server): + with NNTP(**server) as srv: + response, count, first, last, name = srv.group(group) + resp, overviews = srv.over((first, last)) + for art_num, over in reversed(overviews): + yield over + +def read_article(msg_id, **server): + with NNTP(**server) as srv: + response, info = srv.article(msg_id) + return email.message_from_bytes(b"\r\n".join(info.lines)) + +def whatsnew(date_last_check, group="*", **server): + fill = dict() + if "user" in server: fill["user"] = server["user"] + if "password" in server: fill["password"] = server["password"] + if "host" in server: fill["host"] = server["host"] + if "port" in server: fill["port"] = server["port"] + + with NNTP(**fill) as srv: + response, groups = srv.newgroups(date_last_check) + for g in groups: + yield g + + response, articles = srv.newnews(group, date_last_check) + for msg_id in articles: + response, info = srv.article(msg_id) + yield email.message_from_bytes(b"\r\n".join(info.lines)) + + +def format_article(art, **response_args): + art["X-FromName"], art["X-FromEmail"] = parseaddr(art["From"] if "From" in art else "") + if art["X-FromName"] == '': art["X-FromName"] = art["X-FromEmail"] + + date = mktime_tz(parsedate_tz(art["Date"])) + if date < time.time() - 120: + title = "\x0314In \x0F\x03{0:02d}{Newsgroups}\x0F\x0314: on \x0F{Date}\x0314 by \x0F\x03{0:02d}{X-FromName}\x0F \x02{Subject}\x0F" + else: + title = "\x0314In \x0F\x03{0:02d}{Newsgroups}\x0F\x0314: by \x0F\x03{0:02d}{X-FromName}\x0F \x02{Subject}\x0F" + + return Response(art.get_payload().replace('\n', ' '), + title=title.format(adler32(art["Newsgroups"].encode()) & 0xf, adler32(art["X-FromEmail"].encode()) & 0xf, **{h: decode_header(i) for h,i in art.items()}), + **response_args) + + +watches = dict() + +def _indexServer(**kwargs): + if "user" not in kwargs: kwargs["user"] = "" + if "password" not in kwargs: kwargs["password"] = "" + if "host" not in kwargs: kwargs["host"] = "" + if "port" not in kwargs: kwargs["port"] = 119 + return "{user}:{password}@{host}:{port}".format(**kwargs) + +def _newevt(**args): + context.add_event(ModuleEvent(call=_fini, call_data=args, interval=42)) + +def _fini(to_server, to_channel, lastcheck, group, server): + print("fini called") + _newevt(to_server=to_server, to_channel=to_channel, group=group, lastcheck=datetime.now(), server=server) + n = 0 + for art in whatsnew(lastcheck, group, **server): + n += 1 + if n > 10: + continue + context.send_response(to_server, format_article(art, channel=to_channel)) + if n > 10: + context.send_response(to_server, Response("... and %s others news" % (n - 10), channel=to_channel)) + +def watch(to_server, to_channel, group="*", lastcheck=None, **server): + if lastcheck is None: + lastcheck = datetime.now() + _newevt(to_server=to_server, to_channel=to_channel, group=group, lastcheck=lastcheck, server=server) + + +# MODULE INTERFACE #################################################### + +keywords_server = { + "host=HOST": "hostname or IP of the NNTP server", + "port=PORT": "port of the NNTP server", + "user=USERNAME": "username to use to connect to the server", + "password=PASSWORD": "password to use to connect to the server", +} + +@hook.command("nntp_groups", + help="Show list of existing groups", + help_usage={ + None: "Display all groups", + "PATTERN": "Filter on group matching the PATTERN" + }, + keywords=keywords_server) +def cmd_groups(msg): + if "host" not in msg.kwargs: + raise IMException("please give a hostname in keywords") + + return Response(["\x02\x03{0:02d}{1}\x0F: {2}".format(adler32(g[0].encode()) & 0xf, *g) for g in list_groups(msg.args[0] if len(msg.args) > 0 else "*", **msg.kwargs)], + channel=msg.channel, + title="Matching groups on %s" % msg.kwargs["host"]) + + +@hook.command("nntp_overview", + help="Show an overview of articles in given group(s)", + help_usage={ + "GROUP": "Filter on group matching the PATTERN" + }, + keywords=keywords_server) +def cmd_overview(msg): + if "host" not in msg.kwargs: + raise IMException("please give a hostname in keywords") + + if not len(msg.args): + raise IMException("which group would you overview?") + + for g in msg.args: + arts = [] + for grp in read_group(g, **msg.kwargs): + grp["X-FromName"], grp["X-FromEmail"] = parseaddr(grp["from"] if "from" in grp else "") + if grp["X-FromName"] == '': grp["X-FromName"] = grp["X-FromEmail"] + + arts.append("On {date}, from \x03{0:02d}{X-FromName}\x0F \x02{subject}\x0F: \x0314{message-id}\x0F".format(adler32(grp["X-FromEmail"].encode()) & 0xf, **{h: decode_header(i) for h,i in grp.items()})) + + if len(arts): + yield Response(arts, + channel=msg.channel, + title="In \x03{0:02d}{1}\x0F".format(adler32(g[0].encode()) & 0xf, g)) + + +@hook.command("nntp_read", + help="Read an article from a server", + help_usage={ + "MSG_ID": "Read the given message" + }, + keywords=keywords_server) +def cmd_read(msg): + if "host" not in msg.kwargs: + raise IMException("please give a hostname in keywords") + + for msgid in msg.args: + if not re.match("<.*>", msgid): + msgid = "<" + msgid + ">" + art = read_article(msgid, **msg.kwargs) + yield format_article(art, channel=msg.channel) + + +@hook.command("nntp_watch", + help="Launch an event looking for new groups and articles on a server", + help_usage={ + None: "Watch all groups", + "PATTERN": "Limit the watch on group matching this PATTERN" + }, + keywords=keywords_server) +def cmd_watch(msg): + if "host" not in msg.kwargs: + raise IMException("please give a hostname in keywords") + + if not msg.frm_owner: + raise IMException("sorry, this command is currently limited to the owner") + + wnnode = ModuleState("watched_newsgroup") + wnnode["id"] = _indexServer(**msg.kwargs) + wnnode["to_server"] = msg.server + wnnode["to_channel"] = msg.channel + wnnode["group"] = msg.args[0] if len(msg.args) > 0 else "*" + + wnnode["user"] = msg.kwargs["user"] if "user" in msg.kwargs else "" + wnnode["password"] = msg.kwargs["password"] if "password" in msg.kwargs else "" + wnnode["host"] = msg.kwargs["host"] if "host" in msg.kwargs else "" + wnnode["port"] = msg.kwargs["port"] if "port" in msg.kwargs else 119 + + context.data.addChild(wnnode) + watch(**wnnode.attributes) + + return Response("Ok ok, I watch this newsgroup!", channel=msg.channel) From f1da640a5b1ff984a4e26fb130334e4ebf2b8503 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Tue, 25 Sep 2018 20:42:51 +0200 Subject: [PATCH 223/271] tools/web: fix isURL function --- nemubot/tools/web.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nemubot/tools/web.py b/nemubot/tools/web.py index 1a4fbd7..ab20643 100644 --- a/nemubot/tools/web.py +++ b/nemubot/tools/web.py @@ -23,7 +23,7 @@ from nemubot.exception import IMException def isURL(url): """Return True if the URL can be parsed""" o = urlparse(_getNormalizedURL(url)) - return o.netloc == "" and o.path == "" + return o.netloc != "" and o.path != "" def _getNormalizedURL(url): From 67dee382a6a7b9a8fb1e833436f0b0ba5d862e4e Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Tue, 25 Sep 2018 20:43:21 +0200 Subject: [PATCH 224/271] New module smmry, using https://smmry.com/ API --- modules/smmry.py | 86 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 modules/smmry.py diff --git a/modules/smmry.py b/modules/smmry.py new file mode 100644 index 0000000..af6304a --- /dev/null +++ b/modules/smmry.py @@ -0,0 +1,86 @@ +"""Summarize texts""" + +# PYTHON STUFFS ####################################################### + +from urllib.parse import quote + +from nemubot import context +from nemubot.exception import IMException +from nemubot.hooks import hook +from nemubot.tools import web + +from nemubot.module.more import Response + + +# GLOBALS ############################################################# + +URL_API = "https://api.smmry.com/?SM_API_KEY=%s&SM_WITH_ENCODE" + + +# LOADING ############################################################# + +def load(context): + if not context.config or "apikey" not in context.config: + raise ImportError("You need a Smmry API key in order to use this " + "module. Add it to the module configuration file:\n" + "<module name=\"smmry\" apikey=\"XXXXXXXXXXXXXXXX\" " + "/>\nRegister at https://smmry.com/partner") + global URL_API + URL_API = URL_API % context.config["apikey"] + + +# MODULE INTERFACE #################################################### + +@hook.command("smmry", + help="Summarize the following words/command return", + help_usage={ + "WORDS/CMD": "" + }) +def cmd_smmry(msg): + if not len(msg.args): + raise IMException("indicate a text to sum up") + + res = Response(channel=msg.channel) + + if web.isURL(" ".join(msg.args)): + smmry = web.getJSON(URL_API + "&SM_URL=" + quote(" ".join(msg.args)), timeout=23) + else: + cnt = "" + for r in context.subtreat(context.subparse(msg, " ".join(msg.args))): + if isinstance(r, Response): + for i in range(len(r.messages) - 1, -1, -1): + if isinstance(r.messages[i], list): + for j in range(len(r.messages[i]) - 1, -1, -1): + cnt += r.messages[i][j] + "\n" + elif isinstance(r.messages[i], str): + cnt += r.messages[i] + "\n" + else: + cnt += str(r.messages) + "\n" + + elif isinstance(r, Text): + cnt += r.message + "\n" + + else: + cnt += str(r) + "\n" + + smmry = web.getJSON(URL_API, body="sm_api_input=" + quote(cnt), timeout=23) + + if "sm_api_error" in smmry: + if smmry["sm_api_error"] == 0: + title = "Internal server problem (not your fault)" + elif smmry["sm_api_error"] == 1: + title = "Incorrect submission variables" + elif smmry["sm_api_error"] == 2: + title = "Intentional restriction (low credits?)" + elif smmry["sm_api_error"] == 3: + title = "Summarization error" + else: + title = "Unknown error" + raise IMException(title + ": " + smmry['sm_api_message'].lower()) + + if "sm_api_title" in smmry and smmry["sm_api_title"] != "": + res.append_message(smmry["sm_api_content"], title=smmry["sm_api_title"]) + else: + res.append_message(smmry["sm_api_content"]) + + return res From 15064204d8c1ec6681c15ed516115dba13b1df06 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Tue, 25 Sep 2018 20:46:43 +0200 Subject: [PATCH 225/271] smmry: don't ask to URLencode returned texts --- modules/smmry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/smmry.py b/modules/smmry.py index af6304a..711f672 100644 --- a/modules/smmry.py +++ b/modules/smmry.py @@ -14,7 +14,7 @@ from nemubot.module.more import Response # GLOBALS ############################################################# -URL_API = "https://api.smmry.com/?SM_API_KEY=%s&SM_WITH_ENCODE" +URL_API = "https://api.smmry.com/?SM_API_KEY=%s" # LOADING ############################################################# From 8224d1120706fe29fcca506f536eb043224bbdd2 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Wed, 26 Sep 2018 05:50:44 +0200 Subject: [PATCH 226/271] smmry: use URLStack from urlreducer module --- modules/smmry.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/modules/smmry.py b/modules/smmry.py index 711f672..ee3332d 100644 --- a/modules/smmry.py +++ b/modules/smmry.py @@ -10,6 +10,7 @@ from nemubot.hooks import hook from nemubot.tools import web from nemubot.module.more import Response +from nemubot.module.urlreducer import LAST_URLS # GLOBALS ############################################################# @@ -38,7 +39,11 @@ def load(context): }) def cmd_smmry(msg): if not len(msg.args): - raise IMException("indicate a text to sum up") + global LAST_URLS + if msg.channel in LAST_URLS and len(LAST_URLS[msg.channel]) > 0: + msg.args.append(LAST_URLS[msg.channel].pop()) + else: + raise IMException("I have no more URL to sum up.") res = Response(channel=msg.channel) From 46541cb35e9fdd12b6afedcb6bece396b6c5b529 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Wed, 26 Sep 2018 06:15:44 +0200 Subject: [PATCH 227/271] smmry: handle some more options --- modules/smmry.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/modules/smmry.py b/modules/smmry.py index ee3332d..6545934 100644 --- a/modules/smmry.py +++ b/modules/smmry.py @@ -36,6 +36,14 @@ def load(context): help="Summarize the following words/command return", help_usage={ "WORDS/CMD": "" + }, + keywords={ + "length=7": "The number of sentences returned, default 7", + "break": "inserts the string [BREAK] between sentences", + "ignore_length": "returns summary regardless of quality or length", + "quote_avoid": "sentences with quotations will be excluded", + "question_avoid": "sentences with question will be excluded", + "exclamation_avoid": "sentences with exclamation marks will be excluded", }) def cmd_smmry(msg): if not len(msg.args): @@ -45,10 +53,22 @@ def cmd_smmry(msg): else: raise IMException("I have no more URL to sum up.") + URL = URL_API + if "length" in msg.kwargs: + if int(msg.kwargs["length"]) > 0 : + URL += "&SM_LENGTH=" + msg.kwargs["length"] + else: + msg.kwargs["ignore_length"] = True + if "break" in msg.kwargs: URL += "&SM_WITH_BREAK" + if "ignore_length" in msg.kwargs: URL += "&SM_IGNORE_LENGTH" + if "quote_avoid" in msg.kwargs: URL += "&SM_QUOTE_AVOID" + if "question_avoid" in msg.kwargs: URL += "&SM_QUESTION_AVOID" + if "exclamation_avoid" in msg.kwargs: URL += "&SM_EXCLAMATION_AVOID" + res = Response(channel=msg.channel) if web.isURL(" ".join(msg.args)): - smmry = web.getJSON(URL_API + "&SM_URL=" + quote(" ".join(msg.args)), timeout=23) + smmry = web.getJSON(URL + "&SM_URL=" + quote(" ".join(msg.args)), timeout=23) else: cnt = "" for r in context.subtreat(context.subparse(msg, " ".join(msg.args))): @@ -68,7 +88,7 @@ def cmd_smmry(msg): else: cnt += str(r) + "\n" - smmry = web.getJSON(URL_API, body="sm_api_input=" + quote(cnt), timeout=23) + smmry = web.getJSON(URL, body="sm_api_input=" + quote(cnt), timeout=23) if "sm_api_error" in smmry: if smmry["sm_api_error"] == 0: From 445a66ea90eb8344f0e1ecadf02a9cbbd8d07729 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Wed, 26 Sep 2018 06:35:44 +0200 Subject: [PATCH 228/271] hooks: keywords can have optional values: place a question mark before = { "keyword?=X": "help about keyword (precise the default value if needed)" } --- nemubot/hooks/keywords/dict.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nemubot/hooks/keywords/dict.py b/nemubot/hooks/keywords/dict.py index e1429fc..c2d3f2e 100644 --- a/nemubot/hooks/keywords/dict.py +++ b/nemubot/hooks/keywords/dict.py @@ -43,7 +43,7 @@ class Dict(Abstract): def check(self, mkw): for k in mkw: - if (mkw[k] and k not in self.chk_args) or (not mkw[k] and k not in self.chk_noarg): + if ((k + "?") not in self.chk_args) and ((mkw[k] and k not in self.chk_args) or (not mkw[k] and k not in self.chk_noarg)): if mkw[k] and k in self.chk_noarg: raise KeywordException("Keyword %s doesn't take value." % k) elif not mkw[k] and k in self.chk_args: From 10b8ce894025cbb77d432b67ca5b236a42d92ca8 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Wed, 26 Sep 2018 06:45:44 +0200 Subject: [PATCH 229/271] smmry: add keywords options --- modules/smmry.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/smmry.py b/modules/smmry.py index 6545934..b1fe72c 100644 --- a/modules/smmry.py +++ b/modules/smmry.py @@ -38,6 +38,7 @@ def load(context): "WORDS/CMD": "" }, keywords={ + "keywords?=X": "Returns keywords instead of summary (count optional)", "length=7": "The number of sentences returned, default 7", "break": "inserts the string [BREAK] between sentences", "ignore_length": "returns summary regardless of quality or length", @@ -64,6 +65,7 @@ def cmd_smmry(msg): if "quote_avoid" in msg.kwargs: URL += "&SM_QUOTE_AVOID" if "question_avoid" in msg.kwargs: URL += "&SM_QUESTION_AVOID" if "exclamation_avoid" in msg.kwargs: URL += "&SM_EXCLAMATION_AVOID" + if "keywords" in msg.kwargs and msg.kwargs["keywords"] is not None and int(msg.kwargs["keywords"]) > 0: URL += "&SM_KEYWORD_COUNT=" + msg.kwargs["keywords"] res = Response(channel=msg.channel) @@ -103,6 +105,9 @@ def cmd_smmry(msg): title = "Unknown error" raise IMException(title + ": " + smmry['sm_api_message'].lower()) + if "keywords" in msg.kwargs: + smmry["sm_api_content"] = ", ".join(smmry["sm_api_keyword_array"]) + if "sm_api_title" in smmry and smmry["sm_api_title"] != "": res.append_message(smmry["sm_api_content"], title=smmry["sm_api_title"]) else: From 86059327025ea86bc0ca79a1379cebd17fa0f710 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Mon, 3 Dec 2018 23:55:25 +0100 Subject: [PATCH 230/271] imdb: follow imdb.com evolutions --- modules/imdb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/imdb.py b/modules/imdb.py index a938c7b..5234c39 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -24,7 +24,7 @@ def get_movie_by_id(imdbid): return { "imdbID": imdbid, - "Title": soup.body.find('h1').next_element.strip(), + "Title": soup.body.find('h1').contents[0].strip(), "Year": soup.body.find(id="titleYear").find("a").text.strip() if soup.body.find(id="titleYear") else ", ".join([y.text.strip() for y in soup.body.find(attrs={"class": "seasons-and-year-nav"}).find_all("div")[3].find_all("a")[:-1]]), "Duration": soup.body.find(attrs={"class": "subtext"}).find("time").text.strip(), "imdbRating": soup.body.find(attrs={"class": "ratingValue"}).find("strong").text.strip(), @@ -32,7 +32,7 @@ def get_movie_by_id(imdbid): "Plot": re.sub(r"\s+", " ", soup.body.find(attrs={"class": "summary_text"}).text).strip(), "Type": "TV Series" if soup.find(id="title-episode-widget") else "Movie", - "Genre": ", ".join([x.text.strip() for x in soup.body.find(id="titleStoryLine").find_all("a") if x.get("href") is not None and x.get("href")[:7] == "/genre/"]), + "Genre": ", ".join([x.text.strip() for x in soup.body.find(id="titleStoryLine").find_all("a") if x.get("href") is not None and x.get("href")[:21] == "/search/title?genres="]), "Country": ", ".join([x.text.strip() for x in soup.body.find(id="titleDetails").find_all("a") if x.get("href") is not None and x.get("href")[:32] == "/search/title?country_of_origin="]), "Credits": " ; ".join([x.find("h4").text.strip() + " " + (", ".join([y.text.strip() for y in x.find_all("a") if y.get("href") is not None and y.get("href")[:6] == "/name/"])) for x in soup.body.find_all(attrs={"class": "credit_summary_item"})]), } From b349d223706871a5a909a8b50367c3691b9bfac7 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Sun, 30 Dec 2018 00:42:21 +0100 Subject: [PATCH 231/271] events: ModuleEvent don't store function argument anymore --- modules/networking/watchWebsite.py | 14 +++---- modules/nntp.py | 3 +- modules/worldcup.py | 7 ++-- nemubot/event/__init__.py | 65 +++++------------------------- 4 files changed, 23 insertions(+), 66 deletions(-) diff --git a/modules/networking/watchWebsite.py b/modules/networking/watchWebsite.py index adedbee..d6b806f 100644 --- a/modules/networking/watchWebsite.py +++ b/modules/networking/watchWebsite.py @@ -1,5 +1,6 @@ """Alert on changes on websites""" +from functools import partial import logging from random import randint import urllib.parse @@ -209,15 +210,14 @@ def start_watching(site, offset=0): offset -- offset time to delay the launch of the first check """ - o = urlparse(getNormalizedURL(site["url"]), "http") - #print_debug("Add %s event for site: %s" % (site["type"], o.netloc)) + #o = urlparse(getNormalizedURL(site["url"]), "http") + #print("Add %s event for site: %s" % (site["type"], o.netloc)) try: - evt = ModuleEvent(func=fwatch, - cmp_data=site["lastcontent"], - func_data=site["url"], offset=offset, - interval=site.getInt("time"), - call=alert_change, call_data=site) + evt = ModuleEvent(func=partial(fwatch, url=site["url"]), + cmp=site["lastcontent"], + offset=offset, interval=site.getInt("time"), + call=partial(alert_change, site=site)) site["_evt_id"] = add_event(evt) except IMException: logger.exception("Unable to watch %s", site["url"]) diff --git a/modules/nntp.py b/modules/nntp.py index 67757d1..e15c48b 100644 --- a/modules/nntp.py +++ b/modules/nntp.py @@ -4,6 +4,7 @@ import email from email.utils import mktime_tz, parseaddr, parsedate_tz +from functools import partial from nntplib import NNTP, decode_header import re import time @@ -89,7 +90,7 @@ def _indexServer(**kwargs): return "{user}:{password}@{host}:{port}".format(**kwargs) def _newevt(**args): - context.add_event(ModuleEvent(call=_fini, call_data=args, interval=42)) + context.add_event(ModuleEvent(call=partial(_fini, **args), interval=42)) def _fini(to_server, to_channel, lastcheck, group, server): print("fini called") diff --git a/modules/worldcup.py b/modules/worldcup.py index 764991d..e72f1ac 100644 --- a/modules/worldcup.py +++ b/modules/worldcup.py @@ -3,6 +3,7 @@ """The 2014,2018 football worldcup module""" from datetime import datetime, timezone +from functools import partial import json import re from urllib.parse import quote @@ -21,7 +22,7 @@ from nemubot.module.more import Response API_URL="http://worldcup.sfg.io/%s" def load(context): - context.add_event(ModuleEvent(func=lambda url: urlopen(url, timeout=10).read().decode(), func_data=API_URL % "matches/current?by_date=DESC", call=current_match_new_action, interval=30)) + context.add_event(ModuleEvent(func=partial(lambda url: urlopen(url, timeout=10).read().decode(), API_URL % "matches/current?by_date=DESC"), call=current_match_new_action, interval=30)) def help_full (): @@ -65,10 +66,10 @@ def cmd_watch(msg): context.save() raise IMException("This channel will not anymore receives world cup events.") -def current_match_new_action(matches, osef): +def current_match_new_action(matches): def cmp(om, nm): return len(nm) and (len(om) == 0 or len(nm[0]["home_team_events"]) != len(om[0]["home_team_events"]) or len(nm[0]["away_team_events"]) != len(om[0]["away_team_events"])) - context.add_event(ModuleEvent(func=lambda url: json.loads(urlopen(url).read().decode()), func_data=API_URL % "matches/current?by_date=DESC", cmp=cmp, call=current_match_new_action, interval=30)) + context.add_event(ModuleEvent(func=partial(lambda url: json.loads(urlopen(url).read().decode()), API_URL % "matches/current?by_date=DESC"), cmp=partial(cmp, matches), call=current_match_new_action, interval=30)) for match in matches: if is_valid(match): diff --git a/nemubot/event/__init__.py b/nemubot/event/__init__.py index c471b2e..49c6902 100644 --- a/nemubot/event/__init__.py +++ b/nemubot/event/__init__.py @@ -21,18 +21,14 @@ class ModuleEvent: """Representation of a event initiated by a bot module""" - def __init__(self, call=None, call_data=None, func=None, func_data=None, - cmp=None, cmp_data=None, interval=60, offset=0, times=1): + def __init__(self, call=None, func=None, cmp=None, interval=60, offset=0, times=1): """Initialize the event Keyword arguments: call -- Function to call when the event is realized - call_data -- Argument(s) (single or dict) to pass as argument func -- Function called to check - func_data -- Argument(s) (single or dict) to pass as argument OR if no func, initial data to watch - cmp -- Boolean function called to check changes - cmp_data -- Argument(s) (single or dict) to pass as argument OR if no cmp, data compared to previous + cmp -- Boolean function called to check changes or value to compare with interval -- Time in seconds between each check (default: 60) offset -- Time in seconds added to interval before the first check (default: 0) times -- Number of times the event has to be realized before being removed; -1 for no limit (default: 1) @@ -40,27 +36,12 @@ class ModuleEvent: # What have we to check? self.func = func - self.func_data = func_data # How detect a change? self.cmp = cmp - self.cmp_data = None - if cmp_data is not None: - self.cmp_data = cmp_data - elif self.func is not None: - if self.func_data is None: - self.cmp_data = self.func() - elif isinstance(self.func_data, dict): - self.cmp_data = self.func(**self.func_data) - else: - self.cmp_data = self.func(self.func_data) # What should we call when? self.call = call - if call_data is not None: - self.call_data = call_data - else: - self.call_data = func_data # Store times if isinstance(offset, timedelta): @@ -106,44 +87,18 @@ class ModuleEvent: def check(self): """Run a check and realized the event if this is time""" - # Get initial data - if self.func is None: - d_init = self.func_data - elif self.func_data is None: - d_init = self.func() - elif isinstance(self.func_data, dict): - d_init = self.func(**self.func_data) + # Get new data + if self.func is not None: + d_new = self.func() else: - d_init = self.func(self.func_data) + d_new = None # then compare with current data - if self.cmp is None: - if self.cmp_data is None: - rlz = True - else: - rlz = (d_init != self.cmp_data) - elif self.cmp_data is None: - rlz = self.cmp(d_init) - elif isinstance(self.cmp_data, dict): - rlz = self.cmp(d_init, **self.cmp_data) - else: - rlz = self.cmp(d_init, self.cmp_data) - - if rlz: + if self.cmp is None or (callable(self.cmp) and self.cmp(d_new)) or (not callable(self.cmp) and d_new != self.cmp): self.times -= 1 # Call attended function - if self.call_data is None: - if d_init is None: - self.call() - else: - self.call(d_init) - elif d_init is None: - if isinstance(self.call_data, dict): - self.call(**self.call_data) - else: - self.call(self.call_data) - elif isinstance(self.call_data, dict): - self.call(d_init, **self.call_data) + if self.func is not None: + self.call(d_new) else: - self.call(d_init, self.call_data) + self.call() From fa0f2e93ef97e375149db72f5313c75c1d04e212 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Sun, 30 Dec 2018 10:59:10 +0100 Subject: [PATCH 232/271] whois: update module --- modules/whois.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/modules/whois.py b/modules/whois.py index d6106dd..3edf5be 100644 --- a/modules/whois.py +++ b/modules/whois.py @@ -14,7 +14,7 @@ from nemubot.module.more import Response from nemubot.module.networking.page import headers PASSWD_FILE = None -# You can get one with: curl -b "sessionid=YOURSESSIONID" 'https://accounts.cri.epita.net/api/users/' > users.json +# You can get one with: curl -b "sessionid=YOURSESSIONID" 'https://accounts.cri.epita.net/api/users/?limit=10000' > users.json APIEXTRACT_FILE = None def load(context): @@ -49,7 +49,7 @@ def load(context): class Login: - def __init__(self, line=None, login=None, uidNumber=None, cn=None, promo=None, **kwargs): + def __init__(self, line=None, login=None, uidNumber=None, firstname=None, lastname=None, promo=None, **kwargs): if line is not None: s = line.split(":") self.login = s[0] @@ -61,19 +61,25 @@ class Login: self.login = login self.uid = uidNumber self.promo = promo - self.cn = cn - self.gid = "epita" + promo + self.cn = firstname + " " + lastname + try: + self.gid = "epita" + str(int(promo)) + except: + self.gid = promo def get_promo(self): if hasattr(self, "promo"): return self.promo if hasattr(self, "home"): - return self.home.split("/")[2].replace("_", " ") + try: + return self.home.split("/")[2].replace("_", " ") + except: + return self.gid def get_photo(self): if self.login in context.data.getNode("pics").index: return context.data.getNode("pics").index[self.login]["url"] - for url in [ "https://photos.cri.epita.net/%s", "https://static.acu.epita.fr/photos/%s", "https://static.acu.epita.fr/photos/%s/%%s" % self.gid, "https://intra-bocal.epitech.eu/trombi/%s.jpg", "http://whois.23.tf/p/%s/%%s.jpg" % self.gid ]: + for url in [ "https://photos.cri.epita.fr/%s", "https://intra-bocal.epitech.eu/trombi/%s.jpg" ]: url = url % self.login try: _, status, _, _ = headers(url) @@ -91,7 +97,7 @@ def login_lookup(login, search=False): if APIEXTRACT_FILE: with open(APIEXTRACT_FILE, encoding="utf-8") as f: api = json.load(f) - for l in api: + for l in api["results"]: if (not search and l["login"] == login) or (search and (("login" in l and l["login"].find(login) != -1) or ("cn" in l and l["cn"].find(login) != -1) or ("uid" in l and str(l["uid"]) == login))): yield Login(**l) From 517bf21d2507320f15b135c174e5ad25f3e158f5 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Fri, 1 Feb 2019 17:05:05 +0100 Subject: [PATCH 233/271] imdb: fix series changed attributes --- modules/imdb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/imdb.py b/modules/imdb.py index 5234c39..22eeaa3 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -25,8 +25,8 @@ def get_movie_by_id(imdbid): return { "imdbID": imdbid, "Title": soup.body.find('h1').contents[0].strip(), - "Year": soup.body.find(id="titleYear").find("a").text.strip() if soup.body.find(id="titleYear") else ", ".join([y.text.strip() for y in soup.body.find(attrs={"class": "seasons-and-year-nav"}).find_all("div")[3].find_all("a")[:-1]]), - "Duration": soup.body.find(attrs={"class": "subtext"}).find("time").text.strip(), + "Year": soup.body.find(id="titleYear").find("a").text.strip() if soup.body.find(id="titleYear") else ", ".join([y.text.strip() for y in soup.body.find(attrs={"class": "seasons-and-year-nav"}).find_all("a")[1:]]), + "Duration": soup.body.find(attrs={"class": "title_wrapper"}).find("time").text.strip() if soup.body.find(attrs={"class": "title_wrapper"}).find("time") else None, "imdbRating": soup.body.find(attrs={"class": "ratingValue"}).find("strong").text.strip(), "imdbVotes": soup.body.find(attrs={"class": "imdbRating"}).find("a").text.strip(), "Plot": re.sub(r"\s+", " ", soup.body.find(attrs={"class": "summary_text"}).text).strip(), From 9417e2ba932789923cb235111553825dc3723465 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Fri, 1 Feb 2019 17:45:08 +0100 Subject: [PATCH 234/271] urlreducer: define DEFAULT_PROVIDER later --- modules/urlreducer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/urlreducer.py b/modules/urlreducer.py index bd7646b..15e47d9 100644 --- a/modules/urlreducer.py +++ b/modules/urlreducer.py @@ -60,12 +60,14 @@ def load(context): # MODULE CORE ######################################################### -def reduce(url, provider=DEFAULT_PROVIDER): +def reduce(url, provider=None): """Ask the url shortner website to reduce given URL Argument: url -- the URL to reduce """ + if provider is None: + provider = DEFAULT_PROVIDER return PROVIDERS[provider][0](PROVIDERS[provider][1], url) From 20c19a72bce1fee3ee6bc944ed582025a72d7766 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Fri, 1 Feb 2019 17:45:30 +0100 Subject: [PATCH 235/271] urlreducer: new function to be used in responses' treat_line --- modules/urlreducer.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/modules/urlreducer.py b/modules/urlreducer.py index 15e47d9..86f4d42 100644 --- a/modules/urlreducer.py +++ b/modules/urlreducer.py @@ -60,6 +60,12 @@ def load(context): # MODULE CORE ######################################################### +def reduce_inline(txt, provider=None): + for url in re.findall("([a-zA-Z0-9+.-]+:(?://)?(?:[^ :/]+:[0-9]+)?[^ :]+)", txt): + txt = txt.replace(url, reduce(url, provider)) + return txt + + def reduce(url, provider=None): """Ask the url shortner website to reduce given URL From 85c418bd06ec6b991190fdfb99df70ce0af8fb83 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Sat, 2 Feb 2019 19:44:47 +0100 Subject: [PATCH 236/271] feed: fix RSS link handling --- nemubot/tools/feed.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/nemubot/tools/feed.py b/nemubot/tools/feed.py index 2873a65..6f8930d 100644 --- a/nemubot/tools/feed.py +++ b/nemubot/tools/feed.py @@ -82,11 +82,16 @@ class RSSEntry: else: self.summary = None - if len(node.getElementsByTagName("link")) > 0 and node.getElementsByTagName("link")[0].hasAttribute("href"): - self.link = node.getElementsByTagName("link")[0].getAttribute("href") + if len(node.getElementsByTagName("link")) > 0: + self.link = node.getElementsByTagName("link")[0].firstChild.nodeValue else: self.link = None + if len(node.getElementsByTagName("enclosure")) > 0 and node.getElementsByTagName("enclosure")[0].hasAttribute("url"): + self.enclosure = node.getElementsByTagName("enclosure")[0].getAttribute("url") + else: + self.enclosure = None + def __repr__(self): return "<RSSEntry title='%s' updated='%s'>" % (self.title, self.pubDate) From 7854e8628f021ea4f96c1257fdf0c6c4aa1ba290 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Sat, 2 Feb 2019 19:56:41 +0100 Subject: [PATCH 237/271] news: reduce link URL by default --- modules/news.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/news.py b/modules/news.py index 40daa92..c4c967a 100644 --- a/modules/news.py +++ b/modules/news.py @@ -13,6 +13,7 @@ from nemubot.hooks import hook from nemubot.tools import web from nemubot.module.more import Response +from nemubot.module.urlreducer import reduce_inline from nemubot.tools.feed import Feed, AtomEntry @@ -50,10 +51,11 @@ def cmd_news(msg): links = [x for x in find_rss_links(url)] if len(links) == 0: links = [ url ] - res = Response(channel=msg.channel, nomore="No more news from %s" % url) + res = Response(channel=msg.channel, nomore="No more news from %s" % url, line_treat=reduce_inline) for n in get_last_news(links[0]): res.append_message("%s published %s: %s %s" % (("\x02" + web.striphtml(n.title) + "\x0F") if n.title else "An article without title", (n.updated.strftime("on %A %d. %B %Y at %H:%M") if n.updated else "someday") if isinstance(n, AtomEntry) else n.pubDate, web.striphtml(n.summary) if n.summary else "", n.link if n.link else "")) + return res From 144551a232d56c573e42bc254a511a14fa0a040e Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Fri, 14 Jun 2019 19:33:51 +0200 Subject: [PATCH 238/271] imdb: fix unrated content --- modules/imdb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/imdb.py b/modules/imdb.py index 22eeaa3..7a42935 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -27,8 +27,8 @@ def get_movie_by_id(imdbid): "Title": soup.body.find('h1').contents[0].strip(), "Year": soup.body.find(id="titleYear").find("a").text.strip() if soup.body.find(id="titleYear") else ", ".join([y.text.strip() for y in soup.body.find(attrs={"class": "seasons-and-year-nav"}).find_all("a")[1:]]), "Duration": soup.body.find(attrs={"class": "title_wrapper"}).find("time").text.strip() if soup.body.find(attrs={"class": "title_wrapper"}).find("time") else None, - "imdbRating": soup.body.find(attrs={"class": "ratingValue"}).find("strong").text.strip(), - "imdbVotes": soup.body.find(attrs={"class": "imdbRating"}).find("a").text.strip(), + "imdbRating": soup.body.find(attrs={"class": "ratingValue"}).find("strong").text.strip() if soup.body.find(attrs={"class": "ratingValue"}) else None, + "imdbVotes": soup.body.find(attrs={"class": "imdbRating"}).find("a").text.strip() if soup.body.find(attrs={"class": "imdbRating"}) else None, "Plot": re.sub(r"\s+", " ", soup.body.find(attrs={"class": "summary_text"}).text).strip(), "Type": "TV Series" if soup.find(id="title-episode-widget") else "Movie", From 87b5ce842deb813e859d4d1b943f79c4c851e4d2 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Thu, 22 Aug 2019 15:51:10 +0200 Subject: [PATCH 239/271] cve: fix module with new cve website --- modules/cve.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/modules/cve.py b/modules/cve.py index b9cf1c3..18d9898 100644 --- a/modules/cve.py +++ b/modules/cve.py @@ -23,7 +23,6 @@ VULN_DATAS = { "description": "vuln-description", "published": "vuln-published-on", "last_modified": "vuln-last-modified-on", - "source": "vuln-source", "base_score": "vuln-cvssv3-base-score-link", "severity": "vuln-cvssv3-base-score-severity", @@ -92,9 +91,9 @@ def get_cve_desc(msg): alert = "" if "base_score" not in cve and "description" in cve: - res.append_message("{alert}From \x02{source}\x0F, last modified on \x02{last_modified}\x0F. {description}".format(alert=alert, **cve), title=cve_id) + res.append_message("{alert}Last modified on \x02{last_modified}\x0F. {description}".format(alert=alert, **cve), title=cve_id) else: metrics = display_metrics(**cve) - res.append_message("{alert}Base score: \x02{base_score} {severity}\x0F (impact: \x02{impact_score}\x0F, exploitability: \x02{exploitability_score}\x0F; {metrics}), from \x02{source}\x0F, last modified on \x02{last_modified}\x0F. {description}".format(alert=alert, metrics=metrics, **cve), title=cve_id) + res.append_message("{alert}Base score: \x02{base_score} {severity}\x0F (impact: \x02{impact_score}\x0F, exploitability: \x02{exploitability_score}\x0F; {metrics}), last modified on \x02{last_modified}\x0F. {description}".format(alert=alert, metrics=metrics, **cve), title=cve_id) return res From 4499677d557d403c0ac7a3ccb70e84621ea5c45c Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Thu, 22 Aug 2019 15:51:36 +0200 Subject: [PATCH 240/271] whois: don't use custom picture anymore --- modules/whois.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/modules/whois.py b/modules/whois.py index 3edf5be..1a5f598 100644 --- a/modules/whois.py +++ b/modules/whois.py @@ -39,10 +39,6 @@ def load(context): context.data.addChild(ModuleState("aliases")) context.data.getNode("aliases").setIndex("from", "alias") - if not context.data.hasNode("pics"): - context.data.addChild(ModuleState("pics")) - context.data.getNode("pics").setIndex("login", "pict") - import nemubot.hooks context.add_hook(nemubot.hooks.Command(cmd_whois, "whois", keywords={"lookup": "Perform a lookup of the begining of the login instead of an exact search."}), "in","Command") @@ -77,8 +73,6 @@ class Login: return self.gid def get_photo(self): - if self.login in context.data.getNode("pics").index: - return context.data.getNode("pics").index[self.login]["url"] for url in [ "https://photos.cri.epita.fr/%s", "https://intra-bocal.epitech.eu/trombi/%s.jpg" ]: url = url % self.login try: From 644a641b1301dbef067d42e52491ffdc5fbeda7f Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Tue, 10 Sep 2019 15:50:17 +0200 Subject: [PATCH 241/271] nntp: fix bad behaviour with UTF-8 encoded headers Read-Also: https://tools.ietf.org/html/rfc3977 --- modules/nntp.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/nntp.py b/modules/nntp.py index e15c48b..c8573bd 100644 --- a/modules/nntp.py +++ b/modules/nntp.py @@ -3,6 +3,7 @@ # PYTHON STUFFS ####################################################### import email +import email.policy from email.utils import mktime_tz, parseaddr, parsedate_tz from functools import partial from nntplib import NNTP, decode_header @@ -45,7 +46,8 @@ def read_group(group, **server): def read_article(msg_id, **server): with NNTP(**server) as srv: response, info = srv.article(msg_id) - return email.message_from_bytes(b"\r\n".join(info.lines)) + return email.message_from_bytes(b"\r\n".join(info.lines), policy=email.policy.SMTPUTF8) + def whatsnew(date_last_check, group="*", **server): fill = dict() From b72871a8c2a5cb38cb3a77074f5fea9a08fcbde0 Mon Sep 17 00:00:00 2001 From: Maxence <max@23.tf> Date: Wed, 18 Sep 2019 13:18:39 -0400 Subject: [PATCH 242/271] Remove rms.py module It should be replaced by a more generic: !news https://www.fsf.org/static/fsforg/rss/events.xml which can be aliased. --- modules/rms.py | 35 ----------------------------------- 1 file changed, 35 deletions(-) delete mode 100644 modules/rms.py diff --git a/modules/rms.py b/modules/rms.py deleted file mode 100644 index e7b89ce..0000000 --- a/modules/rms.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Finding RMS""" - -# PYTHON STUFFS ####################################################### - -from bs4 import BeautifulSoup - -from nemubot.hooks import hook -from nemubot.tools.web import getURLContent, striphtml -from nemubot.module.more import Response - - -# GLOBALS ############################################################# - -URL = 'https://www.fsf.org/events/rms-speeches.html' - - -# MODULE INTERFACE #################################################### - -@hook.command("rms", - help="Lists upcoming RMS events.") -def cmd_rms(msg): - soup = BeautifulSoup(getURLContent(URL), "lxml") - - res = Response(channel=msg.channel, - nomore="", - count=" (%d more event(s))") - - search_res = soup.find("table", {'class':'listing'}) - for item in search_res.tbody.find_all('tr'): - columns = item.find_all('td') - res.append_message("RMS will be in \x02%s\x0F for \x02%s\x0F on \x02%s\x0F." % ( - columns[1].get_text(), - columns[2].get_text().replace('\n', ''), - columns[0].get_text().replace('\n', ''))) - return res From 1f2d297ea152e1f83e0c8490cdedfe5b8cb72e35 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Sat, 21 Sep 2019 01:16:33 +0200 Subject: [PATCH 243/271] Update travis python versions --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d109d2a..8efd20f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,9 @@ language: python python: - - 3.3 - 3.4 - 3.5 + - 3.6 + - 3.7 - nightly install: - pip install -r requirements.txt From aee2da4122a812176731f0c87eaf0a1c7f9e7069 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Sat, 21 Sep 2019 02:01:29 +0200 Subject: [PATCH 244/271] Don't silent Exception in line_treat. Skip the treatment, but log. --- nemubot/module/more.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/nemubot/module/more.py b/nemubot/module/more.py index 018a1ae..206d97a 100644 --- a/nemubot/module/more.py +++ b/nemubot/module/more.py @@ -181,13 +181,16 @@ class Response: return self.nomore if self.line_treat is not None and self.elt == 0: - if isinstance(self.messages[0], list): - for x in self.messages[0]: - print(x, self.line_treat(x)) - self.messages[0] = [self.line_treat(x).replace("\n", " ").strip() for x in self.messages[0]] - else: - self.messages[0] = (self.line_treat(self.messages[0]) - .replace("\n", " ").strip()) + try: + if isinstance(self.messages[0], list): + for x in self.messages[0]: + print(x, self.line_treat(x)) + self.messages[0] = [self.line_treat(x).replace("\n", " ").strip() for x in self.messages[0]] + else: + self.messages[0] = (self.line_treat(self.messages[0]) + .replace("\n", " ").strip()) + except Exception as e: + logger.exception(e) msg = "" if self.title is not None: From b369683914262f41890e653fc700e55df3a5b6e5 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Sat, 9 Nov 2019 14:46:32 +0100 Subject: [PATCH 245/271] nntp: use timestamp from servers to handle desynchronized clocks --- modules/nntp.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/modules/nntp.py b/modules/nntp.py index c8573bd..3aa643e 100644 --- a/modules/nntp.py +++ b/modules/nntp.py @@ -49,14 +49,24 @@ def read_article(msg_id, **server): return email.message_from_bytes(b"\r\n".join(info.lines), policy=email.policy.SMTPUTF8) -def whatsnew(date_last_check, group="*", **server): +servers_lastcheck = dict() + +def whatsnew(group="*", **server): fill = dict() if "user" in server: fill["user"] = server["user"] if "password" in server: fill["password"] = server["password"] if "host" in server: fill["host"] = server["host"] if "port" in server: fill["port"] = server["port"] + idx = _indexServer(**server) + if idx in servers_lastcheck and servers_lastcheck[idx] is not None: + date_last_check = servers_lastcheck[idx] + else: + date_last_check = datetime.now() + with NNTP(**fill) as srv: + response, servers_lastcheck[idx] = srv.date() + response, groups = srv.newgroups(date_last_check) for g in groups: yield g @@ -92,13 +102,12 @@ def _indexServer(**kwargs): return "{user}:{password}@{host}:{port}".format(**kwargs) def _newevt(**args): - context.add_event(ModuleEvent(call=partial(_fini, **args), interval=42)) + context.add_event(ModuleEvent(call=partial(_ticker, **args), interval=42)) -def _fini(to_server, to_channel, lastcheck, group, server): - print("fini called") - _newevt(to_server=to_server, to_channel=to_channel, group=group, lastcheck=datetime.now(), server=server) +def _ticker(to_server, to_channel, group, server): + _newevt(to_server=to_server, to_channel=to_channel, group=group, server=server) n = 0 - for art in whatsnew(lastcheck, group, **server): + for art in whatsnew(group, **server): n += 1 if n > 10: continue @@ -106,10 +115,8 @@ def _fini(to_server, to_channel, lastcheck, group, server): if n > 10: context.send_response(to_server, Response("... and %s others news" % (n - 10), channel=to_channel)) -def watch(to_server, to_channel, group="*", lastcheck=None, **server): - if lastcheck is None: - lastcheck = datetime.now() - _newevt(to_server=to_server, to_channel=to_channel, group=group, lastcheck=lastcheck, server=server) +def watch(to_server, to_channel, group="*", **server): + _newevt(to_server=to_server, to_channel=to_channel, group=group, server=server) # MODULE INTERFACE #################################################### From d56c2396c04c2e6d6b8aba5509f587edb714b4b6 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Sun, 1 Dec 2019 17:49:01 +0100 Subject: [PATCH 246/271] repology: new module --- modules/repology.py | 94 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 modules/repology.py diff --git a/modules/repology.py b/modules/repology.py new file mode 100644 index 0000000..8dbc6da --- /dev/null +++ b/modules/repology.py @@ -0,0 +1,94 @@ +# coding=utf-8 + +"""Repology.org module: the packaging hub""" + +import datetime +import re + +from nemubot import context +from nemubot.exception import IMException +from nemubot.hooks import hook +from nemubot.tools import web +from nemubot.tools.xmlparser.node import ModuleState + +nemubotversion = 4.0 + +from nemubot.module.more import Response + +URL_REPOAPI = "https://repology.org/api/v1/project/%s" + +def get_json_project(project): + prj = web.getJSON(URL_REPOAPI % (project)) + + return prj + + +@hook.command("repology", + help="Display version information about a package", + help_usage={ + "PACKAGE_NAME": "Retrieve informations about PACKAGE_NAME", + }, + keywords={ + "distro=DISTRO": "filter by disto", + "status=STATUS[,STATUS...]": "filter by status", + }) +def cmd_repology(msg): + if len(msg.args) == 0: + raise IMException("Please provide at least a package name") + + res = Response(channel=msg.channel, nomore="No more information on package") + + for project in msg.args: + prj = get_json_project(project) + if len(prj) == 0: + raise IMException("Unable to find package " + project) + + pkg_versions = {} + pkg_maintainers = {} + pkg_licenses = {} + summary = None + + for repo in prj: + # Apply filters + if "distro" in msg.kwargs and repo["repo"].find(msg.kwargs["distro"]) < 0: + continue + if "status" in msg.kwargs and repo["status"] not in msg.kwargs["status"].split(","): + continue + + name = repo["visiblename"] if "visiblename" in repo else repo["name"] + status = repo["status"] if "status" in repo else "unknown" + if name not in pkg_versions: + pkg_versions[name] = {} + if status not in pkg_versions[name]: + pkg_versions[name][status] = [] + if repo["version"] not in pkg_versions[name][status]: + pkg_versions[name][status].append(repo["version"]) + + if "maintainers" in repo: + if name not in pkg_maintainers: + pkg_maintainers[name] = [] + for maintainer in repo["maintainers"]: + if maintainer not in pkg_maintainers[name]: + pkg_maintainers[name].append(maintainer) + + if "licenses" in repo: + if name not in pkg_licenses: + pkg_licenses[name] = [] + for lic in repo["licenses"]: + if lic not in pkg_licenses[name]: + pkg_licenses[name].append(lic) + + if "summary" in repo and summary is None: + summary = repo["summary"] + + for pkgname in sorted(pkg_versions.keys()): + m = "Package " + pkgname + " (" + summary + ")" + if pkgname in pkg_licenses: + m += " under " + ", ".join(pkg_licenses[pkgname]) + m += ": " + " - ".join([status + ": " + ", ".join(pkg_versions[pkgname][status]) for status in ["newest", "devel", "unique", "outdated", "legacy", "rolling", "noscheme", "untrusted", "ignored"] if status in pkg_versions[pkgname]]) + if "distro" in msg.kwargs and pkgname in pkg_maintainers: + m += " - Maintained by " + ", ".join(pkg_maintainers[pkgname]) + + res.append_message(m) + + return res From f17f8b9dfa02f8013c740a66a7d9aa5e60b6d6bd Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Fri, 29 Nov 2019 15:54:01 +0100 Subject: [PATCH 247/271] nntp: keep in memory latests news seen to avoid loop --- modules/nntp.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/modules/nntp.py b/modules/nntp.py index 3aa643e..7fdceb4 100644 --- a/modules/nntp.py +++ b/modules/nntp.py @@ -50,6 +50,7 @@ def read_article(msg_id, **server): servers_lastcheck = dict() +servers_lastseen = dict() def whatsnew(group="*", **server): fill = dict() @@ -64,6 +65,9 @@ def whatsnew(group="*", **server): else: date_last_check = datetime.now() + if idx not in servers_lastseen: + servers_lastseen[idx] = [] + with NNTP(**fill) as srv: response, servers_lastcheck[idx] = srv.date() @@ -73,8 +77,14 @@ def whatsnew(group="*", **server): response, articles = srv.newnews(group, date_last_check) for msg_id in articles: - response, info = srv.article(msg_id) - yield email.message_from_bytes(b"\r\n".join(info.lines)) + if msg_id not in servers_lastseen[idx]: + servers_lastseen[idx].append(msg_id) + response, info = srv.article(msg_id) + yield email.message_from_bytes(b"\r\n".join(info.lines)) + + # Clean huge lists + if len(servers_lastseen[idx]) > 42: + servers_lastseen[idx] = servers_lastseen[idx][23:] def format_article(art, **response_args): From 904ad723165c4b860dbcc99dfcc13afa3d5ba0f8 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Sat, 30 Nov 2019 01:51:01 +0100 Subject: [PATCH 248/271] suivi: fix usps --- modules/suivi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/suivi.py b/modules/suivi.py index 637f64f..f62673b 100644 --- a/modules/suivi.py +++ b/modules/suivi.py @@ -132,7 +132,7 @@ def get_usps_info(usps_id): usps_data = getURLContent(usps_parcelurl) soup = BeautifulSoup(usps_data) - if (soup.find(class_="tracking_history") + if (soup.find(id="trackingHistory_1") and soup.find(class_="tracking_history").find(class_="row_notification") and soup.find(class_="tracking_history").find(class_="row_top").find_all("td")): notification = soup.find(class_="tracking_history").find(class_="row_notification").text.strip() From 37a230e70e283eb66b2891b75d16ba79e57bcaac Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Mon, 2 Dec 2019 19:31:57 +0100 Subject: [PATCH 249/271] suivi: kuse new laposte API to get infos --- modules/suivi.py | 42 +++++++++++++----------------------------- 1 file changed, 13 insertions(+), 29 deletions(-) diff --git a/modules/suivi.py b/modules/suivi.py index f62673b..87abe47 100644 --- a/modules/suivi.py +++ b/modules/suivi.py @@ -9,7 +9,7 @@ import re from nemubot.hooks import hook from nemubot.exception import IMException -from nemubot.tools.web import getURLContent, getJSON +from nemubot.tools.web import getURLContent, getURLHeaders, getJSON from nemubot.module.more import Response @@ -76,32 +76,17 @@ def get_colisprive_info(track_id): def get_laposte_info(laposte_id): - data = urllib.parse.urlencode({'id': laposte_id}) - laposte_baseurl = "http://www.part.csuivi.courrier.laposte.fr/suivi/index" + status, laposte_headers = getURLHeaders("https://www.laposte.fr/outils/suivre-vos-envois?" + urllib.parse.urlencode({'code': laposte_id})) - laposte_data = getURLContent(laposte_baseurl, data.encode('utf-8')) - soup = BeautifulSoup(laposte_data) - search_res = soup.find(class_='resultat_rech_simple_table').tbody.tr - if (soup.find(class_='resultat_rech_simple_table').thead - and soup.find(class_='resultat_rech_simple_table').thead.tr - and len(search_res.find_all('td')) > 3): - field = search_res.find('td') - poste_id = field.get_text() + laposte_cookie = None + for k,v in laposte_headers: + if k.lower() == "set-cookie" and v.find("access_token") >= 0: + laposte_cookie = v.split(";")[0] - field = field.find_next('td') - poste_type = field.get_text() + laposte_data = getJSON("https://api.laposte.fr/ssu/v1/suivi-unifie/idship/%s?lang=fr_FR" % urllib.parse.quote(laposte_id), header={"Accept": "application/json", "Cookie": laposte_cookie}) - field = field.find_next('td') - poste_date = field.get_text() - - field = field.find_next('td') - poste_location = field.get_text() - - field = field.find_next('td') - poste_status = field.get_text() - - return (poste_type.lower(), poste_id.strip(), poste_status.lower(), - poste_location, poste_date) + shipment = laposte_data["shipment"] + return (shipment["product"], shipment["idShip"], shipment["event"][0]["label"], shipment["event"][0]["date"]) def get_postnl_info(postnl_id): @@ -210,11 +195,10 @@ def handle_tnt(tracknum): def handle_laposte(tracknum): info = get_laposte_info(tracknum) if info: - poste_type, poste_id, poste_status, poste_location, poste_date = info - return ("Le courrier de type \x02%s\x0F : \x02%s\x0F est actuellement " - "\x02%s\x0F dans la zone \x02%s\x0F (Mis à jour le \x02%s\x0F" - ")." % (poste_type, poste_id, poste_status, - poste_location, poste_date)) + poste_type, poste_id, poste_status, poste_date = info + return ("\x02%s\x0F : \x02%s\x0F est actuellement " + "\x02%s\x0F (Mis à jour le \x02%s\x0F" + ")." % (poste_type, poste_id, poste_status, poste_date)) def handle_postnl(tracknum): From faa575964552873f9029da00137c6b41fe5f6170 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Tue, 3 Dec 2019 13:20:36 +0100 Subject: [PATCH 250/271] suivi: add UPS tracking infos --- modules/suivi.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/modules/suivi.py b/modules/suivi.py index 87abe47..a54b722 100644 --- a/modules/suivi.py +++ b/modules/suivi.py @@ -75,6 +75,17 @@ def get_colisprive_info(track_id): return status +def get_ups_info(track_id): + data = json.dumps({'Locale': "en_US", 'TrackingNumber': [track_id]}) + track_baseurl = "https://www.ups.com/track/api/Track/GetStatus?loc=en_US" + track_data = getJSON(track_baseurl, data.encode('utf-8'), header={"Content-Type": "application/json"}) + return (track_data["trackDetails"][0]["trackingNumber"], + track_data["trackDetails"][0]["packageStatus"], + track_data["trackDetails"][0]["shipmentProgressActivities"][0]["date"] + " " + track_data["trackDetails"][0]["shipmentProgressActivities"][0]["time"], + track_data["trackDetails"][0]["shipmentProgressActivities"][0]["location"], + track_data["trackDetails"][0]["shipmentProgressActivities"][0]["activityScan"]) + + def get_laposte_info(laposte_id): status, laposte_headers = getURLHeaders("https://www.laposte.fr/outils/suivre-vos-envois?" + urllib.parse.urlencode({'code': laposte_id})) @@ -217,6 +228,13 @@ def handle_usps(tracknum): return ("USPS \x02{tracknum}\x0F: {last_status} in \x02{last_location}\x0F as of {last_date}: {notif}".format(tracknum=tracknum, notif=notif, last_date=last_date, last_status=last_status.lower(), last_location=last_location)) +def handle_ups(tracknum): + info = get_ups_info(tracknum) + if info: + tracknum, status, last_date, last_location, last_status = info + return ("UPS \x02{tracknum}\x0F: {status}: in \x02{last_location}\x0F as of {last_date}: {last_status}".format(tracknum=tracknum, status=status, last_date=last_date, last_status=last_status.lower(), last_location=last_location)) + + def handle_colissimo(tracknum): info = get_colissimo_info(tracknum) if info: @@ -267,6 +285,7 @@ TRACKING_HANDLERS = { 'fedex': handle_fedex, 'dhl': handle_dhl, 'usps': handle_usps, + 'ups': handle_ups, } From 5ec2f2997b53535b786f40cbeee33d9f17254371 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Sun, 18 Oct 2020 16:14:00 +0200 Subject: [PATCH 251/271] Add drone CI --- .drone.yml | 26 ++++++++++++++++++++++++++ Dockerfile | 25 +++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 .drone.yml create mode 100644 Dockerfile diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..0db1f86 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,26 @@ +--- +kind: pipeline +type: docker +name: default + +platform: + os: linux + arch: arm + +steps: + - name: build + image: python:alpine + commands: + - pip install --no-cache-dir -r requirements.txt + - pip install . + + - name: docker + image: plugins/docker + settings: + repo: nemunaire/nemubot + auto_tag: true + auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} + username: + from_secret: docker_username + password: + from_secret: docker_password diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0a345e2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM python:alpine as pybuild + +WORKDIR /usr/src/app + +COPY requirements.txt ./ +RUN apk add --no-cache bash build-base capstone-dev && \ + pip install --no-cache-dir -r requirements.txt && \ + pip install bs4 capstone dnspython + + +FROM python:alpine + +RUN apk add --no-cache capstone w3m + +WORKDIR /usr/src/app + +VOLUME /var/lib/nemubot + +COPY requirements.txt ./ + +COPY --from=pybuild /usr/lib/python3.9 /usr/lib/python3.9 +COPY . . + +ENTRYPOINT [ "python", "-m", "nemubot", "-d", "-M", "/usr/src/app/modules" ] +CMD [ "-D", "/var/lib/nemubot" ] \ No newline at end of file From 13c643fc19faaa0c03fffee62b97f0091db82522 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Sat, 24 Oct 2020 00:13:34 +0200 Subject: [PATCH 252/271] Add missing deps in container --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 0a345e2..c61d8be 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ RUN apk add --no-cache bash build-base capstone-dev && \ FROM python:alpine -RUN apk add --no-cache capstone w3m +RUN apk add --no-cache capstone mandoc-doc man-db w3m youtube-dl WORKDIR /usr/src/app @@ -18,7 +18,7 @@ VOLUME /var/lib/nemubot COPY requirements.txt ./ -COPY --from=pybuild /usr/lib/python3.9 /usr/lib/python3.9 +COPY --from=pybuild /usr/local/lib/python3.9 /usr/lib/python3.9 COPY . . ENTRYPOINT [ "python", "-m", "nemubot", "-d", "-M", "/usr/src/app/modules" ] From 8dd6b9d47100a65d50ba69522e06aa55a32b60a9 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Tue, 1 Dec 2020 00:47:14 +0100 Subject: [PATCH 253/271] Fix a strange problem with saved PID file between runs --- Dockerfile | 2 +- nemubot/__main__.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index c61d8be..015d678 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,5 +21,5 @@ COPY requirements.txt ./ COPY --from=pybuild /usr/local/lib/python3.9 /usr/lib/python3.9 COPY . . -ENTRYPOINT [ "python", "-m", "nemubot", "-d", "-M", "/usr/src/app/modules" ] +ENTRYPOINT [ "python", "-m", "nemubot", "-d", "-P", "", "-M", "/usr/src/app/modules" ] CMD [ "-D", "/var/lib/nemubot" ] \ No newline at end of file diff --git a/nemubot/__main__.py b/nemubot/__main__.py index abb290b..4275d95 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -71,8 +71,8 @@ def main(): # Resolve relatives paths args.data_path = os.path.abspath(os.path.expanduser(args.data_path)) - args.pidfile = os.path.abspath(os.path.expanduser(args.pidfile)) - args.socketfile = os.path.abspath(os.path.expanduser(args.socketfile)) + args.pidfile = os.path.abspath(os.path.expanduser(args.pidfile)) if args.pidfile is not None and args.pidfile != "" else None + args.socketfile = os.path.abspath(os.path.expanduser(args.socketfile)) if args.socketfile is not None and args.socketfile != "" else None 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)] From 60a9ec92b7cbcd056e870e447c1760a5b110b69b Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Mon, 10 May 2021 20:47:00 +0200 Subject: [PATCH 254/271] Rework Dockerfile --- Dockerfile | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 015d678..23116ec 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:alpine as pybuild +FROM python:alpine WORKDIR /usr/src/app @@ -8,17 +8,12 @@ RUN apk add --no-cache bash build-base capstone-dev && \ pip install bs4 capstone dnspython -FROM python:alpine - -RUN apk add --no-cache capstone mandoc-doc man-db w3m youtube-dl - WORKDIR /usr/src/app VOLUME /var/lib/nemubot -COPY requirements.txt ./ +RUN apk add --no-cache mandoc-doc man-db w3m youtube-dl -COPY --from=pybuild /usr/local/lib/python3.9 /usr/lib/python3.9 COPY . . ENTRYPOINT [ "python", "-m", "nemubot", "-d", "-P", "", "-M", "/usr/src/app/modules" ] From 68c61f40d3569eee5bf262b90cb755b008cc88d1 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Thu, 6 Jan 2022 11:19:13 +0100 Subject: [PATCH 255/271] Rework Dockerfile and run as user --- Dockerfile | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index 23116ec..83f3240 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,19 +2,19 @@ FROM python:alpine WORKDIR /usr/src/app -COPY requirements.txt ./ -RUN apk add --no-cache bash build-base capstone-dev && \ +COPY requirements.txt /usr/src/app/ +RUN apk add --no-cache bash build-base capstone-dev mandoc-doc man-db w3m youtube-dl aspell aspell-fr && \ pip install --no-cache-dir -r requirements.txt && \ - pip install bs4 capstone dnspython - - -WORKDIR /usr/src/app + pip install bs4 capstone dnspython && \ + apk del build-base capstone-dev VOLUME /var/lib/nemubot -RUN apk add --no-cache mandoc-doc man-db w3m youtube-dl +COPY . /usr/src/app/ -COPY . . +RUN ./setup.py install +WORKDIR /var/lib/nemubot +USER guest ENTRYPOINT [ "python", "-m", "nemubot", "-d", "-P", "", "-M", "/usr/src/app/modules" ] CMD [ "-D", "/var/lib/nemubot" ] \ No newline at end of file From 9f83e5b17895ad72586f1279d17b18e653d3231c Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Thu, 6 Jan 2022 14:10:30 +0100 Subject: [PATCH 256/271] Dockerfile: Point home into volume --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 83f3240..44bb745 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,8 @@ COPY requirements.txt /usr/src/app/ RUN apk add --no-cache bash build-base capstone-dev mandoc-doc man-db w3m youtube-dl aspell aspell-fr && \ pip install --no-cache-dir -r requirements.txt && \ pip install bs4 capstone dnspython && \ - apk del build-base capstone-dev + apk del build-base capstone-dev && \ + ln -s /var/lib/nemubot/home /home/nemubot VOLUME /var/lib/nemubot From 861ca0afddc625da6e19606c86f0819b80965ab1 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Tue, 17 Jan 2023 21:55:25 +0100 Subject: [PATCH 257/271] Try to connect multiple times (with different servers if any) --- nemubot/__main__.py | 16 +++++++++++----- nemubot/config/server.py | 4 ++-- nemubot/server/socket.py | 5 +++-- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 4275d95..6a8b265 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -155,12 +155,18 @@ def main(): # Preset each server in this file for server in config.servers: - srv = server.server(config) # Add the server in the context - if context.add_server(srv): - logger.info("Server '%s' successfully added.", srv.name) - else: - logger.error("Can't add server '%s'.", srv.name) + for i in [0,1,2,3]: + srv = server.server(config, trynb=i) + try: + if context.add_server(srv): + logger.info("Server '%s' successfully added.", srv.name) + else: + logger.error("Can't add server '%s'.", srv.name) + except: + logger.error("Unable to connect to '%s'.", srv.name) + continue + break # Load module and their configuration for mod in config.modules: diff --git a/nemubot/config/server.py b/nemubot/config/server.py index 14ca9a8..17bfaee 100644 --- a/nemubot/config/server.py +++ b/nemubot/config/server.py @@ -33,7 +33,7 @@ class Server: return True - def server(self, parent): + def server(self, parent, trynb=0): from nemubot.server import factory for a in ["nick", "owner", "realname", "encoding"]: @@ -42,4 +42,4 @@ class Server: self.caps += parent.caps - return factory(self.uri, caps=self.caps, channels=self.channels, **self.args) + return factory(self.uri, caps=self.caps, channels=self.channels, trynb=trynb, **self.args) diff --git a/nemubot/server/socket.py b/nemubot/server/socket.py index a6be620..bf55bf5 100644 --- a/nemubot/server/socket.py +++ b/nemubot/server/socket.py @@ -79,8 +79,9 @@ class _Socket(AbstractServer): class SocketServer(_Socket): - def __init__(self, host, port, bind=None, **kwargs): - (family, type, proto, canonname, self._sockaddr) = socket.getaddrinfo(host, port, proto=socket.IPPROTO_TCP)[0] + def __init__(self, host, port, bind=None, trynb=0, **kwargs): + destlist = socket.getaddrinfo(host, port, proto=socket.IPPROTO_TCP) + (family, type, proto, canonname, self._sockaddr) = destlist[trynb%len(destlist)] super().__init__(fdClass=socket.socket, family=family, type=type, proto=proto, **kwargs) From a8472ecc297acfe6a3c4b7178b896a60f27f7882 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Wed, 18 Jan 2023 15:27:38 +0100 Subject: [PATCH 258/271] CI: Fix build on arm --- .drone.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index 0db1f86..7022c43 100644 --- a/.drone.yml +++ b/.drone.yml @@ -15,7 +15,7 @@ steps: - pip install . - name: docker - image: plugins/docker + image: plugins/docker:linux-arm settings: repo: nemunaire/nemubot auto_tag: true From 45a27b477d4b02373055b9efd86492d7795a19a7 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Mon, 8 May 2023 19:07:12 +0200 Subject: [PATCH 259/271] syno: Fix new service URL --- modules/syno.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/syno.py b/modules/syno.py index 6f6a625..78f0b7d 100644 --- a/modules/syno.py +++ b/modules/syno.py @@ -29,7 +29,7 @@ def load(context): # MODULE CORE ######################################################### def get_french_synos(word): - url = "http://www.crisco.unicaen.fr/des/synonymes/" + quote(word) + url = "https://crisco.unicaen.fr/des/synonymes/" + quote(word) page = web.getURLContent(url) best = list(); synos = list(); anton = list() From 84fef789b584092841b8d161d4e788d994799c01 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Tue, 23 May 2023 09:20:21 +0200 Subject: [PATCH 260/271] Ignore decoding error when charset is erroneous --- nemubot/tools/web.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nemubot/tools/web.py b/nemubot/tools/web.py index ab20643..a545b19 100644 --- a/nemubot/tools/web.py +++ b/nemubot/tools/web.py @@ -191,9 +191,9 @@ def getURLContent(url, body=None, timeout=7, header=None, decode_error=False, import http.client if res.status == http.client.OK or res.status == http.client.SEE_OTHER: - return data.decode(charset).strip() + return data.decode(charset, errors='ignore').strip() elif decode_error: - return data.decode(charset).strip() + return data.decode(charset, errors='ignore').strip() else: raise IMException("A HTTP error occurs: %d - %s" % (res.status, http.client.responses[res.status])) From 38b5b1eabdbdd58869ba9ed93b54e693c8d1ea84 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@noreply.git.nemunai.re> Date: Sat, 14 Oct 2023 23:01:11 +0000 Subject: [PATCH 261/271] Also build for arm64 --- .drone.yml | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index 7022c43..7e0e1ee 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1,7 +1,7 @@ --- kind: pipeline type: docker -name: default +name: default-arm platform: os: linux @@ -24,3 +24,29 @@ steps: from_secret: docker_username password: from_secret: docker_password +--- +kind: pipeline +type: docker +name: default-arm64 + +platform: + os: linux + arch: arm64 + +steps: + - name: build + image: python:alpine + commands: + - pip install --no-cache-dir -r requirements.txt + - pip install . + + - name: docker + image: plugins/docker + settings: + repo: nemunaire/nemubot + auto_tag: true + auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} + username: + from_secret: docker_username + password: + from_secret: docker_password From ac432fabcccf9e78aa7769dd6ebe0d51887b513d Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Sun, 15 Oct 2023 01:19:12 +0200 Subject: [PATCH 262/271] Keep in 3.11 --- .drone.yml | 4 ++-- Dockerfile | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.drone.yml b/.drone.yml index 7e0e1ee..dc8f0aa 100644 --- a/.drone.yml +++ b/.drone.yml @@ -9,7 +9,7 @@ platform: steps: - name: build - image: python:alpine + image: python:3.11-alpine commands: - pip install --no-cache-dir -r requirements.txt - pip install . @@ -35,7 +35,7 @@ platform: steps: - name: build - image: python:alpine + image: python:3.11-alpine commands: - pip install --no-cache-dir -r requirements.txt - pip install . diff --git a/Dockerfile b/Dockerfile index 44bb745..4fb3fc6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:alpine +FROM python:3.11-alpine WORKDIR /usr/src/app From 23f043673fdade2a753ebbc3484c35f8773c390b Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Fri, 7 Feb 2025 17:39:08 +0100 Subject: [PATCH 263/271] Add openai module --- Dockerfile | 2 +- modules/openai.py | 60 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 modules/openai.py diff --git a/Dockerfile b/Dockerfile index 4fb3fc6..a027b9e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ WORKDIR /usr/src/app COPY requirements.txt /usr/src/app/ RUN apk add --no-cache bash build-base capstone-dev mandoc-doc man-db w3m youtube-dl aspell aspell-fr && \ pip install --no-cache-dir -r requirements.txt && \ - pip install bs4 capstone dnspython && \ + pip install bs4 capstone dnspython openai && \ apk del build-base capstone-dev && \ ln -s /var/lib/nemubot/home /home/nemubot diff --git a/modules/openai.py b/modules/openai.py new file mode 100644 index 0000000..1e3efaa --- /dev/null +++ b/modules/openai.py @@ -0,0 +1,60 @@ +"""Perform requests to openai""" + +# PYTHON STUFFS ####################################################### + +from openai import OpenAI + +from nemubot import context +from nemubot.hooks import hook + +from nemubot.module.more import Response + + +# LOADING ############################################################# + +CLIENT = None +MODEL = "gpt-4" +ENDPOINT = None + +def load(context): + global CLIENT, ENDPOINT, MODEL + if not context.config or ("apikey" not in context.config and "endpoint" not in context.config): + raise ImportError ("You need a OpenAI API key in order to use " + "this module. Add it to the module configuration: " + "\n<module name=\"openai\" " + "apikey=\"XXXXXX-XXXXXXXXXX\" endpoint=\"https://...\" model=\"gpt-4\" />") + kwargs = { + "api_key": context.config["apikey"] or "", + } + + if "endpoint" in context.config: + ENDPOINT = context.config["endpoint"] + kwargs["base_url"] = ENDPOINT + + CLIENT = OpenAI(**kwargs) + + if "model" in context.config: + MODEL = context.config["model"] + + +# MODULE INTERFACE #################################################### + +@hook.ask() +def parseask(msg): + chat_completion = CLIENT.chat.completions.create( + messages=[ + { + "role": "system", + "content": "You are a kind multilingual assistant. Respond to the user request in 255 characters maximum. Be conscise, go directly to the point. Never add useless terms.", + }, + { + "role": "user", + "content": msg.message, + } + ], + model=MODEL, + ) + + return Response(chat_completion.choices[0].message.content, + msg.channel, + msg.frm) From ea0ec42a4b152743b654d3ef3af61a7bd5077681 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Fri, 7 Feb 2025 21:38:11 +0100 Subject: [PATCH 264/271] openai: Add commands list_models and set_model --- modules/openai.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/modules/openai.py b/modules/openai.py index 1e3efaa..b9b6e21 100644 --- a/modules/openai.py +++ b/modules/openai.py @@ -6,6 +6,7 @@ from openai import OpenAI from nemubot import context from nemubot.hooks import hook +from nemubot.tools import web from nemubot.module.more import Response @@ -39,6 +40,32 @@ def load(context): # MODULE INTERFACE #################################################### +@hook.command("list_models", + help="list available LLM") +def cmd_listllm(msg): + llms = web.getJSON(ENDPOINT + "/models", timeout=6) + return Response(message=[m for m in map(lambda i: i["id"], llms["data"])], title="Here is the available models", channel=msg.channel) + + +@hook.command("set_model", + help="Set the model to use when talking to nemubot") +def cmd_setllm(msg): + if len(msg.args) != 1: + raise IMException("Indicate 1 model to use") + + wanted_model = msg.args[0] + + llms = web.getJSON(ENDPOINT + "/models", timeout=6) + for model in llms["data"]: + if wanted_model == model["id"]: + break + else: + raise IMException("Unable to set such model: unknown") + + MODEL = wanted_model + return Response("New model in use: " + wanted_model, channel=msg.channel) + + @hook.ask() def parseask(msg): chat_completion = CLIENT.chat.completions.create( From 32ebe42f41356666758b8b9815c202d5fddd2df5 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Fri, 7 Feb 2025 23:38:49 +0100 Subject: [PATCH 265/271] Don't compile for arm (requires rust ?????) --- .drone.yml | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/.drone.yml b/.drone.yml index dc8f0aa..dccc156 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1,32 +1,6 @@ --- kind: pipeline type: docker -name: default-arm - -platform: - os: linux - arch: arm - -steps: - - name: build - image: python:3.11-alpine - commands: - - pip install --no-cache-dir -r requirements.txt - - pip install . - - - name: docker - image: plugins/docker:linux-arm - settings: - repo: nemunaire/nemubot - auto_tag: true - auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} - username: - from_secret: docker_username - password: - from_secret: docker_password ---- -kind: pipeline -type: docker name: default-arm64 platform: From 622159f6b52fbbdc076595670653022916d484f0 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Fri, 6 Mar 2026 15:47:41 +0700 Subject: [PATCH 266/271] server: Add threaded server implementation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- nemubot/server/threaded.py | 132 +++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 nemubot/server/threaded.py diff --git a/nemubot/server/threaded.py b/nemubot/server/threaded.py new file mode 100644 index 0000000..eb1ae19 --- /dev/null +++ b/nemubot/server/threaded.py @@ -0,0 +1,132 @@ +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2026 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/>. + +import logging +import os +import queue + +from nemubot.bot import sync_act + + +class ThreadedServer: + + """A server backed by a library running in its own thread. + + Uses an os.pipe() as a fake file descriptor to integrate with the bot's + select.poll() main loop without requiring direct socket access. + + When the library thread has a message ready, it calls _push_message(), + which writes a wakeup byte to the pipe's write end. The bot's poll loop + sees the read end become readable, calls async_read(), which drains the + message queue and yields already-parsed bot-level messages. + + This abstraction lets any IM library (IRC via python-irc, Matrix via + matrix-nio, …) plug into nemubot without touching bot.py. + """ + + def __init__(self, name): + self._name = name + self._logger = logging.getLogger("nemubot.server." + name) + self._queue = queue.Queue() + self._pipe_r, self._pipe_w = os.pipe() + + + @property + def name(self): + return self._name + + def fileno(self): + return self._pipe_r + + + # Open/close + + def connect(self): + """Start the library and register the pipe read-end with the poll loop.""" + self._logger.info("Starting connection") + self._start() + sync_act("sckt", "register", self._pipe_r) + + def _start(self): + """Override: start the library's connection (e.g. launch a thread).""" + raise NotImplementedError + + def close(self): + """Unregister from poll, stop the library, and close the pipe.""" + self._logger.info("Closing connection") + sync_act("sckt", "unregister", self._pipe_r) + self._stop() + for fd in (self._pipe_w, self._pipe_r): + try: + os.close(fd) + except OSError: + pass + + def _stop(self): + """Override: stop the library thread gracefully.""" + pass + + + # Writes + + def send_response(self, response): + """Override: send a response via the underlying library.""" + raise NotImplementedError + + def async_write(self): + """No-op: writes go directly through the library, not via poll.""" + pass + + + # Read + + def _push_message(self, msg): + """Called from the library thread to enqueue a bot-level message. + + Writes a wakeup byte to the pipe so the main loop wakes up and + calls async_read(). + """ + self._queue.put(msg) + try: + os.write(self._pipe_w, b'\x00') + except OSError: + pass # pipe closed during shutdown + + def async_read(self): + """Called by the bot when the pipe is readable. + + Drains the wakeup bytes and yields all queued bot messages. + """ + try: + os.read(self._pipe_r, 256) + except OSError: + return + while not self._queue.empty(): + try: + yield self._queue.get_nowait() + except queue.Empty: + break + + def parse(self, msg): + """Messages pushed via _push_message are already bot-level — pass through.""" + yield msg + + + # Exceptions + + def exception(self, flags): + """Called by the bot on POLLERR/POLLHUP/POLLNVAL.""" + self._logger.warning("Exception on server %s: flags=0x%x", self._name, flags) From de2c37a54a82d6d617f586a65b1f9a07195fb78c Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Fri, 6 Mar 2026 15:53:24 +0700 Subject: [PATCH 267/271] matrix: Add Matrix server support via matrix-nio Implement Matrix protocol support with MatrixServer (ThreadedServer subclass), a Matrix message printer, factory URI parsing for matrix:// schemes, and matrix-nio[e2e] dependency. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- Dockerfile | 4 +- nemubot/message/printer/Matrix.py | 69 +++++++++++ nemubot/server/Matrix.py | 200 ++++++++++++++++++++++++++++++ nemubot/server/__init__.py | 32 +++++ requirements.txt | 1 + 5 files changed, 304 insertions(+), 2 deletions(-) create mode 100644 nemubot/message/printer/Matrix.py create mode 100644 nemubot/server/Matrix.py diff --git a/Dockerfile b/Dockerfile index a027b9e..b830622 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,8 +3,8 @@ FROM python:3.11-alpine WORKDIR /usr/src/app COPY requirements.txt /usr/src/app/ -RUN apk add --no-cache bash build-base capstone-dev mandoc-doc man-db w3m youtube-dl aspell aspell-fr && \ - pip install --no-cache-dir -r requirements.txt && \ +RUN apk add --no-cache bash build-base capstone-dev mandoc-doc man-db w3m youtube-dl aspell aspell-fr py3-matrix-nio && \ + pip install --no-cache-dir --ignore-installed -r requirements.txt && \ pip install bs4 capstone dnspython openai && \ apk del build-base capstone-dev && \ ln -s /var/lib/nemubot/home /home/nemubot diff --git a/nemubot/message/printer/Matrix.py b/nemubot/message/printer/Matrix.py new file mode 100644 index 0000000..ad1b99e --- /dev/null +++ b/nemubot/message/printer/Matrix.py @@ -0,0 +1,69 @@ +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2026 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 nemubot.message.visitor import AbstractVisitor + + +class Matrix(AbstractVisitor): + + """Visitor that sends bot responses as Matrix room messages. + + Instead of accumulating text like the IRC printer does, each visit_* + method calls send_func(room_id, text) directly for every destination room. + """ + + def __init__(self, send_func): + """ + Argument: + send_func -- callable(room_id: str, text: str) that sends a plain-text + message to the given Matrix room + """ + self._send = send_func + + def visit_Text(self, msg): + if isinstance(msg.message, str): + for room in msg.to: + self._send(room, msg.message) + else: + # Nested message object — let it visit itself + msg.message.accept(self) + + def visit_DirectAsk(self, msg): + text = msg.message if isinstance(msg.message, str) else str(msg.message) + # Rooms that are NOT the designated nick → prefix with "nick: " + others = [to for to in msg.to if to != msg.designated] + if len(others) == 0 or len(others) != len(msg.to): + # At least one room IS the designated target → send plain + for room in msg.to: + self._send(room, text) + if len(others): + # Other rooms → prefix with nick + for room in others: + self._send(room, "%s: %s" % (msg.designated, text)) + + def visit_Command(self, msg): + parts = ["!" + msg.cmd] + if msg.args: + parts.extend(msg.args) + for room in msg.to: + self._send(room, " ".join(parts)) + + def visit_OwnerCommand(self, msg): + parts = ["`" + msg.cmd] + if msg.args: + parts.extend(msg.args) + for room in msg.to: + self._send(room, " ".join(parts)) diff --git a/nemubot/server/Matrix.py b/nemubot/server/Matrix.py new file mode 100644 index 0000000..ed4b746 --- /dev/null +++ b/nemubot/server/Matrix.py @@ -0,0 +1,200 @@ +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2026 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/>. + +import asyncio +import shlex +import threading + +import nemubot.message as message +from nemubot.server.threaded import ThreadedServer + + +class Matrix(ThreadedServer): + + """Matrix server implementation using matrix-nio's AsyncClient. + + Runs an asyncio event loop in a daemon thread. Incoming room messages are + converted to nemubot bot messages and pushed through the pipe; outgoing + responses are sent via the async client from the same event loop. + """ + + def __init__(self, homeserver, user_id, password=None, access_token=None, + owner=None, nick=None, channels=None, **kwargs): + """Prepare a connection to a Matrix homeserver. + + Keyword arguments: + homeserver -- base URL of the homeserver, e.g. "https://matrix.org" + user_id -- full MXID (@user:server) or bare localpart + password -- login password (required if no access_token) + access_token -- pre-obtained access token (alternative to password) + owner -- MXID of the bot owner (marks frm_owner on messages) + nick -- display name / prefix for DirectAsk detection + channels -- list of room IDs / aliases to join on connect + """ + + # Ensure fully-qualified MXID + if not user_id.startswith("@"): + host = homeserver.split("//")[-1].rstrip("/") + user_id = "@%s:%s" % (user_id, host) + + super().__init__(name=user_id) + + self.homeserver = homeserver + self.user_id = user_id + self.password = password + self.access_token = access_token + self.owner = owner + self.nick = nick or user_id + + self._initial_rooms = channels or [] + self._client = None + self._loop = None + self._thread = None + + + # Open/close + + def _start(self): + self._thread = threading.Thread( + target=self._run_loop, + daemon=True, + name="nemubot.Matrix/" + self._name, + ) + self._thread.start() + + def _stop(self): + if self._client and self._loop and not self._loop.is_closed(): + try: + asyncio.run_coroutine_threadsafe( + self._client.close(), self._loop + ).result(timeout=5) + except Exception: + self._logger.exception("Error while closing Matrix client") + if self._thread: + self._thread.join(timeout=5) + + + # Asyncio thread + + def _run_loop(self): + self._loop = asyncio.new_event_loop() + asyncio.set_event_loop(self._loop) + try: + self._loop.run_until_complete(self._async_main()) + except Exception: + self._logger.exception("Unhandled exception in Matrix event loop") + finally: + self._loop.close() + + async def _async_main(self): + from nio import AsyncClient, LoginError, RoomMessageText + + self._client = AsyncClient(self.homeserver, self.user_id) + + if self.access_token: + self._client.access_token = self.access_token + self._logger.info("Using provided access token for %s", self.user_id) + elif self.password: + resp = await self._client.login(self.password) + if isinstance(resp, LoginError): + self._logger.error("Matrix login failed: %s", resp.message) + return + self._logger.info("Logged in to Matrix as %s", self.user_id) + else: + self._logger.error("Need either password or access_token to connect") + return + + self._client.add_event_callback(self._on_room_message, RoomMessageText) + + for room in self._initial_rooms: + await self._client.join(room) + self._logger.info("Joined room %s", room) + + await self._client.sync_forever(timeout=30000, full_state=True) + + + # Incoming messages + + async def _on_room_message(self, room, event): + """Callback invoked by matrix-nio for each m.room.message event.""" + + if event.sender == self.user_id: + return # ignore own messages + + text = event.body + room_id = room.room_id + frm = event.sender + + common_args = { + "server": self.name, + "to": [room_id], + "to_response": [room_id], + "frm": frm, + "frm_owner": frm == self.owner, + } + + if len(text) > 1 and text[0] == '!': + text = text[1:].strip() + try: + args = shlex.split(text) + except ValueError: + args = text.split(' ') + msg = message.Command(cmd=args[0], args=args[1:], **common_args) + + elif (text.lower().startswith(self.nick.lower() + ":") + or text.lower().startswith(self.nick.lower() + ",")): + text = text[len(self.nick) + 1:].strip() + msg = message.DirectAsk(designated=self.nick, message=text, + **common_args) + + else: + msg = message.Text(message=text, **common_args) + + self._push_message(msg) + + + # Outgoing messages + + def send_response(self, response): + if response is None: + return + if isinstance(response, list): + for r in response: + self.send_response(r) + return + + from nemubot.message.printer.Matrix import Matrix as MatrixPrinter + printer = MatrixPrinter(self._send_text) + response.accept(printer) + + def _send_text(self, room_id, text): + """Thread-safe: schedule a Matrix room_send on the asyncio loop.""" + if not self._client or not self._loop or self._loop.is_closed(): + self._logger.warning("Cannot send: Matrix client not ready") + return + future = asyncio.run_coroutine_threadsafe( + self._client.room_send( + room_id=room_id, + message_type="m.room.message", + content={"msgtype": "m.text", "body": text}, + ignore_unverified_devices=True, + ), + self._loop, + ) + future.add_done_callback( + lambda f: self._logger.warning("Matrix send error: %s", f.exception()) + if not f.cancelled() and f.exception() else None + ) diff --git a/nemubot/server/__init__.py b/nemubot/server/__init__.py index a39c491..9e186ed 100644 --- a/nemubot/server/__init__.py +++ b/nemubot/server/__init__.py @@ -72,5 +72,37 @@ def factory(uri, ssl=False, **init_args): from ssl import wrap_socket srv._fd = context.wrap_socket(srv._fd, server_hostname=o.hostname) + elif o.scheme == "matrix": + # matrix://localpart:password@homeserver.tld/!room:homeserver.tld + # matrix://localpart:password@homeserver.tld/%23alias:homeserver.tld + # Use matrixs:// for https (default) vs http + args = dict(init_args) + + homeserver = "https://" + o.hostname + if o.port is not None: + homeserver += ":%d" % o.port + args["homeserver"] = homeserver + + if o.username is not None: + args["user_id"] = o.username + if o.password is not None: + args["password"] = unquote(o.password) + + # Parse rooms from path (comma-separated, URL-encoded) + if o.path and o.path != "/": + rooms = [unquote(r) for r in o.path.lstrip("/").split(",") if r] + if rooms: + args.setdefault("channels", []).extend(rooms) + + params = parse_qs(o.query) + if "token" in params: + args["access_token"] = params["token"][0] + if "nick" in params: + args["nick"] = params["nick"][0] + if "owner" in params: + args["owner"] = params["owner"][0] + + from nemubot.server.Matrix import Matrix as MatrixServer + srv = MatrixServer(**args) return srv diff --git a/requirements.txt b/requirements.txt index e69de29..45eefe2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1 @@ +matrix-nio From 26282cb81dbbf787db626975c78b51b888bac0df Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Fri, 6 Mar 2026 21:42:32 +0700 Subject: [PATCH 268/271] server: Replace hand-rolled IRC with irc (jaraco) library Switch the IRC server implementation from the custom socket-based parser to the irc Python library (SingleServerIRCBot), gaining automatic exponential-backoff reconnection, built-in PING/PONG handling, and nick-collision recovery for free. - Add IRCLib server (server/IRCLib.py) extending ThreadedServer: _IRCBotAdapter wraps SingleServerIRCBot with a threading.Event stop flag so shutdown is clean and on_disconnect skips reconnect when stopping. subparse() is implemented directly for alias/grep/rnd/cat. - Add IRCLib printer (message/printer/IRCLib.py) calling connection.privmsg() directly instead of building raw PRIVMSG strings. - Update factory to use IRCLib for irc:// and ircs://; SSL is now passed as a connect_factory kwarg rather than post-hoc socket wrapping. - Add irc to requirements.txt. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- nemubot/message/printer/IRC.py | 25 -- nemubot/message/printer/IRCLib.py | 67 ++++++ nemubot/server/DCC.py | 239 ------------------ nemubot/server/IRC.py | 276 --------------------- nemubot/server/IRCLib.py | 375 +++++++++++++++++++++++++++++ nemubot/server/__init__.py | 30 +-- nemubot/server/factory_test.py | 54 ----- nemubot/server/message/IRC.py | 210 ---------------- nemubot/server/message/__init__.py | 15 -- nemubot/server/message/abstract.py | 33 --- nemubot/server/test_IRC.py | 50 ---- requirements.txt | 1 + setup.py | 1 - 13 files changed, 453 insertions(+), 923 deletions(-) delete mode 100644 nemubot/message/printer/IRC.py create mode 100644 nemubot/message/printer/IRCLib.py delete mode 100644 nemubot/server/DCC.py delete mode 100644 nemubot/server/IRC.py create mode 100644 nemubot/server/IRCLib.py delete mode 100644 nemubot/server/factory_test.py delete mode 100644 nemubot/server/message/IRC.py delete mode 100644 nemubot/server/message/__init__.py delete mode 100644 nemubot/server/message/abstract.py delete mode 100644 nemubot/server/test_IRC.py diff --git a/nemubot/message/printer/IRC.py b/nemubot/message/printer/IRC.py deleted file mode 100644 index df9cb9f..0000000 --- a/nemubot/message/printer/IRC.py +++ /dev/null @@ -1,25 +0,0 @@ -# Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2016 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 nemubot.message import Text -from nemubot.message.printer.socket import Socket as SocketPrinter - - -class IRC(SocketPrinter): - - def visit_Text(self, msg): - self.pp += "PRIVMSG %s :" % ",".join(msg.to) - super().visit_Text(msg) diff --git a/nemubot/message/printer/IRCLib.py b/nemubot/message/printer/IRCLib.py new file mode 100644 index 0000000..abd1f2f --- /dev/null +++ b/nemubot/message/printer/IRCLib.py @@ -0,0 +1,67 @@ +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2026 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 nemubot.message.visitor import AbstractVisitor + + +class IRCLib(AbstractVisitor): + + """Visitor that sends bot responses via an irc.client.ServerConnection. + + Unlike the socket-based IRC printer (which builds a raw PRIVMSG string), + this calls connection.privmsg() directly so the library handles encoding, + line-length capping, and any internal locking. + """ + + def __init__(self, connection): + self._conn = connection + + def _send(self, target, text): + try: + self._conn.privmsg(target, text) + except Exception: + pass # drop silently during reconnection + + # Visitor methods + + def visit_Text(self, msg): + if isinstance(msg.message, str): + for target in msg.to: + self._send(target, msg.message) + else: + msg.message.accept(self) + + def visit_DirectAsk(self, msg): + text = msg.message if isinstance(msg.message, str) else str(msg.message) + # Mirrors socket.py logic: + # rooms that are NOT the designated nick get a "nick: " prefix + others = [to for to in msg.to if to != msg.designated] + if len(others) == 0 or len(others) != len(msg.to): + for target in msg.to: + self._send(target, text) + if others: + for target in others: + self._send(target, "%s: %s" % (msg.designated, text)) + + def visit_Command(self, msg): + parts = ["!" + msg.cmd] + list(msg.args) + for target in msg.to: + self._send(target, " ".join(parts)) + + def visit_OwnerCommand(self, msg): + parts = ["`" + msg.cmd] + list(msg.args) + for target in msg.to: + self._send(target, " ".join(parts)) diff --git a/nemubot/server/DCC.py b/nemubot/server/DCC.py deleted file mode 100644 index f5d4b8f..0000000 --- a/nemubot/server/DCC.py +++ /dev/null @@ -1,239 +0,0 @@ -# Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 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/>. - -import imp -import os -import re -import socket -import sys -import time -import threading -import traceback - -import nemubot.message as message -import nemubot.server as server - -#Store all used ports -PORTS = list() - -class DCC(server.AbstractServer): - def __init__(self, srv, dest, socket=None): - super().__init__(name="Nemubot DCC server") - - self.error = False # An error has occur, closing the connection? - self.messages = list() # Message queued before connexion - - # Informations about the sender - self.sender = dest - if self.sender is not None: - self.nick = (self.sender.split('!'))[0] - if self.nick != self.sender: - self.realname = (self.sender.split('!'))[1] - else: - self.realname = self.nick - - # Keep the server - self.srv = srv - self.treatement = self.treat_msg - - # Found a port for the connection - self.port = self.foundPort() - - if self.port is None: - self._logger.critical("No more available slot for DCC connection") - self.setError("Il n'y a plus de place disponible sur le serveur" - " pour initialiser une session DCC.") - - def foundPort(self): - """Found a free port for the connection""" - for p in range(65432, 65535): - if p not in PORTS: - PORTS.append(p) - return p - return None - - @property - def id(self): - """Gives the server identifiant""" - return self.srv.id + "/" + self.sender - - def setError(self, msg): - self.error = True - self.srv.send_msg_usr(self.sender, msg) - - def accept_user(self, host, port): - """Accept a DCC connection""" - self.s = socket.socket() - try: - self.s.connect((host, port)) - self._logger.info("Accepted user from %s:%d for %s", host, port, self.sender) - self.connected = True - self.stop = False - except: - self.connected = False - self.error = True - return False - self.start() - return True - - - def request_user(self, type="CHAT", filename="CHAT", size=""): - """Create a DCC connection""" - #Open the port - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - try: - s.bind(('', self.port)) - except: - try: - self.port = self.foundPort() - s.bind(('', self.port)) - except: - self.setError("Une erreur s'est produite durant la tentative" - " d'ouverture d'une session DCC.") - return False - self._logger.info("Listening on %d for %s", self.port, self.sender) - - #Send CTCP request for DCC - self.srv.send_ctcp(self.sender, - "DCC %s %s %d %d %s" % (type, filename, self.srv.ip, - self.port, size), - "PRIVMSG") - - s.listen(1) - #Waiting for the client - (self.s, addr) = s.accept() - self._logger.info("Connected by %d", addr) - self.connected = True - return True - - def send_dcc_raw(self, line): - self.s.sendall(line + b'\n') - - def send_dcc(self, msg, to = None): - """If we talk to this user, send a message through this connection - else, send the message to the server class""" - if to is None or to == self.sender or to == self.nick: - if self.error: - self.srv.send_msg_final(self.nick, msg) - elif not self.connected or self.s is None: - try: - self.start() - except RuntimeError: - pass - self.messages.append(msg) - else: - for line in msg.split("\n"): - self.send_dcc_raw(line.encode()) - else: - self.srv.send_dcc(msg, to) - - def send_file(self, filename): - """Send a file over DCC""" - if os.path.isfile(filename): - self.messages = filename - try: - self.start() - except RuntimeError: - pass - else: - self._logger.error("File not found `%s'", filename) - - def run(self): - self.stopping.clear() - - # Send file connection - if not isinstance(self.messages, list): - self.request_user("SEND", - os.path.basename(self.messages), - os.path.getsize(self.messages)) - if self.connected: - with open(self.messages, 'rb') as f: - d = f.read(268435456) #Packets size: 256Mo - while d: - self.s.sendall(d) - self.s.recv(4) #The client send a confirmation after each packet - d = f.read(268435456) #Packets size: 256Mo - - # Messages connection - else: - if not self.connected: - if not self.request_user(): - #TODO: do something here - return False - - #Start by sending all queued messages - for mess in self.messages: - self.send_dcc(mess) - - time.sleep(1) - - readbuffer = b'' - self.nicksize = len(self.srv.nick) - self.Bnick = self.srv.nick.encode() - while not self.stop: - raw = self.s.recv(1024) #recieve server messages - if not raw: - break - readbuffer = readbuffer + raw - temp = readbuffer.split(b'\n') - readbuffer = temp.pop() - - for line in temp: - self.treatement(line) - - if self.connected: - self.s.close() - self.connected = False - - #Remove from DCC connections server list - if self.realname in self.srv.dcc_clients: - del self.srv.dcc_clients[self.realname] - - self._logger.info("Closing connection with %s", self.nick) - self.stopping.set() - if self.closing_event is not None: - self.closing_event() - #Rearm Thread - threading.Thread.__init__(self) - - def treat_msg(self, line): - """Treat a receive message, *can be overwritten*""" - if line == b'NEMUBOT###': - bot = self.srv.add_networkbot(self.srv, self.sender, self) - self.treatement = bot.treat_msg - self.send_dcc("NEMUBOT###") - elif (line[:self.nicksize] == self.Bnick and - line[self.nicksize+1:].strip()[:10] == b'my name is'): - name = line[self.nicksize+1:].strip()[11:].decode('utf-8', - 'replace') - if re.match("^[a-zA-Z0-9_-]+$", name): - if name not in self.srv.dcc_clients: - del self.srv.dcc_clients[self.sender] - self.nick = name - self.sender = self.nick + "!" + self.realname - self.srv.dcc_clients[self.realname] = self - self.send_dcc("Hi " + self.nick) - else: - self.send_dcc("This nickname is already in use" - ", please choose another one.") - else: - self.send_dcc("The name you entered contain" - " invalid char.") - else: - self.srv.treat_msg( - (":%s PRIVMSG %s :" % ( - self.sender,self.srv.nick)).encode() + line, - True) diff --git a/nemubot/server/IRC.py b/nemubot/server/IRC.py deleted file mode 100644 index 2096a63..0000000 --- a/nemubot/server/IRC.py +++ /dev/null @@ -1,276 +0,0 @@ -# Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 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 -import re -import socket - -from nemubot.channel import Channel -from nemubot.message.printer.IRC import IRC as IRCPrinter -from nemubot.server.message.IRC import IRC as IRCMessage -from nemubot.server.socket import SocketServer - - -class IRC(SocketServer): - - """Concrete implementation of a connexion to an IRC server""" - - def __init__(self, host="localhost", port=6667, owner=None, - nick="nemubot", username=None, password=None, - realname="Nemubot", encoding="utf-8", caps=None, - channels=list(), on_connect=None, **kwargs): - """Prepare a connection with an IRC server - - Keyword arguments: - host -- host to join - port -- port on the host to reach - ssl -- is this server using a TLS socket - owner -- bot's owner - nick -- bot's nick - username -- the username as sent to server - password -- if a password is required to connect to the server - realname -- the bot's realname - encoding -- the encoding used on the whole server - caps -- client capabilities to register on the server - channels -- list of channels to join on connection - on_connect -- generator to call when connection is done - """ - - self.username = username if username is not None else nick - self.password = password - self.nick = nick - self.owner = owner - self.realname = realname - - super().__init__(name=self.username + "@" + host + ":" + str(port), - host=host, port=port, **kwargs) - self.printer = IRCPrinter - - self.encoding = encoding - - # Keep a list of joined channels - self.channels = dict() - - # Server/client capabilities - self.capabilities = caps - - # Register CTCP capabilities - self.ctcp_capabilities = dict() - - def _ctcp_clientinfo(msg, cmds): - """Response to CLIENTINFO CTCP message""" - return " ".join(self.ctcp_capabilities.keys()) - - def _ctcp_dcc(msg, cmds): - """Response to DCC CTCP message""" - try: - import ipaddress - ip = ipaddress.ip_address(int(cmds[3])) - port = int(cmds[4]) - conn = DCC(srv, msg.sender) - except: - return "ERRMSG invalid parameters provided as DCC CTCP request" - - self._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: - self._logger.error("DCC: unable to connect to %s:%d", ip, port) - return "ERRMSG unable to connect to %s:%d" % (ip, port) - - import nemubot - - self.ctcp_capabilities["ACTION"] = lambda msg, cmds: None - self.ctcp_capabilities["CLIENTINFO"] = _ctcp_clientinfo - #self.ctcp_capabilities["DCC"] = _ctcp_dcc - self.ctcp_capabilities["FINGER"] = lambda msg, cmds: "VERSION nemubot v%s" % nemubot.__version__ - self.ctcp_capabilities["NEMUBOT"] = lambda msg, cmds: "NEMUBOT %s" % nemubot.__version__ - self.ctcp_capabilities["PING"] = lambda msg, cmds: "PING %s" % " ".join(cmds[1:]) - self.ctcp_capabilities["SOURCE"] = lambda msg, cmds: "SOURCE https://github.com/nemunaire/nemubot" - self.ctcp_capabilities["TIME"] = lambda msg, cmds: "TIME %s" % (datetime.now()) - self.ctcp_capabilities["USERINFO"] = lambda msg, cmds: "USERINFO %s" % self.realname - self.ctcp_capabilities["VERSION"] = lambda msg, cmds: "VERSION nemubot v%s" % nemubot.__version__ - - # TODO: Temporary fix, waiting for hook based CTCP management - self.ctcp_capabilities["TYPING"] = lambda msg, cmds: None - - self._logger.debug("CTCP capabilities setup: %s", ", ".join(self.ctcp_capabilities)) - - - # Register hooks on some IRC CMD - self.hookscmd = dict() - - # Respond to PING - def _on_ping(msg): - self.write(b"PONG :" + msg.params[0]) - self.hookscmd["PING"] = _on_ping - - # Respond to 001 - def _on_connect(msg): - # First, send user defined command - if on_connect is not None: - if callable(on_connect): - toc = on_connect() - else: - toc = on_connect - if toc is not None: - for oc in toc: - self.write(oc) - # Then, JOIN some channels - for chn in channels: - if chn.password: - self.write("JOIN %s %s" % (chn.name, chn.password)) - else: - self.write("JOIN %s" % chn.name) - self.hookscmd["001"] = _on_connect - - # Respond to ERROR - def _on_error(msg): - self.close() - self.hookscmd["ERROR"] = _on_error - - # Respond to CAP - def _on_cap(msg): - if len(msg.params) != 3 or msg.params[1] != b"LS": - return - server_caps = msg.params[2].decode().split(" ") - for cap in self.capabilities: - if cap not in server_caps: - self.capabilities.remove(cap) - if len(self.capabilities) > 0: - self.write("CAP REQ :" + " ".join(self.capabilities)) - self.write("CAP END") - self.hookscmd["CAP"] = _on_cap - - # Respond to JOIN - def _on_join(msg): - if len(msg.params) == 0: - return - - for chname in msg.decode(msg.params[0]).split(","): - # Register the channel - chan = Channel(chname) - self.channels[chname] = chan - self.hookscmd["JOIN"] = _on_join - # Respond to PART - def _on_part(msg): - if len(msg.params) != 1 and len(msg.params) != 2: - return - - for chname in msg.params[0].split(b","): - if chname in self.channels: - if msg.frm == self.nick: - del self.channels[chname] - elif msg.frm in self.channels[chname].people: - del self.channels[chname].people[msg.frm] - self.hookscmd["PART"] = _on_part - # Respond to 331/RPL_NOTOPIC,332/RPL_TOPIC,TOPIC - def _on_topic(msg): - if len(msg.params) != 1 and len(msg.params) != 2: - return - if msg.params[0] in self.channels: - if len(msg.params) == 1 or len(msg.params[1]) == 0: - self.channels[msg.params[0]].topic = None - else: - self.channels[msg.params[0]].topic = msg.decode(msg.params[1]) - self.hookscmd["331"] = _on_topic - self.hookscmd["332"] = _on_topic - self.hookscmd["TOPIC"] = _on_topic - # Respond to 353/RPL_NAMREPLY - def _on_353(msg): - if len(msg.params) == 3: - msg.params.pop(0) # 353: like RFC 1459 - if len(msg.params) != 2: - return - if msg.params[0] in self.channels: - for nk in msg.decode(msg.params[1]).split(" "): - res = re.match("^(?P<level>[^a-zA-Z[\]\\`_^{|}])(?P<nickname>[a-zA-Z[\]\\`_^{|}][a-zA-Z0-9[\]\\`_^{|}-]*)$") - self.channels[msg.params[0]].people[res.group("nickname")] = res.group("level") - self.hookscmd["353"] = _on_353 - - # Respond to INVITE - def _on_invite(msg): - if len(msg.params) != 2: - return - self.write("JOIN " + msg.decode(msg.params[1])) - self.hookscmd["INVITE"] = _on_invite - - # Respond to ERR_NICKCOLLISION - def _on_nickcollision(msg): - self.nick += "_" - self.write("NICK " + self.nick) - self.hookscmd["433"] = _on_nickcollision - self.hookscmd["436"] = _on_nickcollision - - # Handle CTCP requests - def _on_ctcp(msg): - if len(msg.params) != 2 or not msg.is_ctcp: - return - cmds = msg.decode(msg.params[1][1:len(msg.params[1])-1]).split(' ') - if cmds[0] in self.ctcp_capabilities: - res = self.ctcp_capabilities[cmds[0]](msg, cmds) - else: - res = "ERRMSG Unknown or unimplemented CTCP request" - if res is not None: - self.write("NOTICE %s :\x01%s\x01" % (msg.frm, res)) - self.hookscmd["PRIVMSG"] = _on_ctcp - - - # Open/close - - def connect(self): - super().connect() - - if self.password is not None: - self.write("PASS :" + self.password) - if self.capabilities is not None: - self.write("CAP LS") - self.write("NICK :" + self.nick) - self.write("USER %s %s bla :%s" % (self.username, socket.getfqdn(), self.realname)) - - - def close(self): - if not self._fd._closed: - self.write("QUIT") - return super().close() - - - # Writes: as inherited - - # Read - - def async_read(self): - for line in super().async_read(): - # PING should be handled here, so start parsing here :/ - msg = IRCMessage(line, self.encoding) - - if msg.cmd in self.hookscmd: - self.hookscmd[msg.cmd](msg) - - yield msg - - - def parse(self, msg): - mes = msg.to_bot_message(self) - if mes is not None: - yield mes - - - 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) diff --git a/nemubot/server/IRCLib.py b/nemubot/server/IRCLib.py new file mode 100644 index 0000000..cdd13cf --- /dev/null +++ b/nemubot/server/IRCLib.py @@ -0,0 +1,375 @@ +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2026 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 +import shlex +import threading + +import irc.bot +import irc.client +import irc.connection + +import nemubot.message as message +from nemubot.server.threaded import ThreadedServer + + +class _IRCBotAdapter(irc.bot.SingleServerIRCBot): + + """Internal adapter that bridges the irc library event model to nemubot. + + Subclasses SingleServerIRCBot to get automatic reconnection, PING/PONG, + and nick-collision handling for free. + """ + + def __init__(self, server_name, push_fn, channels, on_connect_cmds, + nick, server_list, owner=None, realname="Nemubot", + encoding="utf-8", **connect_params): + super().__init__(server_list, nick, realname, **connect_params) + self._nemubot_name = server_name + self._push = push_fn + self._channels_to_join = channels + self._on_connect_cmds = on_connect_cmds or [] + self.owner = owner + self.encoding = encoding + self._stop_event = threading.Event() + + + # Event loop control + + def start(self): + """Run the reactor loop until stop() is called.""" + self._connect() + while not self._stop_event.is_set(): + self.reactor.process_once(timeout=0.2) + + def stop(self): + """Signal the loop to exit and disconnect cleanly.""" + self._stop_event.set() + try: + self.connection.disconnect("Goodbye") + except Exception: + pass + + def on_disconnect(self, connection, event): + """Reconnect automatically unless we are shutting down.""" + if not self._stop_event.is_set(): + super().on_disconnect(connection, event) + + + # Connection lifecycle + + def on_welcome(self, connection, event): + """001 — run on_connect commands then join channels.""" + for cmd in self._on_connect_cmds: + if callable(cmd): + for c in (cmd() or []): + connection.send_raw(c) + else: + connection.send_raw(cmd) + + for ch in self._channels_to_join: + if isinstance(ch, tuple): + connection.join(ch[0], ch[1] if len(ch) > 1 else "") + elif hasattr(ch, 'name'): + connection.join(ch.name, getattr(ch, 'password', "") or "") + else: + connection.join(str(ch)) + + def on_invite(self, connection, event): + """Auto-join on INVITE.""" + if event.arguments: + connection.join(event.arguments[0]) + + + # CTCP + + def on_ctcp(self, connection, event): + """Handle CTCP requests (irc library >= 19 dispatches all to on_ctcp).""" + nick = irc.client.NickMask(event.source).nick + ctcp_type = event.arguments[0].upper() if event.arguments else "" + ctcp_arg = event.arguments[1] if len(event.arguments) > 1 else "" + self._reply_ctcp(connection, nick, ctcp_type, ctcp_arg) + + # Fallbacks for older irc library versions that dispatch per-type + def on_ctcpversion(self, connection, event): + import nemubot + nick = irc.client.NickMask(event.source).nick + connection.ctcp_reply(nick, "VERSION nemubot v%s" % nemubot.__version__) + + def on_ctcpping(self, connection, event): + nick = irc.client.NickMask(event.source).nick + arg = event.arguments[0] if event.arguments else "" + connection.ctcp_reply(nick, "PING %s" % arg) + + def _reply_ctcp(self, connection, nick, ctcp_type, ctcp_arg): + import nemubot + responses = { + "ACTION": None, # handled as on_action + "CLIENTINFO": "CLIENTINFO ACTION CLIENTINFO FINGER PING SOURCE TIME USERINFO VERSION", + "FINGER": "FINGER nemubot v%s" % nemubot.__version__, + "PING": "PING %s" % ctcp_arg, + "SOURCE": "SOURCE https://github.com/nemunaire/nemubot", + "TIME": "TIME %s" % datetime.now(), + "USERINFO": "USERINFO Nemubot", + "VERSION": "VERSION nemubot v%s" % nemubot.__version__, + } + if ctcp_type in responses and responses[ctcp_type] is not None: + connection.ctcp_reply(nick, responses[ctcp_type]) + + + # Incoming messages + + def _decode(self, text): + if isinstance(text, bytes): + try: + return text.decode("utf-8") + except UnicodeDecodeError: + return text.decode(self.encoding, "replace") + return text + + def _make_message(self, connection, source, target, text): + """Convert raw IRC event data into a nemubot bot message.""" + nick = irc.client.NickMask(source).nick + text = self._decode(text) + bot_nick = connection.get_nickname() + is_channel = irc.client.is_channel(target) + to = [target] if is_channel else [nick] + to_response = [target] if is_channel else [nick] + + common = dict( + server=self._nemubot_name, + to=to, + to_response=to_response, + frm=nick, + frm_owner=(nick == self.owner), + ) + + # "botname: text" or "botname, text" + if (text.startswith(bot_nick + ":") or + text.startswith(bot_nick + ",")): + inner = text[len(bot_nick) + 1:].strip() + return message.DirectAsk(designated=bot_nick, message=inner, + **common) + + # "!command [args]" + if len(text) > 1 and text[0] == '!': + inner = text[1:].strip() + try: + args = shlex.split(inner) + except ValueError: + args = inner.split() + if args: + # Extract @key=value named arguments (same logic as IRC.py) + kwargs = {} + while len(args) > 1: + arg = args[1] + if len(arg) > 2 and arg[0:2] == '\\@': + args[1] = arg[1:] + elif len(arg) > 1 and arg[0] == '@': + arsp = arg[1:].split("=", 1) + kwargs[arsp[0]] = arsp[1] if len(arsp) == 2 else None + args.pop(1) + continue + break + return message.Command(cmd=args[0], args=args[1:], + kwargs=kwargs, **common) + + return message.Text(message=text, **common) + + def on_pubmsg(self, connection, event): + msg = self._make_message( + connection, event.source, event.target, + event.arguments[0] if event.arguments else "", + ) + if msg: + self._push(msg) + + def on_privmsg(self, connection, event): + nick = irc.client.NickMask(event.source).nick + msg = self._make_message( + connection, event.source, nick, + event.arguments[0] if event.arguments else "", + ) + if msg: + self._push(msg) + + def on_action(self, connection, event): + """CTCP ACTION (/me) — delivered as a plain Text message.""" + nick = irc.client.NickMask(event.source).nick + text = "/me %s" % (event.arguments[0] if event.arguments else "") + is_channel = irc.client.is_channel(event.target) + to = [event.target] if is_channel else [nick] + self._push(message.Text( + message=text, + server=self._nemubot_name, + to=to, to_response=to, + frm=nick, frm_owner=(nick == self.owner), + )) + + +class IRCLib(ThreadedServer): + + """IRC server using the irc Python library (jaraco). + + Compared to the hand-rolled IRC.py implementation, this gets: + - Automatic exponential-backoff reconnection + - PING/PONG handled transparently + - Nick-collision suffix logic built-in + """ + + def __init__(self, host="localhost", port=6667, nick="nemubot", + username=None, password=None, realname="Nemubot", + encoding="utf-8", owner=None, channels=None, + on_connect=None, ssl=False, **kwargs): + """Prepare a connection to an IRC server. + + Keyword arguments: + host -- IRC server hostname + port -- IRC server port (default 6667) + nick -- bot's nickname + username -- username for USER command (defaults to nick) + password -- server password (sent as PASS) + realname -- bot's real name + encoding -- fallback encoding for non-UTF-8 servers + owner -- nick of the bot's owner (sets frm_owner on messages) + channels -- list of channel names / (name, key) tuples to join + on_connect -- list of raw IRC commands (or a callable returning one) + to send after receiving 001 + ssl -- wrap the connection in TLS + """ + name = (username or nick) + "@" + host + ":" + str(port) + super().__init__(name=name) + + self._host = host + self._port = int(port) + self._nick = nick + self._username = username or nick + self._password = password + self._realname = realname + self._encoding = encoding + self.owner = owner + self._channels = channels or [] + self._on_connect_cmds = on_connect + self._ssl = ssl + + self._bot = None + self._thread = None + + + # ThreadedServer hooks + + def _start(self): + server_list = [irc.bot.ServerSpec(self._host, self._port, + self._password)] + + connect_params = {"username": self._username} + + if self._ssl: + import ssl as ssl_mod + ctx = ssl_mod.create_default_context() + host = self._host # capture for closure + connect_params["connect_factory"] = irc.connection.Factory( + wrapper=lambda sock: ctx.wrap_socket(sock, + server_hostname=host) + ) + + self._bot = _IRCBotAdapter( + server_name=self.name, + push_fn=self._push_message, + channels=self._channels, + on_connect_cmds=self._on_connect_cmds, + nick=self._nick, + server_list=server_list, + owner=self.owner, + realname=self._realname, + encoding=self._encoding, + **connect_params, + ) + self._thread = threading.Thread( + target=self._bot.start, + daemon=True, + name="nemubot.IRC/" + self.name, + ) + self._thread.start() + + def _stop(self): + if self._bot: + self._bot.stop() + if self._thread: + self._thread.join(timeout=5) + + + # Outgoing messages + + def send_response(self, response): + if response is None: + return + if isinstance(response, list): + for r in response: + self.send_response(r) + return + if not self._bot: + return + + from nemubot.message.printer.IRCLib import IRCLib as IRCLibPrinter + printer = IRCLibPrinter(self._bot.connection) + response.accept(printer) + + + # subparse: re-parse a plain string in the context of an existing message + # (used by alias, rnd, grep, cat, smmry, sms modules) + + def subparse(self, orig, cnt): + bot_nick = (self._bot.connection.get_nickname() + if self._bot else self._nick) + common = dict( + server=self.name, + to=orig.to, + to_response=orig.to_response, + frm=orig.frm, + frm_owner=orig.frm_owner, + date=orig.date, + ) + text = cnt + + if (text.startswith(bot_nick + ":") or + text.startswith(bot_nick + ",")): + inner = text[len(bot_nick) + 1:].strip() + return message.DirectAsk(designated=bot_nick, message=inner, + **common) + + if len(text) > 1 and text[0] == '!': + inner = text[1:].strip() + try: + args = shlex.split(inner) + except ValueError: + args = inner.split() + if args: + kwargs = {} + while len(args) > 1: + arg = args[1] + if len(arg) > 2 and arg[0:2] == '\\@': + args[1] = arg[1:] + elif len(arg) > 1 and arg[0] == '@': + arsp = arg[1:].split("=", 1) + kwargs[arsp[0]] = arsp[1] if len(arsp) == 2 else None + args.pop(1) + continue + break + return message.Command(cmd=args[0], args=args[1:], + kwargs=kwargs, **common) + + return message.Text(message=text, **common) diff --git a/nemubot/server/__init__.py b/nemubot/server/__init__.py index 9e186ed..db9ad87 100644 --- a/nemubot/server/__init__.py +++ b/nemubot/server/__init__.py @@ -24,13 +24,13 @@ def factory(uri, ssl=False, **init_args): if o.scheme == "irc" or o.scheme == "ircs": # https://www.w3.org/Addressing/draft-mirashi-url-irc-01.txt # https://www-archive.mozilla.org/projects/rt-messaging/chatzilla/irc-urls.html - args = init_args + args = dict(init_args) if o.scheme == "ircs": ssl = True if o.hostname is not None: args["host"] = o.hostname if o.port is not None: args["port"] = o.port if o.username is not None: args["username"] = o.username - if o.password is not None: args["password"] = o.password + if o.password is not None: args["password"] = unquote(o.password) modifiers = o.path.split(",") target = unquote(modifiers.pop(0)[1:]) @@ -41,37 +41,27 @@ def factory(uri, ssl=False, **init_args): if "msg" in params: if "on_connect" not in args: args["on_connect"] = [] - args["on_connect"].append("PRIVMSG %s :%s" % (target, params["msg"])) + args["on_connect"].append("PRIVMSG %s :%s" % (target, params["msg"][0])) if "key" in params: if "channels" not in args: args["channels"] = [] - args["channels"].append((target, params["key"])) + args["channels"].append((target, params["key"][0])) if "pass" in params: - args["password"] = params["pass"] + args["password"] = params["pass"][0] if "charset" in params: - args["encoding"] = params["charset"] + args["encoding"] = params["charset"][0] - # if "channels" not in args and "isnick" not in modifiers: - args["channels"] = [ target ] + args["channels"] = [target] - from nemubot.server.IRC import IRC as IRCServer + args["ssl"] = ssl + + from nemubot.server.IRCLib import IRCLib as IRCServer srv = IRCServer(**args) - if ssl: - try: - from ssl import create_default_context - context = create_default_context() - except ImportError: - # Python 3.3 compat - from ssl import SSLContext, PROTOCOL_TLSv1 - context = SSLContext(PROTOCOL_TLSv1) - from ssl import wrap_socket - srv._fd = context.wrap_socket(srv._fd, server_hostname=o.hostname) - elif o.scheme == "matrix": # matrix://localpart:password@homeserver.tld/!room:homeserver.tld # matrix://localpart:password@homeserver.tld/%23alias:homeserver.tld diff --git a/nemubot/server/factory_test.py b/nemubot/server/factory_test.py deleted file mode 100644 index efc3130..0000000 --- a/nemubot/server/factory_test.py +++ /dev/null @@ -1,54 +0,0 @@ -# Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 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/>. - -import unittest - -from nemubot.server import factory - -class TestFactory(unittest.TestCase): - - def test_IRC1(self): - from nemubot.server.IRC import IRC as IRCServer - import socket - import ssl - - # <host>: If omitted, the client must connect to a prespecified default IRC server. - server = factory("irc:///") - self.assertIsInstance(server, IRCServer) - self.assertIsInstance(server._fd, socket.socket) - self.assertIn(server._sockaddr[0], ["127.0.0.1", "::1"]) - - server = factory("irc://2.2.2.2") - self.assertIsInstance(server, IRCServer) - self.assertEqual(server._sockaddr[0], "2.2.2.2") - - server = factory("ircs://1.2.1.2") - self.assertIsInstance(server, IRCServer) - self.assertIsInstance(server._fd, ssl.SSLSocket) - - server = factory("irc://1.2.3.4:6667") - self.assertIsInstance(server, IRCServer) - self.assertEqual(server._sockaddr[0], "1.2.3.4") - self.assertEqual(server._sockaddr[1], 6667) - - server = factory("ircs://4.3.2.1:194/") - self.assertIsInstance(server, IRCServer) - self.assertEqual(server._sockaddr[0], "4.3.2.1") - self.assertEqual(server._sockaddr[1], 194) - - -if __name__ == '__main__': - unittest.main() diff --git a/nemubot/server/message/IRC.py b/nemubot/server/message/IRC.py deleted file mode 100644 index 5ccd735..0000000 --- a/nemubot/server/message/IRC.py +++ /dev/null @@ -1,210 +0,0 @@ -# Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2016 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, timezone -import re -import shlex - -import nemubot.message as message -from nemubot.server.message.abstract import Abstract - -mgx = re.compile(b'''^(?:@(?P<tags>[^ ]+)\ )? - (?::(?P<prefix> - (?P<nick>[^!@ ]+) - (?: !(?P<user>[^@ ]+))? - (?:@(?P<host>[^ ]*))? - )\ )? - (?P<command>(?:[a-zA-Z]+|[0-9]{3})) - (?P<params>(?:\ [^:][^ ]*)*)(?:\ :(?P<trailing>.*))? - $''', re.X) - -class IRC(Abstract): - - """Class responsible for parsing IRC messages""" - - def __init__(self, raw, encoding="utf-8"): - self.encoding = encoding - self.tags = { 'time': datetime.now(timezone.utc) } - self.params = list() - - p = mgx.match(raw.rstrip()) - - if p is None: - raise Exception("Not a valid IRC message: %s" % raw) - - # Parse tags if exists: @aaa=bbb;ccc;example.com/ddd=eee - if p.group("tags"): - for tgs in self.decode(p.group("tags")).split(';'): - tag = tgs.split('=') - if len(tag) > 1: - self.add_tag(tag[0], tag[1]) - else: - self.add_tag(tag[0]) - - # Parse prefix if exists: :nick!user@host.com - self.prefix = self.decode(p.group("prefix")) - self.nick = self.decode(p.group("nick")) - self.user = self.decode(p.group("user")) - self.host = self.decode(p.group("host")) - - # Parse command - self.cmd = self.decode(p.group("command")) - - # Parse params - if p.group("params") is not None and p.group("params") != b'': - for param in p.group("params").strip().split(b' '): - self.params.append(param) - - if p.group("trailing") is not None: - self.params.append(p.group("trailing")) - - - def add_tag(self, key, value=None): - """Add an IRCv3.2 Message Tags - - Arguments: - key -- tag identifier (unique for the message) - value -- optional value for the tag - """ - - # Treat special tags - if key == "time" and value is not None: - import calendar, time - value = datetime.fromtimestamp(calendar.timegm(time.strptime(value, "%Y-%m-%dT%H:%M:%S.%fZ")), timezone.utc) - - # Store tag - self.tags[key] = value - - - @property - def is_ctcp(self): - """Analyze a message, to determine if this is a CTCP one""" - return self.cmd == "PRIVMSG" and len(self.params) == 2 and len(self.params[1]) > 1 and (self.params[1][0] == 0x01 or self.params[1][1] == 0x01) - - - def decode(self, s): - """Decode the content string usign a specific encoding - - Argument: - s -- string to decode - """ - - if isinstance(s, bytes): - try: - s = s.decode() - except UnicodeDecodeError: - s = s.decode(self.encoding, 'replace') - return s - - - - def to_server_string(self, client=True): - """Pretty print the message to close to original input string - - Keyword argument: - client -- export as a client-side string if true - """ - - res = ";".join(["@%s=%s" % (k, v if not isinstance(v, datetime) else v.strftime("%Y-%m-%dT%H:%M:%S.%fZ")) for k, v in self.tags.items()]) - - if not client: - res += " :%s!%s@%s" % (self.nick, self.user, self.host) - - res += " " + self.cmd - - if len(self.params) > 0: - - if len(self.params) > 1: - res += " " + self.decode(b" ".join(self.params[:-1])) - res += " :" + self.decode(self.params[-1]) - - return res - - - def to_bot_message(self, srv): - """Convert to one of concrete implementation of AbstractMessage - - Argument: - srv -- the server from the message was received - """ - - if self.cmd == "PRIVMSG" or self.cmd == "NOTICE": - - receivers = self.decode(self.params[0]).split(',') - - common_args = { - "server": srv.name, - "date": self.tags["time"], - "to": receivers, - "to_response": [r if r != srv.nick else self.nick for r in receivers], - "frm": self.nick, - "frm_owner": self.nick == srv.owner - } - - # If CTCP, remove 0x01 - if self.is_ctcp: - text = self.decode(self.params[1][1:len(self.params[1])-1]) - else: - text = self.decode(self.params[1]) - - if text.find(srv.nick) == 0 and len(text) > len(srv.nick) + 2 and text[len(srv.nick)] == ":": - designated = srv.nick - text = text[len(srv.nick) + 1:].strip() - else: - designated = None - - # Is this a command? - if len(text) > 1 and text[0] == '!': - text = text[1:].strip() - - # Split content by words - try: - args = shlex.split(text) - except ValueError: - args = text.split(' ') - - # Extract explicit named arguments: @key=value or just @key, only at begening - kwargs = {} - while len(args) > 1: - arg = args[1] - if len(arg) > 2: - if arg[0:2] == '\\@': - args[1] = arg[1:] - elif arg[0] == '@': - arsp = arg[1:].split("=", 1) - if len(arsp) == 2: - kwargs[arsp[0]] = arsp[1] - else: - kwargs[arg[1:]] = None - args.pop(1) - continue - # Futher argument are considered as normal argument (this helps for subcommand treatment) - break - - return message.Command(cmd=args[0], - args=args[1:], - kwargs=kwargs, - **common_args) - - # Is this an ask for this bot? - elif designated is not None: - return message.DirectAsk(designated=designated, message=text, **common_args) - - # Normal message - else: - return message.Text(message=text, **common_args) - - return None diff --git a/nemubot/server/message/__init__.py b/nemubot/server/message/__init__.py deleted file mode 100644 index 57f3468..0000000 --- a/nemubot/server/message/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2015 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/>. diff --git a/nemubot/server/message/abstract.py b/nemubot/server/message/abstract.py deleted file mode 100644 index 624e453..0000000 --- a/nemubot/server/message/abstract.py +++ /dev/null @@ -1,33 +0,0 @@ -# Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2016 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/>. - -class Abstract: - - def to_bot_message(self, srv): - """Convert to one of concrete implementation of AbstractMessage - - Argument: - srv -- the server from the message was received - """ - - raise NotImplemented - - - def to_server_string(self, **kwargs): - """Pretty print the message to close to original input string - """ - - raise NotImplemented diff --git a/nemubot/server/test_IRC.py b/nemubot/server/test_IRC.py deleted file mode 100644 index 552a1d3..0000000 --- a/nemubot/server/test_IRC.py +++ /dev/null @@ -1,50 +0,0 @@ -import unittest - -import nemubot.server.IRC as IRC - - -class TestIRCMessage(unittest.TestCase): - - - def setUp(self): - self.msg = IRC.IRCMessage(b":toto!titi@RZ-3je16g.re PRIVMSG #the-channel :Can you parse this message?") - - - def test_parsing(self): - self.assertEqual(self.msg.prefix, "toto!titi@RZ-3je16g.re") - self.assertEqual(self.msg.nick, "toto") - self.assertEqual(self.msg.user, "titi") - self.assertEqual(self.msg.host, "RZ-3je16g.re") - - self.assertEqual(len(self.msg.params), 2) - - self.assertEqual(self.msg.params[0], b"#the-channel") - self.assertEqual(self.msg.params[1], b"Can you parse this message?") - - - def test_prettyprint(self): - bst1 = self.msg.to_server_string(False) - msg2 = IRC.IRCMessage(bst1.encode()) - - bst2 = msg2.to_server_string(False) - msg3 = IRC.IRCMessage(bst2.encode()) - - bst3 = msg3.to_server_string(False) - - self.assertEqual(bst2, bst3) - - - def test_tags(self): - self.assertEqual(len(self.msg.tags), 1) - self.assertIn("time", self.msg.tags) - - self.msg.add_tag("time") - self.assertEqual(len(self.msg.tags), 1) - - self.msg.add_tag("toto") - self.assertEqual(len(self.msg.tags), 2) - self.assertIn("toto", self.msg.tags) - - -if __name__ == '__main__': - unittest.main() diff --git a/requirements.txt b/requirements.txt index 45eefe2..e037895 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ +irc matrix-nio diff --git a/setup.py b/setup.py index 94c1274..7b5bdcd 100755 --- a/setup.py +++ b/setup.py @@ -71,7 +71,6 @@ setup( 'nemubot.message.printer', 'nemubot.module', 'nemubot.server', - 'nemubot.server.message', 'nemubot.tools', 'nemubot.tools.xmlparser', ], From 310f9330914e5a1b90122e24ae07509c51c80432 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Fri, 6 Mar 2026 22:14:20 +0700 Subject: [PATCH 269/271] bot: fix duplicate unregister KeyError and improve connection error logging Silently ignore KeyError when unregistering an already-removed FD from the poll loop (servers can queue multiple close events). Also include the exception message when a server connection fails at startup. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- nemubot/__main__.py | 4 ++-- nemubot/bot.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 6a8b265..7070639 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -163,8 +163,8 @@ def main(): logger.info("Server '%s' successfully added.", srv.name) else: logger.error("Can't add server '%s'.", srv.name) - except: - logger.error("Unable to connect to '%s'.", srv.name) + except Exception as e: + logger.error("Unable to connect to '%s': %s", srv.name, e) continue break diff --git a/nemubot/bot.py b/nemubot/bot.py index 6aa5ed6..21f7178 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -218,7 +218,10 @@ class Bot(threading.Thread): elif args[0] == "register": self._poll.register(int(args[1]), select.POLLIN | select.POLLPRI) elif args[0] == "unregister": - self._poll.unregister(int(args[1])) + try: + self._poll.unregister(int(args[1])) + except KeyError: + pass except: logger.exception("Unhandled excpetion during action:") From 9d7c278d1aa1eece9468933e0d353e1473a68349 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Fri, 6 Mar 2026 23:08:27 +0700 Subject: [PATCH 270/271] bot: Fix sequential message processing with proper consumer pool Replace the flawed cnsr_thrd_size threshold with cnsr_active, which tracks the number of consumers currently executing a task. A new consumer thread is now spawned the moment the queue is non-empty and all existing consumers are busy, enabling true parallel execution of slow and fast commands. The pool is capped at os.cpu_count() threads. - bot.py: replace cnsr_thrd_size with cnsr_active + cnsr_lock + cnsr_max - consumer.py: increment/decrement cnsr_active around stm.run(), remove itself from cnsr_thrd under the lock, mark thread as daemon Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- nemubot/bot.py | 33 ++++++++++++++++++++------------- nemubot/consumer.py | 23 +++++++++++++++-------- 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/nemubot/bot.py b/nemubot/bot.py index 21f7178..2b6e15c 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -143,11 +143,16 @@ class Bot(threading.Thread): return res self.treater.hm.add_hook(nemubot.hooks.Command(_help_msg, "help"), "in", "Command") + import os from queue import Queue - # Messages to be treated - self.cnsr_queue = Queue() - self.cnsr_thrd = list() - self.cnsr_thrd_size = -1 + # Messages to be treated — shared across all server connections. + # cnsr_active tracks consumers currently inside stm.run() (not idle), + # which lets us spawn a new thread the moment all existing ones are busy. + self.cnsr_queue = Queue() + self.cnsr_thrd = list() + self.cnsr_lock = threading.Lock() + self.cnsr_active = 0 # consumers currently executing a task + self.cnsr_max = os.cpu_count() or 4 # upper bound on concurrent consumer threads def __del__(self): @@ -234,14 +239,15 @@ class Bot(threading.Thread): 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() + # Spawn a new consumer whenever the queue has work and every + # existing consumer is already busy executing a task. + with self.cnsr_lock: + while (not self.cnsr_queue.empty() + and self.cnsr_active >= len(self.cnsr_thrd) + and len(self.cnsr_thrd) < self.cnsr_max): + c = Consumer(self) + self.cnsr_thrd.append(c) + c.start() sync_queue = None logger.info("Ending main loop") @@ -518,7 +524,8 @@ class Bot(threading.Thread): srv.close() logger.info("Stop consumers") - k = self.cnsr_thrd + with self.cnsr_lock: + k = list(self.cnsr_thrd) for cnsr in k: cnsr.stop = True diff --git a/nemubot/consumer.py b/nemubot/consumer.py index 3a58219..a9a4146 100644 --- a/nemubot/consumer.py +++ b/nemubot/consumer.py @@ -105,18 +105,25 @@ class Consumer(threading.Thread): def __init__(self, context): self.context = context self.stop = False - super().__init__(name="Nemubot consumer") + super().__init__(name="Nemubot consumer", daemon=True) 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() + try: + stm = self.context.cnsr_queue.get(True, 1) + except queue.Empty: + break - except queue.Empty: - pass + with self.context.cnsr_lock: + self.context.cnsr_active += 1 + try: + stm.run(self.context) + finally: + self.context.cnsr_queue.task_done() + with self.context.cnsr_lock: + self.context.cnsr_active -= 1 finally: - self.context.cnsr_thrd_size -= 2 - self.context.cnsr_thrd.remove(self) + with self.context.cnsr_lock: + self.context.cnsr_thrd.remove(self) From 27bd0c50c19127ab1e15fcdd2e91937d804c1cd2 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier <nemunaire@nemunai.re> Date: Sat, 7 Mar 2026 15:55:24 +0700 Subject: [PATCH 271/271] server: Fix on_disconnect AttributeError when irc library lacks the method Replace super().on_disconnect() call (absent in some irc library versions) with self.jump_server() which is the actual reconnect method provided by SingleServerIRCBot. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- nemubot/server/IRCLib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nemubot/server/IRCLib.py b/nemubot/server/IRCLib.py index cdd13cf..eb7c16f 100644 --- a/nemubot/server/IRCLib.py +++ b/nemubot/server/IRCLib.py @@ -66,7 +66,7 @@ class _IRCBotAdapter(irc.bot.SingleServerIRCBot): def on_disconnect(self, connection, event): """Reconnect automatically unless we are shutting down.""" if not self._stop_event.is_set(): - super().on_disconnect(connection, event) + self.jump_server() # Connection lifecycle