From eefcf96516afcd67b97dfe84dea06d999af0f984 Mon Sep 17 00:00:00 2001 From: Nemunaire Date: Tue, 22 Apr 2014 17:00:47 +0200 Subject: [PATCH 001/674] DDG module: display redirect link on bang request --- modules/ddg/DDGSearch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ddg/DDGSearch.py b/modules/ddg/DDGSearch.py index 77aee50..67d0429 100644 --- a/modules/ddg/DDGSearch.py +++ b/modules/ddg/DDGSearch.py @@ -10,7 +10,7 @@ class DDGSearch: def __init__(self, terms): self.terms = terms - raw = urlopen("https://api.duckduckgo.com/?q=%s&format=xml" % quote(terms), timeout=10) + raw = urlopen("https://api.duckduckgo.com/?q=%s&format=xml&no_redirect=1" % quote(terms), timeout=10) self.ddgres = xmlparser.parse_string(raw.read()) @property From 08f3a31e88e5bf15519ea7c49570e24f134fc15e Mon Sep 17 00:00:00 2001 From: Nemunaire Date: Tue, 22 Apr 2014 17:19:10 +0200 Subject: [PATCH 002/674] DDG module: new !urbandictionnary command that display results from Urban Dictionnary website; closes #29 --- modules/ddg/UrbanDictionnary.py | 27 +++++++++++++++++++++++++++ modules/ddg/__init__.py | 20 ++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 modules/ddg/UrbanDictionnary.py diff --git a/modules/ddg/UrbanDictionnary.py b/modules/ddg/UrbanDictionnary.py new file mode 100644 index 0000000..e3fd13f --- /dev/null +++ b/modules/ddg/UrbanDictionnary.py @@ -0,0 +1,27 @@ +# coding=utf-8 + +import json +from urllib.parse import quote +from urllib.request import urlopen + +class UrbanDictionnary: + def __init__(self, terms): + self.terms = terms + + raw = urlopen("http://api.urbandictionary.com/v0/define?term=%s" % quote(terms), timeout=10) + self.udres = json.loads(raw.read().decode()) + + @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"] + else: + yield "Sorry, no definition found for %s" % self.terms diff --git a/modules/ddg/__init__.py b/modules/ddg/__init__.py index ff50274..46aac3e 100644 --- a/modules/ddg/__init__.py +++ b/modules/ddg/__init__.py @@ -5,6 +5,7 @@ import imp nemubotversion = 3.3 from . import DDGSearch +from . import UrbanDictionnary from . import WFASearch from . import Wikipedia @@ -21,9 +22,11 @@ def load(context): add_hook("cmd_hook", Hook(calculate, "calc")) add_hook("cmd_hook", Hook(wiki, "dico")) add_hook("cmd_hook", Hook(wiki, "wiki")) + add_hook("cmd_hook", Hook(udsearch, "urbandictionnary")) def reload(): imp.reload(DDGSearch) + imp.reload(UrbanDictionnary) imp.reload(WFASearch) imp.reload(Wikipedia) @@ -65,6 +68,23 @@ def search(msg): return res +def udsearch(msg): + if len(msg.cmds) <= 1: + return Response(msg.sender, + "Indicate a term to search", + msg.channel, nick=msg.nick) + + s = UrbanDictionnary.UrbanDictionnary(' '.join(msg.cmds[1:])) + + res = Response(msg.sender, channel=msg.channel, nomore="No more results", + count=" (%d more definitions)") + + for d in s.definitions: + res.append_message(d) + + return res + + def calculate(msg): if len(msg.cmds) <= 1: return Response(msg.sender, From d38ebd372cee83a6a1775424fda5143e821836ee Mon Sep 17 00:00:00 2001 From: Nemunaire Date: Tue, 22 Apr 2014 17:23:03 +0200 Subject: [PATCH 003/674] DDG module: add examples on !ud commands --- modules/ddg/UrbanDictionnary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ddg/UrbanDictionnary.py b/modules/ddg/UrbanDictionnary.py index e3fd13f..904669c 100644 --- a/modules/ddg/UrbanDictionnary.py +++ b/modules/ddg/UrbanDictionnary.py @@ -22,6 +22,6 @@ class UrbanDictionnary: def definitions(self): if self.udres and "list" in self.udres: for d in self.udres["list"]: - yield d["definition"] + yield d["definition"] + "\n" + d["example"] else: yield "Sorry, no definition found for %s" % self.terms From 26502abe35a6b6775acd69fda66e7dc8254cb651 Mon Sep 17 00:00:00 2001 From: Nemunaire Date: Wed, 30 Apr 2014 22:19:32 +0200 Subject: [PATCH 004/674] New exception: IRCException When raised in a module, respond in the channel with the given string as Response content --- exception.py | 29 +++++++++++++++++++++++++++++ hooks.py | 40 ++++++++++++++++++++++------------------ importer.py | 2 ++ 3 files changed, 53 insertions(+), 18 deletions(-) create mode 100644 exception.py diff --git a/exception.py b/exception.py new file mode 100644 index 0000000..7acfaf8 --- /dev/null +++ b/exception.py @@ -0,0 +1,29 @@ +# coding=utf-8 + +# Nemubot is a modulable IRC bot, built around XML configuration files. +# Copyright (C) 2012 Mercier Pierre-Olivier +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from response import Response + +class IRCException(Exception): + + def __init__(self, message, personnal=True): + super(IRCException, self).__init__(message) + self.message = message + self.personnal = personnal + + def fill_response(self, msg): + return Response(msg.sender, self.message, channel=msg.channel, nick=(msg.nick if self.personnal else None)) diff --git a/hooks.py b/hooks.py index ea70bc5..e247be3 100644 --- a/hooks.py +++ b/hooks.py @@ -19,6 +19,7 @@ import re from response import Response +from exception import IRCException class MessagesHook: def __init__(self, context, bot): @@ -199,22 +200,25 @@ class Hook: else: call = self.call - if self.data is None: - if data2 is None: - return call(msg) - elif isinstance(data2, dict): - return call(msg, **data2) + try: + if self.data is None: + if data2 is None: + return call(msg) + elif isinstance(data2, dict): + return call(msg, **data2) + else: + return call(msg, data2) + elif isinstance(self.data, dict): + if data2 is None: + return call(msg, **self.data) + else: + return call(msg, data2, **self.data) else: - return call(msg, data2) - elif isinstance(self.data, dict): - if data2 is None: - return call(msg, **self.data) - else: - return call(msg, data2, **self.data) - else: - if data2 is None: - return call(msg, self.data) - elif isinstance(data2, dict): - return call(msg, self.data, **data2) - else: - return call(msg, self.data, data2) + if data2 is None: + return call(msg, self.data) + elif isinstance(data2, dict): + return call(msg, self.data, **data2) + else: + return call(msg, self.data, data2) + except IRCException as e: + return e.fill_response(msg) diff --git a/importer.py b/importer.py index 7f9ed62..402fa12 100644 --- a/importer.py +++ b/importer.py @@ -23,6 +23,7 @@ import os import sys import event +import exception from hooks import Hook import response import xmlparser @@ -172,6 +173,7 @@ class ModuleLoader(SourceLoader): module.ModuleEvent = event.ModuleEvent module.ModuleState = xmlparser.module_state.ModuleState module.Response = response.Response + module.IRCException = exception.IRCException # Load dependancies if module.CONF is not None and module.CONF.hasNode("dependson"): From b925cee08a23123ce258edf200cce27c8ba3a2af Mon Sep 17 00:00:00 2001 From: Nemunaire Date: Wed, 30 Apr 2014 22:41:47 +0200 Subject: [PATCH 005/674] New module: weather; close #7 --- modules/weather.py | 207 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 modules/weather.py diff --git a/modules/weather.py b/modules/weather.py new file mode 100644 index 0000000..24911bd --- /dev/null +++ b/modules/weather.py @@ -0,0 +1,207 @@ +# coding=utf-8 + +import datetime +import json +import re +from urllib.parse import quote +from urllib.request import urlopen + +nemubotversion = 3.3 + +def load(context): + global DATAS + DATAS.setIndex("name", "city") + + if not CONF.hasNode("darkskyapi") or not CONF.getNode("darkskyapi").hasAttribute("key"): + print ("You need a Dark-Sky API key in order to use this " + "module. Add it to the module configuration file:\n\nRegister at " + "http://developer.forecast.io/") + return None + + from hooks import Hook + add_hook("cmd_hook", Hook(cmd_weather, "météo")) + add_hook("cmd_hook", Hook(cmd_alert, "alert")) + + +def help_tiny (): + """Line inserted in the response to the command !help""" + return "The weather module" + +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"]) + )) + +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"]) + )) + + +def treat_coord(msg): + if len(msg.cmds) > 1: + + # catch dans X[jh]$ + if len(msg.cmds) > 3 and msg.cmds[len(msg.cmds) - 2] == "dans" or msg.cmds[len(msg.cmds) - 2] == "in" or msg.cmds[len(msg.cmds) - 2] == "next": + specific = msg.cmds[len(msg.cmds) - 1] + msg.cmds = msg.cmds[:len(msg.cmds) - 2] + else: + specific = None + + j = " ".join(msg.cmds[1:]).lower() + + if len(msg.cmds) == 3: + coords = msg.cmds[1:3] + else: + coords = msg.cmds[1].split(",") + + try: + if len(coords) == 2 and str(float(coords[0])) == coords[0] and str(float(coords[1])) == coords[1]: + return coords, specific + except ValueError: + pass + + if j in DATAS.index: + coords = list() + coords.append(DATAS.index[j]["lat"]) + coords.append(DATAS.index[j]["long"]) + return coords, specific + + raise IRCException("Je ne sais pas où se trouve %s." % " ".join(msg.cmds[1:])) + + else: + raise IRCException("indique-moi un nom de ville ou des coordonnées.") + + +def get_json_weather(coords): + raw = urlopen("https://api.forecast.io/forecast/%s/%s,%s" % (CONF.getNode("darkskyapi")["key"], float(coords[0]), float(coords[1])), timeout=10) + wth = json.loads(raw.read().decode()) + + # First read flags + if "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.") + + return wth + + +def cmd_alert(msg): + coords, specific = treat_coord(msg) + wth = get_json_weather(coords) + + res = Response(msg.sender, channel=msg.channel, nomore="No more weather alert", count=" (%d more alerts)") + + 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"], datetime.datetime.fromtimestamp(int(alert["expires"])).strftime("%c"), alert["description"].replace("\n", " "))) + + return res + +def cmd_weather(msg): + coords, specific = treat_coord(msg) + wth = get_json_weather(coords) + + res = Response(msg.sender, channel=msg.channel, nomore="No more weather information") + + 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"], datetime.datetime.fromtimestamp(int(alert["expires"])).strftime("%c"))) + 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: + gr = re.match(r"^([0-9]*)\s*([a-zA-Z])", specific) + if gr is None or gr.group(1) == "": + gr1 = 1 + else: + gr1 = int(gr.group(1)) + + if gr.group(2).lower() == "h" and gr1 < len(wth["hourly"]["data"]): + hour = wth["hourly"]["data"][gr1] + time = datetime.datetime.fromtimestamp(int(hour["time"])) + res.append_message("\x03\x02At %sh:\x03\x02 %s" % (time.strftime('%H'), format_wth(hour))) + + elif gr.group(2).lower() == "d" and gr1 < len(wth["daily"]["data"]): + day = wth["daily"]["data"][gr1] + time = datetime.datetime.fromtimestamp(int(day["time"])) + res.append_message("\x03\x02On %s:\x03\x02 %s" % (time.strftime('%A'), format_forecast_daily(day))) + + 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"])) + + nextres = "\x03\x02Today:\x03\x02 %s " % wth["daily"]["data"][0]["summary"] + if "minutely" in wth: + nextres += "\x03\x02Next hour:\x03\x02 %s " % wth["minutely"]["summary"] + nextres += "\x03\x02Next 24 hours:\x03\x02 %s \x03\x02Next week:\x03\x02 %s" % (wth["hourly"]["summary"], wth["daily"]["summary"]) + res.append_message(nextres) + + for hour in wth["hourly"]["data"][1:4]: + time = datetime.datetime.fromtimestamp(int(hour["time"])) + res.append_message("\x03\x02At %sh:\x03\x02 %s" % (time.strftime('%H'), format_wth(hour))) + + for day in wth["daily"]["data"][1:]: + time = datetime.datetime.fromtimestamp(int(day["time"])) + res.append_message("\x03\x02On %s:\x03\x02 %s" % (time.strftime('%A'), format_forecast_daily(day))) + + return res + + +gps_ask = re.compile(r"^\s*(?P.*\w)\s*(?:(?:se|est)\s+(?:trouve|situ[ée]*)\s+[aà])\s*(?P-?[0-9]+(?:[,.][0-9]+))[^0-9.](?P-?[0-9]+(?:[,.][0-9]+))\s*$", re.IGNORECASE) + +def parseask(msg): + res = gps_ask.match(msg.content) + if res is not None: + city_name = res.group("city").lower() + gps_lat = res.group("lat").replace(",", ".") + gps_long = res.group("long").replace(",", ".") + + if city_name in DATAS.index: + DATAS.index[city_name]["lat"] = gps_lat + DATAS.index[city_name]["long"] = gps_long + else: + ms = ModuleState("city") + ms.setAttribute("name", city_name) + ms.setAttribute("lat", gps_lat) + ms.setAttribute("long", gps_long) + DATAS.addChild(ms) + save() + return Response(msg.sender, + "ok, j'ai bien noté les coordonnées de %s" % res.group("city"), + msg.channel, msg.nick) From d56f873fd49f17f24516d336980bee583f8e359b Mon Sep 17 00:00:00 2001 From: Nemunaire Date: Thu, 1 May 2014 02:34:43 +0200 Subject: [PATCH 006/674] New feature: can now connect to SSL servers (using TLS v1) --- IRCServer.py | 9 +++++++-- bot.py | 4 ++-- nemuspeak.py | 4 +--- prompt/builtins.py | 3 ++- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/IRCServer.py b/IRCServer.py index f354330..602a597 100644 --- a/IRCServer.py +++ b/IRCServer.py @@ -18,6 +18,7 @@ import errno import os +import ssl import socket import threading import traceback @@ -32,7 +33,7 @@ import xmlparser class IRCServer(server.Server): """Class to interact with an IRC server""" - def __init__(self, node, nick, owner, realname): + def __init__(self, node, nick, owner, realname, ssl=False): """Initialize an IRC server Arguments: @@ -40,7 +41,7 @@ class IRCServer(server.Server): nick -- nick used by the bot on this server owner -- nick used by the bot owner on this server realname -- string used as realname on this server - + ssl -- require SSL? """ server.Server.__init__(self) @@ -49,6 +50,7 @@ class IRCServer(server.Server): self.nick = nick self.owner = owner self.realname = realname + self.ssl = ssl # Listen private messages? self.listen_nick = True @@ -164,6 +166,9 @@ class IRCServer(server.Server): def run(self): if not self.connected: self.s = socket.socket() #Create the socket + if self.ssl: + ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1) + self.s = ctx.wrap_socket(self.s) try: self.s.connect((self.host, self.port)) #Connect to server except socket.error as e: diff --git a/bot.py b/bot.py index 87bd1ea..8b3f70e 100644 --- a/bot.py +++ b/bot.py @@ -183,9 +183,9 @@ class Bot: self.update_timer() - def addServer(self, node, nick, owner, realname): + def addServer(self, node, nick, owner, realname, ssl=False): """Add a new server to the context""" - srv = IRCServer(node, nick, owner, realname) + srv = IRCServer(node, nick, owner, realname, ssl) srv.add_hook = lambda h: self.hooks.add_hook("irc_hook", h, self) srv.add_networkbot = self.add_networkbot srv.send_bot = lambda d: self.send_networkbot(srv, d) diff --git a/nemuspeak.py b/nemuspeak.py index 9501e17..5c804f6 100755 --- a/nemuspeak.py +++ b/nemuspeak.py @@ -2,12 +2,10 @@ # coding=utf-8 import sys -import socket import signal import os import re import subprocess -import shlex import traceback from datetime import datetime from datetime import timedelta @@ -164,7 +162,7 @@ for correct in config.getNodes("correction"): print ("%d corrections loaded"%len(CORRECTIONS)) for serveur in config.getNodes("server"): - srv = Server(serveur, config["nick"], config["owner"], config["realname"]) + srv = Server(serveur, config["nick"], config["owner"], config["realname"], serveur.hasAttribute("ssl")) srv.launch(None) def sighup_h(signum, frame): diff --git a/prompt/builtins.py b/prompt/builtins.py index 512549d..6e7aa5a 100644 --- a/prompt/builtins.py +++ b/prompt/builtins.py @@ -64,7 +64,8 @@ def load_file(filename, context): # Preset each server in this file for server in config.getNodes("server"): if context.addServer(server, config["nick"], - config["owner"], config["realname"]): + config["owner"], config["realname"], + server.hasAttribute("ssl")): print (" Server `%s:%s' successfully added." % (server["server"], server["port"])) else: From 471e40aed13a8fe927159a8524fbda1e691ea156 Mon Sep 17 00:00:00 2001 From: Nemunaire Date: Thu, 1 May 2014 23:36:21 +0200 Subject: [PATCH 007/674] YCC module: never fail, even if bad URL is given --- modules/ycc.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/modules/ycc.py b/modules/ycc.py index 7180ba2..3edf2f5 100644 --- a/modules/ycc.py +++ b/modules/ycc.py @@ -58,15 +58,18 @@ def cmd_ycc(msg): def parselisten(msg): global LAST_URLS - urls = re.findall("([a-zA-Z0-9+.-]+:(//)?[^ ]+)", msg.content) - for (url, osef) in urls: - o = urlparse(url) - if o.scheme != "": - if o.netloc == "ycc.fr" or (o.netloc == "" and len(o.path) < 10): - continue - if msg.channel not in LAST_URLS: - LAST_URLS[msg.channel] = list() - LAST_URLS[msg.channel].append(o.geturl()) + try: + urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?[^ ]+)", msg.content) + for url in urls: + o = urlparse(url) + if o.scheme != "": + if o.netloc == "ycc.fr" or (o.netloc == "" and len(o.path) < 10): + continue + if msg.channel not in LAST_URLS: + LAST_URLS[msg.channel] = list() + LAST_URLS[msg.channel].append(o.geturl()) + except: + pass return False def parseresponse(res): From 818d8b754c2df5107f30f7c77c560b28a3b32397 Mon Sep 17 00:00:00 2001 From: Nemunaire Date: Thu, 1 May 2014 23:44:26 +0200 Subject: [PATCH 008/674] New module: reddit that display information about a subreddit; close #37 --- modules/reddit.py | 77 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 modules/reddit.py diff --git a/modules/reddit.py b/modules/reddit.py new file mode 100644 index 0000000..ef0bec9 --- /dev/null +++ b/modules/reddit.py @@ -0,0 +1,77 @@ +# coding=utf-8 + +import json +import re +import urllib + +nemubotversion = 3.3 + +def load(context): + from hooks import Hook + add_hook("cmd_hook", Hook(cmd_subreddit, "subreddit")) + add_hook("all_post", Hook(parseresponse)) + + +def help_tiny (): + """Line inserted in the response to the command !help""" + return "The subreddit module" + +def help_full (): + return "!subreddit /subreddit/: Display information on the subreddit." + +LAST_SUBS = dict() + +def cmd_subreddit(msg): + global LAST_SUBS + if len(msg.cmds) <= 1: + 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? type !horny or !bored") + else: + subs = msg.cmds[1:] + + all_res = list() + for sub in subs: + sub = re.match(r"^/?(?:(\w)/)?(\w+)/?$", sub) + if sub is not None: + if sub.group(1) is not None and sub.group(1) != "": + where = sub.group(1) + else: + where = "r" + try: + req = urllib.request.Request("http://www.reddit.com/%s/%s/about.json" % (where, sub.group(2)), headers={ 'User-Agent' : "nemubot v3" }) + raw = urllib.request.urlopen(req, timeout=10) + except urllib.error.HTTPError as e: + raise IRCException("HTTP error occurs: %s %s" % (e.code, e.reason)) + sbr = json.loads(raw.read().decode()) + + if "title" in sbr["data"]: + res = Response(msg.sender, channel=msg.channel, nomore="No more information") + res.append_message(("[NSFW] " if sbr["data"]["over18"] else "") + sbr["data"]["title"] + ": " + sbr["data"]["public_description" if sbr["data"]["public_description"] != "" else "description"].replace("\n", " ") + " %s subscriber(s)" % sbr["data"]["subscribers"]) + if sbr["data"]["public_description"] != "": + res.append_message(sbr["data"]["description"].replace("\n", " ")) + all_res.append(res) + else: + all_res.append(Response(msg.sender, "/%s/%s doesn't exist" % (where, sub.group(2)), channel=msg.channel)) + + return all_res + + +def parselisten(msg): + global LAST_SUBS + + try: + urls = re.findall("www.reddit.com(/\w/\w+/?)", msg.content) + for url in urls: + if msg.channel not in LAST_SUBS: + LAST_SUBS[msg.channel] = list() + LAST_SUBS[msg.channel].append(url) + except: + pass + + return False + +def parseresponse(res): + parselisten(res) + return True From a0fca91d06f9b1160a0622b64467e20faab1741d Mon Sep 17 00:00:00 2001 From: Nemunaire Date: Fri, 2 May 2014 00:27:42 +0200 Subject: [PATCH 009/674] Reddit module: Show url in response --- modules/reddit.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/reddit.py b/modules/reddit.py index ef0bec9..4f9af6b 100644 --- a/modules/reddit.py +++ b/modules/reddit.py @@ -32,8 +32,8 @@ def cmd_subreddit(msg): subs = msg.cmds[1:] all_res = list() - for sub in subs: - sub = re.match(r"^/?(?:(\w)/)?(\w+)/?$", sub) + for osub in subs: + sub = re.match(r"^/?(?:(\w)/)?(\w+)/?$", osub) if sub is not None: if sub.group(1) is not None and sub.group(1) != "": where = sub.group(1) @@ -48,12 +48,14 @@ def cmd_subreddit(msg): if "title" in sbr["data"]: res = Response(msg.sender, channel=msg.channel, nomore="No more information") - res.append_message(("[NSFW] " if sbr["data"]["over18"] else "") + sbr["data"]["title"] + ": " + sbr["data"]["public_description" if sbr["data"]["public_description"] != "" else "description"].replace("\n", " ") + " %s subscriber(s)" % sbr["data"]["subscribers"]) + res.append_message(("[NSFW] " if sbr["data"]["over18"] else "") + sbr["data"]["url"] + " " + sbr["data"]["title"] + ": " + sbr["data"]["public_description" if sbr["data"]["public_description"] != "" else "description"].replace("\n", " ") + " %s subscriber(s)" % sbr["data"]["subscribers"]) if sbr["data"]["public_description"] != "": res.append_message(sbr["data"]["description"].replace("\n", " ")) all_res.append(res) else: all_res.append(Response(msg.sender, "/%s/%s doesn't exist" % (where, sub.group(2)), channel=msg.channel)) + else: + all_res.append(Response(msg.sender, "%s is not a valid subreddit" % osub, channel=msg.channel, nick=msg.nick)) return all_res From aa285b1393577ddd99b1e61c46aba6420942dac9 Mon Sep 17 00:00:00 2001 From: Nemunaire Date: Sat, 3 May 2014 01:06:37 +0200 Subject: [PATCH 010/674] Event module: remove vacs command --- modules/events/__init__.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/modules/events/__init__.py b/modules/events/__init__.py index c331157..90b6d3a 100644 --- a/modules/events/__init__.py +++ b/modules/events/__init__.py @@ -19,7 +19,7 @@ def help_tiny (): return "events manager" def help_full (): - return "This module store a lot of events: ny, we, vacs, " + (", ".join(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(DATAS.index.keys())) + "\n!eventslist: gets list of timer\n!start /something/: launch a timer" CONTEXT = None @@ -66,13 +66,6 @@ def cmd_we(msg): "Youhou, on est en week-end depuis %s."), channel=msg.channel) -def cmd_vacances(msg): - return Response(msg.sender, - msg.countdown_format(datetime(2013, 7, 30, 18, 0, 1), - "Il reste %s avant les vacances :)", - "Profitons, c'est les vacances depuis %s."), - channel=msg.channel) - def start_countdown(msg): if msg.cmds[1] not in DATAS.index: From f8999d1e7fde2c7cd24cc8d1f5da09a47a261c24 Mon Sep 17 00:00:00 2001 From: Nemunaire Date: Sat, 3 May 2014 01:25:32 +0200 Subject: [PATCH 011/674] Error during save are now skipped and display --- xmlparser/node.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/xmlparser/node.py b/xmlparser/node.py index 4aa5d2f..5776327 100644 --- a/xmlparser/node.py +++ b/xmlparser/node.py @@ -3,7 +3,9 @@ import xml.sax from datetime import datetime from datetime import date +import sys import time +import traceback class ModuleState: """Tiny tree representation of an XML file""" @@ -175,12 +177,18 @@ class ModuleState: attribs[att] = str(self.attributes[att]) attrs = xml.sax.xmlreader.AttributesImpl(attribs) - gen.startElement(self.name, attrs) + try: + gen.startElement(self.name, attrs) - for child in self.childs: - child.save_node(gen) + for child in self.childs: + child.save_node(gen) - gen.endElement(self.name) + gen.endElement(self.name) + except: + print ("\033[1;31mERROR:\033[0m occurred when saving the " + "following XML node: %s with %s" % (self.name, attrs)) + exc_type, exc_value, exc_traceback = sys.exc_info() + traceback.print_exception(exc_type, exc_value, exc_traceback) def save(self, filename): """Save the current node as root node in a XML file""" From da84a493edc9956bde996a8351ee31bb11c147a7 Mon Sep 17 00:00:00 2001 From: Nemunaire Date: Mon, 5 May 2014 09:55:47 +0200 Subject: [PATCH 012/674] Networking module: add user-agent on isup request; fixes #39 --- modules/networking.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/modules/networking.py b/modules/networking.py index d6431e0..68075b2 100644 --- a/modules/networking.py +++ b/modules/networking.py @@ -3,9 +3,7 @@ import http.client import json import socket -from urllib.parse import quote -from urllib.parse import urlparse -from urllib.request import urlopen +import urllib from tools import web @@ -56,11 +54,12 @@ def cmd_isup(msg): if 1 < len(msg.cmds) < 6: res = list() for url in msg.cmds[1:]: - o = urlparse(url, "http") + o = urllib.parse.urlparse(url, "http") if o.netloc == "": - o = urlparse("http://" + url) + o = urllib.parse.urlparse("http://" + url) if o.netloc != "": - raw = urlopen("http://isitup.org/" + o.netloc + ".json", timeout=10) + req = urllib.request.Request("http://isitup.org/%s.json" % (o.netloc), headers={ 'User-Agent' : "nemubot v3" }) + raw = urllib.request.urlopen(req, timeout=10) isup = json.loads(raw.read().decode()) if "status_code" in isup and isup["status_code"] == 1: res.append(Response(msg.sender, "%s est accessible (temps de reponse : %ss)" % (isup["domain"], isup["response_time"]), channel=msg.channel)) From 14e2c59064fa974df3ee74e43b4ba77faed62cc9 Mon Sep 17 00:00:00 2001 From: Nemunaire Date: Mon, 5 May 2014 10:40:07 +0200 Subject: [PATCH 013/674] Weather module: localize time with timezone; fixes #35 --- modules/weather.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/modules/weather.py b/modules/weather.py index 24911bd..2525cd2 100644 --- a/modules/weather.py +++ b/modules/weather.py @@ -72,6 +72,11 @@ def format_forecast_daily(wth): int(wth["ozone"]) )) +def format_timestamp(timestamp, tzname, tzoffset, format="%c"): + tz = datetime.timezone(datetime.timedelta(hours=tzoffset), tzname) + time = datetime.datetime.fromtimestamp(timestamp, tz=tz) + return time.strftime(format) + def treat_coord(msg): if len(msg.cmds) > 1: @@ -127,7 +132,7 @@ 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"], datetime.datetime.fromtimestamp(int(alert["expires"])).strftime("%c"), alert["description"].replace("\n", " "))) + 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", " "))) return res @@ -140,7 +145,7 @@ 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"], datetime.datetime.fromtimestamp(int(alert["expires"])).strftime("%c"))) + alert_msgs.append("\x03\x02%s\x03\x02 expire on %s" % (alert["title"], format_timestamp(int(alert["expires"]), wth["timezone"], wth["offset"]))) 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: @@ -152,13 +157,11 @@ def cmd_weather(msg): if gr.group(2).lower() == "h" and gr1 < len(wth["hourly"]["data"]): hour = wth["hourly"]["data"][gr1] - time = datetime.datetime.fromtimestamp(int(hour["time"])) - res.append_message("\x03\x02At %sh:\x03\x02 %s" % (time.strftime('%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))) elif gr.group(2).lower() == "d" and gr1 < len(wth["daily"]["data"]): day = wth["daily"]["data"][gr1] - time = datetime.datetime.fromtimestamp(int(day["time"])) - res.append_message("\x03\x02On %s:\x03\x02 %s" % (time.strftime('%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))) else: res.append_message("I don't understand %s or information is not available" % specific) @@ -173,12 +176,12 @@ def cmd_weather(msg): res.append_message(nextres) for hour in wth["hourly"]["data"][1:4]: - time = datetime.datetime.fromtimestamp(int(hour["time"])) - res.append_message("\x03\x02At %sh:\x03\x02 %s" % (time.strftime('%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))) for day in wth["daily"]["data"][1:]: - time = datetime.datetime.fromtimestamp(int(day["time"])) - res.append_message("\x03\x02On %s:\x03\x02 %s" % (time.strftime('%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))) return res From d549d91aca71cd3c41de8bf21faae9ff15c5ab1c Mon Sep 17 00:00:00 2001 From: Nemunaire Date: Mon, 5 May 2014 11:39:04 +0200 Subject: [PATCH 014/674] Networking module: add Whois command; related to issue #4 --- modules/networking.py | 86 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/modules/networking.py b/modules/networking.py index 68075b2..88489ad 100644 --- a/modules/networking.py +++ b/modules/networking.py @@ -1,5 +1,6 @@ # coding=utf-8 +import datetime import http.client import json import socket @@ -11,6 +12,15 @@ nemubotversion = 3.3 def load(context): from hooks import Hook + + if not CONF.hasNode("whoisxmlapi") or not CONF.getNode("whoisxmlapi").hasAttribute("username") or not CONF.getNode("whoisxmlapi").hasAttribute("password"): + print ("You need a WhoisXML API account in order to use the " + "!netwhois feature. Add it to the module configuration file:\n" + "\nRegister at " + "http://www.whoisxmlapi.com/newaccount.php") + else: + add_hook("cmd_hook", Hook(cmd_whois, "netwhois")) + add_hook("cmd_hook", Hook(cmd_traceurl, "traceurl")) add_hook("cmd_hook", Hook(cmd_isup, "isup")) add_hook("cmd_hook", Hook(cmd_curl, "curl")) @@ -48,7 +58,81 @@ def cmd_traceurl(msg): res.append(Response(msg.sender, trace, channel=msg.channel, title="TraceURL")) return res else: - return Response(msg.sender, "Indiquer une URL a tracer !", channel=msg.channel) + return Response(msg.sender, "Indiquer une URL à tracer !", channel=msg.channel) + + +def extractdate(str): + tries = [ + "%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", + "%Y-%m-%d %H:%M:%S%Z", + "%Y-%m-%d %H:%M:%S%z", + "%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 "name" in entity: + ret += entity["name"] + + if "country" in entity or "city" in entity or "telephone" in entity or "email" in entity: + ret += " (from " + if "street1" in entity: + ret += entity["street1"] + " " + if "city" in entity: + ret += entity["city"] + " " + if "state" in entity: + ret += entity["state"] + " " + if "country" in entity: + ret += entity["country"] + " " + if "telephone" in entity: + ret += entity["telephone"] + " " + if "email" in entity: + ret += entity["email"] + " " + ret = ret.rstrip() + ")" + + return ret.lstrip() + +def cmd_whois(msg): + if len(msg.cmds) < 2: + raise IRCException("Indiquer un domaine ou une IP à whois !") + + dom = msg.cmds[1] + + try: + req = urllib.request.Request("http://www.whoisxmlapi.com/whoisserver/WhoisService?rid=1&domainName=%s&outputFormat=json&userName=%s&password=%s" % (urllib.parse.quote(dom), urllib.parse.quote(CONF.getNode("whoisxmlapi")["username"]), urllib.parse.quote(CONF.getNode("whoisxmlapi")["password"])), headers={ 'User-Agent' : "nemubot v3" }) + raw = urllib.request.urlopen(req, timeout=10) + except urllib.error.HTTPError as e: + raise IRCException("HTTP error occurs: %s %s" % (e.code, e.reason)) + + whois = json.loads(raw.read().decode())["WhoisRecord"] + + res = Response(msg.sender, channel=msg.channel, nomore="No more whois information") + + 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"] + " " 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"]), + whois_entityformat(whois["administrativeContact"]), + whois_entityformat(whois["technicalContact"]), + )) + return res def cmd_isup(msg): if 1 < len(msg.cmds) < 6: From 34188c71a5428ad58d5d4b65d9e320fbfeb9b955 Mon Sep 17 00:00:00 2001 From: Nemunaire Date: Mon, 5 May 2014 12:12:19 +0200 Subject: [PATCH 015/674] Networking module: Add W3C validator; fixes #4 --- modules/networking.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/modules/networking.py b/modules/networking.py index 88489ad..6d64739 100644 --- a/modules/networking.py +++ b/modules/networking.py @@ -21,6 +21,7 @@ def load(context): else: add_hook("cmd_hook", Hook(cmd_whois, "netwhois")) + add_hook("cmd_hook", Hook(cmd_w3c, "w3c")) add_hook("cmd_hook", Hook(cmd_traceurl, "traceurl")) add_hook("cmd_hook", Hook(cmd_isup, "isup")) add_hook("cmd_hook", Hook(cmd_curl, "curl")) @@ -200,3 +201,37 @@ def traceURL(url, timeout=5, stack=None): return traceURL(url, timeout, stack) else: return stack + +def cmd_w3c(msg): + if len(msg.cmds) < 2: + raise IRCException("Indiquer une URL à valider !") + + o = urllib.parse.urlparse(msg.cmds[1], "http") + if o.netloc == "": + o = urllib.parse.urlparse("http://" + msg.cmds[1]) + if o.netloc == "": + raise IRCException("Indiquer une URL valide !") + + try: + req = urllib.request.Request("http://validator.w3.org/check?uri=%s&output=json" % (urllib.parse.quote(o.geturl())), headers={ 'User-Agent' : "nemubot v3" }) + raw = urllib.request.urlopen(req, timeout=10) + except urllib.error.HTTPError as e: + raise IRCException("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 "")) + + validator = json.loads(raw.read().decode()) + + res = Response(msg.sender, channel=msg.channel, nomore="No more error") + + res.append_message("%s: status: %s, %s warning(s), %s error(s)" % (validator["url"], headers["X-W3C-Validator-Status"], headers["X-W3C-Validator-Warnings"], headers["X-W3C-Validator-Errors"])) + + for m in validator["messages"]: + res.append_message("%s%s on line %s, col %s: %s" % (m["type"][0].upper(), m["type"][1:], m["lastLine"], m["lastColumn"], m["message"])) + + return res From 02d0d91df9990553a08b54f3d8d34fb9790725e2 Mon Sep 17 00:00:00 2001 From: Nemunaire Date: Mon, 5 May 2014 17:52:55 +0200 Subject: [PATCH 016/674] Networking module: avoid error on unexiststant registrant, administrativeContact or technicalContact --- modules/networking.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/networking.py b/modules/networking.py index 6d64739..f74aa51 100644 --- a/modules/networking.py +++ b/modules/networking.py @@ -129,9 +129,9 @@ def cmd_whois(msg): "\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"]), - whois_entityformat(whois["administrativeContact"]), - whois_entityformat(whois["technicalContact"]), + 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 From 0cceb8226d1bfa47cf89c98d2bb422c86d99e9f8 Mon Sep 17 00:00:00 2001 From: Nemunaire Date: Mon, 5 May 2014 18:25:33 +0200 Subject: [PATCH 017/674] Networking module: display error message when API returns error --- modules/networking.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/modules/networking.py b/modules/networking.py index f74aa51..3014d28 100644 --- a/modules/networking.py +++ b/modules/networking.py @@ -120,7 +120,13 @@ def cmd_whois(msg): except urllib.error.HTTPError as e: raise IRCException("HTTP error occurs: %s %s" % (e.code, e.reason)) - whois = json.loads(raw.read().decode())["WhoisRecord"] + js = json.loads(raw.read().decode()) + + if "ErrorMessage" in js: + err = js["ErrorMessage"] + raise IRCException(js["ErrorMessage"]["msg"]) + + whois = js["WhoisRecord"] res = Response(msg.sender, channel=msg.channel, nomore="No more whois information") From 8a3037d288a83e1120b483ea814868113d1c5658 Mon Sep 17 00:00:00 2001 From: Nemunaire Date: Mon, 5 May 2014 18:45:13 +0200 Subject: [PATCH 018/674] Weather module: add a command to display stored city coordinates; fixes #40 --- modules/weather.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/modules/weather.py b/modules/weather.py index 2525cd2..d902958 100644 --- a/modules/weather.py +++ b/modules/weather.py @@ -22,6 +22,7 @@ def load(context): from hooks import Hook add_hook("cmd_hook", Hook(cmd_weather, "météo")) add_hook("cmd_hook", Hook(cmd_alert, "alert")) + add_hook("cmd_hook", Hook(cmd_coordinates, "coordinates")) def help_tiny (): @@ -124,6 +125,17 @@ def get_json_weather(coords): return wth +def cmd_coordinates(msg): + if len(msg.cmds) <= 1: + raise IRCException("indique-moi un nom de ville.") + + j = msg.cmds[1].lower() + if j not in DATAS.index: + raise IRCException("%s n'est pas une ville connue" % msg.cmds[1]) + + coords = DATAS.index[j] + return Response(msg.sender, "Les coordonnées de %s sont %s,%s" % (msg.cmds[1], coords["lat"], coords["long"]), channel=msg.channel) + def cmd_alert(msg): coords, specific = treat_coord(msg) wth = get_json_weather(coords) From be1beec9805715029934e7da7bc54a99070c0021 Mon Sep 17 00:00:00 2001 From: Nemunaire Date: Mon, 5 May 2014 20:05:58 +0200 Subject: [PATCH 019/674] Networking module: fix regression on traceurl command --- modules/networking.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/networking.py b/modules/networking.py index 3014d28..6557a03 100644 --- a/modules/networking.py +++ b/modules/networking.py @@ -172,7 +172,7 @@ def traceURL(url, timeout=5, stack=None): stack.append('stack overflow :(') return stack - o = urlparse(url, "http") + o = urllib.parse.urlparse(url, "http") if o.netloc == "": return stack if o.scheme == "http": From f8dbb7d2e13cdf11753b8fc827861a58d87c1e88 Mon Sep 17 00:00:00 2001 From: Nemunaire Date: Tue, 27 May 2014 16:05:26 +0200 Subject: [PATCH 020/674] Send directly a PONG instead of doing normal treat --- nemuspeak.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nemuspeak.py b/nemuspeak.py index 5c804f6..e1d2a9e 100755 --- a/nemuspeak.py +++ b/nemuspeak.py @@ -117,7 +117,7 @@ class Server(IRCServer.IRCServer): try: msg = message.Message (line, datetime.now(), private) if msg.cmd == 'PING': - msg.treat (self.mods) + self.send_pong(msg.content) elif msg.cmd == 'PRIVMSG' and self.accepted_channel(msg.channel): if msg.nick != self.owner: g_queue.append((self, msg)) From f97bd593af145864b73bc60f3fd0b972c895c3fe Mon Sep 17 00:00:00 2001 From: Nemunaire Date: Tue, 27 May 2014 16:06:47 +0200 Subject: [PATCH 021/674] Fix bug when no module configuration is present --- modules/networking.py | 2 +- modules/weather.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/networking.py b/modules/networking.py index 6557a03..a6ae68d 100644 --- a/modules/networking.py +++ b/modules/networking.py @@ -13,7 +13,7 @@ nemubotversion = 3.3 def load(context): from hooks import Hook - if not CONF.hasNode("whoisxmlapi") or not CONF.getNode("whoisxmlapi").hasAttribute("username") or not CONF.getNode("whoisxmlapi").hasAttribute("password"): + if not CONF or not CONF.hasNode("whoisxmlapi") or not CONF.getNode("whoisxmlapi").hasAttribute("username") or not CONF.getNode("whoisxmlapi").hasAttribute("password"): print ("You need a WhoisXML API account in order to use the " "!netwhois feature. Add it to the module configuration file:\n" "\nRegister at " diff --git a/modules/weather.py b/modules/weather.py index d902958..ca0f236 100644 --- a/modules/weather.py +++ b/modules/weather.py @@ -12,7 +12,7 @@ def load(context): global DATAS DATAS.setIndex("name", "city") - if not CONF.hasNode("darkskyapi") or not CONF.getNode("darkskyapi").hasAttribute("key"): + if not CONF or not CONF.hasNode("darkskyapi") or not CONF.getNode("darkskyapi").hasAttribute("key"): print ("You need a Dark-Sky API key in order to use this " "module. Add it to the module configuration file:\n\nRegister at " From 6755b88229de8d11954be459c9a84490297c4010 Mon Sep 17 00:00:00 2001 From: Nemunaire Date: Tue, 27 May 2014 16:24:45 +0200 Subject: [PATCH 022/674] Fix spell module with UTF-8 chars; fixes #21 --- modules/spell/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/spell/__init__.py b/modules/spell/__init__.py index 918831b..ebaa7c8 100644 --- a/modules/spell/__init__.py +++ b/modules/spell/__init__.py @@ -26,7 +26,7 @@ def load(context): def cmd_spell(msg): if len(msg.cmds) < 2: - return Response(msg.sender, "Indiquer une orthographe approximative du mot dont vous voulez vérifier l'orthographe.", msg.channel) + return Response(msg.sender, "indique une orthographe approximative du mot dont tu veux vérifier l'orthographe.", msg.channel, msg.nick) lang = "fr" strRes = list() @@ -37,7 +37,7 @@ def cmd_spell(msg): try: r = check_spell(word, lang) except AspellError: - return Response(msg.sender, "Je n'ai pas le dictionnaire `%s' :(" % lang, msg.channel) + return Response(msg.sender, "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) @@ -47,7 +47,7 @@ def cmd_spell(msg): else: add_score(msg.nick, "bad") strRes.append("aucune suggestion pour `%s'" % word) - return Response(msg.sender, strRes, channel=msg.channel) + return Response(msg.sender, strRes, channel=msg.channel, nick=msg.nick) def add_score(nick, t): global DATAS @@ -80,10 +80,10 @@ def cmd_score(msg): return res def check_spell(word, lang='fr'): - a = Aspell([("lang", lang), ("lang", "fr")]) - if a.check(word.encode("iso-8859-15")): + a = Aspell([("lang", lang)]) + if a.check(word.encode("utf-8")): ret = True else: - ret = a.suggest(word.encode("iso-8859-15")) + ret = a.suggest(word.encode("utf-8")) a.close() return ret From f4edaa3c380ad53ce1b27db1ffd6e35ed91a9ae6 Mon Sep 17 00:00:00 2001 From: Nemunaire Date: Fri, 6 Jun 2014 16:16:50 +0200 Subject: [PATCH 023/674] XML node: can create an temporary index for instant use --- xmlparser/node.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/xmlparser/node.py b/xmlparser/node.py index 5776327..c0fc3a3 100644 --- a/xmlparser/node.py +++ b/xmlparser/node.py @@ -95,14 +95,18 @@ class ModuleState: return (isinstance(source, bool) and source) or source == "True" - def setIndex(self, fieldname = "name", tagname = None): - """Defines an hash table to accelerate childs search. You have just to define a common attribute""" - self.index = dict() - self.index_fieldname = fieldname - self.index_tagname = tagname + def tmpIndex(self, fieldname="name", tagname=None): + index = dict() for child in self.childs: if (tagname is None or tagname == child.name) and child.hasAttribute(fieldname): - self.index[child[fieldname]] = child + index[child[fieldname]] = child + return index + + def setIndex(self, fieldname="name", tagname=None): + """Defines an hash table to accelerate childs search. You have just to define a common attribute""" + self.index = self.tmpIndex(fieldname, tagname) + self.index_fieldname = fieldname + self.index_tagname = tagname def __contains__(self, i): """Return true if i is found in the index""" From 31fe146c6a51d1c7a9db61b16e292df7fda4e40d Mon Sep 17 00:00:00 2001 From: Nemunaire Date: Fri, 6 Jun 2014 16:18:31 +0200 Subject: [PATCH 024/674] Fix !eventslist action, closes #44 --- modules/events/__init__.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/modules/events/__init__.py b/modules/events/__init__.py index 90b6d3a..5cbe628 100644 --- a/modules/events/__init__.py +++ b/modules/events/__init__.py @@ -142,14 +142,17 @@ def start_countdown(msg): return Response(msg.sender, "%s commencé le %s"% (msg.cmds[1], datetime.now().strftime("%A %d %B %Y a %H:%M:%S"))) else: - return Response(msg.sender, "%s existe déjà."% (msg.cmds[1])) + return Response(msg.sender, "%s existe déjà."% (msg.cmds[1]), channel=msg.channel, nick=msg.nick) def end_countdown(msg): + if len(msg.cmds) < 2: + return Response(msg.sender, "Quel événement terminer ?", channel=msg.channel, nick=msg.nick) + if msg.cmds[1] in DATAS.index: res = Response(msg.sender, "%s a duré %s." % (msg.cmds[1], msg.just_countdown(datetime.now () - DATAS.index[msg.cmds[1]].getDate("start"))), - channel=msg.channel) + channel=msg.channel, nick=msg.nick) if DATAS.index[msg.cmds[1]]["proprio"] == msg.nick or (msg.cmds[0] == "forceend" and msg.is_owner): CONTEXT.del_event(DATAS.index[msg.cmds[1]]["id"]) DATAS.delChild(DATAS.index[msg.cmds[1]]) @@ -158,10 +161,20 @@ def end_countdown(msg): res.append_message("Vous ne pouvez pas terminer le compteur %s, créé par %s."% (msg.cmds[1], DATAS.index[msg.cmds[1]]["proprio"])) return res else: - return Response(msg.sender, "%s n'est pas un compteur connu."% (msg.cmds[1])) + return Response(msg.sender, "%s n'est pas un compteur connu."% (msg.cmds[1]), channel=msg.channel, nick=msg.nick) def liste(msg): - msg.send_snd ("Compteurs connus : %s." % ", ".join(DATAS.index.keys())) + if len(msg.cmds) > 1: + res = list() + for user in msg.cmds[1:]: + cmptr = [x["name"] for x in DATAS.index.values() if x["proprio"] == user] + if len(cmptr) > 0: + res.append("Compteurs créés par %s : %s" % (user, ", ".join(cmptr))) + else: + res.append("%s n'a pas créé de compteur" % user) + return Response(msg.sender, " ; ".join(res), channel=msg.channel) + else: + return Response(msg.sender, "Compteurs connus : %s." % ", ".join(DATAS.index.keys()), channel=msg.channel) def parseanswer(msg): if msg.cmds[0] in DATAS.index: From 7575c416452d678deee0b8cb4908517f43ada09b Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sat, 14 Jun 2014 00:12:45 +0200 Subject: [PATCH 025/674] New module SMS using new Free Mobile API --- modules/sms.py | 108 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 modules/sms.py diff --git a/modules/sms.py b/modules/sms.py new file mode 100644 index 0000000..fae234a --- /dev/null +++ b/modules/sms.py @@ -0,0 +1,108 @@ +# coding=utf-8 + +import re +import socket +import time +import urllib.error +import urllib.request +import urllib.parse + +nemubotversion = 3.3 + +def load(context): + global DATAS + DATAS.setIndex("name", "phone") + + from hooks import Hook + add_hook("cmd_hook", Hook(cmd_sms, "sms")) + +def help_tiny (): + """Line inserted in the response to the command !help""" + return "Send SMS using SMS API (currently only Free Mobile)" + +def help_full (): + return "!sms /who/[,/who/[,...]] message: send a SMS to /who/." + +def send_sms(frm, api_usr, api_key, content): + content = "<%s> %s" % (frm, content) + + try: + req = urllib.request.Request("https://smsapi.free-mobile.fr/sendmsg?user=%s&pass=%s&msg=%s" % (api_usr, api_key, urllib.parse.quote(content))) + res = urllib.request.urlopen(req, timeout=5) + except socket.timeout: + return "timeout" + except urllib.error.HTTPError as e: + if e.code == 400: + return "paramètre manquant" + elif e.code == 402: + return "paiement requis" + elif e.code == 403 or e.code == 404: + return "clef incorrecte" + elif e.code != 200: + return "erreur inconnue (%d)" % status + except: + return "unknown error" + + return None + + +def cmd_sms(msg): + if len(msg.cmds) <= 2: + raise IRCException("À qui veux-tu envoyer ce SMS ?") + + # Check dests + cur_epoch = time.mktime(time.localtime()); + for u in msg.cmds[1].split(","): + if u not in DATAS.index: + raise IRCException("Désolé, je sais pas comment envoyer de SMS à %s." % u) + elif cur_epoch - DATAS.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) + + # Go! + fails = list() + for u in msg.cmds[1].split(","): + DATAS.index[u]["lastuse"] = cur_epoch + if msg.private: + frm = msg.nick + else: + frm = msg.nick + "@" + msg.channel + test = send_sms(frm, DATAS.index[u]["user"], DATAS.index[u]["key"], " ".join(msg.cmds[2:])) + if test is not None: + fails.append( "%s: %s" % (u, test) ) + + if len(fails) > 0: + return Response(msg.sender, "quelque chose ne s'est pas bien passé durant l'envoi du SMS : " + ", ".join(fails), msg.channel, msg.nick) + else: + return Response(msg.sender, "le SMS a bien été envoyé", msg.channel, msg.nick) + +apiuser_ask = re.compile(r"(utilisateur|user|numéro|numero|compte|abonne|abone|abonné|account)\s+(est|is)\s+(?P[0-9]{7,})", re.IGNORECASE) +apikey_ask = re.compile(r"(clef|key|password|mot de passe?)\s+(?:est|is)?\s+(?P[a-zA-Z0-9]{10,})", re.IGNORECASE) + +def parseask(msg): + if msg.content.find("Free") >= 0 and ( + msg.content.find("API") >= 0 or msg.content.find("api") >= 0) and ( + msg.content.find("SMS") >= 0 or msg.content.find("sms") >= 0): + resuser = apiuser_ask.search(msg.content) + reskey = apikey_ask.search(msg.content) + if resuser is not None and reskey is not None: + apiuser = resuser.group("user") + apikey = reskey.group("key") + + test = send_sms("nemubot", apiuser, apikey, + "Vous avez enregistré vos codes d'authentification dans nemubot, félicitation !") + if test is not None: + return Response(msg.sender, "je n'ai pas pu enregistrer tes identifiants : %s" % test, msg.channel, msg.nick) + + if msg.nick in DATAS.index: + DATAS.index[msg.nick]["user"] = apiuser + DATAS.index[msg.nick]["key"] = apikey + else: + ms = ModuleState("phone") + ms.setAttribute("name", msg.nick) + ms.setAttribute("user", apiuser) + ms.setAttribute("key", apikey) + ms.setAttribute("lastuse", 0) + DATAS.addChild(ms) + save() + return Response(msg.sender, "ok, c'est noté. Je t'ai envoyé un SMS pour tester ;)", + msg.channel, msg.nick) From 05a148fef40acd4a744abef4d9ae8cca00841e53 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Tue, 24 Jun 2014 18:07:28 +0200 Subject: [PATCH 026/674] Web tool now handles HTTPS connections, content is decoded following given charset header --- modules/networking.py | 6 +++++- tools/web.py | 24 +++++++++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/modules/networking.py b/modules/networking.py index a6ae68d..0297076 100644 --- a/modules/networking.py +++ b/modules/networking.py @@ -40,11 +40,13 @@ def cmd_curl(msg): req = web.getURLContent(" ".join(msg.cmds[1:])) if req is not None: res = Response(msg.sender, channel=msg.channel) - for m in req.decode().split("\n"): + for m in req.split("\n"): res.append_message(m) return res else: return Response(msg.sender, "Une erreur est survenue lors de l'accès à cette URL", channel=msg.channel) + except socket.timeout: + return Response(msg.sender, "le délais d'attente a été dépassé durant l'accès à %s" % msg.cmds[1:], channel=msg.channel, nick=msg.nick) except socket.error as e: return Response(msg.sender, e.strerror, channel=msg.channel) else: @@ -117,6 +119,8 @@ def cmd_whois(msg): try: req = urllib.request.Request("http://www.whoisxmlapi.com/whoisserver/WhoisService?rid=1&domainName=%s&outputFormat=json&userName=%s&password=%s" % (urllib.parse.quote(dom), urllib.parse.quote(CONF.getNode("whoisxmlapi")["username"]), urllib.parse.quote(CONF.getNode("whoisxmlapi")["password"])), headers={ 'User-Agent' : "nemubot v3" }) raw = urllib.request.urlopen(req, timeout=10) + except socket.timeout: + raise IRCException("Sorry, the request has timed out.") except urllib.error.HTTPError as e: raise IRCException("HTTP error occurs: %s %s" % (e.code, e.reason)) diff --git a/tools/web.py b/tools/web.py index b0bf2e3..a8a5c3d 100644 --- a/tools/web.py +++ b/tools/web.py @@ -61,7 +61,17 @@ def getPassword(url): def getURLContent(url, timeout=15): """Return page content corresponding to URL or None if any error occurs""" o = urlparse(url) - conn = http.client.HTTPConnection(o.netloc, port=o.port, timeout=timeout) + if o.netloc == "": + o = urlparse("http://" + url) + + if o.scheme == "http": + conn = http.client.HTTPConnection(o.netloc, port=o.port, timeout=timeout) + elif o.scheme == "https": + conn = http.client.HTTPSConnection(o.netloc, port=o.port, timeout=timeout) + elif o.scheme is None or o.scheme == "": + conn = http.client.HTTPConnection(o.netloc, port=80, timeout=timeout) + else: + return None try: if o.query != '': conn.request("GET", o.path + "?" + o.query, None, {"User-agent": "Nemubot v3"}) @@ -70,8 +80,8 @@ def getURLContent(url, timeout=15): except socket.timeout: return None except socket.gaierror: - print (" Unable to receive page %s from %s on %d." - % (o.path, o.netloc, o.port)) + print (" Unable to receive page %s on %s from %s." + % (o.path, o.netloc, url)) return None try: @@ -83,6 +93,14 @@ def getURLContent(url, timeout=15): return None data = res.read(size) + + # Decode content + charset = res.getheader("Content-Type").split(";") + if len(charset) > 1: + for c in charset: + ch = c.split("=") + if ch[0].strip().lower() == "charset" and len(ch) > 1: + data = data.decode(ch[1]) except http.client.BadStatusLine: return None finally: From c5ee6084c0b95731ab2fc4e9f135b9018c1fcef2 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Thu, 26 Jun 2014 23:32:25 +0200 Subject: [PATCH 027/674] Webtool: if charset contain also language, ignore language --- tools/web.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tools/web.py b/tools/web.py index a8a5c3d..4266158 100644 --- a/tools/web.py +++ b/tools/web.py @@ -100,7 +100,12 @@ def getURLContent(url, timeout=15): for c in charset: ch = c.split("=") if ch[0].strip().lower() == "charset" and len(ch) > 1: - data = data.decode(ch[1]) + cha = ch[1].split(".") + if len(cha) > 1: + charset = cha[1] + else: + charset = cha[0] + data = data.decode(charset) except http.client.BadStatusLine: return None finally: From 3e014d4ebe1d90e21d267909c26cabe952ed135b Mon Sep 17 00:00:00 2001 From: nemunaire Date: Mon, 30 Jun 2014 10:59:25 +0200 Subject: [PATCH 028/674] Alias: add commands to list aliases and variables; fixes #49 --- modules/{alias/__init__.py => alias.py} | 33 +++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) rename modules/{alias/__init__.py => alias.py} (79%) diff --git a/modules/alias/__init__.py b/modules/alias.py similarity index 79% rename from modules/alias/__init__.py rename to modules/alias.py index 6904f19..7aebef5 100644 --- a/modules/alias/__init__.py +++ b/modules/alias.py @@ -9,6 +9,8 @@ nemubotversion = 3.3 def load(context): """Load this module""" from hooks import Hook + add_hook("cmd_hook", Hook(cmd_listalias, "listalias")) + add_hook("cmd_hook", Hook(cmd_listvars, "listvars")) add_hook("cmd_hook", Hook(cmd_unalias, "unalias")) add_hook("cmd_hook", Hook(cmd_alias, "alias")) add_hook("cmd_hook", Hook(cmd_set, "set")) @@ -31,10 +33,11 @@ def help_tiny (): def help_full (): return "TODO" -def set_variable(name, value): +def set_variable(name, value, creator): var = ModuleState("variable") var["name"] = name var["value"] = value + var["creator"] = creator DATAS.getNode("variables").addChild(var) def get_variable(name, msg=None): @@ -55,12 +58,38 @@ def get_variable(name, msg=None): def cmd_set(msg): if len (msg.cmds) > 2: - set_variable(msg.cmds[1], " ".join(msg.cmds[2:])) + set_variable(msg.cmds[1], " ".join(msg.cmds[2:]), msg.nick) res = Response(msg.sender, "Variable \$%s définie." % msg.cmds[1]) save() return res return Response(msg.sender, "!set prend au minimum deux arguments : le nom de la variable et sa valeur.") +def cmd_listalias(msg): + if len(msg.cmds) > 1: + res = list() + for user in msg.cmds[1:]: + als = [x["alias"] for x in DATAS.getNode("aliases").index.values() if x["creator"] == user] + if len(als) > 0: + res.append("Alias créés par %s : %s" % (user, ", ".join(als))) + else: + res.append("%s n'a pas encore créé d'alias" % user) + return Response(msg.sender, " ; ".join(res), channel=msg.channel) + else: + return Response(msg.sender, "Alias connus : %s." % ", ".join(DATAS.getNode("aliases").index.keys()), channel=msg.channel) + +def cmd_listvars(msg): + if len(msg.cmds) > 1: + res = list() + for user in msg.cmds[1:]: + als = [x["alias"] for x in DATAS.getNode("variables").index.values() if x["creator"] == user] + if len(als) > 0: + res.append("Variables créées par %s : %s" % (user, ", ".join(als))) + else: + res.append("%s n'a pas encore créé de variable" % user) + return Response(msg.sender, " ; ".join(res), channel=msg.channel) + else: + return Response(msg.sender, "Variables connues : %s." % ", ".join(DATAS.getNode("variables").index.keys()), channel=msg.channel) + def cmd_alias(msg): if len (msg.cmds) > 1: res = list() From bead2e31b2e42f2b9090c05e357a37dc4bbe140d Mon Sep 17 00:00:00 2001 From: nemunaire Date: Mon, 30 Jun 2014 10:59:56 +0200 Subject: [PATCH 029/674] Fix stacktrace on ACTION CTCP --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 8b3f70e..6cb004c 100644 --- a/bot.py +++ b/bot.py @@ -76,7 +76,7 @@ class Bot: def init_ctcp_capabilities(self): """Reset existing CTCP capabilities to default one""" - self.ctcp_capabilities["ACTION"] = lambda msg: print ("ACTION receive") + self.ctcp_capabilities["ACTION"] = lambda srv, msg: print ("ACTION receive") self.ctcp_capabilities["CLIENTINFO"] = self._ctcp_clientinfo self.ctcp_capabilities["DCC"] = self._ctcp_dcc self.ctcp_capabilities["NEMUBOT"] = lambda srv, msg: _ctcp_response( From 971e3c882db38cbb0fd7ecdcf64625c15ddb0415 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Fri, 4 Jul 2014 02:13:26 +0200 Subject: [PATCH 030/674] Fix wiktionary display --- modules/ddg/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/ddg/__init__.py b/modules/ddg/__init__.py index 46aac3e..83c5895 100644 --- a/modules/ddg/__init__.py +++ b/modules/ddg/__init__.py @@ -128,7 +128,6 @@ def wiki(msg): if site == "wiktionary.org": tout = [result for result in s.nextRes if result.find("\x03\x16 :\x03\x16 ") != 0] if len(tout) > 0: - tout.remove(tout[0]) defI=1 for t in tout: if t.find("# ") == 0: From 63f24c7b5934240a2b05a54e313d8503326756d6 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Fri, 4 Jul 2014 18:23:56 +0200 Subject: [PATCH 031/674] New module worldcup --- modules/worldcup.py | 219 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 modules/worldcup.py diff --git a/modules/worldcup.py b/modules/worldcup.py new file mode 100644 index 0000000..609ac47 --- /dev/null +++ b/modules/worldcup.py @@ -0,0 +1,219 @@ +# coding=utf-8 + +import datetime +import json +import re +from urllib.parse import quote +from urllib.request import urlopen + +nemubotversion = 3.3 + +API_URL="http://worldcup.sfg.io/%s" + +def load(context): + from hooks import Hook + add_hook("cmd_hook", Hook(cmd_watch, "watch_worldcup")) + add_hook("cmd_hook", Hook(cmd_worldcup, "worldcup")) + + from event import ModuleEvent + add_event(ModuleEvent(func=lambda url: urlopen(url).read().decode(), func_data=API_URL % "matches/current?by_date=DESC", call=current_match_new_action, intervalle=30)) + + +def help_tiny (): + """Line inserted in the response to the command !help""" + return "The 2014 football worldcup module" + +def help_full (): + return "!worldcup: do something." + + +def start_watch(msg): + global DATAS + w = ModuleState("watch") + w["server"] = msg.server + w["channel"] = msg.channel + w["proprio"] = msg.nick + w["sender"] = msg.sender + w["start"] = datetime.datetime.now() + DATAS.addChild(w) + save() + raise IRCException("This channel is now watching world cup events!") + +def cmd_watch(msg): + global DATAS + + # Get current state + node = None + for n in DATAS.getChilds(): + if n["server"] == msg.server and n["channel"] == msg.channel: + node = n + break + + if len(msg.cmds) >= 2: + if msg.cmds[1] == "stop" and node is not None: + DATAS.delChild(node) + save() + raise IRCException("This channel will not anymore receives world cup events.") + elif msg.cmds[1] == "start" and node is None: + start_watch(msg) + else: + raise IRCException("Use only start or stop as first argument") + else: + if node is None: + start_watch(msg) + else: + DATAS.delChild(node) + save() + raise IRCException("This channel will not anymore receives world cup events.") + +def current_match_new_action(match_str, osef): + global DATAS + + add_event(ModuleEvent(func=lambda url: urlopen(url).read().decode(), func_data=API_URL % "matches/current?by_date=DESC", call=current_match_new_action, intervalle=30)) + + matches = json.loads(match_str) + + for match in matches: + if is_valid(match): + events = sort_events(match["home_team"], match["away_team"], match["home_team_events"], match["away_team_events"]) + msg = "Match %s vs. %s ; score %s - %s" % (match["home_team"]["country"], match["away_team"]["country"], match["home_team"]["goals"], match["away_team"]["goals"]) + + if len(events) > 0: + msg += " ; à la " + txt_event(events[0]) + + for n in DATAS.getChilds(): + send_response(n["server"], Response(n["sender"], msg, channel=n["channel"])) + +def is_int(s): + try: + int(s) + return True + except ValueError: + return False + +def sort_events(teamA, teamB, eventA, eventB): + res = [] + + for e in eventA: + e["team"] = teamA + res.append(e) + for e in eventB: + e["team"] = teamB + res.append(e) + + return sorted(res, key=lambda evt: int(evt["time"][0:2]), reverse=True) + +def detail_event(evt): + if evt == "yellow-card": + return "carton jaune pour" + elif evt == "yellow-card-second": + return "second carton jaune pour" + elif evt == "red-card": + return "carton rouge pour" + elif evt == "substitution-in" or evt == "substitution-in halftime": + return "joueur entrant :" + elif evt == "substitution-out" or evt == "substitution-out halftime": + return "joueur sortant :" + elif evt == "goal": + return "but de" + elif evt == "goal-own": + return "but contre son camp de" + elif evt == "goal-penalty": + return "but (pénalty) de" + 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"]) + +def prettify(match): + matchdate_local = datetime.datetime.strptime(match["datetime"].replace(':', ''), "%Y-%m-%dT%H%M%S.%f%z") + matchdate = matchdate_local - (matchdate_local.utcoffset() - datetime.timedelta(hours=2)) + 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"])] + else: + msgs = list() + msg = "" + 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.datetime.now(matchdate.tzinfo) - matchdate_local).seconds / 60) + + msg += "%s %d - %d %s" % (match["home_team"]["country"], match["home_team"]["goals"], match["away_team"]["goals"], match["away_team"]["country"]) + + events = sort_events(match["home_team"], match["away_team"], match["home_team_events"], match["away_team_events"]) + + if len(events) > 0: + msg += " ; dernière action, à la " + txt_event(events[0]) + msgs.append(msg) + + for e in events[1:]: + msgs.append("À la " + txt_event(e)) + else: + msgs.append(msg) + + return msgs + + +def is_valid(match): + return isinstance(match, dict) and ( + isinstance(match.get('home_team'), dict) and + 'goals' in match.get('home_team') + ) and ( + isinstance(match.get('away_team'), dict) and + 'goals' in match.get('away_team') + ) or isinstance(match.get('group_id'), int) + +def get_match(url, matchid): + allm = get_matches(url) + for m in allm: + if int(m["match_number"]) == matchid: + return [ m ] + +def get_matches(url): + try: + raw = urlopen(url) + except: + raise IRCException("requête invalide") + matches = json.loads(raw.read().decode()) + + for match in matches: + if is_valid(match): + yield match + +def cmd_worldcup(msg): + res = Response(msg.sender, channel=msg.channel, nomore="No more match to display", count=" (%d more matches)") + nb = len(msg.cmds) + + url = None + if nb == 2: + if msg.cmds[1] == "today" or msg.cmds[1] == "aujourd'hui": + url = "matches/today?by_date=ASC" + elif msg.cmds[1] == "tomorrow" or msg.cmds[1] == "demain": + url = "matches/tomorrow?by_date=ASC" + elif msg.cmds[1] == "all" or msg.cmds[1] == "tout" or msg.cmds[1] == "tous": + url = "matches/" + elif len(msg.cmds[1]) == 3: + url = "matches/country?fifa_code=%s&by_date=DESC" % msg.cmds[1] + elif is_int(msg.cmds[1]): + url = int(msg.cmds[1]) + else: + raise IRCException("unrecognized request; choose between 'today', 'tomorrow', a FIFA country code or a match identifier") + + if url is None: + url = "matches/current?by_date=ASC" + res.nomore = "There is no match currently" + + if isinstance(url, int): + matches = get_match(API_URL % "matches/", url) + else: + matches = [m for m in get_matches(API_URL % url)] + + for match in matches: + if len(matches) == 1: + res.count = " (%d more actions)" + for m in prettify(match): + res.append_message(m) + else: + res.append_message(prettify(match)[0]) + + return res From fa77a3b323ada286f909b0da7a7921490380528d Mon Sep 17 00:00:00 2001 From: nemunaire Date: Tue, 8 Jul 2014 02:44:20 +0200 Subject: [PATCH 032/674] Fix decoding of some pages --- modules/syno.py | 2 +- tools/web.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/syno.py b/modules/syno.py index 047fe03..2d0a98a 100644 --- a/modules/syno.py +++ b/modules/syno.py @@ -52,7 +52,7 @@ def get_synos(word): page = web.getURLContent(url) if page is not None: synos = list() - for line in page.decode().split("\n"): + for line in page.split("\n"): if re.match("[ \t]*]*>.*[ \t]*.*", line) is not None: for elt in re.finditer(">&[^;]+;([^&]*)&[^;]+;<", line): synos.append(elt.group(1)) diff --git a/tools/web.py b/tools/web.py index 4266158..88a52ff 100644 --- a/tools/web.py +++ b/tools/web.py @@ -95,8 +95,9 @@ def getURLContent(url, timeout=15): data = res.read(size) # Decode content - charset = res.getheader("Content-Type").split(";") - if len(charset) > 1: + charset = "utf-8" + lcharset = res.getheader("Content-Type").split(";") + if len(lcharset) > 1: for c in charset: ch = c.split("=") if ch[0].strip().lower() == "charset" and len(ch) > 1: @@ -105,14 +106,13 @@ def getURLContent(url, timeout=15): charset = cha[1] else: charset = cha[0] - data = data.decode(charset) except http.client.BadStatusLine: return None finally: conn.close() if res.status == http.client.OK or res.status == http.client.SEE_OTHER: - return data + return data.decode(charset) elif res.status == http.client.FOUND or res.status == http.client.MOVED_PERMANENTLY: return getURLContent(res.getheader("Location"), timeout) else: From 95ceeeaec9e9d7f2b39424961cd9ed1d93f03dd4 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Tue, 8 Jul 2014 03:03:33 +0200 Subject: [PATCH 033/674] networking module: new feature: w3m to dump page instead of displaying raw HTML --- modules/networking.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/modules/networking.py b/modules/networking.py index 0297076..ea43b7a 100644 --- a/modules/networking.py +++ b/modules/networking.py @@ -4,6 +4,7 @@ import datetime import http.client import json import socket +import subprocess import urllib from tools import web @@ -22,6 +23,7 @@ def load(context): add_hook("cmd_hook", Hook(cmd_whois, "netwhois")) add_hook("cmd_hook", Hook(cmd_w3c, "w3c")) + add_hook("cmd_hook", Hook(cmd_w3m, "w3m")) add_hook("cmd_hook", Hook(cmd_traceurl, "traceurl")) add_hook("cmd_hook", Hook(cmd_isup, "isup")) add_hook("cmd_hook", Hook(cmd_curl, "curl")) @@ -34,6 +36,18 @@ def help_tiny (): def help_full (): return "!traceurl /url/: Follow redirections from /url/." +def cmd_w3m(msg): + if len(msg.cmds) > 1: + args = ["w3m", "-T", "text/html", "-dump"] + args.append(msg.cmds[1]) + res = Response(msg.sender, channel=msg.channel) + with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc: + for line in proc.stdout.read().split(b"\n"): + res.append_message(line.decode()) + return res + else: + raise IRCException("Veuillez indiquer une URL à visiter.") + def cmd_curl(msg): if len(msg.cmds) > 1: try: From 4e1fc0bca15189efb96a011082d2ffd054e07763 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Tue, 8 Jul 2014 03:28:01 +0200 Subject: [PATCH 034/674] networking module: new feature curly; closes #41 --- modules/networking.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/modules/networking.py b/modules/networking.py index ea43b7a..4015ed2 100644 --- a/modules/networking.py +++ b/modules/networking.py @@ -27,6 +27,7 @@ def load(context): add_hook("cmd_hook", Hook(cmd_traceurl, "traceurl")) add_hook("cmd_hook", Hook(cmd_isup, "isup")) add_hook("cmd_hook", Hook(cmd_curl, "curl")) + add_hook("cmd_hook", Hook(cmd_curly, "curly")) def help_tiny (): @@ -67,6 +68,36 @@ def cmd_curl(msg): return Response(msg.sender, "Veuillez indiquer une URL à visiter.", channel=msg.channel) +def cmd_curly(msg): + if len(msg.cmds) > 1: + url = msg.cmds[1] + o = urllib.parse.urlparse(url, "http") + if o.netloc == "": + raise IRCException("URL invalide") + if o.scheme == "http": + conn = http.client.HTTPConnection(o.netloc, port=o.port, timeout=5) + else: + conn = http.client.HTTPSConnection(o.netloc, port=o.port, timeout=5) + try: + conn.request("HEAD", o.path, None, {"User-agent": "Nemubot v3"}) + except socket.timeout: + raise IRCException("Délais d'attente dépassé") + except socket.gaierror: + print (" Unable to receive page %s from %s on %d." + % (o.path, o.netloc, o.port)) + raise IRCException("Une erreur innatendue est survenue") + + try: + res = conn.getresponse() + except http.client.BadStatusLine: + raise IRCException("Une erreur est survenue") + finally: + conn.close() + + return Response(msg.sender, "Entêtes de la page %s : HTTP/%s, statut : %d %s ; headers : %s" % (url, res.version, res.status, res.reason, ", ".join(["\x03\x02" + h + "\x03\x02: " + v for h, v in res.getheaders()])), channel=msg.channel) + else: + raise IRCException("Veuillez indiquer une URL à visiter.") + def cmd_traceurl(msg): if 1 < len(msg.cmds) < 6: res = list() From 75def22e57b9face07cd0be1060923d6aca88129 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Wed, 9 Jul 2014 15:19:32 +0200 Subject: [PATCH 035/674] W3C module: handle document wide messages --- modules/networking.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/networking.py b/modules/networking.py index 4015ed2..98f1d16 100644 --- a/modules/networking.py +++ b/modules/networking.py @@ -287,6 +287,9 @@ def cmd_w3c(msg): res.append_message("%s: status: %s, %s warning(s), %s error(s)" % (validator["url"], headers["X-W3C-Validator-Status"], headers["X-W3C-Validator-Warnings"], headers["X-W3C-Validator-Errors"])) for m in validator["messages"]: - res.append_message("%s%s on line %s, col %s: %s" % (m["type"][0].upper(), m["type"][1:], m["lastLine"], m["lastColumn"], m["message"])) + if "lastLine" not in m: + res.append_message("%s%s: %s" % (m["type"][0].upper(), m["type"][1:], m["message"])) + else: + res.append_message("%s%s on line %s, col %s: %s" % (m["type"][0].upper(), m["type"][1:], m["lastLine"], m["lastColumn"], m["message"])) return res From de85344b84e7533e2c4bc5cef828c497c6ee07ba Mon Sep 17 00:00:00 2001 From: nemunaire Date: Wed, 9 Jul 2014 15:27:50 +0200 Subject: [PATCH 036/674] Refresh sample configuration files --- bot_sample.xml | 34 ++++++++++++++++++++++++---------- speak_sample.xml | 10 ++++++++-- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/bot_sample.xml b/bot_sample.xml index 8b19830..42749ab 100644 --- a/bot_sample.xml +++ b/bot_sample.xml @@ -1,13 +1,27 @@ - - + + + - - - - - - - - + + + + + + + + + + + + + + + + + diff --git a/speak_sample.xml b/speak_sample.xml index c1c6f61..b7c3ff5 100644 --- a/speak_sample.xml +++ b/speak_sample.xml @@ -1,12 +1,17 @@ - + + - + + + + + @@ -24,4 +29,5 @@ + From 85edb9489d04db713c866b7bb9da7dd35dcfafe7 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Thu, 10 Jul 2014 23:35:41 +0200 Subject: [PATCH 037/674] Choice module: append nick to avoid response stating with a command --- modules/rnd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/rnd.py b/modules/rnd.py index 198983c..ff27634 100644 --- a/modules/rnd.py +++ b/modules/rnd.py @@ -9,4 +9,4 @@ def load(context): add_hook("cmd_hook", Hook(cmd_choice, "choice")) def cmd_choice(msg): - return Response(msg.sender, random.choice(msg.cmds[1:]), channel=msg.channel) + return Response(msg.sender, random.choice(msg.cmds[1:]), channel=msg.channel, nick=msg.nick) From 509e85f55a994f2d63ff6b884c2dc3e4835c799a Mon Sep 17 00:00:00 2001 From: nemunaire Date: Thu, 10 Jul 2014 23:38:00 +0200 Subject: [PATCH 038/674] SMS module: after reload, datas are not correctly typed --- modules/sms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/sms.py b/modules/sms.py index fae234a..ab8c766 100644 --- a/modules/sms.py +++ b/modules/sms.py @@ -55,7 +55,7 @@ def cmd_sms(msg): for u in msg.cmds[1].split(","): if u not in DATAS.index: raise IRCException("Désolé, je sais pas comment envoyer de SMS à %s." % u) - elif cur_epoch - DATAS.index[u]["lastuse"] < 42: + elif cur_epoch - float(DATAS.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) # Go! From f6d4600df3cdf2abf198dfd85abb469dc89cca12 Mon Sep 17 00:00:00 2001 From: Bertrand Cournaud Date: Fri, 11 Jul 2014 15:38:30 +0200 Subject: [PATCH 039/674] Add conjugaison module --- modules/conjugaison.py | 98 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 modules/conjugaison.py diff --git a/modules/conjugaison.py b/modules/conjugaison.py new file mode 100644 index 0000000..6b5d31e --- /dev/null +++ b/modules/conjugaison.py @@ -0,0 +1,98 @@ +# coding=utf-8 + +import re +import traceback +import sys +from urllib.parse import quote + +from tools import web +from tools.web import striphtml + +nemubotversion = 3.3 + +def help_tiny (): + return "Find french conjugaison" + +def help_full (): + return "!conjugaison : give the conjugaison for in ." + +def load(context): + from hooks import Hook + add_hook("cmd_hook", Hook(cmd_conjug, "conjugaison")) + + +def cmd_conjug(msg): + if len(msg.cmds) < 3: + return Response(msg.sender, + "Demande incorrecte.\n %s" % help_full(), + msg.channel) + tens = msg.cmds[1] + verb = msg.cmds[2] + try: + conjug = get_conjug(verb, tens) + except: + conjug = None + exc_type, exc_value, exc_traceback = sys.exc_info() + traceback.print_exception(exc_type, exc_value, + exc_traceback) + + if conjug is None: + return Response(msg.sender, + "Une erreur s'est produite durant la recherche" + " du verbe %s" % verb, msg.channel) + elif len(conjug) > 0: + return Response(msg.sender, conjug, msg.channel, + title="Conjugaison de %s" % verb) + else: + return Response(msg.sender, + "Aucune conjugaison de %s n'a été trouvé" % verb, + msg.channel) + return False + + +def get_conjug(verb, stringTens): + url = "http://leconjugueur.lefigaro.fr/conjugaison/verbe/" + quote(verb.encode("ISO-8859-1")) + ".html" + print_debug (url) + page = web.getURLContent(url) + if page is not None: + for line in page.split("\n"): + if re.search('
', line) is not None: + return compute_line(line, stringTens) + else: + return None + +def compute_line(line, stringTens): + res = list() + idTemps = get_conjug_for_tens(stringTens) + + if idTemps is None: + return Response(msg.sender, + "Le temps que vous avez spécifiez n'existe pas", msg.channel) + + index = line.index('
([^/]*/b>)", newLine): +# res.append(strip_tags(elt.group(1))) + res.append(striphtml(elt.group(1))) + + return res + + +def get_conjug_for_tens(stringTens): + dic = {'pr' : '0', + 'ps' : '12', + 'pa' : '112', + 'pc' : '100', + 'f' : '18', + 'fa' : '118', + 'spr' : '24', + 'spa' : '124', + 'ii' : '6', + 'pqp' : '106'} + + return dic[stringTens] + From 0f01d285284b2dd2a432b94a154cb686c37dc9af Mon Sep 17 00:00:00 2001 From: nemunaire Date: Fri, 11 Jul 2014 16:37:06 +0200 Subject: [PATCH 040/674] Webtool: New function to decode htmlentities --- tools/web.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tools/web.py b/tools/web.py index 88a52ff..4a59878 100644 --- a/tools/web.py +++ b/tools/web.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 . +from html.entities import name2codepoint import http.client import json import re @@ -136,7 +137,12 @@ def getJSON(url, timeout=15): # Other utils +def htmlentitydecode(s): + """Decode htmlentities""" + return re.sub('&(%s);' % '|'.join(name2codepoint), + lambda m: chr(name2codepoint[m.group(1)]), s) + def striphtml(data): """Remove HTML tags from text""" p = re.compile(r'<.*?>') - return p.sub('', data).replace("(", "/(").replace(")", ")/").replace(""", "\"") + return htmlentitydecode(p.sub('', data).replace("(", "/(").replace(")", ")/").replace(""", "\"")) From fde217dfde73dac779e13d5b9ce143613c792fbc Mon Sep 17 00:00:00 2001 From: Bertrand Cournaud Date: Tue, 15 Jul 2014 11:28:38 +0200 Subject: [PATCH 041/674] Define a dictonary for the tens list --- modules/conjugaison.py | 58 +++++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/modules/conjugaison.py b/modules/conjugaison.py index 6b5d31e..9c724f2 100644 --- a/modules/conjugaison.py +++ b/modules/conjugaison.py @@ -7,9 +7,37 @@ from urllib.parse import quote from tools import web from tools.web import striphtml +from collections import defaultdict nemubotversion = 3.3 +dic = {'pr' : '0', + 'ps' : '12', + 'pa' : '112', + 'pc' : '100', + 'f' : '18', + 'fa' : '118', + 'spr' : '24', + 'spa' : '124', + 'ii' : '6', + 'pqp' : '106'} + +s = [('present', '0'), ('présent', '0'), ('pr', '0'), + ('passé simple', '12'), ('passe simple', '12'), ('ps', '12'), + ('passé antérieur', '112'), ('passe anterieur', '112'), ('pa', '112'), + ('passé composé', '100'), ('passe compose', '100'), ('pc', '100'), + ('futur', '18'), ('f', '18'), + ('futur antérieur', '118'), ('futur anterieur', '118'), ('fa', '118'), + ('subjonctif présent', '24'), ('subjonctif present', '24'), ('spr', '24'), + ('subjonctif passé', '124'), ('subjonctif passe', '124'), ('spa', '124'), + ('plus que parfait', '106'), ('pqp', '106'), + ('imparfait', '6'), ('ii', '6')] + +d = defaultdict(list) + +for k, v in s: + d[k].append(v) + def help_tiny (): return "Find french conjugaison" @@ -63,36 +91,24 @@ def get_conjug(verb, stringTens): def compute_line(line, stringTens): res = list() - idTemps = get_conjug_for_tens(stringTens) + try: + idTemps = d[stringTens] + except: + res.append("Le temps demandé n'existe pas") + return res - if idTemps is None: - return Response(msg.sender, - "Le temps que vous avez spécifiez n'existe pas", msg.channel) + if len(idTemps) == 0: + res.append("Le temps demandé n'existe pas") + return res - index = line.index('
([^/]*/b>)", newLine): -# res.append(strip_tags(elt.group(1))) res.append(striphtml(elt.group(1))) return res - - -def get_conjug_for_tens(stringTens): - dic = {'pr' : '0', - 'ps' : '12', - 'pa' : '112', - 'pc' : '100', - 'f' : '18', - 'fa' : '118', - 'spr' : '24', - 'spa' : '124', - 'ii' : '6', - 'pqp' : '106'} - - return dic[stringTens] From e5741ce1cba512b3268af62460401b211ec62e22 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Thu, 17 Jul 2014 11:58:24 +0200 Subject: [PATCH 042/674] cmd_server: new command top to display bot load --- modules/cmd_server.py | 16 ++++++++++++++++ modules/cmd_server.xml | 1 + 2 files changed, 17 insertions(+) diff --git a/modules/cmd_server.py b/modules/cmd_server.py index 3624c26..8248da9 100644 --- a/modules/cmd_server.py +++ b/modules/cmd_server.py @@ -16,6 +16,9 @@ # 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 networkbot import NetworkBot nemubotversion = 3.3 @@ -199,3 +202,16 @@ def zap(data, toks, context, prompt): prompt.selectedServer.connected = not prompt.selectedServer.connected else: print (" Please SELECT a server or give its name in argument.") + +def top(data, toks, context, prompt): + """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("################ Stack trace for thread %u ################" % th.ident) + traceback.print_stack(sys._current_frames()[th.ident]) diff --git a/modules/cmd_server.xml b/modules/cmd_server.xml index e37c1e4..38c595b 100644 --- a/modules/cmd_server.xml +++ b/modules/cmd_server.xml @@ -11,4 +11,5 @@ + From a8ce37a372b2b11bcadcd6574077b86b15300754 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Thu, 17 Jul 2014 12:01:04 +0200 Subject: [PATCH 043/674] Cleaner consumers --- IRCServer.py | 18 +++++++++--------- consumer.py | 2 ++ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/IRCServer.py b/IRCServer.py index 602a597..c208b44 100644 --- a/IRCServer.py +++ b/IRCServer.py @@ -258,15 +258,15 @@ class IRCServer(server.Server): print ("\033[1;35mWarning:\033[0m Nemubot talks to himself: %s" % msg) traceback.print_stack() if line is not None and channel is not None: - if self.s is None: - print ("\033[1;35mWarning:\033[0m Attempt to send message on a non connected server: %s: %s" % (self.id, line)) - traceback.print_stack() - elif len(line) < 442: - self.s.send (("%s %s :%s%s" % (cmd, channel, line, endl)).encode ()) - else: - print ("\033[1;35mWarning:\033[0m Message truncated due to size (%d ; max : 442) : %s" % (len(line), line)) - traceback.print_stack() - self.s.send (("%s %s :%s%s" % (cmd, channel, line[0:442]+"...", endl)).encode ()) + if self.s is None: + print ("\033[1;35mWarning:\033[0m Attempt to send message on a non connected server: %s: %s" % (self.id, line)) + traceback.print_stack() + elif len(line) < 442: + self.s.send (("%s %s :%s%s" % (cmd, channel, line, endl)).encode ()) + else: + print ("\033[1;35mWarning:\033[0m Message truncated due to size (%d ; max : 442) : %s" % (len(line), line)) + traceback.print_stack() + self.s.send (("%s %s :%s%s" % (cmd, channel, line[0:442]+"<…>", endl)).encode ()) def send_msg_usr(self, user, msg): """Send a message to a user instead of a channel""" diff --git a/consumer.py b/consumer.py index a443dca..dc2bcd5 100644 --- a/consumer.py +++ b/consumer.py @@ -136,8 +136,10 @@ class Consumer(threading.Thread): while not self.stop: stm = self.context.cnsr_queue.get(True, 20) stm.run(self.context) + self.context.cnsr_queue.task_done() except queue.Empty: pass finally: self.context.cnsr_thrd_size -= 2 + self.context.cnsr_thrd.remove(self) From ba1b5774bb43859271371ebc71be069469255cb8 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Thu, 17 Jul 2014 12:02:16 +0200 Subject: [PATCH 044/674] Too long close is identified --- nemubot.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nemubot.py b/nemubot.py index 5948304..61b740c 100755 --- a/nemubot.py +++ b/nemubot.py @@ -73,4 +73,7 @@ if __name__ == "__main__": exc_value)[0]) print ("\nWaiting for other threads shuts down...") + + # Indeed, the server socket is waiting for receiving some data + sys.exit(0) From 82198160fdb27cf045d518df133882b70f12235b Mon Sep 17 00:00:00 2001 From: nemunaire Date: Thu, 17 Jul 2014 12:04:34 +0200 Subject: [PATCH 045/674] Worlcup module: add a timeout to urlopen to avoid infinite event --- modules/worldcup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/worldcup.py b/modules/worldcup.py index 609ac47..b5df61b 100644 --- a/modules/worldcup.py +++ b/modules/worldcup.py @@ -16,7 +16,7 @@ def load(context): add_hook("cmd_hook", Hook(cmd_worldcup, "worldcup")) from event import ModuleEvent - add_event(ModuleEvent(func=lambda url: urlopen(url).read().decode(), func_data=API_URL % "matches/current?by_date=DESC", call=current_match_new_action, intervalle=30)) + 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, intervalle=30)) def help_tiny (): From fabdbc7c47b6503607f5e50792bb9fa85386d7f6 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Thu, 17 Jul 2014 12:20:08 +0200 Subject: [PATCH 046/674] v3.3 now considered stable --- bot.py | 2 +- importer.py | 12 ------------ nemubot.py | 1 - 3 files changed, 1 insertion(+), 14 deletions(-) diff --git a/bot.py b/bot.py index 6cb004c..ff66b8a 100644 --- a/bot.py +++ b/bot.py @@ -37,7 +37,7 @@ class Bot: def __init__(self, ip, realname, mp=list()): # Bot general informations self.version = 3.3 - self.version_txt = "3.3-dev" + self.version_txt = "3.3" # Save various informations self.ip = ip diff --git a/importer.py b/importer.py index 402fa12..8249b1f 100644 --- a/importer.py +++ b/importer.py @@ -167,8 +167,6 @@ class ModuleLoader(SourceLoader): module.DATAS = None module.save = lambda: False module.CONF = self.config - module.has_access = lambda msg: mod_has_access(module, - module.CONF, msg) module.ModuleEvent = event.ModuleEvent module.ModuleState = xmlparser.module_state.ModuleState @@ -249,16 +247,6 @@ def mod_save(mod, datas_path): mod.DATAS.save(datas_path + "/" + mod.name + ".xml") mod.print_debug("Saving!") -def mod_has_access(mod, config, msg): - if config is not None and config.hasNode("channel"): - for chan in config.getNodes("channel"): - if (chan["server"] is None or chan["server"] == msg.srv.id) and ( - chan["channel"] is None or chan["channel"] == msg.channel): - return True - return False - else: - return True - def mod_send_response(context, server, res): if server in context.servers: context.servers[server].send_response(res, None) diff --git a/nemubot.py b/nemubot.py index 61b740c..4e77007 100755 --- a/nemubot.py +++ b/nemubot.py @@ -35,7 +35,6 @@ if __name__ == "__main__": prmpt = prompt.Prompt() # Register the hook for futur import - import sys sys.meta_path.append(importer.ModuleFinder(context, prmpt)) #Add modules dir path From fee11aca422a1f9b9ecbc3c5574d6cee5c229ca2 Mon Sep 17 00:00:00 2001 From: Bertrand Cournaud Date: Wed, 23 Jul 2014 14:54:00 +0200 Subject: [PATCH 047/674] Add imdb search module. \!imdb for info about a movie, \!imdbs to search a movie --- modules/imdb.py | 76 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 modules/imdb.py diff --git a/modules/imdb.py b/modules/imdb.py new file mode 100644 index 0000000..68b46f0 --- /dev/null +++ b/modules/imdb.py @@ -0,0 +1,76 @@ +# coding=utf-8 + +import urllib.request +import json + +nemubotversion = 3.3 + +def help_tiny (): + return "Find info about a movie" + +def help_full (): + return "!imdb " + +def load(context): + from hooks import Hook + add_hook("cmd_hook", Hook(cmd_imdb, "imdb")) + add_hook("cmd_hook", Hook(cmd_search, "imdbs")) + + +def cmd_imdb(msg): + if (len(msg.cmds) < 2): + return Response(msg.sender, + "Demande incorrecte.\n %s" % help_full(), + msg.channel) + + movie_name = msg.cmds[1] + + for x in range(2, len(msg.cmds)): + movie_name += urllib.parse.quote(' ') + urllib.parse.quote(msg.cmds[x]) + + url = "http://www.omdbapi.com/?t=" + movie_name + print_debug(url) + response = urllib.request.urlopen(url) + + data = json.loads(response.read().decode()) + string = "\x02IMDB Rating\x0F: " + data['imdbRating'] + "\n\x02Plot\x0F: " + data['Plot'] + + res = Response(msg.sender, + string, + msg.channel) + res.append_message("\x02Released\x0F: " + data['Released'] + + " \x02Type\x0F: " + data['Type'] + + " \x02Genre\x0F: " + data['Genre'] + + " \x02Director\x0F: " + data['Director'] + + " \x02Writer\x0F: " + data['Writer'] + + " \x02Actors\x0F: " + data['Actors'] + + " \x02Country\x0F: " + data['Country']) + + return res + +def cmd_search(msg): + + movie_name = msg.cmds[1] + + for x in range(2, len(msg.cmds)): + movie_name += urllib.parse.quote(' ') + urllib.parse.quote(msg.cmds[x]) + + url = "http://www.omdbapi.com/?s=" + movie_name + print_debug(url) + + raw = urllib.request.urlopen(url) + data = json.loads(raw.read().decode()) + + search = data['Search'] + + movie_list = "" + + for i in range(0, len(search)): + movie_list += "\x02Title\x0F: " + search[i]['Title'] + movie_list += " \x02Year\x0F: " + search[i]['Year'] + movie_list += " \x02Type\x0F:" + search[i]['Type'] + movie_list += " |--| " + + res = Response(msg.sender, movie_list, msg.channel) + return res + From cd16bf8de99f0a3dcf70778b04dd80648a8dbe14 Mon Sep 17 00:00:00 2001 From: Bertrand Cournaud Date: Wed, 23 Jul 2014 15:00:01 +0200 Subject: [PATCH 048/674] Update conjugaison module by adding tens --- modules/conjugaison.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/modules/conjugaison.py b/modules/conjugaison.py index 9c724f2..3d359e9 100644 --- a/modules/conjugaison.py +++ b/modules/conjugaison.py @@ -54,8 +54,16 @@ def cmd_conjug(msg): return Response(msg.sender, "Demande incorrecte.\n %s" % help_full(), msg.channel) + tens = msg.cmds[1] - verb = msg.cmds[2] + + for i in range(2, len(msg.cmds) - 1): + tens += " " + msg.cmds[i] + + print_debug(tens) + + verb = msg.cmds[len(msg.cmds) - 1] + try: conjug = get_conjug(verb, tens) except: From 9eb3357148bfee89e6095f7133360ff5f7c1bf02 Mon Sep 17 00:00:00 2001 From: Bertrand Cournaud Date: Wed, 23 Jul 2014 15:01:14 +0200 Subject: [PATCH 049/674] Clean conjugaison module --- modules/conjugaison.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/modules/conjugaison.py b/modules/conjugaison.py index 3d359e9..d4e778b 100644 --- a/modules/conjugaison.py +++ b/modules/conjugaison.py @@ -11,17 +11,6 @@ from collections import defaultdict nemubotversion = 3.3 -dic = {'pr' : '0', - 'ps' : '12', - 'pa' : '112', - 'pc' : '100', - 'f' : '18', - 'fa' : '118', - 'spr' : '24', - 'spa' : '124', - 'ii' : '6', - 'pqp' : '106'} - s = [('present', '0'), ('présent', '0'), ('pr', '0'), ('passé simple', '12'), ('passe simple', '12'), ('ps', '12'), ('passé antérieur', '112'), ('passe anterieur', '112'), ('pa', '112'), From 411d8cdc41df878186147576ef6698e9fbc55ae6 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Wed, 23 Jul 2014 16:32:42 +0200 Subject: [PATCH 050/674] New prompt command: netstat that display information about connected networkbot --- modules/cmd_server.py | 21 +++++++++++++++++++++ modules/cmd_server.xml | 1 + 2 files changed, 22 insertions(+) diff --git a/modules/cmd_server.py b/modules/cmd_server.py index 8248da9..e36f7b7 100644 --- a/modules/cmd_server.py +++ b/modules/cmd_server.py @@ -215,3 +215,24 @@ def top(data, toks, context, prompt): if th.is_alive(): print("################ Stack trace for thread %u ################" % th.ident) traceback.print_stack(sys._current_frames()[th.ident]) + +def netstat(data, toks, context, prompt): + """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/modules/cmd_server.xml b/modules/cmd_server.xml index 38c595b..f87789a 100644 --- a/modules/cmd_server.xml +++ b/modules/cmd_server.xml @@ -12,4 +12,5 @@ + From be80e843236de5c4b991a48516081a9a377b0e10 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Jul 2014 05:32:07 +0200 Subject: [PATCH 051/674] Added SAP tcode lookup module --- bot_sample.xml | 1 + modules/sap.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 modules/sap.py diff --git a/bot_sample.xml b/bot_sample.xml index 42749ab..5343a87 100644 --- a/bot_sample.xml +++ b/bot_sample.xml @@ -23,5 +23,6 @@ + diff --git a/modules/sap.py b/modules/sap.py new file mode 100644 index 0000000..efbc556 --- /dev/null +++ b/modules/sap.py @@ -0,0 +1,42 @@ +# coding=utf-8 + +import urllib.request +import json +import re +from tools import web +from tools.web import striphtml + +nemubotversion = 3.3 + +def help_tiny (): + return "Find information about an SAP transaction codes" + +def help_full (): + return "!tcode " + +def load(context): + from hooks import Hook + add_hook("cmd_hook", Hook(cmd_tcode, "tcode")) + + +def cmd_tcode(msg): + if (len(msg.cmds) < 2): + return Response(msg.sender, + "Demande incorrecte.\n %s" % help_full(), + msg.channel) + + res = Response(msg.sender, None, msg.channel) + + request = urllib.parse.quote(msg.cmds[1]) + url = "http://www.tcodesearch.com/tcodes/search?q=" + request + page = web.getURLContent(url) + + if page is not None: + index = page.index('
') + len('
') + end = page[index:].index('
')+index + strscope = page[index:end] + for tcode in re.finditer(' ([a-zA-Z0-9_]*) - ([^\n]*)\n', strscope): + res.append_message("\x02" + tcode.group(1)+"\x0F - "+striphtml(tcode.group(2))) + return res + else: + return None From d575c0d6d326112d1398997de669644afc67b23a Mon Sep 17 00:00:00 2001 From: nemunaire Date: Fri, 25 Jul 2014 12:52:10 +0200 Subject: [PATCH 052/674] tools.web: Avoid explicit redirection loop --- tools/web.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/web.py b/tools/web.py index 4a59878..7678a7d 100644 --- a/tools/web.py +++ b/tools/web.py @@ -114,7 +114,7 @@ def getURLContent(url, timeout=15): if res.status == http.client.OK or res.status == http.client.SEE_OTHER: return data.decode(charset) - elif res.status == http.client.FOUND or res.status == http.client.MOVED_PERMANENTLY: + elif (res.status == http.client.FOUND or res.status == http.client.MOVED_PERMANENTLY) and res.getheader("Location") != url: return getURLContent(res.getheader("Location"), timeout) else: return None From 9b010544b5b6bc3bb5ab79f26419a0dc0b8f831e Mon Sep 17 00:00:00 2001 From: nemunaire Date: Fri, 25 Jul 2014 12:53:07 +0200 Subject: [PATCH 053/674] Last PR modules: made some stabilization modifications --- modules/conjugaison.py | 54 +++++++++------------------- modules/imdb.py | 82 +++++++++++++++++++----------------------- modules/sap.py | 21 +++++------ 3 files changed, 62 insertions(+), 95 deletions(-) diff --git a/modules/conjugaison.py b/modules/conjugaison.py index d4e778b..ab30b7e 100644 --- a/modules/conjugaison.py +++ b/modules/conjugaison.py @@ -40,63 +40,42 @@ def load(context): def cmd_conjug(msg): if len(msg.cmds) < 3: - return Response(msg.sender, - "Demande incorrecte.\n %s" % help_full(), - msg.channel) - - tens = msg.cmds[1] - - for i in range(2, len(msg.cmds) - 1): - tens += " " + msg.cmds[i] + raise IRCException("donne moi un temps et un verbe, et je te donnerai sa conjugaison!") + tens = ' '.join(msg.cmds[1:-1]) print_debug(tens) - verb = msg.cmds[len(msg.cmds) - 1] + verb = msg.cmds[-1] - try: - conjug = get_conjug(verb, tens) - except: - conjug = None - exc_type, exc_value, exc_traceback = sys.exc_info() - traceback.print_exception(exc_type, exc_value, - exc_traceback) + conjug = get_conjug(verb, tens) - if conjug is None: - return Response(msg.sender, - "Une erreur s'est produite durant la recherche" - " du verbe %s" % verb, msg.channel) - elif len(conjug) > 0: - return Response(msg.sender, conjug, msg.channel, + if len(conjug) > 0: + return Response(msg.sender, conjug, channel=msg.channel, title="Conjugaison de %s" % verb) else: - return Response(msg.sender, - "Aucune conjugaison de %s n'a été trouvé" % verb, - msg.channel) - return False + raise IRCException("aucune conjugaison de '%s' n'a été trouvé" % verb) def get_conjug(verb, stringTens): - url = "http://leconjugueur.lefigaro.fr/conjugaison/verbe/" + quote(verb.encode("ISO-8859-1")) + ".html" + url = ("http://leconjugueur.lefigaro.fr/conjugaison/verbe/%s.html" % + quote(verb.encode("ISO-8859-1"))) print_debug (url) page = web.getURLContent(url) + if page is not None: for line in page.split("\n"): if re.search('
', line) is not None: return compute_line(line, stringTens) - else: - return None + return list() def compute_line(line, stringTens): - res = list() try: - idTemps = d[stringTens] + idTemps = d[stringTens] except: - res.append("Le temps demandé n'existe pas") - return res + raise IRCException("le temps demandé n'existe pas") if len(idTemps) == 0: - res.append("Le temps demandé n'existe pas") - return res + raise IRCException("le temps demandé n'existe pas") index = line.index('
([^/]*/b>)", newLine): - res.append(striphtml(elt.group(1))) - + res.append(striphtml(elt.group(1).replace("", "\x02").replace("", "\x0F"))) return res - diff --git a/modules/imdb.py b/modules/imdb.py index 68b46f0..8ddfd27 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -6,10 +6,10 @@ import json nemubotversion = 3.3 def help_tiny (): - return "Find info about a movie" + return "Show many information about a movie or serie" def help_full (): - return "!imdb " + return "Search a movie title with: !imdbs ; View movie details with !imdb " def load(context): from hooks import Hook @@ -18,59 +18,51 @@ def load(context): def cmd_imdb(msg): - if (len(msg.cmds) < 2): - return Response(msg.sender, - "Demande incorrecte.\n %s" % help_full(), - msg.channel) + if len(msg.cmds) < 2: + raise IRCException("precise a movie/serie title!") - movie_name = msg.cmds[1] - - for x in range(2, len(msg.cmds)): - movie_name += urllib.parse.quote(' ') + urllib.parse.quote(msg.cmds[x]) - - url = "http://www.omdbapi.com/?t=" + movie_name + url = "http://www.omdbapi.com/?t=%s" % urllib.parse.quote(' '.join(msg.cmds[1:])) print_debug(url) + response = urllib.request.urlopen(url) - data = json.loads(response.read().decode()) - string = "\x02IMDB Rating\x0F: " + data['imdbRating'] + "\n\x02Plot\x0F: " + data['Plot'] - res = Response(msg.sender, - string, - msg.channel) - res.append_message("\x02Released\x0F: " + data['Released'] - + " \x02Type\x0F: " + data['Type'] - + " \x02Genre\x0F: " + data['Genre'] - + " \x02Director\x0F: " + data['Director'] - + " \x02Writer\x0F: " + data['Writer'] - + " \x02Actors\x0F: " + data['Actors'] - + " \x02Country\x0F: " + data['Country']) + if "Error" in data: + raise IRCException(data["Error"]) + + elif "Response" in data and data["Response"] == "True": + res = Response(msg.sender, 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 \x02from\x0F %s \x02released on\x0F %s; \x02genre:\x0F %s; \x02directed by:\x0F %s; \x02writed by:\x0F %s; \x02main actors:\x0F %s" + % (data['Type'], data['Country'], data['Released'], data['Genre'], data['Director'], data['Writer'], data['Actors'])) + return res + + else: + raise IRCException("An error occurs during movie search") + - return res - def cmd_search(msg): + url = "http://www.omdbapi.com/?s=%s" % urllib.parse.quote(' '.join(msg.cmds[1:])) + print_debug(url) - movie_name = msg.cmds[1] + raw = urllib.request.urlopen(url) + data = json.loads(raw.read().decode()) - for x in range(2, len(msg.cmds)): - movie_name += urllib.parse.quote(' ') + urllib.parse.quote(msg.cmds[x]) + if "Error" in data: + raise IRCException(data["Error"]) - url = "http://www.omdbapi.com/?s=" + movie_name - print_debug(url) + elif "Search" in data: + movies = list() - raw = urllib.request.urlopen(url) - data = json.loads(raw.read().decode()) + for m in data['Search']: + movies.append("\x02%s\x0F (%s of %s)" % (m['Title'], m['Type'], m['Year'])) - search = data['Search'] - - movie_list = "" - - for i in range(0, len(search)): - movie_list += "\x02Title\x0F: " + search[i]['Title'] - movie_list += " \x02Year\x0F: " + search[i]['Year'] - movie_list += " \x02Type\x0F:" + search[i]['Type'] - movie_list += " |--| " - - res = Response(msg.sender, movie_list, msg.channel) - return res + return Response(msg.sender, movies, title="Titles found", channel=msg.channel) + else: + raise IRCException("An error occurs during movie search") diff --git a/modules/sap.py b/modules/sap.py index efbc556..410c9e2 100644 --- a/modules/sap.py +++ b/modules/sap.py @@ -20,23 +20,20 @@ def load(context): def cmd_tcode(msg): - if (len(msg.cmds) < 2): - return Response(msg.sender, - "Demande incorrecte.\n %s" % help_full(), - msg.channel) + if len(msg.cmds) != 2: + raise IRCException("indicate a transaction code or a keyword to search!") - res = Response(msg.sender, None, msg.channel) - - request = urllib.parse.quote(msg.cmds[1]) - url = "http://www.tcodesearch.com/tcodes/search?q=" + request + url = "http://www.tcodesearch.com/tcodes/search?q=%s" % urllib.parse.quote(msg.cmds[1]) page = web.getURLContent(url) + res = Response(msg.sender, channel=msg.channel, + nomore="No more transaction code", count=" (%d more tcodes)") + if page is not None: index = page.index('<div id="searchresults">') + len('<div id="searchresults">') end = page[index:].index('</div>')+index strscope = page[index:end] for tcode in re.finditer('<strong> ([a-zA-Z0-9_]*)</strong> - ([^\n]*)\n', strscope): - res.append_message("\x02" + tcode.group(1)+"\x0F - "+striphtml(tcode.group(2))) - return res - else: - return None + res.append_message("\x02%s\x0F - %s" % (tcode.group(1), striphtml(tcode.group(2)))) + + return res From d16f57f8d5e705938619630d6c4532ccdb98ffe4 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 25 Jul 2014 15:12:37 +0200 Subject: [PATCH 054/674] Refresh old code and add antonyme search --- modules/syno.py | 81 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 53 insertions(+), 28 deletions(-) diff --git a/modules/syno.py b/modules/syno.py index 2d0a98a..cfb16a4 100644 --- a/modules/syno.py +++ b/modules/syno.py @@ -17,45 +17,70 @@ def help_full (): def load(context): from hooks import Hook - add_hook("cmd_hook", Hook(cmd_syno, "syno")) - add_hook("cmd_hook", Hook(cmd_syno, "synonyme")) - + add_hook("cmd_hook", Hook(cmd_syno, "synonymes")) + add_hook("cmd_hook", Hook(cmd_anto, "antonymes")) def cmd_syno(msg): - if 1 < len(msg.cmds) < 6: - for word in msg.cmds[1:]: - try: - synos = get_synos(word) - except: - synos = None - exc_type, exc_value, exc_traceback = sys.exc_info() - traceback.print_exception(exc_type, exc_value, - exc_traceback) + return go("synonymes", msg) - if synos is None: - return Response(msg.sender, - "Une erreur s'est produite durant la recherche" - " d'un synonyme de %s" % word, msg.channel) - elif len(synos) > 0: - return Response(msg.sender, synos, msg.channel, - title="Synonymes de %s" % word) - else: - return Response(msg.sender, - "Aucun synonymes de %s n'a été trouvé" % word, - msg.channel) - return False +def cmd_anto(msg): + return go("antonymes", msg) + +def go(what, msg): + if len(msg.cmds) < 2: + raise IRCException("de quel mot veux-tu connaître la liste des synonymes ?") + + word = ' '.join(msg.cmds[1:]) + try: + best, synos, anton = get_synos(word) + except: + best, synos, anton = (list(), list(), list()) + + if what == "synonymes": + if len(synos) > 0: + res = Response(msg.sender, best, channel=msg.channel, + title="Synonymes de %s" % word) + res.append_message(synos) + return res + else: + raise IRCException("Aucun synonyme de %s n'a été trouvé" % word) + + elif what == "antonymes": + if len(anton) > 0: + res = Response(msg.sender, anton, channel=msg.channel, + title="Antonymes de %s" % word) + return res + else: + raise IRCException("Aucun antonyme de %s n'a été trouvé" % word) + + else: + raise IRCException("WHAT?!") def get_synos(word): url = "http://www.crisco.unicaen.fr/des/synonymes/" + quote(word.encode("ISO-8859-1")) print_debug (url) page = web.getURLContent(url) + if page is not None: + best = list() synos = list() + anton = list() for line in page.split("\n"): - if re.match("[ \t]*<tr[^>]*>.*</tr>[ \t]*</table>.*", line) is not None: - for elt in re.finditer(">&[^;]+;([^&]*)&[^;]+;<", line): + + if line.find("!-- Fin liste des antonymes --") > 0: + for elt in re.finditer(">([^<>]+)</a>", line): + anton.append(elt.group(1)) + + elif line.find("!--Fin liste des synonymes--") > 0: + for elt in re.finditer(">([^<>]+)</a>", line): synos.append(elt.group(1)) - return synos + + elif re.match("[ \t]*<tr[^>]*>.*</tr>[ \t]*</table>.*", line) is not None: + for elt in re.finditer(">&[^;]+;([^&]*)&[^;]+;<", line): + best.append(elt.group(1)) + + return (best, synos, anton) + else: - return None + return (list(), list(), list()) From 1a3912cc4fabaf6fb26ee860ebfbf07423064dcd Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 25 Jul 2014 16:38:01 +0200 Subject: [PATCH 055/674] YCC module: dusting & anchors are correctly passed; fixes #59 --- modules/ycc.py | 49 ++++++++++++++++++++++++------------------------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/modules/ycc.py b/modules/ycc.py index 3edf2f5..7f0cc9c 100644 --- a/modules/ycc.py +++ b/modules/ycc.py @@ -8,11 +8,11 @@ from urllib.request import urlopen nemubotversion = 3.3 def help_tiny (): - """Line inserted in the response to the command !help""" - return "Gets YCC urls" + """Line inserted in the response to the command !help""" + return "Gets YCC urls" def help_full (): - return "!ycc [<url>]: with an argument, reduce the given <url> thanks to ycc.fr; without argument, reduce the last URL said on the current channel." + return "!ycc [<url>]: with an argument, reduce the given <url> thanks to ycc.fr; without argument, reduce the last URL said on the current channel." def load(context): from hooks import Hook @@ -23,11 +23,11 @@ LAST_URLS = dict() def gen_response(res, msg, srv): if res is None: - return Response(msg.sender, "La situation est embarassante, il semblerait que YCC soit down :(", msg.channel) + raise IRCException("la situation est embarassante, il semblerait que YCC soit down :(") elif isinstance(res, str): return Response(msg.sender, "URL pour %s : %s" % (srv, res), msg.channel) else: - return Response(msg.sender, "Mauvaise URL : %s" % srv, msg.channel) + raise IRCException("mauvaise URL : %s" % srv) def cmd_ycc(msg): if len(msg.cmds) == 1: @@ -35,26 +35,25 @@ def cmd_ycc(msg): if msg.channel in LAST_URLS and len(LAST_URLS[msg.channel]) > 0: msg.cmds.append(LAST_URLS[msg.channel].pop()) else: - return Response(msg.sender, "Je n'ai pas d'autre URL à réduire.", msg.channel) + raise IRCException("je n'ai pas d'autre URL à réduire.") - if len(msg.cmds) < 6: - res = list() - for url in msg.cmds[1:]: - o = urlparse(url, "http") - if o.scheme != "": - snd_url = "http://ycc.fr/redirection/create/" + quote(url, "/:%#@&=?") - print_debug(snd_url) - raw = urlopen(snd_url, timeout=10) - if o.netloc == "": - res.append(gen_response(raw.read().decode(), msg, o.scheme)) - else: - res.append(gen_response(raw.read().decode(), msg, o.netloc)) + if len(msg.cmds) > 5: + raise IRCException("je ne peux pas réduire autant d'URL d'un seul coup.") + + res = list() + for url in msg.cmds[1:]: + o = urlparse(url, "http") + if o.scheme != "": + snd_url = "http://ycc.fr/redirection/create/" + quote(url, "/:%@&=?") + print_debug(snd_url) + raw = urlopen(snd_url, timeout=10) + if o.netloc == "": + res.append(gen_response(raw.read().decode(), msg, o.scheme)) else: - res.append(gen_response(False, msg, url)) - return res - else: - return Response(msg.sender, "je ne peux pas réduire autant d'URL " - "d'un seul coup.", msg.channel, nick=msg.nick) + res.append(gen_response(raw.read().decode(), msg, o.netloc)) + else: + res.append(gen_response(False, msg, url)) + return res def parselisten(msg): global LAST_URLS @@ -67,9 +66,9 @@ def parselisten(msg): continue if msg.channel not in LAST_URLS: LAST_URLS[msg.channel] = list() - LAST_URLS[msg.channel].append(o.geturl()) + LAST_URLS[msg.channel].append(url) except: - pass + pass return False def parseresponse(res): From 495c1f0efaa785b3831d7fb0c1ca901765752239 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 25 Jul 2014 17:45:01 +0200 Subject: [PATCH 056/674] Refactor translation module --- modules/translate.py | 156 ++++++++++++++++++++++--------------------- 1 file changed, 81 insertions(+), 75 deletions(-) diff --git a/modules/translate.py b/modules/translate.py index 60838f0..e1e31c2 100644 --- a/modules/translate.py +++ b/modules/translate.py @@ -5,6 +5,7 @@ import re import socket import json from urllib.parse import quote +from urllib.request import urlopen nemubotversion = 3.3 @@ -12,86 +13,91 @@ import xmlparser 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" def load(context): - from hooks import Hook - add_hook("cmd_hook", Hook(cmd_translate, "translate")) - add_hook("cmd_hook", Hook(cmd_translate, "traduction")) - add_hook("cmd_hook", Hook(cmd_translate, "traduit")) - add_hook("cmd_hook", Hook(cmd_translate, "traduire")) - - -def cmd_translate(msg): - global LANG - startWord = 1 - if msg.cmds[startWord] in LANG: - langTo = msg.cmds[startWord] - startWord += 1 - else: - langTo = "fr" - if msg.cmds[startWord] in LANG: - langFrom = langTo - langTo = msg.cmds[startWord] - startWord += 1 - else: - if langTo == "en": - langFrom = "fr" - else: - langFrom = "en" - - (res, page) = getPage(' '.join(msg.cmds[startWord:]), langFrom, langTo) - if res == http.client.OK: - wres = json.loads(page.decode()) - if "Error" in wres: - return Response(msg.sender, wres["Note"], msg.channel) - else: - start = "Traduction de %s : "%' '.join(msg.cmds[startWord:]) - if "Entries" in wres["term0"]: - if "SecondTranslation" in wres["term0"]["Entries"]["0"]: - return Response(msg.sender, start + - wres["term0"]["Entries"]["0"]["FirstTranslation"]["term"] + - " ; " + - wres["term0"]["Entries"]["0"]["SecondTranslation"]["term"], - msg.channel) - else: - return Response(msg.sender, start + - wres["term0"]["Entries"]["0"]["FirstTranslation"]["term"], - msg.channel) - elif "PrincipalTranslations" in wres["term0"]: - if "1" in wres["term0"]["PrincipalTranslations"]: - return Response(msg.sender, start + - wres["term0"]["PrincipalTranslations"]["0"]["FirstTranslation"]["term"] + - " ; " + - wres["term0"]["PrincipalTranslations"]["1"]["FirstTranslation"]["term"], - msg.channel) - else: - return Response(msg.sender, start + - wres["term0"]["PrincipalTranslations"]["0"]["FirstTranslation"]["term"], - msg.channel) - else: - return Response(msg.sender, "Une erreur s'est produite durant la recherche" - " d'une traduction de %s" - % ' '.join(msg.cmds[startWord:]), - msg.channel) - - -def getPage(terms, langfrom="fr", langto="en"): - conn = http.client.HTTPConnection("api.wordreference.com", timeout=5) - try: - conn.request("GET", "/0.8/%s/json/%s%s/%s" % ( - CONF.getNode("wrapi")["key"], langfrom, langto, quote(terms))) - except socket.gaierror: - print ("impossible de récupérer la page WordReference.") - return (http.client.INTERNAL_SERVER_ERROR, None) - except (TypeError, KeyError): + global URL + if not CONF or not CONF.hasNode("wrapi") or not CONF.getNode("wrapi").hasAttribute("key"): print ("You need a WordReference API key in order to use this module." " Add it to the module configuration file:\n<wrapi key=\"XXXXX\"" " />\nRegister at " "http://www.wordreference.com/docs/APIregistration.aspx") - return (http.client.INTERNAL_SERVER_ERROR, None) + return None + else: + URL = URL % CONF.getNode("wrapi")["key"] - res = conn.getresponse() - data = res.read() + from hooks import Hook + add_hook("cmd_hook", Hook(cmd_translate, "translate")) - conn.close() - return (res.status, data) + +def help_tiny(): + """Line inserted in the response to the command !help""" + return "Translation module" + +def help_full(): + return "!translate [lang] <term>[ <term>[...]]: Found translation of <term> from/to english to/from <lang>. Data © WordReference.com" + + +def cmd_translate(msg): + if len(msg.cmds) < 2: + raise IRCException("which word would you translate?") + + if len(msg.cmds) > 3 and msg.cmds[1] == "en" and msg.cmds[2] in LANG: + langFrom = "en" + langTo = msg.cmds[2] + term = ' '.join(msg.cmds[3:]) + elif len(msg.cmds) > 2 and msg.cmds[1] in LANG: + langFrom = msg.cmds[1] + if langFrom == "en": + langTo = "fr" + else: + langTo = "en" + term = ' '.join(msg.cmds[2:]) + else: + langFrom = "en" + langTo = "fr" + term = ' '.join(msg.cmds[1:]) + + try: + raw = urlopen(URL % (langFrom, langTo, quote(term))) + except: + raise IRCException("invalid request") + wres = json.loads(raw.read().decode()) + + if "Error" in wres: + raise IRCException(wres["Note"]) + + else: + res = Response(msg.sender, channel=msg.channel, + nomore="No more translation") + for k, t in wres.items(): + if len(k) > 4 and k[:4] == "term": + if "Entries" in t: + ent = t["Entries"] + else: + ent = t["PrincipalTranslations"] + + for i in ent: + res.append_message("Translation of %s%s: %s" % ( + ent[i]["OriginalTerm"]["term"], + meaning(ent[i]["OriginalTerm"]), + extract_traslation(ent[i]))) + return res + +def meaning(entry): + ret = list() + if "sense" in entry and len(entry["sense"]) > 0: + ret.append('« %s »' % entry["sense"]) + if "usage" in entry and len(entry["usage"]) > 0: + ret.append(entry["usage"]) + if len(ret) > 0: + return " as " + "/".join(ret) + else: + return "" + +def extract_traslation(entry): + ret = list() + for i in [ "FirstTranslation", "SecondTranslation", "ThirdTranslation", "FourthTranslation" ]: + if i in entry: + ret.append("\x03\x02%s\x03\x02%s" % (entry[i]["term"], meaning(entry[i]))) + return ", ".join(ret) From 5bec50744c0a03394aa47810961861b4c0686959 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 25 Jul 2014 18:02:30 +0200 Subject: [PATCH 057/674] Dusting some modules --- modules/man.py | 2 +- modules/sleepytime.py | 4 +- modules/spell/__init__.py | 2 +- modules/velib.py | 25 ++++------- modules/watchWebsite/__init__.py | 77 +++++++++++++------------------- 5 files changed, 43 insertions(+), 67 deletions(-) diff --git a/modules/man.py b/modules/man.py index 00edc8e..e12b84a 100644 --- a/modules/man.py +++ b/modules/man.py @@ -13,7 +13,7 @@ def load(context): def help_tiny (): """Line inserted in the response to the command !help""" - return "Read man on IRC" + return "Read manual pages on IRC" def help_full (): return "!man [0-9] /what/: gives informations about /what/." diff --git a/modules/sleepytime.py b/modules/sleepytime.py index b53a2e5..a97d509 100644 --- a/modules/sleepytime.py +++ b/modules/sleepytime.py @@ -37,7 +37,7 @@ def cmd_sleep(msg): g.append(f[i+1].strftime("%H:%M")) return Response(msg.sender, "You should try to fall asleep at one of the following" - " times: %s" % ', '.join(g), msg.channel) + " times: %s" % ', '.join(g), channel=msg.channel) # Just get awake times else: @@ -49,4 +49,4 @@ def cmd_sleep(msg): return Response(msg.sender, "If you head to bed right now, you should try to wake" " up at one of the following times: %s" % - ', '.join(g), msg.channel) + ', '.join(g), channel=msg.channel) diff --git a/modules/spell/__init__.py b/modules/spell/__init__.py index ebaa7c8..07f69ff 100644 --- a/modules/spell/__init__.py +++ b/modules/spell/__init__.py @@ -26,7 +26,7 @@ def load(context): def cmd_spell(msg): if len(msg.cmds) < 2: - return Response(msg.sender, "indique une orthographe approximative du mot dont tu veux vérifier l'orthographe.", msg.channel, msg.nick) + raise IRCException("indique une orthographe approximative du mot dont tu veux vérifier l'orthographe.") lang = "fr" strRes = list() diff --git a/modules/velib.py b/modules/velib.py index 8385476..f001778 100644 --- a/modules/velib.py +++ b/modules/velib.py @@ -57,21 +57,17 @@ def print_station_status(msg, station): (available, free) = station_status(station) if available is not None and free is not None: return Response(msg.sender, - "%s: à la station %s : %d vélib et %d points d'attache" - " disponibles." % (msg.nick, station, available, free), - msg.channel) - else: - return Response(msg.sender, - "%s: station %s inconnue." % (msg.nick, station), - msg.channel) + "à la station %s : %d vélib et %d points d'attache" + " disponibles." % (station, available, free), + channel=msg.channel, nick=msg.nick) + raise IRCException("station %s inconnue." % station) def ask_stations(msg): """Hook entry from !velib""" global DATAS if len(msg.cmds) > 5: - return Response(msg.sender, - "Demande-moi moins de stations à la fois.", - msg.channel, nick=msg.nick) + raise IRCException("demande-moi moins de stations à la fois.") + elif len(msg.cmds) > 1: for station in msg.cmds[1:]: if re.match("^[0-9]{4,5}$", station): @@ -79,10 +75,7 @@ def ask_stations(msg): elif station in DATAS.index: return print_station_status(msg, DATAS.index[station]["number"]) else: - return Response(msg.sender, - "numéro de station invalide.", - msg.channel, nick=msg.nick) + raise IRCException("numéro de station invalide.") + else: - return Response(msg.sender, - "Pour quelle station ?", - msg.channel, nick=msg.nick) + raise IRCException("pour quelle station ?") diff --git a/modules/watchWebsite/__init__.py b/modules/watchWebsite/__init__.py index 1f69158..ba56450 100644 --- a/modules/watchWebsite/__init__.py +++ b/modules/watchWebsite/__init__.py @@ -32,18 +32,11 @@ def load(context): print("No alert defined for this site: " + site["url"]) #DATAS.delChild(site) -def unload(context): - """Unregister watched website""" - # Useless in 3.3? -# for site in DATAS.getNodes("watch"): -# context.del_event(site["evt_id"]) - pass - def getPageContent(url): """Returns the content of the given url""" print_debug("Get page %s" % url) try: - raw = urlopen(url, timeout=15) + raw = urlopen(url, timeout=10) return raw.read().decode() except: return None @@ -60,8 +53,7 @@ def start_watching(site): def del_site(msg): if len(msg.cmds) <= 1: - return Response(msg.sender, "quel site dois-je arrêter de surveiller ?", - msg.channel, msg.nick) + raise IRCException("quel site dois-je arrêter de surveiller ?") url = msg.cmds[1] @@ -71,53 +63,44 @@ def del_site(msg): for a in site.getNodes("alert"): if a["channel"] == msg.channel: if (msg.sender == a["sender"] or msg.is_owner): - site.delChild(a) - if not site.hasNode("alert"): - del_event(site["_evt_id"]) - DATAS.delChild(site) - save() - return Response(msg.sender, - "je ne surveille désormais plus cette URL.", - channel=msg.channel, nick=msg.nick) - else: - return Response(msg.sender, - "Vous ne pouvez pas supprimer cette URL.", - channel=msg.channel, nick=msg.nick) - return Response(msg.sender, - "je ne surveillais pas cette URL, impossible de la supprimer.", - channel=msg.channel, nick=msg.nick) - return Response(msg.sender, "je ne surveillais pas cette URL pour vous.", - channel=msg.channel, nick=msg.nick) + raise IRCException("vous ne pouvez pas supprimer cette URL.") + site.delChild(a) + if not site.hasNode("alert"): + del_event(site["_evt_id"]) + DATAS.delChild(site) + save() + return Response(msg.sender, + "je ne surveille désormais plus cette URL.", + channel=msg.channel, nick=msg.nick) + raise IRCException("je ne surveillais pas cette URL !") def add_site(msg, diffType="diff"): print (diffType) if len(msg.cmds) <= 1: - return Response(msg.sender, "quel site dois-je surveiller ?", - msg.channel, msg.nick) + raise IRCException("quel site dois-je surveiller ?") url = msg.cmds[1] o = urlparse(url, "http") - if o.netloc != "": - alert = ModuleState("alert") - alert["sender"] = msg.sender - alert["server"] = msg.server - alert["channel"] = msg.channel - alert["message"] = "%s a changé !" % url + if o.netloc == "": + raise IRCException("je ne peux pas surveiller cette URL") - if url not in DATAS.index: - watch = ModuleState("watch") - watch["type"] = diffType - watch["url"] = url - watch["time"] = 123 - DATAS.addChild(watch) - watch.addChild(alert) - start_watching(watch) - else: - DATAS.index[url].addChild(alert) + alert = ModuleState("alert") + alert["sender"] = msg.sender + alert["server"] = msg.server + alert["channel"] = msg.channel + alert["message"] = "%s a changé !" % url + + if url not in DATAS.index: + watch = ModuleState("watch") + watch["type"] = diffType + watch["url"] = url + watch["time"] = 123 + DATAS.addChild(watch) + watch.addChild(alert) + start_watching(watch) else: - return Response(msg.sender, "je ne peux pas surveiller cette URL", - channel=msg.channel, nick=msg.nick) + DATAS.index[url].addChild(alert) save() return Response(msg.sender, channel=msg.channel, nick=msg.nick, From eccf4ddf7a5c1ce0958d49fc8f58c141d0cf824d Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 25 Jul 2014 18:10:23 +0200 Subject: [PATCH 058/674] Translation module: allow only translation to or from english (due to wordreference restrinctions) --- modules/translate.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/translate.py b/modules/translate.py index e1e31c2..50388b6 100644 --- a/modules/translate.py +++ b/modules/translate.py @@ -42,8 +42,10 @@ def cmd_translate(msg): if len(msg.cmds) < 2: raise IRCException("which word would you translate?") - if len(msg.cmds) > 3 and msg.cmds[1] == "en" and msg.cmds[2] in LANG: - langFrom = "en" + if len(msg.cmds) > 3 and msg.cmds[1] in LANG and msg.cmds[2] in LANG: + if msg.cmds[1] != "en" and msg.cmds[2] != "en": + raise IRCException("sorry, I can only translate to or from english") + langFrom = msg.cmds[1] langTo = msg.cmds[2] term = ' '.join(msg.cmds[3:]) elif len(msg.cmds) > 2 and msg.cmds[1] in LANG: From b34a73cea6af88a9570b039b631462a6f9663c8f Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 4 Aug 2014 02:32:29 +0200 Subject: [PATCH 059/674] Fix exception on empty message --- bot.py | 4 ++-- modules/alias.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/bot.py b/bot.py index ff66b8a..42b4576 100644 --- a/bot.py +++ b/bot.py @@ -415,11 +415,11 @@ class Bot: return self.treat_prvmsg_ask(msg, srv) # Owner commands - elif msg.content[0] == '`' and msg.nick == srv.owner: + elif len(msg.content) > 0 and msg.content[0] == '`' and msg.nick == srv.owner: #TODO: owner commands pass - elif msg.content[0] == '!' and len(msg.content) > 1: + elif len(msg.content) > 0 and msg.content[0] == '!' and len(msg.content) > 1: # Remove the ! msg.cmds[0] = msg.cmds[0][1:] diff --git a/modules/alias.py b/modules/alias.py index 7aebef5..cd5eb5e 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -154,7 +154,8 @@ def treat_variables(res): return True def treat_alias(msg, hooks_cache): - if msg.cmd == "PRIVMSG" and (len(msg.cmds[0]) > 0 and msg.cmds[0][0] == "!" + if msg.cmd == "PRIVMSG" and len(msg.cmds) > 0 and (len(msg.cmds[0]) > 0 + and msg.cmds[0][0] == "!" and msg.cmds[0][1:] in DATAS.getNode("aliases").index and msg.cmds[0][1:] not in hooks_cache("cmd_hook")): msg.content = msg.content.replace(msg.cmds[0], From 7a55840aeb0c77830adb37f3a33ac298d45f3ca8 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 6 Aug 2014 16:02:10 +0200 Subject: [PATCH 060/674] Translate module: add indication of more available translation --- modules/translate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/translate.py b/modules/translate.py index 50388b6..7e0fe0d 100644 --- a/modules/translate.py +++ b/modules/translate.py @@ -71,6 +71,7 @@ def cmd_translate(msg): else: res = Response(msg.sender, channel=msg.channel, + count=" (%d more meanings)", nomore="No more translation") for k, t in wres.items(): if len(k) > 4 and k[:4] == "term": From d985b71373144a3f882e873e3eee6acc7505171e Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 6 Aug 2014 16:10:10 +0200 Subject: [PATCH 061/674] Rnd module: fix random choice between 0 possibility --- modules/rnd.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/rnd.py b/modules/rnd.py index ff27634..24b2e26 100644 --- a/modules/rnd.py +++ b/modules/rnd.py @@ -9,4 +9,7 @@ def load(context): add_hook("cmd_hook", Hook(cmd_choice, "choice")) def cmd_choice(msg): - return Response(msg.sender, random.choice(msg.cmds[1:]), channel=msg.channel, nick=msg.nick) + if len(msg.cmds) > 1: + return Response(msg.sender, random.choice(msg.cmds[1:]), channel=msg.channel, nick=msg.nick) + else: + raise IRCException("indicate some terms to pick!") From c15127feb84f50b396d4f0ca1b0ebee97ea816c6 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 6 Aug 2014 16:20:51 +0200 Subject: [PATCH 062/674] WatchWebsite module: dusting & fix unwatch authorizations --- modules/watchWebsite.xml | 6 ------ modules/watchWebsite/__init__.py | 7 ++++++- 2 files changed, 6 insertions(+), 7 deletions(-) delete mode 100644 modules/watchWebsite.xml diff --git a/modules/watchWebsite.xml b/modules/watchWebsite.xml deleted file mode 100644 index 7a116e9..0000000 --- a/modules/watchWebsite.xml +++ /dev/null @@ -1,6 +0,0 @@ -<?xml version="1.0" ?> -<nemubotmodule name="watchWebsite"> - <message type="cmd" name="watch" call="add_site" data="diff" /> - <message type="cmd" name="updown" call="add_site" data="updown" /> - <message type="cmd" name="unwatch" call="del_site" /> -</nemubotmodule> diff --git a/modules/watchWebsite/__init__.py b/modules/watchWebsite/__init__.py index ba56450..985a7fc 100644 --- a/modules/watchWebsite/__init__.py +++ b/modules/watchWebsite/__init__.py @@ -24,6 +24,11 @@ def help_full (): def load(context): """Register watched website""" + from hooks import Hook + add_hook("cmd_hook", Hook(add_site, "watch", data="diff")) + add_hook("cmd_hook", Hook(add_site, "updown", data="updown")) + add_hook("cmd_hook", Hook(del_site, "unwatch")) + DATAS.setIndex("url", "watch") for site in DATAS.getNodes("watch"): if site.hasNode("alert"): @@ -62,7 +67,7 @@ def del_site(msg): site = DATAS.index[url] for a in site.getNodes("alert"): if a["channel"] == msg.channel: - if (msg.sender == a["sender"] or msg.is_owner): + if not (msg.sender == a["sender"] or msg.is_owner): raise IRCException("vous ne pouvez pas supprimer cette URL.") site.delChild(a) if not site.hasNode("alert"): From 99c6a5c2719b6f40e2bdbbdb9ea9d7045240cd0d Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 7 Aug 2014 19:13:52 +0200 Subject: [PATCH 063/674] Start a message by nemubot: is equivalent to talk in private message --- bot.py | 21 +++++++++++---------- consumer.py | 1 + message.py | 2 +- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/bot.py b/bot.py index 42b4576..9995308 100644 --- a/bot.py +++ b/bot.py @@ -354,6 +354,13 @@ class Bot: def treat_pre(self, msg, srv): """Treat a message before all other treatment""" + # Treat all messages starting with 'nemubot:' as distinct commands + if msg.cmd == "PRIVMSG" and msg.content.find("%s:"%srv.nick) == 0: + # Remove the bot name + msg.content = msg.content[len(srv.nick)+1:].strip() + msg.parse_content() + msg.private = True + for h, lvl, store, bot in self.create_cache("all_pre"): if h.is_matching(None, server=srv): h.run(msg, self.create_cache) @@ -407,19 +414,12 @@ class Bot: else: return _ctcp_response(msg.sender, "ERRMSG Unknown or unimplemented CTCP request") - # Treat all messages starting with 'nemubot:' as distinct commands - elif msg.content.find("%s:"%srv.nick) == 0: - # Remove the bot name - msg.content = msg.content[len(srv.nick)+1:].strip() - - return self.treat_prvmsg_ask(msg, srv) - # Owner commands - elif len(msg.content) > 0 and msg.content[0] == '`' and msg.nick == srv.owner: + if len(msg.content) > 1 and msg.content[0] == '`' and msg.nick == srv.owner: #TODO: owner commands pass - elif len(msg.content) > 0 and msg.content[0] == '!' and len(msg.content) > 1: + elif len(msg.content) > 1 and msg.content[0] == '!': # Remove the ! msg.cmds[0] = msg.cmds[0][1:] @@ -450,8 +450,9 @@ class Bot: else: res = self.treat_answer(msg, srv) + # Assume the message starts with nemubot: - if (res is None or len(res) <= 0) and msg.private: + if not res and msg.private: return self.treat_prvmsg_ask(msg, srv) return res diff --git a/consumer.py b/consumer.py index dc2bcd5..d2df9fb 100644 --- a/consumer.py +++ b/consumer.py @@ -87,6 +87,7 @@ class MessageConsumer: msg.server = self.srv.id if msg.cmd == "PRIVMSG": msg.is_owner = (msg.nick == self.srv.owner) + msg.private = msg.private or msg.channel == self.srv.nick res = self.treat_in(context, msg) except: print ("\033[1;31mERROR:\033[0m occurred during the " diff --git a/message.py b/message.py index d14a16d..1a158ac 100644 --- a/message.py +++ b/message.py @@ -41,7 +41,7 @@ def save(): class Message: - def __init__ (self, line, timestamp, private = False): + def __init__ (self, line, timestamp, private=False): self.raw = line self.time = timestamp self.channel = None From 1e139b3afacdd98cedaa219d7783189846641383 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 8 Aug 2014 18:22:14 +0200 Subject: [PATCH 064/674] Translate module: fix order of meanings --- modules/translate.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/modules/translate.py b/modules/translate.py index 7e0fe0d..fb8f649 100644 --- a/modules/translate.py +++ b/modules/translate.py @@ -73,14 +73,15 @@ def cmd_translate(msg): res = Response(msg.sender, channel=msg.channel, count=" (%d more meanings)", nomore="No more translation") - for k, t in wres.items(): + 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 ent: + for i in sorted(ent.keys()): res.append_message("Translation of %s%s: %s" % ( ent[i]["OriginalTerm"]["term"], meaning(ent[i]["OriginalTerm"]), @@ -103,4 +104,6 @@ def extract_traslation(entry): for i in [ "FirstTranslation", "SecondTranslation", "ThirdTranslation", "FourthTranslation" ]: if i in entry: ret.append("\x03\x02%s\x03\x02%s" % (entry[i]["term"], meaning(entry[i]))) + if "Note" in entry and entry["Note"]: + ret.append("note: %s" % entry["Note"]) return ", ".join(ret) From 22b6392cc88c0425aa9da3a6ca557207eb10f6c7 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 8 Aug 2014 18:30:39 +0200 Subject: [PATCH 065/674] Alias module: can create of aliases; fixes #27 --- modules/alias.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/modules/alias.py b/modules/alias.py index cd5eb5e..7edd95b 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -154,17 +154,23 @@ def treat_variables(res): return True def treat_alias(msg, hooks_cache): - if msg.cmd == "PRIVMSG" and len(msg.cmds) > 0 and (len(msg.cmds[0]) > 0 - and msg.cmds[0][0] == "!" - and msg.cmds[0][1:] in DATAS.getNode("aliases").index - and msg.cmds[0][1:] not in hooks_cache("cmd_hook")): - msg.content = msg.content.replace(msg.cmds[0], - DATAS.getNode("aliases").index[msg.cmds[0][1:]]["origin"], 1) + if msg.cmd == "PRIVMSG": + if len(msg.cmds) > 0 and (len(msg.cmds[0]) > 0 + and msg.cmds[0][0] == "!" + and msg.cmds[0][1:] in DATAS.getNode("aliases").index + and msg.cmds[0][1:] not in hooks_cache("cmd_hook")): + msg.content = msg.content.replace(msg.cmds[0], + DATAS.getNode("aliases").index[msg.cmds[0][1:]]["origin"], 1) - msg.content = replace_variables(msg.content, msg) + msg.parse_content() - msg.parse_content() - return True + treat_alias(msg, hooks_cache) + return True + + else: + msg.content = replace_variables(msg.content, msg) + msg.parse_content() + return False return False From 7aaa65d4a69179de479d36db336e1699b29edaff Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 8 Aug 2014 19:09:18 +0200 Subject: [PATCH 066/674] XMLnode: when no index, look for attribute on in keyword use --- xmlparser/node.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/xmlparser/node.py b/xmlparser/node.py index c0fc3a3..254dc31 100644 --- a/xmlparser/node.py +++ b/xmlparser/node.py @@ -110,7 +110,10 @@ class ModuleState: def __contains__(self, i): """Return true if i is found in the index""" - return i in self.index + if self.index: + return i in self.index + else: + return self.hasAttribute(i) def hasAttribute(self, name): """DOM like method""" From 3ae01da380efee3da371a58184eb10f43a7a3666 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 8 Aug 2014 19:45:41 +0200 Subject: [PATCH 067/674] Events module: dusting --- modules/events.py | 246 +++++++++++++++++++++++++++++++++++++ modules/events.xml | 14 --- modules/events/__init__.py | 244 ------------------------------------ 3 files changed, 246 insertions(+), 258 deletions(-) create mode 100644 modules/events.py delete mode 100644 modules/events.xml delete mode 100644 modules/events/__init__.py diff --git a/modules/events.py b/modules/events.py new file mode 100644 index 0000000..de61c9e --- /dev/null +++ b/modules/events.py @@ -0,0 +1,246 @@ +# coding=utf-8 + +import imp +import re +import sys +from datetime import timedelta +from datetime import datetime +import time +import threading +import traceback + +nemubotversion = 3.3 + +from event import ModuleEvent +from hooks import Hook + +def help_tiny (): + """Line inserted in the response to the command !help""" + return "events manager" + +def help_full (): + return "This module store a lot of events: ny, we, " + (", ".join(DATAS.index.keys())) + "\n!eventslist: gets list of timer\n!start /something/: launch a timer" + +def load(context): + global DATAS + #Define the index + DATAS.setIndex("name") + + add_hook("cmd_hook", Hook(start_countdown, "start")) + add_hook("cmd_hook", Hook(end_countdown, "end")) + add_hook("cmd_hook", Hook(end_countdown, "forceend")) + add_hook("cmd_hook", Hook(liste, "eventslist")) + + add_hook("cmd_hook", Hook(cmd_gouter, "goûter")) + add_hook("cmd_hook", Hook(cmd_we, "week-end")) + + for evt in DATAS.index.keys(): + if DATAS.index[evt].hasAttribute("end"): + event = ModuleEvent(call=fini, call_data=dict(strend=DATAS.index[evt])) + event.end = DATAS.index[evt].getDate("end") + idt = context.add_event(event) + if idt is not None: + DATAS.index[evt]["id"] = idt + + +def fini(d, strend): + send_response(strend["server"], Response(strend["sender"], "%s arrivé à échéance." % strend["name"], channel=strend["channel"], nick=strend["proprio"])) + DATAS.delChild(DATAS.index[strend["name"]]) + save() + +def cmd_gouter(msg): + ndate = datetime.today() + ndate = datetime(ndate.year, ndate.month, ndate.day, 16, 42) + return Response(msg.sender, + msg.countdown_format(ndate, + "Le goûter aura lieu dans %s, préparez vos biscuits !", + "Nous avons %s de retard pour le goûter :("), + channel=msg.channel) + +def cmd_we(msg): + ndate = datetime.today() + timedelta(5 - datetime.today().weekday()) + ndate = datetime(ndate.year, ndate.month, ndate.day, 0, 0, 1) + return Response(msg.sender, + msg.countdown_format(ndate, + "Il reste %s avant le week-end, courage ;)", + "Youhou, on est en week-end depuis %s."), + channel=msg.channel) + +def start_countdown(msg): + if len(msg.cmds) < 2: + raise IRCException("indique le nom d'un événement à chronométrer") + if msg.cmds[1] in DATAS.index: + raise IRCException("%s existe déjà." % msg.cmds[1]) + + strnd = ModuleState("strend") + strnd["server"] = msg.server + strnd["channel"] = msg.channel + strnd["proprio"] = msg.nick + strnd["sender"] = msg.sender + strnd["start"] = datetime.now() + strnd["name"] = msg.cmds[1] + DATAS.addChild(strnd) + + evt = ModuleEvent(call=fini, call_data=dict(strend=strnd)) + + if len(msg.cmds) > 2: + result1 = re.findall("([0-9]+)([smhdjwyaSMHDJWYA])?", msg.cmds[2]) + result2 = re.match("(.*[^0-9])?([0-3]?[0-9])/([0-1]?[0-9])/((19|20)?[01239][0-9])", msg.cmds[2]) + result3 = re.match("(.*[^0-9])?([0-2]?[0-9]):([0-5]?[0-9])(:([0-5]?[0-9]))?", msg.cmds[2]) + if result2 is not None or result3 is not None: + try: + now = datetime.now() + if result3 is None or result3.group(5) is None: sec = 0 + else: sec = int(result3.group(5)) + if result3 is None or result3.group(3) is None: minu = 0 + else: minu = int(result3.group(3)) + if result3 is None or result3.group(2) is None: hou = 0 + else: hou = int(result3.group(2)) + 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) + elif result2 is not None: + strnd["end"] = datetime(int(result2.group(4)), int(result2.group(3)), int(result2.group(2))) + 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) + else: + strnd["end"] = datetime(now.year, now.month, now.day + 1, hou, minu, sec) + evt.end = strnd.getDate("end") + strnd["id"] = add_event(evt) + except: + DATAS.delChild(strnd) + raise IRCException("Mauvais format de date pour l'événement %s. Il n'a pas été créé." % msg.cmds[1]) + + elif result1 is not None and len(result1) > 0: + strnd["end"] = datetime.now() + for (t, g) in result1: + if g is None or g == "" or g == "m" or g == "M": + strnd["end"] += timedelta(minutes=int(t)) + elif g == "h" or g == "H": + strnd["end"] += timedelta(hours=int(t)) + elif g == "d" or g == "D" or g == "j" or g == "J": + strnd["end"] += timedelta(days=int(t)) + elif g == "w" or g == "W": + strnd["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) + else: + strnd["end"] += timedelta(seconds=int(t)) + evt.end = strnd.getDate("end") + strnd["id"] = add_event(evt) + + save() + if "end" in strnd: + return Response(msg.sender, "%s commencé le %s et se terminera le %s." % + (msg.cmds[1], datetime.now().strftime("%A %d %B %Y à %H:%M:%S"), + strnd.getDate("end").strftime("%A %d %B %Y à %H:%M:%S"))) + else: + return Response(msg.sender, "%s commencé le %s"% (msg.cmds[1], + datetime.now().strftime("%A %d %B %Y à %H:%M:%S"))) + +def end_countdown(msg): + if len(msg.cmds) < 2: + raise IRCException("quel événement terminer ?") + + if msg.cmds[1] in DATAS.index: + if DATAS.index[msg.cmds[1]]["proprio"] == msg.nick or (msg.cmds[0] == "forceend" and msg.is_owner): + duration = msg.just_countdown(datetime.now() - DATAS.index[msg.cmds[1]].getDate("start")) + del_event(DATAS.index[msg.cmds[1]]["id"]) + DATAS.delChild(DATAS.index[msg.cmds[1]]) + save() + return Response(msg.sender, "%s a duré %s." % (msg.cmds[1], duration), + channel=msg.channel, nick=msg.nick) + else: + raise IRCException("Vous ne pouvez pas terminer le compteur %s, créé par %s." % (msg.cmds[1], DATAS.index[msg.cmds[1]]["proprio"])) + else: + return Response(msg.sender, "%s n'est pas un compteur connu."% (msg.cmds[1]), channel=msg.channel, nick=msg.nick) + +def liste(msg): + if len(msg.cmds) > 1: + res = list() + for user in msg.cmds[1:]: + cmptr = [x["name"] for x in DATAS.index.values() if x["proprio"] == user] + if len(cmptr) > 0: + res.append("Compteurs créés par %s : %s" % (user, ", ".join(cmptr))) + else: + res.append("%s n'a pas créé de compteur" % user) + return Response(msg.sender, " ; ".join(res), channel=msg.channel) + else: + return Response(msg.sender, "Compteurs connus : %s." % ", ".join(DATAS.index.keys()), channel=msg.channel) + +def parseanswer(msg): + if msg.cmds[0] in DATAS.index: + res = Response(msg.sender, channel=msg.channel) + + # Avoid message starting by ! which can be interpreted as command by other bots + if msg.cmds[0][0] == "!": + res.nick = msg.nick + + if DATAS.index[msg.cmds[0]].name == "strend": + if DATAS.index[msg.cmds[0]].hasAttribute("end"): + res.append_message("%s commencé il y a %s et se terminera dans %s." % (msg.cmds[0], msg.just_countdown(datetime.now() - DATAS.index[msg.cmds[0]].getDate("start")), msg.just_countdown(DATAS.index[msg.cmds[0]].getDate("end") - datetime.now()))) + else: + res.append_message("%s commencé il y a %s." % (msg.cmds[0], msg.just_countdown(datetime.now() - DATAS.index[msg.cmds[0]].getDate("start")))) + else: + res.append_message(msg.countdown_format(DATAS.index[msg.cmds[0]].getDate("start"), DATAS.index[msg.cmds[0]]["msg_before"], DATAS.index[msg.cmds[0]]["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) + +def parseask(msg): + if RGXP_ask.match(msg.content) is not None: + name = re.match("^.*!([^ \"'@!]+).*$", msg.content) + if name is None: + raise IRCException("il faut que tu attribues une commande à l'événement.") + if name.group(1) in DATAS.index: + raise IRCException("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.content, re.I) + if texts is not None and texts.group(3) is not None: + extDate = msg.extractDate() + if extDate is None or extDate == "": + raise IRCException("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 msg_before.find("%s") == -1 or msg_after.find("%s") == -1: + raise IRCException("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["sender"] = msg.sender + evt["name"] = name.group(1) + evt["start"] = extDate + evt["msg_after"] = msg_after + evt["msg_before"] = msg_before + DATAS.addChild(evt) + save() + return Response(msg.sender, + "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["sender"] = msg.sender + evt["name"] = name.group(1) + evt["msg_before"] = texts.group (2) + DATAS.addChild(evt) + save() + return Response(msg.sender, "Nouvelle commande !%s ajoutée avec succès." % name.group(1)) + + else: + raise IRCException("Veuillez indiquez les messages d'attente et d'après événement entre guillemets.") diff --git a/modules/events.xml b/modules/events.xml deleted file mode 100644 index a96794d..0000000 --- a/modules/events.xml +++ /dev/null @@ -1,14 +0,0 @@ -<?xml version="1.0" ?> -<nemubotmodule name="events"> - <message type="cmd" name="start" call="start_countdown" /> - <message type="cmd" name="end" call="end_countdown" /> - <message type="cmd" name="forceend" call="end_countdown" /> - <message type="cmd" name="eventlist" call="liste" /> - <message type="cmd" name="eventslist" call="liste" /> - <message type="cmd" name="eventliste" call="liste" /> - <message type="cmd" name="eventsliste" call="liste" /> - <message type="cmd" name="gouter" call="cmd_gouter" /> - <message type="cmd" name="goûter" call="cmd_gouter" /> - <message type="cmd" name="week-end" call="cmd_we" /> - <message type="cmd" name="weekend" call="cmd_we" /> -</nemubotmodule> diff --git a/modules/events/__init__.py b/modules/events/__init__.py deleted file mode 100644 index 5cbe628..0000000 --- a/modules/events/__init__.py +++ /dev/null @@ -1,244 +0,0 @@ -# coding=utf-8 - -import imp -import re -import sys -from datetime import timedelta -from datetime import datetime -import time -import threading -import traceback - -nemubotversion = 3.3 - -from event import ModuleEvent -from hooks import Hook - -def help_tiny (): - """Line inserted in the response to the command !help""" - return "events manager" - -def help_full (): - return "This module store a lot of events: ny, we, " + (", ".join(DATAS.index.keys())) + "\n!eventslist: gets list of timer\n!start /something/: launch a timer" - -CONTEXT = None - -def load(context): - global DATAS, CONTEXT - CONTEXT = context - #Define the index - DATAS.setIndex("name") - - for evt in DATAS.index.keys(): - if DATAS.index[evt].hasAttribute("end"): - event = ModuleEvent(call=fini, call_data=dict(strend=DATAS.index[evt])) - event.end = DATAS.index[evt].getDate("end") - idt = context.add_event(event) - if idt is not None: - DATAS.index[evt]["id"] = idt - - -def fini(d, strend): - for server in CONTEXT.servers.keys(): - if not strend.hasAttribute("server") or server == strend["server"]: - if strend["channel"] == CONTEXT.servers[server].nick: - CONTEXT.servers[server].send_msg_usr(strend["sender"], "%s: %s arrivé à échéance." % (strend["proprio"], strend["name"])) - else: - CONTEXT.servers[server].send_msg(strend["channel"], "%s: %s arrivé à échéance." % (strend["proprio"], strend["name"])) - DATAS.delChild(DATAS.index[strend["name"]]) - save() - -def cmd_gouter(msg): - ndate = datetime.today() - ndate = datetime(ndate.year, ndate.month, ndate.day, 16, 42) - return Response(msg.sender, - msg.countdown_format(ndate, - "Le goûter aura lieu dans %s, préparez vos biscuits !", - "Nous avons %s de retard pour le goûter :("), - channel=msg.channel) - -def cmd_we(msg): - ndate = datetime.today() + timedelta(5 - datetime.today().weekday()) - ndate = datetime(ndate.year, ndate.month, ndate.day, 0, 0, 1) - return Response(msg.sender, - msg.countdown_format(ndate, - "Il reste %s avant le week-end, courage ;)", - "Youhou, on est en week-end depuis %s."), - channel=msg.channel) - -def start_countdown(msg): - if msg.cmds[1] not in DATAS.index: - - strnd = ModuleState("strend") - strnd["server"] = msg.server - strnd["channel"] = msg.channel - strnd["proprio"] = msg.nick - strnd["sender"] = msg.sender - strnd["start"] = datetime.now() - strnd["name"] = msg.cmds[1] - DATAS.addChild(strnd) - - evt = ModuleEvent(call=fini, call_data=dict(strend=strnd)) - - if len(msg.cmds) > 2: - result1 = re.findall("([0-9]+)([smhdjwyaSMHDJWYA])?", msg.cmds[2]) - result2 = re.match("(.*[^0-9])?([0-3]?[0-9])/([0-1]?[0-9])/((19|20)?[01239][0-9])", msg.cmds[2]) - result3 = re.match("(.*[^0-9])?([0-2]?[0-9]):([0-5]?[0-9])(:([0-5]?[0-9]))?", msg.cmds[2]) - if result2 is not None or result3 is not None: - try: - now = datetime.now() - if result3 is None or result3.group(5) is None: sec = 0 - else: sec = int(result3.group(5)) - if result3 is None or result3.group(3) is None: minu = 0 - else: minu = int(result3.group(3)) - if result3 is None or result3.group(2) is None: hou = 0 - else: hou = int(result3.group(2)) - - 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) - elif result2 is not None: - strnd["end"] = datetime(int(result2.group(4)), int(result2.group(3)), int(result2.group(2))) - 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) - else: - strnd["end"] = datetime(now.year, now.month, now.day + 1, hou, minu, sec) - - evt.end = strnd.getDate("end") - strnd["id"] = CONTEXT.add_event(evt) - save() - return Response(msg.sender, "%s commencé le %s et se terminera le %s." % - (msg.cmds[1], datetime.now().strftime("%A %d %B %Y a %H:%M:%S"), - strnd.getDate("end").strftime("%A %d %B %Y a %H:%M:%S"))) - except: - DATAS.delChild(strnd) - return Response(msg.sender, - "Mauvais format de date pour l'evenement %s. Il n'a pas ete cree." % msg.cmds[1]) - elif result1 is not None and len(result1) > 0: - strnd["end"] = datetime.now() - for (t, g) in result1: - if g is None or g == "" or g == "m" or g == "M": - strnd["end"] += timedelta(minutes=int(t)) - elif g == "h" or g == "H": - strnd["end"] += timedelta(hours=int(t)) - elif g == "d" or g == "D" or g == "j" or g == "J": - strnd["end"] += timedelta(days=int(t)) - elif g == "w" or g == "W": - strnd["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) - else: - strnd["end"] += timedelta(seconds=int(t)) - evt.end = strnd.getDate("end") - strnd["id"] = CONTEXT.add_event(evt) - save() - return Response(msg.sender, "%s commencé le %s et se terminera le %s." % - (msg.cmds[1], datetime.now().strftime("%A %d %B %Y a %H:%M:%S"), - strnd.getDate("end").strftime("%A %d %B %Y a %H:%M:%S"))) - save() - return Response(msg.sender, "%s commencé le %s"% (msg.cmds[1], - datetime.now().strftime("%A %d %B %Y a %H:%M:%S"))) - else: - return Response(msg.sender, "%s existe déjà."% (msg.cmds[1]), channel=msg.channel, nick=msg.nick) - -def end_countdown(msg): - if len(msg.cmds) < 2: - return Response(msg.sender, "Quel événement terminer ?", channel=msg.channel, nick=msg.nick) - - if msg.cmds[1] in DATAS.index: - res = Response(msg.sender, - "%s a duré %s." % (msg.cmds[1], - msg.just_countdown(datetime.now () - DATAS.index[msg.cmds[1]].getDate("start"))), - channel=msg.channel, nick=msg.nick) - if DATAS.index[msg.cmds[1]]["proprio"] == msg.nick or (msg.cmds[0] == "forceend" and msg.is_owner): - CONTEXT.del_event(DATAS.index[msg.cmds[1]]["id"]) - DATAS.delChild(DATAS.index[msg.cmds[1]]) - save() - else: - res.append_message("Vous ne pouvez pas terminer le compteur %s, créé par %s."% (msg.cmds[1], DATAS.index[msg.cmds[1]]["proprio"])) - return res - else: - return Response(msg.sender, "%s n'est pas un compteur connu."% (msg.cmds[1]), channel=msg.channel, nick=msg.nick) - -def liste(msg): - if len(msg.cmds) > 1: - res = list() - for user in msg.cmds[1:]: - cmptr = [x["name"] for x in DATAS.index.values() if x["proprio"] == user] - if len(cmptr) > 0: - res.append("Compteurs créés par %s : %s" % (user, ", ".join(cmptr))) - else: - res.append("%s n'a pas créé de compteur" % user) - return Response(msg.sender, " ; ".join(res), channel=msg.channel) - else: - return Response(msg.sender, "Compteurs connus : %s." % ", ".join(DATAS.index.keys()), channel=msg.channel) - -def parseanswer(msg): - if msg.cmds[0] in DATAS.index: - if DATAS.index[msg.cmds[0]].name == "strend": - if DATAS.index[msg.cmds[0]].hasAttribute("end"): - return Response(msg.sender, "%s commencé il y a %s et se terminera dans %s." % (msg.cmds[0], msg.just_countdown(datetime.now() - DATAS.index[msg.cmds[0]].getDate("start")), msg.just_countdown(DATAS.index[msg.cmds[0]].getDate("end") - datetime.now())), channel=msg.channel) - else: - return Response(msg.sender, "%s commencé il y a %s." % (msg.cmds[0], msg.just_countdown(datetime.now () - DATAS.index[msg.cmds[0]].getDate("start"))), channel=msg.channel) - else: - save() - return Response(msg.sender, msg.countdown_format (DATAS.index[msg.cmds[0]].getDate("start"), DATAS.index[msg.cmds[0]]["msg_before"], DATAS.index[msg.cmds[0]]["msg_after"]), channel=msg.channel) - -def parseask(msg): - msgl = msg.content.lower() - if re.match("^.*((create|new) +(a|an|a +new|an *other)? *(events?|commande?)|(nouvel(le)?|ajoute|cr[ée]{1,3}) +(un)? *([eé]v[ée]nements?|commande?)).*$", msgl) is not None: - name = re.match("^.*!([^ \"'@!]+).*$", msg.content) - if name is not None and name.group (1) not in DATAS.index: - texts = re.match("^[^\"]*(avant|après|apres|before|after)?[^\"]*\"([^\"]+)\"[^\"]*((avant|après|apres|before|after)?.*\"([^\"]+)\".*)?$", msg.content) - if texts is not None and texts.group (3) is not None: - extDate = msg.extractDate () - if extDate is None or extDate == "": - return Response(msg.sender, "La date de l'événement est invalide...", channel=msg.channel) - else: - 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 and msg_after.find ("%s") != -1: - evt = ModuleState("event") - evt["server"] = msg.server - evt["channel"] = msg.channel - evt["proprio"] = msg.nick - evt["sender"] = msg.sender - evt["name"] = name.group(1) - evt["start"] = extDate - evt["msg_after"] = msg_after - evt["msg_before"] = msg_before - DATAS.addChild(evt) - save() - return Response(msg.sender, - "Nouvel événement !%s ajouté avec succès." % name.group(1), - msg.channel) - else: - return Response(msg.sender, - "Pour que l'événement soit valide, ajouter %s à" - " l'endroit où vous voulez que soit ajouté le" - " compte à rebours.") - 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["sender"] = msg.sender - evt["name"] = name.group(1) - evt["msg_before"] = texts.group (2) - DATAS.addChild(evt) - save() - return Response(msg.sender, "Nouvelle commande !%s ajoutée avec succès." % name.group(1)) - else: - return Response(msg.sender, "Veuillez indiquez les messages d'attente et d'après événement entre guillemets.") - elif name is None: - return Response(msg.sender, "Veuillez attribuer une commande à l'événement.") - else: - return Response(msg.sender, "Un événement portant ce nom existe déjà.") From ccff1c8b1e32b4e3268ee8201b213a326ad17d2c Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 11 Aug 2014 14:55:25 +0200 Subject: [PATCH 068/674] Birthday module: dusting --- modules/birthday.py | 14 ++++++++------ modules/birthday.xml | 5 ----- 2 files changed, 8 insertions(+), 11 deletions(-) delete mode 100644 modules/birthday.xml diff --git a/modules/birthday.py b/modules/birthday.py index 74013e0..9bb172d 100644 --- a/modules/birthday.py +++ b/modules/birthday.py @@ -10,16 +10,20 @@ from xmlparser.node import ModuleState nemubotversion = 3.3 def load(context): + from hooks import Hook + add_hook("cmd_hook", Hook(cmd_anniv, "anniv")) + add_hook("cmd_hook", Hook(cmd_age, "age")) + global DATAS DATAS.setIndex("name", "birthday") -def help_tiny (): +def help_tiny(): """Line inserted in the response to the command !help""" return "People birthdays and ages" -def help_full (): +def help_full(): return "!anniv /who/: gives the remaining time before the anniversary of /who/\n!age /who/: gives the age of /who/\nIf /who/ is not given, gives the remaining time before your anniversary.\n\n To set yout birthday, say it to nemubot :)" @@ -83,8 +87,7 @@ def cmd_age(msg): return True def parseask(msg): - msgl = msg.content.lower () - if re.match("^.*(date de naissance|birthday|geburtstag|née? |nee? le|born on).*$", msgl) is not None: + if re.match("^.*(date de naissance|birthday|geburtstag|née? |nee? le|born on).*$", msgl, re.I) is not None: try: extDate = msg.extractDate() if extDate is None or extDate.year > datetime.now().year: @@ -107,5 +110,4 @@ def parseask(msg): msg.channel, msg.nick) except: - return Response(msg.sender, "ta date de naissance ne paraît pas valide...", - msg.channel, msg.nick) + raise IRCException("ta date de naissance ne paraît pas valide.") diff --git a/modules/birthday.xml b/modules/birthday.xml deleted file mode 100644 index e03a15b..0000000 --- a/modules/birthday.xml +++ /dev/null @@ -1,5 +0,0 @@ -<?xml version="1.0" ?> -<nemubotmodule name="birthday"> - <message type="cmd" name="anniv" call="cmd_anniv" /> - <message type="cmd" name="age" call="cmd_age" /> -</nemubotmodule> From 582da6746fe1f74b4897ef529204c0507a8a832e Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 12 Aug 2014 17:45:38 +0200 Subject: [PATCH 069/674] Clean importer (following PEP8); print function in module now handles many argument as Python print --- importer.py | 66 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/importer.py b/importer.py index 8249b1f..579393c 100644 --- a/importer.py +++ b/importer.py @@ -144,25 +144,53 @@ class ModuleLoader(SourceLoader): # Set module common functions and datas module.__LOADED__ = True + def prnt(*args): + print("[%s]" % module.name, *args) + def prnt_dbg(*args): + if module.DEBUG: + print("{%s}" % module.name, *args) + + def mod_save(): + module.print_debug("Saving DATAS...") + module.DATAS.save(self.context.datas_path + "/" + module.name + ".xml") + + def send_response(server, res): + if server in self.context.servers: + return self.context.servers[server].send_response(res, None) + else: + print("\033[1;35mWarning:\033[0m Try to send a message to the unknown server: %s" % server) + return False + + def add_hook(store, hook): + return self.context.hooks.add_hook(store, hook, module) + def del_hook(store, hook): + return self.context.hooks.del_hook(store, hook) + def add_event(evt, eid=None): + return self.context.add_event(evt, eid, module_src=module) + def add_event_eid(evt, eid): + return add_event(evt, eid) + def del_event(evt): + return self.context.del_event(evt, module_src=module) + # Set module common functions and datas module.REGISTERED_HOOKS = list() module.REGISTERED_EVENTS = list() module.DEBUG = False module.DIR = self.mpath module.name = fullname - module.print = lambda msg: print("[%s] %s"%(module.name, msg)) - module.print_debug = lambda msg: mod_print_dbg(module, msg) - module.send_response = lambda srv, res: mod_send_response(self.context, srv, res) - module.add_hook = lambda store, hook: self.context.hooks.add_hook(store, hook, module) - module.del_hook = lambda store, hook: self.context.hooks.del_hook(store, hook) - module.add_event = lambda evt: self.context.add_event(evt, module_src=module) - module.add_event_eid = lambda evt, eid: self.context.add_event(evt, eid, module_src=module) - module.del_event = lambda evt: self.context.del_event(evt, module_src=module) + module.print = prnt + module.print_debug = prnt_dbg + module.send_response = send_response + module.add_hook = add_hook + module.del_hook = del_hook + module.add_event = add_event + module.add_event_eid = add_event_eid + module.del_event = del_event if not hasattr(module, "NODATA"): module.DATAS = xmlparser.parse_file(self.context.datas_path + module.name + ".xml") - module.save = lambda: mod_save(module, self.context.datas_path) + module.save = mod_save else: module.DATAS = None module.save = lambda: False @@ -232,23 +260,3 @@ def register_hooks(module, context, prompt): context.hooks.add_hook("ask_default", Hook(module.parseask), module) if hasattr(module, "parselisten"): context.hooks.add_hook("msg_default", Hook(module.parselisten), module) - -########################## -# # -# Module functions # -# # -########################## - -def mod_print_dbg(mod, msg): - if mod.DEBUG: - print("{%s} %s"%(mod.name, msg)) - -def mod_save(mod, datas_path): - mod.DATAS.save(datas_path + "/" + mod.name + ".xml") - mod.print_debug("Saving!") - -def mod_send_response(context, server, res): - if server in context.servers: - context.servers[server].send_response(res, None) - else: - print("\033[1;35mWarning:\033[0m Try to send a message to the unknown server: %s" % server) From 1464f92c8776a1c2642e206e63c882940d6fe8cd Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 12 Aug 2014 17:51:37 +0200 Subject: [PATCH 070/674] More explicit module unloading messages --- bot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 9995308..25dd67d 100644 --- a/bot.py +++ b/bot.py @@ -228,7 +228,7 @@ class Bot: def unload_module(self, name, verb=False): """Unload a module""" if name in self.modules: - print (name) + self.modules[name].print_debug("Unloading module %s" % name) self.modules[name].save() if hasattr(self.modules[name], "unload"): self.modules[name].unload(self) @@ -240,6 +240,7 @@ class Bot: self.del_event(e) # Remove from the dict del self.modules[name] + print(" Module `%s' successfully unloaded." % name) return True return False From 23bc61cce0d51999ac4b38dbb46907b8e61efe5e Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 12 Aug 2014 20:08:55 +0200 Subject: [PATCH 071/674] Can register hooks thanks to Python decorator; fixes #43 --- hooks.py | 11 ++++++++++- importer.py | 13 +++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/hooks.py b/hooks.py index e247be3..f9002e0 100644 --- a/hooks.py +++ b/hooks.py @@ -165,7 +165,7 @@ class MessagesHook: class Hook: """Class storing hook informations""" - def __init__(self, call, name=None, data=None, regexp=None, channels=list(), server=None, end=None, call_end=None): + def __init__(self, call, name=None, data=None, regexp=None, channels=list(), server=None, end=None, call_end=None, help=None): self.name = name self.end = end self.call = call @@ -178,6 +178,7 @@ class Hook: self.times = -1 self.server = server self.channels = channels + self.help = help def is_matching(self, strcmp, channel=None, server=None): """Test if the current hook correspond to the message""" @@ -222,3 +223,11 @@ class Hook: return call(msg, self.data, data2) except IRCException as e: return e.fill_response(msg) + +last_registered = [] + +def hook(store, *args, **kargs): + def sec(call): + last_registered.append((store, Hook(call, *args, **kargs))) + return call + return sec diff --git a/importer.py b/importer.py index 579393c..d7d0c80 100644 --- a/importer.py +++ b/importer.py @@ -24,7 +24,7 @@ import sys import event import exception -from hooks import Hook +import hooks import response import xmlparser @@ -241,6 +241,11 @@ def add_cap_hook(prompt, module, cmd): def register_hooks(module, context, prompt): """Register all available hooks""" + # Register decorated functions + for s, h in hooks.last_registered: + context.hooks.add_hook(s, h, module) + hooks.last_registered = [] + if module.CONF is not None: # Register command hooks if module.CONF.hasNode("command"): @@ -255,8 +260,8 @@ def register_hooks(module, context, prompt): # Register legacy hooks if hasattr(module, "parseanswer"): - context.hooks.add_hook("cmd_default", Hook(module.parseanswer), module) + context.hooks.add_hook("cmd_default", hooks.Hook(module.parseanswer), module) if hasattr(module, "parseask"): - context.hooks.add_hook("ask_default", Hook(module.parseask), module) + context.hooks.add_hook("ask_default", hooks.Hook(module.parseask), module) if hasattr(module, "parselisten"): - context.hooks.add_hook("msg_default", Hook(module.parselisten), module) + context.hooks.add_hook("msg_default", hooks.Hook(module.parselisten), module) From fe0f120038941e576f8efbb1d9a5e2892516e02a Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 12 Aug 2014 20:10:19 +0200 Subject: [PATCH 072/674] Using newly added Python decorator for hook registration --- modules/alias.py | 18 +++++++-------- modules/birthday.py | 7 +++--- modules/bonneannee.py | 21 +++++++----------- modules/conjugaison.py | 14 +++++------- modules/ddg/Wikipedia.py | 4 ++-- modules/ddg/__init__.py | 38 ++++++++++++++++---------------- modules/events.py | 16 ++++++-------- modules/imdb.py | 10 ++++----- modules/man.py | 13 +++++------ modules/networking.py | 21 ++++++++---------- modules/reddit.py | 12 +++++----- modules/rnd.py | 7 +++--- modules/sap.py | 8 +++---- modules/sleepytime.py | 13 +++++------ modules/sms.py | 10 ++++----- modules/spell/__init__.py | 14 +++++------- modules/syno.py | 16 ++++++-------- modules/watchWebsite/__init__.py | 11 ++++----- modules/ycc.py | 13 +++++------ 19 files changed, 119 insertions(+), 147 deletions(-) diff --git a/modules/alias.py b/modules/alias.py index 7edd95b..2ded1c5 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -4,19 +4,12 @@ import re import sys from datetime import datetime +from hooks import hook + nemubotversion = 3.3 def load(context): """Load this module""" - from hooks import Hook - add_hook("cmd_hook", Hook(cmd_listalias, "listalias")) - add_hook("cmd_hook", Hook(cmd_listvars, "listvars")) - add_hook("cmd_hook", Hook(cmd_unalias, "unalias")) - add_hook("cmd_hook", Hook(cmd_alias, "alias")) - add_hook("cmd_hook", Hook(cmd_set, "set")) - add_hook("all_pre", Hook(treat_alias)) - add_hook("all_post", Hook(treat_variables)) - global DATAS if not DATAS.hasNode("aliases"): DATAS.addChild(ModuleState("aliases")) @@ -56,6 +49,7 @@ def get_variable(name, msg=None): else: return "" +@hook("cmd_hook", "set") def cmd_set(msg): if len (msg.cmds) > 2: set_variable(msg.cmds[1], " ".join(msg.cmds[2:]), msg.nick) @@ -64,6 +58,7 @@ def cmd_set(msg): return res return Response(msg.sender, "!set prend au minimum deux arguments : le nom de la variable et sa valeur.") +@hook("cmd_hook", "listalias") def cmd_listalias(msg): if len(msg.cmds) > 1: res = list() @@ -77,6 +72,7 @@ def cmd_listalias(msg): else: return Response(msg.sender, "Alias connus : %s." % ", ".join(DATAS.getNode("aliases").index.keys()), channel=msg.channel) +@hook("cmd_hook", "listvars") def cmd_listvars(msg): if len(msg.cmds) > 1: res = list() @@ -90,6 +86,7 @@ def cmd_listvars(msg): else: return Response(msg.sender, "Variables connues : %s." % ", ".join(DATAS.getNode("variables").index.keys()), channel=msg.channel) +@hook("cmd_hook", "alias") def cmd_alias(msg): if len (msg.cmds) > 1: res = list() @@ -108,6 +105,7 @@ def cmd_alias(msg): return Response(msg.sender, "!alias prend en argument l'alias à étendre.", channel=msg.channel) +@hook("cmd_hook", "unalias") def cmd_unalias(msg): if len (msg.cmds) > 1: res = list() @@ -145,6 +143,7 @@ def replace_variables(cnt, msg=None): return " ".join(cnt) +@hook("all_post") def treat_variables(res): for i in range(0, len(res.messages)): if isinstance(res.messages[i], list): @@ -153,6 +152,7 @@ def treat_variables(res): res.messages[i] = replace_variables(res.messages[i], res) return True +@hook("all_pre") def treat_alias(msg, hooks_cache): if msg.cmd == "PRIVMSG": if len(msg.cmds) > 0 and (len(msg.cmds[0]) > 0 diff --git a/modules/birthday.py b/modules/birthday.py index 9bb172d..82a745e 100644 --- a/modules/birthday.py +++ b/modules/birthday.py @@ -5,15 +5,12 @@ import sys from datetime import datetime from datetime import date +from hooks import hook from xmlparser.node import ModuleState nemubotversion = 3.3 def load(context): - from hooks import Hook - add_hook("cmd_hook", Hook(cmd_anniv, "anniv")) - add_hook("cmd_hook", Hook(cmd_age, "age")) - global DATAS DATAS.setIndex("name", "birthday") @@ -44,6 +41,7 @@ def findName(msg): return (matches, name) +@hook("cmd_hook", "anniv") def cmd_anniv(msg): (matches, name) = findName(msg) if len(matches) == 1: @@ -71,6 +69,7 @@ def cmd_anniv(msg): " de %s. Quand est-il né ?" % name, msg.channel, msg.nick) +@hook("cmd_hook", "age") def cmd_age(msg): (matches, name) = findName(msg) if len(matches) == 1: diff --git a/modules/bonneannee.py b/modules/bonneannee.py index 9c65b2d..a72ca68 100644 --- a/modules/bonneannee.py +++ b/modules/bonneannee.py @@ -2,24 +2,17 @@ from datetime import datetime +from hooks import hook + nemubotversion = 3.3 +yr = datetime.today().year +yrn = datetime.today().year + 1 + def load(context): - yr = datetime.today().year - yrn = datetime.today().year + 1 - d = datetime(yrn, 1, 1, 0, 0, 0) - datetime.now() -# d = datetime(yr, 12, 31, 19, 34, 0) - datetime.now() add_event(ModuleEvent(intervalle=0, offset=d.total_seconds(), call=bonneannee)) - from hooks import Hook - add_hook("cmd_rgxp", Hook(cmd_timetoyear, data=yrn, regexp="^[0-9]{4}$")) - add_hook("cmd_hook", Hook(cmd_newyear, str(yrn), yrn)) - add_hook("cmd_hook", Hook(cmd_newyear, "ny", yrn)) - add_hook("cmd_hook", Hook(cmd_newyear, "newyear", yrn)) - add_hook("cmd_hook", Hook(cmd_newyear, "new-year", yrn)) - add_hook("cmd_hook", Hook(cmd_newyear, "new year", yrn)) - def bonneannee(): txt = "Bonne année %d !" % datetime.today().year print (txt) @@ -27,10 +20,11 @@ def bonneannee(): send_response("localhost:2771", Response(None, txt, "#yaka")) send_response("localhost:2771", Response(None, txt, "#epita2014")) send_response("localhost:2771", Response(None, txt, "#ykar")) - send_response("localhost:2771", Response(None, txt, "#ordissimo")) send_response("localhost:2771", Response(None, txt, "#42sh")) send_response("localhost:2771", Response(None, txt, "#nemubot")) +@hook("cmd_hook", "newyear") +@hook("cmd_hook", str(yrn), yrn) def cmd_newyear(msg, yr): return Response(msg.sender, msg.countdown_format(datetime(yr, 1, 1, 0, 0, 1), @@ -38,6 +32,7 @@ def cmd_newyear(msg, yr): "Nous faisons déjà la fête depuis %s !"), channel=msg.channel) +@hook("cmd_rgxp", data=yrn, regexp="^[0-9]{4}$") def cmd_timetoyear(msg, cur): yr = int(msg.cmds[0]) diff --git a/modules/conjugaison.py b/modules/conjugaison.py index ab30b7e..42c085e 100644 --- a/modules/conjugaison.py +++ b/modules/conjugaison.py @@ -5,6 +5,7 @@ import traceback import sys from urllib.parse import quote +from hooks import hook from tools import web from tools.web import striphtml from collections import defaultdict @@ -27,17 +28,14 @@ d = defaultdict(list) for k, v in s: d[k].append(v) -def help_tiny (): +def help_tiny(): return "Find french conjugaison" -def help_full (): +def help_full(): return "!conjugaison <tens> <verb>: give the conjugaison for <verb> in <tens>." -def load(context): - from hooks import Hook - add_hook("cmd_hook", Hook(cmd_conjug, "conjugaison")) - +@hook("cmd_hook", "conjugaison", help="!conjugaison <tens> <verb>: give the conjugaison for <verb> in <tens>.") def cmd_conjug(msg): if len(msg.cmds) < 3: raise IRCException("donne moi un temps et un verbe, et je te donnerai sa conjugaison!") @@ -50,8 +48,8 @@ def cmd_conjug(msg): conjug = get_conjug(verb, tens) if len(conjug) > 0: - return Response(msg.sender, conjug, channel=msg.channel, - title="Conjugaison de %s" % verb) + return Response(msg.sender, conjug, channel=msg.channel, + title="Conjugaison de %s" % verb) else: raise IRCException("aucune conjugaison de '%s' n'a été trouvé" % verb) diff --git a/modules/ddg/Wikipedia.py b/modules/ddg/Wikipedia.py index 314af38..5da4211 100644 --- a/modules/ddg/Wikipedia.py +++ b/modules/ddg/Wikipedia.py @@ -10,7 +10,7 @@ class Wikipedia: def __init__(self, terms, lang="fr", site="wikipedia.org", section=0): self.terms = terms self.lang = lang - self.curRT = 0 + self.curRT = section raw = urllib.request.urlopen(urllib.request.Request("http://" + self.lang + "." + site + "/w/api.php?format=xml&redirects&action=query&prop=revisions&rvprop=content&titles=%s" % (quote(terms)), headers={"User-agent": "Nemubot v3"})) self.wres = xmlparser.parse_string(raw.read()) @@ -18,7 +18,7 @@ class Wikipedia: self.wres = None else: self.wres = self.wres.getFirstNode("query").getFirstNode("pages").getFirstNode("page").getFirstNode("revisions").getFirstNode("rev").getContent() - self.wres = striplink(self.wres) + self.wres = striplink(self.wres) @property def nextRes(self): diff --git a/modules/ddg/__init__.py b/modules/ddg/__init__.py index 83c5895..560e897 100644 --- a/modules/ddg/__init__.py +++ b/modules/ddg/__init__.py @@ -2,6 +2,8 @@ import imp +from hooks import hook + nemubotversion = 3.3 from . import DDGSearch @@ -13,17 +15,6 @@ def load(context): global CONF WFASearch.CONF = CONF - from hooks import Hook - add_hook("cmd_hook", Hook(define, "define")) - add_hook("cmd_hook", Hook(search, "search")) - add_hook("cmd_hook", Hook(search, "ddg")) - add_hook("cmd_hook", Hook(search, "g")) - add_hook("cmd_hook", Hook(calculate, "wa")) - add_hook("cmd_hook", Hook(calculate, "calc")) - add_hook("cmd_hook", Hook(wiki, "dico")) - add_hook("cmd_hook", Hook(wiki, "wiki")) - add_hook("cmd_hook", Hook(udsearch, "urbandictionnary")) - def reload(): imp.reload(DDGSearch) imp.reload(UrbanDictionnary) @@ -31,6 +22,7 @@ def reload(): imp.reload(Wikipedia) +@hook("cmd_hook", "define") def define(msg): if len(msg.cmds) <= 1: return Response(msg.sender, @@ -45,7 +37,7 @@ def define(msg): return res - +@hook("cmd_hook", "search") def search(msg): if len(msg.cmds) <= 1: return Response(msg.sender, @@ -68,6 +60,7 @@ def search(msg): return res +@hook("cmd_hook", "urbandictionnary") def udsearch(msg): if len(msg.cmds) <= 1: return Response(msg.sender, @@ -85,6 +78,7 @@ def udsearch(msg): return res +@hook("cmd_hook", "calculate") def calculate(msg): if len(msg.cmds) <= 1: return Response(msg.sender, @@ -104,7 +98,19 @@ def calculate(msg): return Response(msg.sender, s.error, msg.channel) -def wiki(msg): +@hook("cmd_hook", "wikipedia") +def wikipedia(msg): + return wiki("wikipedia.org", 0, msg) + +@hook("cmd_hook", "wiktionary") +def wiktionary(msg): + return wiki("wiktionary.org", 1, msg) + +@hook("cmd_hook", "etymology") +def wiktionary(msg): + return wiki("wiktionary.org", 0, msg) + +def wiki(site, section, msg): if len(msg.cmds) <= 1: return Response(msg.sender, "Indicate a term to search", @@ -115,12 +121,6 @@ def wiki(msg): else: lang = "fr" extract = 1 - if msg.cmds[0] == "dico": - site = "wiktionary.org" - section = 1 - else: - site = "wikipedia.org" - section = 0 s = Wikipedia.Wikipedia(' '.join(msg.cmds[extract:]), lang, site, section) diff --git a/modules/events.py b/modules/events.py index de61c9e..59e5e82 100644 --- a/modules/events.py +++ b/modules/events.py @@ -12,7 +12,7 @@ import traceback nemubotversion = 3.3 from event import ModuleEvent -from hooks import Hook +from hooks import Hook, hook def help_tiny (): """Line inserted in the response to the command !help""" @@ -26,14 +26,6 @@ def load(context): #Define the index DATAS.setIndex("name") - add_hook("cmd_hook", Hook(start_countdown, "start")) - add_hook("cmd_hook", Hook(end_countdown, "end")) - add_hook("cmd_hook", Hook(end_countdown, "forceend")) - add_hook("cmd_hook", Hook(liste, "eventslist")) - - add_hook("cmd_hook", Hook(cmd_gouter, "goûter")) - add_hook("cmd_hook", Hook(cmd_we, "week-end")) - for evt in DATAS.index.keys(): if DATAS.index[evt].hasAttribute("end"): event = ModuleEvent(call=fini, call_data=dict(strend=DATAS.index[evt])) @@ -48,6 +40,7 @@ def fini(d, strend): DATAS.delChild(DATAS.index[strend["name"]]) save() +@hook("cmd_hook", "goûter") def cmd_gouter(msg): ndate = datetime.today() ndate = datetime(ndate.year, ndate.month, ndate.day, 16, 42) @@ -57,6 +50,7 @@ def cmd_gouter(msg): "Nous avons %s de retard pour le goûter :("), channel=msg.channel) +@hook("cmd_hook", "week-end") def cmd_we(msg): ndate = datetime.today() + timedelta(5 - datetime.today().weekday()) ndate = datetime(ndate.year, ndate.month, ndate.day, 0, 0, 1) @@ -66,6 +60,7 @@ def cmd_we(msg): "Youhou, on est en week-end depuis %s."), channel=msg.channel) +@hook("cmd_hook", "start", help="!start /something/: launch a timer") def start_countdown(msg): if len(msg.cmds) < 2: raise IRCException("indique le nom d'un événement à chronométrer") @@ -140,6 +135,8 @@ def start_countdown(msg): return Response(msg.sender, "%s commencé le %s"% (msg.cmds[1], datetime.now().strftime("%A %d %B %Y à %H:%M:%S"))) +@hook("cmd_hook", "end") +@hook("cmd_hook", "forceend") def end_countdown(msg): if len(msg.cmds) < 2: raise IRCException("quel événement terminer ?") @@ -157,6 +154,7 @@ def end_countdown(msg): else: return Response(msg.sender, "%s n'est pas un compteur connu."% (msg.cmds[1]), channel=msg.channel, nick=msg.nick) +@hook("cmd_hook", "eventslist", help="!eventslist: gets list of timer") def liste(msg): if len(msg.cmds) > 1: res = list() diff --git a/modules/imdb.py b/modules/imdb.py index 8ddfd27..b7b6b41 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -3,6 +3,8 @@ import urllib.request import json +from hooks import hook + nemubotversion = 3.3 def help_tiny (): @@ -11,12 +13,7 @@ def help_tiny (): def help_full (): return "Search a movie title with: !imdbs <approximative title> ; View movie details with !imdb <title>" -def load(context): - from hooks import Hook - add_hook("cmd_hook", Hook(cmd_imdb, "imdb")) - add_hook("cmd_hook", Hook(cmd_search, "imdbs")) - - +@hook("cmd_hook", "imdb", help="View movie details with !imdb <title>") def cmd_imdb(msg): if len(msg.cmds) < 2: raise IRCException("precise a movie/serie title!") @@ -46,6 +43,7 @@ def cmd_imdb(msg): raise IRCException("An error occurs during movie search") +@hook("cmd_hook", "imdbs", help="!imdbs <approximative title> to search a movie title") def cmd_search(msg): url = "http://www.omdbapi.com/?s=%s" % urllib.parse.quote(' '.join(msg.cmds[1:])) print_debug(url) diff --git a/modules/man.py b/modules/man.py index e12b84a..5177cfd 100644 --- a/modules/man.py +++ b/modules/man.py @@ -4,22 +4,20 @@ import subprocess import re import os +from hooks import hook + nemubotversion = 3.3 -def load(context): - from hooks import Hook - add_hook("cmd_hook", Hook(cmd_man, "MAN")) - add_hook("cmd_hook", Hook(cmd_whatis, "man")) - -def help_tiny (): +def help_tiny(): """Line inserted in the response to the command !help""" return "Read manual pages on IRC" -def help_full (): +def help_full(): return "!man [0-9] /what/: gives informations about /what/." RGXP_s = re.compile(b'\x1b\\[[0-9]+m') +@hook("cmd_hook", "MAN") def cmd_man(msg): args = ["man"] num = None @@ -48,6 +46,7 @@ def cmd_man(msg): return res +@hook("cmd_hook", "man") def cmd_whatis(msg): args = ["whatis", " ".join(msg.cmds[1:])] diff --git a/modules/networking.py b/modules/networking.py index 98f1d16..e1352c1 100644 --- a/modules/networking.py +++ b/modules/networking.py @@ -7,13 +7,12 @@ import socket import subprocess import urllib +from hooks import Hook, hook from tools import web nemubotversion = 3.3 def load(context): - from hooks import Hook - if not CONF or not CONF.hasNode("whoisxmlapi") or not CONF.getNode("whoisxmlapi").hasAttribute("username") or not CONF.getNode("whoisxmlapi").hasAttribute("password"): print ("You need a WhoisXML API account in order to use the " "!netwhois feature. Add it to the module configuration file:\n" @@ -22,21 +21,14 @@ def load(context): else: add_hook("cmd_hook", Hook(cmd_whois, "netwhois")) - add_hook("cmd_hook", Hook(cmd_w3c, "w3c")) - add_hook("cmd_hook", Hook(cmd_w3m, "w3m")) - add_hook("cmd_hook", Hook(cmd_traceurl, "traceurl")) - add_hook("cmd_hook", Hook(cmd_isup, "isup")) - add_hook("cmd_hook", Hook(cmd_curl, "curl")) - add_hook("cmd_hook", Hook(cmd_curly, "curly")) - - -def help_tiny (): +def help_tiny(): """Line inserted in the response to the command !help""" return "The networking module" -def help_full (): +def help_full(): return "!traceurl /url/: Follow redirections from /url/." +@hook("cmd_hook", "w3m") def cmd_w3m(msg): if len(msg.cmds) > 1: args = ["w3m", "-T", "text/html", "-dump"] @@ -49,6 +41,7 @@ def cmd_w3m(msg): else: raise IRCException("Veuillez indiquer une URL à visiter.") +@hook("cmd_hook", "curl") def cmd_curl(msg): if len(msg.cmds) > 1: try: @@ -68,6 +61,7 @@ def cmd_curl(msg): return Response(msg.sender, "Veuillez indiquer une URL à visiter.", channel=msg.channel) +@hook("cmd_hook", "curly") def cmd_curly(msg): if len(msg.cmds) > 1: url = msg.cmds[1] @@ -98,6 +92,7 @@ def cmd_curly(msg): else: raise IRCException("Veuillez indiquer une URL à visiter.") +@hook("cmd_hook", "traceurl") def cmd_traceurl(msg): if 1 < len(msg.cmds) < 6: res = list() @@ -190,6 +185,7 @@ def cmd_whois(msg): )) return res +@hook("cmd_hook", "isup") def cmd_isup(msg): if 1 < len(msg.cmds) < 6: res = list() @@ -257,6 +253,7 @@ def traceURL(url, timeout=5, stack=None): else: return stack +@hook("cmd_hook", "w3c") def cmd_w3c(msg): if len(msg.cmds) < 2: raise IRCException("Indiquer une URL à valider !") diff --git a/modules/reddit.py b/modules/reddit.py index 4f9af6b..11f1253 100644 --- a/modules/reddit.py +++ b/modules/reddit.py @@ -6,21 +6,18 @@ import urllib nemubotversion = 3.3 -def load(context): - from hooks import Hook - add_hook("cmd_hook", Hook(cmd_subreddit, "subreddit")) - add_hook("all_post", Hook(parseresponse)) +from hooks import hook - -def help_tiny (): +def help_tiny(): """Line inserted in the response to the command !help""" return "The subreddit module" -def help_full (): +def help_full(): return "!subreddit /subreddit/: Display information on the subreddit." LAST_SUBS = dict() +@hook("cmd_hook", "subreddit", help="!subreddit /subreddit/: Display information on the subreddit.") def cmd_subreddit(msg): global LAST_SUBS if len(msg.cmds) <= 1: @@ -74,6 +71,7 @@ def parselisten(msg): return False +@hook("all_post") def parseresponse(res): parselisten(res) return True diff --git a/modules/rnd.py b/modules/rnd.py index 24b2e26..9be87b9 100644 --- a/modules/rnd.py +++ b/modules/rnd.py @@ -2,12 +2,11 @@ import random +from hooks import hook + nemubotversion = 3.3 -def load(context): - from hooks import Hook - add_hook("cmd_hook", Hook(cmd_choice, "choice")) - +@hook("cmd_hook", "choice") def cmd_choice(msg): if len(msg.cmds) > 1: return Response(msg.sender, random.choice(msg.cmds[1:]), channel=msg.channel, nick=msg.nick) diff --git a/modules/sap.py b/modules/sap.py index 410c9e2..8f18c70 100644 --- a/modules/sap.py +++ b/modules/sap.py @@ -3,6 +3,8 @@ import urllib.request import json import re + +from hooks import hook from tools import web from tools.web import striphtml @@ -14,11 +16,7 @@ def help_tiny (): def help_full (): return "!tcode <transaction code|keywords>" -def load(context): - from hooks import Hook - add_hook("cmd_hook", Hook(cmd_tcode, "tcode")) - - +@hook("cmd_hook", "tcode") def cmd_tcode(msg): if len(msg.cmds) != 2: raise IRCException("indicate a transaction code or a keyword to search!") diff --git a/modules/sleepytime.py b/modules/sleepytime.py index a97d509..4a3b78f 100644 --- a/modules/sleepytime.py +++ b/modules/sleepytime.py @@ -5,21 +5,18 @@ import imp from datetime import datetime from datetime import timedelta +from hooks import hook + nemubotversion = 3.3 -def help_tiny (): +def help_tiny(): """Line inserted in the response to the command !help""" return "as http://sleepyti.me/, give you the best time to go to bed" -def help_full (): +def help_full(): return "If you would like to sleep soon, use !sleepytime to know the best time to wake up; use !sleepytime hh:mm if you want to wake up at hh:mm" -def load(context): - from hooks import Hook - add_hook("cmd_hook", Hook(cmd_sleep, "sleeptime")) - add_hook("cmd_hook", Hook(cmd_sleep, "sleepytime")) - - +@hook("cmd_hook", "sleepytime", help="If you would like to sleep soon, use !sleepytime to know the best time to wake up; use !sleepytime hh:mm if you want to wake up at hh:mm") def cmd_sleep(msg): if len (msg.cmds) > 1 and re.match("[0-9]{1,2}[h':.,-]([0-9]{1,2})?[m'\":.,-]?", msg.cmds[1]) is not None: diff --git a/modules/sms.py b/modules/sms.py index ab8c766..e2c5f6a 100644 --- a/modules/sms.py +++ b/modules/sms.py @@ -7,20 +7,19 @@ import urllib.error import urllib.request import urllib.parse +from hooks import hook + nemubotversion = 3.3 def load(context): global DATAS DATAS.setIndex("name", "phone") - from hooks import Hook - add_hook("cmd_hook", Hook(cmd_sms, "sms")) - -def help_tiny (): +def help_tiny(): """Line inserted in the response to the command !help""" return "Send SMS using SMS API (currently only Free Mobile)" -def help_full (): +def help_full(): return "!sms /who/[,/who/[,...]] message: send a SMS to /who/." def send_sms(frm, api_usr, api_key, content): @@ -46,6 +45,7 @@ def send_sms(frm, api_usr, api_key, content): return None +@hook("cmd_hook", "sms") def cmd_sms(msg): if len(msg.cmds) <= 2: raise IRCException("À qui veux-tu envoyer ce SMS ?") diff --git a/modules/spell/__init__.py b/modules/spell/__init__.py index 07f69ff..b20072b 100644 --- a/modules/spell/__init__.py +++ b/modules/spell/__init__.py @@ -3,27 +3,24 @@ import re from urllib.parse import quote +from hooks import hook + from .pyaspell import Aspell from .pyaspell import AspellError nemubotversion = 3.3 -def help_tiny (): +def help_tiny(): return "Check words spelling" -def help_full (): +def help_full(): return "!spell [<lang>] <word>: give the correct spelling of <word> in <lang=fr>." def load(context): global DATAS DATAS.setIndex("name", "score") - from hooks import Hook - add_hook("cmd_hook", Hook(cmd_spell, "spell")) - add_hook("cmd_hook", Hook(cmd_spell, "orthographe")) - add_hook("cmd_hook", Hook(cmd_score, "spellscore")) - - +@hook("cmd_hook", "spell") def cmd_spell(msg): if len(msg.cmds) < 2: raise IRCException("indique une orthographe approximative du mot dont tu veux vérifier l'orthographe.") @@ -62,6 +59,7 @@ def add_score(nick, t): DATAS.index[nick][t] = 1 save() +@hook("cmd_hook", "spellscore") def cmd_score(msg): global DATAS res = list() diff --git a/modules/syno.py b/modules/syno.py index cfb16a4..9a37ddf 100644 --- a/modules/syno.py +++ b/modules/syno.py @@ -5,24 +5,22 @@ import traceback import sys from urllib.parse import quote +from hooks import hook from tools import web nemubotversion = 3.3 -def help_tiny (): - return "Find french synonyms" +def help_tiny(): + return "Find french synonyms" -def help_full (): - return "!syno <word>: give a list of synonyms for <word>." - -def load(context): - from hooks import Hook - add_hook("cmd_hook", Hook(cmd_syno, "synonymes")) - add_hook("cmd_hook", Hook(cmd_anto, "antonymes")) +def help_full(): + return "!syno <word>: give a list of synonyms for <word>." +@hook("cmd_hook", "synonymes", help="!syno <word>: give a list of synonyms for <word>.") def cmd_syno(msg): return go("synonymes", msg) +@hook("cmd_hook", "antonymes") def cmd_anto(msg): return go("antonymes", msg) diff --git a/modules/watchWebsite/__init__.py b/modules/watchWebsite/__init__.py index 985a7fc..9e7845a 100644 --- a/modules/watchWebsite/__init__.py +++ b/modules/watchWebsite/__init__.py @@ -11,6 +11,8 @@ import urllib.parse from urllib.parse import urlparse from urllib.request import urlopen +from hooks import hook + from .atom import Atom nemubotversion = 3.3 @@ -24,11 +26,6 @@ def help_full (): def load(context): """Register watched website""" - from hooks import Hook - add_hook("cmd_hook", Hook(add_site, "watch", data="diff")) - add_hook("cmd_hook", Hook(add_site, "updown", data="updown")) - add_hook("cmd_hook", Hook(del_site, "unwatch")) - DATAS.setIndex("url", "watch") for site in DATAS.getNodes("watch"): if site.hasNode("alert"): @@ -56,6 +53,7 @@ def start_watching(site): site["_evt_id"] = add_event(evt) +@hook("cmd_hook", "unwatch") def del_site(msg): if len(msg.cmds) <= 1: raise IRCException("quel site dois-je arrêter de surveiller ?") @@ -79,6 +77,9 @@ def del_site(msg): channel=msg.channel, nick=msg.nick) raise IRCException("je ne surveillais pas cette URL !") + +@hook("cmd_hook", "watch", data="diff") +@hook("cmd_hook", "updown", data="updown") def add_site(msg, diffType="diff"): print (diffType) if len(msg.cmds) <= 1: diff --git a/modules/ycc.py b/modules/ycc.py index 7f0cc9c..878effa 100644 --- a/modules/ycc.py +++ b/modules/ycc.py @@ -5,20 +5,17 @@ from urllib.parse import urlparse from urllib.parse import quote from urllib.request import urlopen +from hooks import hook + nemubotversion = 3.3 -def help_tiny (): +def help_tiny(): """Line inserted in the response to the command !help""" return "Gets YCC urls" -def help_full (): +def help_full(): return "!ycc [<url>]: with an argument, reduce the given <url> thanks to ycc.fr; without argument, reduce the last URL said on the current channel." -def load(context): - from hooks import Hook - add_hook("cmd_hook", Hook(cmd_ycc, "ycc")) - add_hook("all_post", Hook(parseresponse)) - LAST_URLS = dict() def gen_response(res, msg, srv): @@ -29,6 +26,7 @@ def gen_response(res, msg, srv): else: raise IRCException("mauvaise URL : %s" % srv) +@hook("cmd_hook", "ycc", help="!ycc [<url>]: with an argument, reduce the given <url> thanks to ycc.fr; without argument, reduce the last URL said on the current channel.") def cmd_ycc(msg): if len(msg.cmds) == 1: global LAST_URLS @@ -71,6 +69,7 @@ def parselisten(msg): pass return False +@hook("all_post") def parseresponse(res): parselisten(res) return True From ef50f6a1c91f41f2f18c2c937818c316a5f62933 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 13 Aug 2014 15:25:29 +0200 Subject: [PATCH 073/674] Birthday module: fix regexp string used --- modules/birthday.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/birthday.py b/modules/birthday.py index 82a745e..7119af0 100644 --- a/modules/birthday.py +++ b/modules/birthday.py @@ -86,7 +86,7 @@ def cmd_age(msg): return True def parseask(msg): - if re.match("^.*(date de naissance|birthday|geburtstag|née? |nee? le|born on).*$", msgl, re.I) is not None: + if re.match("^.*(date de naissance|birthday|geburtstag|née? |nee? le|born on).*$", msg.content, re.I) is not None: try: extDate = msg.extractDate() if extDate is None or extDate.year > datetime.now().year: From 94a9a9a30b8c2592278d8630e94add3bead89b5a Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 13 Aug 2014 15:53:55 +0200 Subject: [PATCH 074/674] Switch to v3.4 branch --- bot.py | 4 +- modules/alias.py | 2 +- modules/birthday.py | 2 +- modules/bonneannee.py | 2 +- modules/chronos.py | 96 ----------- modules/chronos.xml | 6 - modules/cmd_server.py | 2 +- modules/conjugaison.py | 2 +- modules/ddg/__init__.py | 2 +- modules/events.py | 2 +- modules/imdb.py | 2 +- modules/man.py | 2 +- modules/networking.py | 2 +- modules/nextstop/__init__.py | 2 +- modules/qcm.xml | 7 - modules/qcm/Course.py | 31 ---- modules/qcm/Question.py | 93 ----------- modules/qcm/QuestionFile.py | 16 -- modules/qcm/Session.py | 67 -------- modules/qcm/User.py | 27 ---- modules/qcm/__init__.py | 197 ---------------------- modules/qd/DelayedTuple.py | 32 ---- modules/qd/GameUpdater.py | 60 ------- modules/qd/QDWrapper.py | 20 --- modules/qd/Score.py | 126 --------------- modules/qd/__init__.py | 224 -------------------------- modules/reddit.py | 2 +- modules/rnd.py | 2 +- modules/sap.py | 2 +- modules/sleepytime.py | 2 +- modules/sms.py | 2 +- modules/soutenance.xml | 5 - modules/soutenance/Delayed.py | 13 -- modules/soutenance/SiteSoutenances.py | 179 -------------------- modules/soutenance/Soutenance.py | 11 -- modules/soutenance/__init__.py | 48 ------ modules/spell/__init__.py | 2 +- modules/syno.py | 2 +- modules/translate.py | 2 +- modules/velib.py | 2 +- modules/watchWebsite/__init__.py | 2 +- modules/weather.py | 2 +- modules/whereis.xml | 28 ---- modules/whereis/Delayed.py | 5 - modules/whereis/UpdatedStorage.py | 57 ------- modules/whereis/User.py | 35 ---- modules/whereis/__init__.py | 206 ----------------------- modules/worldcup.py | 2 +- modules/ycc.py | 2 +- modules/youtube.py | 51 ------ 50 files changed, 26 insertions(+), 1666 deletions(-) delete mode 100644 modules/chronos.py delete mode 100644 modules/chronos.xml delete mode 100644 modules/qcm.xml delete mode 100644 modules/qcm/Course.py delete mode 100644 modules/qcm/Question.py delete mode 100644 modules/qcm/QuestionFile.py delete mode 100644 modules/qcm/Session.py delete mode 100644 modules/qcm/User.py delete mode 100644 modules/qcm/__init__.py delete mode 100644 modules/qd/DelayedTuple.py delete mode 100644 modules/qd/GameUpdater.py delete mode 100644 modules/qd/QDWrapper.py delete mode 100644 modules/qd/Score.py delete mode 100644 modules/qd/__init__.py delete mode 100644 modules/soutenance.xml delete mode 100644 modules/soutenance/Delayed.py delete mode 100644 modules/soutenance/SiteSoutenances.py delete mode 100644 modules/soutenance/Soutenance.py delete mode 100644 modules/soutenance/__init__.py delete mode 100644 modules/whereis.xml delete mode 100644 modules/whereis/Delayed.py delete mode 100644 modules/whereis/UpdatedStorage.py delete mode 100644 modules/whereis/User.py delete mode 100644 modules/whereis/__init__.py delete mode 100644 modules/youtube.py diff --git a/bot.py b/bot.py index 25dd67d..47363de 100644 --- a/bot.py +++ b/bot.py @@ -36,8 +36,8 @@ ID_letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" class Bot: def __init__(self, ip, realname, mp=list()): # Bot general informations - self.version = 3.3 - self.version_txt = "3.3" + self.version = 3.4 + self.version_txt = "3.4-dev" # Save various informations self.ip = ip diff --git a/modules/alias.py b/modules/alias.py index 2ded1c5..8b8eea6 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -6,7 +6,7 @@ from datetime import datetime from hooks import hook -nemubotversion = 3.3 +nemubotversion = 3.4 def load(context): """Load this module""" diff --git a/modules/birthday.py b/modules/birthday.py index 7119af0..bc9efcd 100644 --- a/modules/birthday.py +++ b/modules/birthday.py @@ -8,7 +8,7 @@ from datetime import date from hooks import hook from xmlparser.node import ModuleState -nemubotversion = 3.3 +nemubotversion = 3.4 def load(context): global DATAS diff --git a/modules/bonneannee.py b/modules/bonneannee.py index a72ca68..2d08038 100644 --- a/modules/bonneannee.py +++ b/modules/bonneannee.py @@ -4,7 +4,7 @@ from datetime import datetime from hooks import hook -nemubotversion = 3.3 +nemubotversion = 3.4 yr = datetime.today().year yrn = datetime.today().year + 1 diff --git a/modules/chronos.py b/modules/chronos.py deleted file mode 100644 index 261cb09..0000000 --- a/modules/chronos.py +++ /dev/null @@ -1,96 +0,0 @@ -# coding=utf-8 - -from datetime import datetime -from datetime import timedelta -from urllib.parse import quote - -from tools import web - -nemubotversion = 3.3 - -def help_tiny (): - """Line inserted in the response to the command !help""" - return "Gets informations about current and next Épita courses" - -def help_full (): - return "!chronos [spé] : gives current and next courses." - - -def get_courses(classe=None, room=None, teacher=None, date=None): - url = CONF.getNode("server")["url"] - if classe is not None: - url += "&class=" + quote(classe) - if room is not None: - url += "&room=" + quote(room) - if teacher is not None: - url += "&teacher=" + quote(teacher) - #TODO: date, not implemented at 23.tf - - print_debug(url) - response = web.getXML(url) - if response is not None: - print_debug(response) - return response.getNodes("course") - else: - return None - -def get_next_courses(classe=None, room=None, teacher=None, date=None): - courses = get_courses(classe, room, teacher, date) - now = datetime.now() - for c in courses: - start = c.getFirstNode("start").getDate() - - if now > start: - return c - return None - -def get_near_courses(classe=None, room=None, teacher=None, date=None): - courses = get_courses(classe, room, teacher, date) - return courses[0] - -def cmd_chronos(msg): - if len(msg.cmds) > 1: - classe = msg.cmds[1] - else: - classe = "" - - res = Response(msg.sender, channel=msg.channel, nomore="Je n'ai pas d'autre cours à afficher") - - courses = get_courses(classe) - print_debug(courses) - if courses is not None: - now = datetime.now() - tomorrow = now + timedelta(days=1) - for c in courses: - idc = c.getFirstNode("id").getContent() - crs = c.getFirstNode("title").getContent() - start = c.getFirstNode("start").getDate() - end = c.getFirstNode("end").getDate() - where = c.getFirstNode("where").getContent() - teacher = c.getFirstNode("teacher").getContent() - students = c.getFirstNode("students").getContent() - - if now > start: - title = "Actuellement " - msg = "\x03\x02" + crs + "\x03\x02 jusqu'" - if end < tomorrow: - msg += "à \x03\x02" + end.strftime("%H:%M") - else: - msg += "au \x03\x02" + end.strftime("%a %d à %H:%M") - msg += "\x03\x02 en \x03\x02" + where + "\x03\x02" - else: - title = "Prochainement " - duration = (end - start).total_seconds() / 60 - - msg = "\x03\x02" + crs + "\x03\x02 le \x03\x02" + start.strftime("%a %d à %H:%M") + "\x03\x02 pour " + "%dh%02d" % (int(duration / 60), duration % 60) + " en \x03\x02" + where + "\x03\x02" - - if teacher != "": - msg += " avec " + teacher - if students != "": - msg += " pour les " + students - - res.append_message(msg, title) - else: - res.append_message("Aucun cours n'a été trouvé") - - return res diff --git a/modules/chronos.xml b/modules/chronos.xml deleted file mode 100644 index 2dcf2b6..0000000 --- a/modules/chronos.xml +++ /dev/null @@ -1,6 +0,0 @@ -<?xml version="1.0" ?> -<nemubotmodule name="chronos"> - <server url="http://chronos.23.tf/index.php?xml" /> - <message type="cmd" name="chronos" call="cmd_chronos" /> - <message type="cmd" name="Χρονος" call="cmd_chronos" /> -</nemubotmodule> diff --git a/modules/cmd_server.py b/modules/cmd_server.py index e36f7b7..b898546 100644 --- a/modules/cmd_server.py +++ b/modules/cmd_server.py @@ -21,7 +21,7 @@ import sys from networkbot import NetworkBot -nemubotversion = 3.3 +nemubotversion = 3.4 NODATA = True def getserver(toks, context, prompt): diff --git a/modules/conjugaison.py b/modules/conjugaison.py index 42c085e..f8838df 100644 --- a/modules/conjugaison.py +++ b/modules/conjugaison.py @@ -10,7 +10,7 @@ from tools import web from tools.web import striphtml from collections import defaultdict -nemubotversion = 3.3 +nemubotversion = 3.4 s = [('present', '0'), ('présent', '0'), ('pr', '0'), ('passé simple', '12'), ('passe simple', '12'), ('ps', '12'), diff --git a/modules/ddg/__init__.py b/modules/ddg/__init__.py index 560e897..6896df4 100644 --- a/modules/ddg/__init__.py +++ b/modules/ddg/__init__.py @@ -4,7 +4,7 @@ import imp from hooks import hook -nemubotversion = 3.3 +nemubotversion = 3.4 from . import DDGSearch from . import UrbanDictionnary diff --git a/modules/events.py b/modules/events.py index 59e5e82..193cca7 100644 --- a/modules/events.py +++ b/modules/events.py @@ -9,7 +9,7 @@ import time import threading import traceback -nemubotversion = 3.3 +nemubotversion = 3.4 from event import ModuleEvent from hooks import Hook, hook diff --git a/modules/imdb.py b/modules/imdb.py index b7b6b41..e7c89a0 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -5,7 +5,7 @@ import json from hooks import hook -nemubotversion = 3.3 +nemubotversion = 3.4 def help_tiny (): return "Show many information about a movie or serie" diff --git a/modules/man.py b/modules/man.py index 5177cfd..7ad888f 100644 --- a/modules/man.py +++ b/modules/man.py @@ -6,7 +6,7 @@ import os from hooks import hook -nemubotversion = 3.3 +nemubotversion = 3.4 def help_tiny(): """Line inserted in the response to the command !help""" diff --git a/modules/networking.py b/modules/networking.py index e1352c1..7536063 100644 --- a/modules/networking.py +++ b/modules/networking.py @@ -10,7 +10,7 @@ import urllib from hooks import Hook, hook from tools import web -nemubotversion = 3.3 +nemubotversion = 3.4 def load(context): if not CONF or not CONF.hasNode("whoisxmlapi") or not CONF.getNode("whoisxmlapi").hasAttribute("username") or not CONF.getNode("whoisxmlapi").hasAttribute("password"): diff --git a/modules/nextstop/__init__.py b/modules/nextstop/__init__.py index 71816a8..6875239 100644 --- a/modules/nextstop/__init__.py +++ b/modules/nextstop/__init__.py @@ -6,7 +6,7 @@ from xml.dom.minidom import parseString from .external.src import ratp -nemubotversion = 3.3 +nemubotversion = 3.4 def load(context): global DATAS diff --git a/modules/qcm.xml b/modules/qcm.xml deleted file mode 100644 index 05a7076..0000000 --- a/modules/qcm.xml +++ /dev/null @@ -1,7 +0,0 @@ -<?xml version="1.0" ?> -<nemubotmodule name="qcm"> - <file name="main" url="/var/www/nemunai.re/bot/htdocs/questions.xml"/> - <file name="courses" url="/var/www/nemunai.re/bot/htdocs/courses.xml"/> - <file name="users" url="/var/www/nemunai.re/bot/htdocs/users.xml"/> - <server url="bot.nemunai.re" /> -</nemubotmodule> \ No newline at end of file diff --git a/modules/qcm/Course.py b/modules/qcm/Course.py deleted file mode 100644 index 9cddf1a..0000000 --- a/modules/qcm/Course.py +++ /dev/null @@ -1,31 +0,0 @@ -# coding=utf-8 - -COURSES = None - -class Course: - def __init__(self, iden): - global COURSES - if iden in COURSES.index: - self.node = COURSES.index[iden] - else: - self.node = { "code":"N/A", "name":"N/A", "branch":"N/A" } - - @property - def id(self): - return self.node["xml:id"] - - @property - def code(self): - return self.node["code"] - - @property - def name(self): - return self.node["name"] - - @property - def branch(self): - return self.node["branch"] - - @property - def validated(self): - return int(self.node["validated"]) > 0 diff --git a/modules/qcm/Question.py b/modules/qcm/Question.py deleted file mode 100644 index 6895680..0000000 --- a/modules/qcm/Question.py +++ /dev/null @@ -1,93 +0,0 @@ -# coding=utf-8 - -from datetime import datetime -import hashlib -import http.client -import socket -from urllib.parse import quote - -from .Course import Course -from .User import User - -QUESTIONS = None - -class Question: - def __init__(self, node): - self.node = node - - @property - def ident(self): - return self.node["xml:id"] - - @property - def id(self): - return self.node["xml:id"] - - @property - def question(self): - return self.node["question"] - - @property - def course(self): - return Course(self.node["course"]) - - @property - def answers(self): - return self.node.getNodes("answer") - - @property - def validator(self): - return User(self.node["validator"]) - - @property - def writer(self): - return User(self.node["writer"]) - - @property - def validated(self): - return self.node["validated"] - - @property - def addedtime(self): - return datetime.fromtimestamp(float(self.node["addedtime"])) - - @property - def author(self): - return User(self.node["writer"]) - - def report(self, raison="Sans raison"): - conn = http.client.HTTPConnection(CONF.getNode("server")["url"], timeout=10) - try: - conn.request("GET", "report.php?id=" + hashlib.md5(self.id.encode()).hexdigest() + "&raison=" + quote(raison)) - except socket.gaierror: - print ("[%s] impossible de récupérer la page %s."%(s, p)) - return False - res = conn.getresponse() - conn.close() - return (res.status == http.client.OK) - - @property - def tupleInfo(self): - return (self.author.username, self.validator.username, self.addedtime) - - @property - def bestAnswer(self): - best = self.answers[0] - for answer in self.answers: - if best.getInt("score") < answer.getInt("score"): - best = answer - return best["answer"] - - def isCorrect(self, msg): - msg = msg.lower().replace(" ", "") - for answer in self.answers: - if msg == answer["answer"].lower().replace(" ", ""): - return True - return False - - def getScore(self, msg): - msg = msg.lower().replace(" ", "") - for answer in self.answers: - if msg == answer["answer"].lower().replace(" ", ""): - return answer.getInt("score") - return 0 diff --git a/modules/qcm/QuestionFile.py b/modules/qcm/QuestionFile.py deleted file mode 100644 index 48ed23f..0000000 --- a/modules/qcm/QuestionFile.py +++ /dev/null @@ -1,16 +0,0 @@ -# coding=utf-8 - -import module_states_file as xmlparser - -from .Question import Question - -class QuestionFile: - def __init__(self, filename): - self.questions = xmlparser.parse_file(filename) - self.questions.setIndex("xml:id") - - def getQuestion(self, ident): - if ident in self.questions.index: - return Question(self.questions.index[ident]) - else: - return None diff --git a/modules/qcm/Session.py b/modules/qcm/Session.py deleted file mode 100644 index 11ab46b..0000000 --- a/modules/qcm/Session.py +++ /dev/null @@ -1,67 +0,0 @@ -# coding=utf-8 - -import threading - -SESSIONS = dict() - -from . import Question - -from response import Response - -class Session: - def __init__(self, srv, chan, sender): - self.questions = list() - self.current = -1 - self.score = 0 - self.good = 0 - self.bad = 0 - self.trys = 0 - self.timer = None - self.server = srv - self.channel = chan - self.sender = sender - - def addQuestion(self, ident): - if ident not in self.questions: - self.questions.append(ident) - return True - return False - - def next_question(self): - self.trys = 0 - self.current += 1 - return self.question - - @property - def question(self): - if self.current >= 0 and self.current < len(self.questions): - return Question.Question(Question.QUESTIONS.index[self.questions[self.current]]) - else: - return None - - def askNext(self, bfr = ""): - global SESSIONS - self.timer = None - nextQ = self.next_question() - if nextQ is not None: - if self.sender.split("!")[0] != self.channel: - self.server.send_response(Response(self.sender, "%s%s" % (bfr, nextQ.question), self.channel, nick=self.sender.split("!")[0])) - else: - self.server.send_response(Response(self.sender, "%s%s" % (bfr, nextQ.question), self.channel)) - else: - if self.good > 1: - goodS = "s" - else: - goodS = "" - - if self.sender.split("!")[0] != self.channel: - self.server.send_response(Response(self.sender, "%sFini, tu as donné %d bonne%s réponse%s sur %d questions." % (self.sender, bfr, self.good, goodS, goodS, len(self.questions)), self.channel, nick=self.sender.split("!")[0])) - else: - self.server.send_response(Response(self.sender, "%sFini, tu as donné %d bonne%s réponse%s sur %d questions." % (self.sender, bfr, self.good, goodS, goodS, len(self.questions)), self.channel)) - del SESSIONS[self.sender] - - def prepareNext(self, lag = 3): - if self.timer is None: - self.timer = threading.Timer(lag, self.askNext) - self.timer.start() - diff --git a/modules/qcm/User.py b/modules/qcm/User.py deleted file mode 100644 index 5f18831..0000000 --- a/modules/qcm/User.py +++ /dev/null @@ -1,27 +0,0 @@ -# coding=utf-8 - -USERS = None - -class User: - def __init__(self, iden): - global USERS - if iden in USERS.index: - self.node = USERS.index[iden] - else: - self.node = { "username":"N/A", "email":"N/A" } - - @property - def id(self): - return self.node["xml:id"] - - @property - def username(self): - return self.node["username"] - - @property - def email(self): - return self.node["email"] - - @property - def validated(self): - return int(self.node["validated"]) > 0 diff --git a/modules/qcm/__init__.py b/modules/qcm/__init__.py deleted file mode 100644 index b8b01df..0000000 --- a/modules/qcm/__init__.py +++ /dev/null @@ -1,197 +0,0 @@ -# coding=utf-8 - -from datetime import datetime -import http.client -import re -import random -import sys -import time - -import xmlparser - -nemubotversion = 3.2 - -def help_tiny (): - """Line inserted in the response to the command !help""" - return "MCQ module, working with http://bot.nemunai.re/" - -def help_full (): - return "!qcm [<nbQuest>] [<theme>]" - -from . import Question -from . import Course -from . import Session - -def load(context): - CONF.setIndex("name", "file") - -def buildSession(msg, categ = None, nbQuest = 10, channel = False): - if Question.QUESTIONS is None: - Question.QUESTIONS = xmlparser.parse_file(CONF.index["main"]["url"]) - Question.QUESTIONS.setIndex("xml:id") - Course.COURSES = xmlparser.parse_file(CONF.index["courses"]["url"]) - Course.COURSES.setIndex("xml:id") - User.USERS = xmlparser.parse_file(CONF.index["users"]["url"]) - User.USERS.setIndex("xml:id") - #Remove no validated questions - keys = list() - for k in Question.QUESTIONS.index.keys(): - keys.append(k) - for ques in keys: - if Question.QUESTIONS.index[ques]["validated"] != "1" or Question.QUESTIONS.index[ques]["reported"] == "1": - del Question.QUESTIONS.index[ques] - - #Apply filter - QS = list() - if categ is not None and len(categ) > 0: - #Find course id corresponding to categ - courses = list() - for c in Course.COURSES.childs: - if c["code"] in categ: - courses.append(c["xml:id"]) - - #Keep only questions matching course or branch - for q in Question.QUESTIONS.index.keys(): - if (Question.QUESTIONS.index[q]["branch"] is not None and Question.QUESTIONS.index[q]["branch"].find(categ)) or Question.QUESTIONS.index[q]["course"] in courses: - QS.append(q) - else: - for q in Question.QUESTIONS.index.keys(): - QS.append(q) - - nbQuest = min(nbQuest, len(QS)) - - if channel: - sess = Session.Session(msg.srv, msg.channel, msg.channel) - else: - sess = Session.Session(msg.srv, msg.channel, msg.sender) - maxQuest = len(QS) - 1 - for i in range(0, nbQuest): - while True: - q = QS[random.randint(0, maxQuest)] - if sess.addQuestion(q): - break - if channel: - Session.SESSIONS[msg.channel] = sess - else: - Session.SESSIONS[msg.realname] = sess - - -def askQuestion(msg, bfr = ""): - return Session.SESSIONS[msg.realname].askNext(bfr) - -def parseanswer(msg): - global DATAS - if msg.cmd[0] == "qcm" or msg.cmd[0] == "qcmchan" or msg.cmd[0] == "simulateqcm": - if msg.realname in Session.SESSIONS: - if len(msg.cmd) > 1: - if msg.cmd[1] == "stop" or msg.cmd[1] == "end": - sess = Session.SESSIONS[msg.realname] - if sess.good > 1: goodS = "s" - else: goodS = "" - del Session.SESSIONS[msg.realname] - return Response(msg.sender, - "Fini, tu as donné %d bonne%s réponse%s sur %d questions." % (sess.good, goodS, goodS, sess.current), - msg.channel, nick=msg.nick) - elif msg.cmd[1] == "next" or msg.cmd[1] == "suivant" or msg.cmd[1] == "suivante": - return askQuestion(msg) - return Response(msg.sender, "tu as déjà une session de QCM en cours, finis-la avant d'en commencer une nouvelle.", msg.channel, msg.nick) - elif msg.channel in Session.SESSIONS: - if len(msg.cmd) > 1: - if msg.cmd[1] == "stop" or msg.cmd[1] == "end": - sess = Session.SESSIONS[msg.channel] - if sess.good > 1: goodS = "s" - else: goodS = "" - del Session.SESSIONS[msg.channel] - return Response(msg.sender, "Fini, vous avez donné %d bonne%s réponse%s sur %d questions." % (sess.good, goodS, goodS, sess.current), msg.channel) - elif msg.cmd[1] == "next" or msg.cmd[1] == "suivant" or msg.cmd[1] == "suivante": - Session.SESSIONS[msg.channel].prepareNext(1) - return True - else: - nbQuest = 10 - filtre = list() - if len(msg.cmd) > 1: - for cmd in msg.cmd[1:]: - try: - tmp = int(cmd) - nbQuest = tmp - except ValueError: - filtre.append(cmd.upper()) - if len(filtre) == 0: - filtre = None - if msg.channel in Session.SESSIONS: - return Response(msg.sender, "Il y a deja une session de QCM sur ce chan.") - else: - buildSession(msg, filtre, nbQuest, msg.cmd[0] == "qcmchan") - if msg.cmd[0] == "qcm": - return askQuestion(msg) - elif msg.cmd[0] == "qcmchan": - return Session.SESSIONS[msg.channel].askNext() - else: - del Session.SESSIONS[msg.realname] - return Response(msg.sender, "QCM de %d questions" % len(Session.SESSIONS[msg.realname].questions), msg.channel) - return True - elif msg.realname in Session.SESSIONS: - if msg.cmd[0] == "info" or msg.cmd[0] == "infoquestion": - return Response(msg.sender, "Cette question a été écrite par %s et validée par %s, le %s" % Session.SESSIONS[msg.realname].question.tupleInfo, msg.channel) - elif msg.cmd[0] == "report" or msg.cmd[0] == "reportquestion": - if len(msg.cmd) == 1: - return Response(msg.sender, "Veuillez indiquer une raison de report", msg.channel) - elif Session.SESSIONS[msg.realname].question.report(' '.join(msg.cmd[1:])): - return Response(msg.sender, "Cette question vient d'être signalée.", msg.channel) - Session.SESSIONS[msg.realname].askNext() - else: - return Response(msg.sender, "Une erreur s'est produite lors du signalement de la question, veuillez recommencer plus tard.", msg.channel) - elif msg.channel in Session.SESSIONS: - if msg.cmd[0] == "info" or msg.cmd[0] == "infoquestion": - return Response(msg.sender, "Cette question a été écrite par %s et validée par %s, le %s" % Session.SESSIONS[msg.channel].question.tupleInfo, msg.channel) - elif msg.cmd[0] == "report" or msg.cmd[0] == "reportquestion": - if len(msg.cmd) == 1: - return Response(msg.sender, "Veuillez indiquer une raison de report", msg.channel) - elif Session.SESSIONS[msg.channel].question.report(' '.join(msg.cmd[1:])): - Session.SESSIONS[msg.channel].prepareNext() - return Response(msg.sender, "Cette question vient d'être signalée.", msg.channel) - else: - return Response(msg.sender, "Une erreur s'est produite lors du signalement de la question, veuillez recommencer plus tard.", msg.channel) - else: - if msg.cmd[0] == "listecours": - if Course.COURSES is None: - return Response(msg.sender, "La liste de cours n'est pas encore construite, lancez un QCM pour la construire.", msg.channel) - else: - res = Response(msg.sender, channel=msg.channel, title="Liste des cours existants : ") - res.append_message([cours["code"] + " (" + cours["name"] + ")" for cours in Course.COURSES.getNodes("course")]) - return res - elif msg.cmd[0] == "refreshqcm": - Question.QUESTIONS = None - Course.COURSES = None - User.USERS = None - return True - return False - -def parseask(msg): - if msg.realname in Session.SESSIONS: - dest = msg.realname - - if Session.SESSIONS[dest].question.isCorrect(msg.content): - Session.SESSIONS[dest].good += 1 - Session.SESSIONS[dest].score += Session.SESSIONS[dest].question.getScore(msg.content) - return askQuestion(msg, "correct ; ") - else: - Session.SESSIONS[dest].bad += 1 - if Session.SESSIONS[dest].trys == 0: - Session.SESSIONS[dest].trys = 1 - return Response(msg.sender, "non, essaie encore :p", msg.channel, msg.nick) - else: - return askQuestion(msg, "non, la bonne reponse était : %s ; " % Session.SESSIONS[dest].question.bestAnswer) - - elif msg.channel in Session.SESSIONS: - dest = msg.channel - - if Session.SESSIONS[dest].question.isCorrect(msg.content): - Session.SESSIONS[dest].good += 1 - Session.SESSIONS[dest].score += Session.SESSIONS[dest].question.getScore(msg.content) - Session.SESSIONS[dest].prepareNext() - return Response(msg.sender, "correct :)", msg.channel, nick=msg.nick) - else: - Session.SESSIONS[dest].bad += 1 - return Response(msg.sender, "non, essaie encore :p", msg.channel, nick=msg.nick) - return False diff --git a/modules/qd/DelayedTuple.py b/modules/qd/DelayedTuple.py deleted file mode 100644 index a81ac5d..0000000 --- a/modules/qd/DelayedTuple.py +++ /dev/null @@ -1,32 +0,0 @@ -# coding=utf-8 - -import re -import threading - -class DelayedTuple: - def __init__(self, regexp, great): - self.delayEvnt = threading.Event() - self.msg = None - self.regexp = regexp - self.great = great - - def triche(self, res): - if res is not None: - return re.match(".*" + self.regexp + ".*", res.lower() + " ") is None - else: - return True - - def perfect(self, res): - if res is not None: - return re.match(".*" + self.great + ".*", res.lower() + " ") is not None - else: - return False - - def good(self, res): - if res is not None: - return re.match(".*" + self.regexp + ".*", res.lower() + " ") is not None - else: - return False - - def wait(self, timeout): - self.delayEvnt.wait(timeout) diff --git a/modules/qd/GameUpdater.py b/modules/qd/GameUpdater.py deleted file mode 100644 index 7449489..0000000 --- a/modules/qd/GameUpdater.py +++ /dev/null @@ -1,60 +0,0 @@ -# coding=utf-8 - -from datetime import datetime -import random -import threading -from .DelayedTuple import DelayedTuple - -DELAYED = dict() - -LASTQUESTION = 99999 - -class GameUpdater(threading.Thread): - def __init__(self, msg, bfrseen): - self.msg = msg - self.bfrseen = bfrseen - threading.Thread.__init__(self) - - def run(self): - global DELAYED, LASTQUESTION - - if self.bfrseen is not None: - seen = datetime.now() - self.bfrseen - rnd = random.randint(0, int(seen.seconds/90)) - else: - rnd = 1 - - if rnd != 0: - QUESTIONS = CONF.getNodes("question") - - if self.msg.channel == "#nemutest": - quest = 9 - else: - if LASTQUESTION >= len(QUESTIONS): - print (QUESTIONS) - random.shuffle(QUESTIONS) - LASTQUESTION = 0 - quest = LASTQUESTION - LASTQUESTION += 1 - - question = QUESTIONS[quest]["question"] - regexp = QUESTIONS[quest]["regexp"] - great = QUESTIONS[quest]["great"] - self.msg.send_chn("%s: %s" % (self.msg.nick, question)) - - DELAYED[self.msg.nick] = DelayedTuple(regexp, great) - - DELAYED[self.msg.nick].wait(20) - - if DELAYED[self.msg.nick].triche(DELAYED[self.msg.nick].msg): - getUser(self.msg.nick).playTriche() - self.msg.send_chn("%s: Tricheur !" % self.msg.nick) - elif DELAYED[self.msg.nick].perfect(DELAYED[self.msg.nick].msg): - if random.randint(0, 10) == 1: - getUser(self.msg.nick).bonusQuestion() - self.msg.send_chn("%s: Correct !" % self.msg.nick) - else: - self.msg.send_chn("%s: J'accepte" % self.msg.nick) - del DELAYED[self.msg.nick] - SCORES.save(self.msg.nick) - save() diff --git a/modules/qd/QDWrapper.py b/modules/qd/QDWrapper.py deleted file mode 100644 index 41b2eff..0000000 --- a/modules/qd/QDWrapper.py +++ /dev/null @@ -1,20 +0,0 @@ -# coding=utf-8 - -from tools.wrapper import Wrapper -from .Score import Score - -class QDWrapper(Wrapper): - def __init__(self, datas): - Wrapper.__init__(self) - self.DATAS = datas - self.stateName = "player" - self.attName = "name" - - def __getitem__(self, i): - if i in self.cache: - return self.cache[i] - else: - sc = Score() - sc.parse(Wrapper.__getitem__(self, i)) - self.cache[i] = sc - return sc diff --git a/modules/qd/Score.py b/modules/qd/Score.py deleted file mode 100644 index 52c5692..0000000 --- a/modules/qd/Score.py +++ /dev/null @@ -1,126 +0,0 @@ -# coding=utf-8 - -from datetime import datetime - -class Score: - """Manage the user's scores""" - def __init__(self): - #FourtyTwo - self.ftt = 0 - #TwentyThree - self.twt = 0 - self.pi = 0 - self.notfound = 0 - self.tententen = 0 - self.leet = 0 - self.great = 0 - self.bad = 0 - self.triche = 0 - self.last = None - self.changed = False - - def parse(self, item): - self.ftt = item.getInt("fourtytwo") - self.twt = item.getInt("twentythree") - self.pi = item.getInt("pi") - self.notfound = item.getInt("notfound") - self.tententen = item.getInt("tententen") - self.leet = item.getInt("leet") - self.great = item.getInt("great") - self.bad = item.getInt("bad") - self.triche = item.getInt("triche") - - def save(self, state): - state.setAttribute("fourtytwo", self.ftt) - state.setAttribute("twentythree", self.twt) - state.setAttribute("pi", self.pi) - state.setAttribute("notfound", self.notfound) - state.setAttribute("tententen", self.tententen) - state.setAttribute("leet", self.leet) - state.setAttribute("great", self.great) - state.setAttribute("bad", self.bad) - state.setAttribute("triche", self.triche) - - def merge(self, other): - self.ftt += other.ftt - self.twt += other.twt - self.pi += other.pi - self.notfound += other.notfound - self.tententen += other.tententen - self.leet += other.leet - self.great += other.great - self.bad += other.bad - self.triche += other.triche - - def newWinner(self): - self.ftt = 0 - self.twt = 0 - self.pi = 1 - self.notfound = 1 - self.tententen = 0 - self.leet = 1 - self.great = -1 - self.bad = -4 - self.triche = 0 - - def isWinner(self): - return self.great >= 42 - - def playFtt(self): - if self.canPlay(): - self.ftt += 1 - def playTwt(self): - if self.canPlay(): - self.twt += 1 - def playSuite(self): - self.canPlay() - self.twt += 1 - self.great += 1 - def playPi(self): - if self.canPlay(): - self.pi += 1 - def playNotfound(self): - if self.canPlay(): - self.notfound += 1 - def playTen(self): - if self.canPlay(): - self.tententen += 1 - def playLeet(self): - if self.canPlay(): - self.leet += 1 - def playGreat(self): - if self.canPlay(): - self.great += 1 - def playBad(self): - if self.canPlay(): - self.bad += 1 - self.great += 1 - def playTriche(self): - self.triche += 1 - def oupsTriche(self): - self.triche -= 1 - def bonusQuestion(self): - return - - def toTuple(self): - return (self.ftt, self.twt, self.pi, self.notfound, self.tententen, self.leet, self.great, self.bad, self.triche) - - def canPlay(self): - now = datetime.now() - ret = self.last == None or self.last.minute != now.minute or self.last.hour != now.hour or self.last.day != now.day - self.changed = self.changed or ret - return ret - - def hasChanged(self): - if self.changed: - self.changed = False - self.last = datetime.now() - return True - else: - return False - - def score(self): - return (self.ftt * 2 + self.great * 5 + self.leet * 13.37 + (self.pi + 1) * 3.1415 * (self.notfound + 1) + self.tententen * 10 + self.twt - (self.bad + 1) * 10 * (self.triche * 5 + 1) + 7) - - def details(self): - return "42: %d, 23: %d, leet: %d, pi: %d, 404: %d, 10: %d, great: %d, bad: %d, triche: %d = %d."%(self.ftt, self.twt, self.leet, self.pi, self.notfound, self.tententen, self.great, self.bad, self.triche, self.score()) diff --git a/modules/qd/__init__.py b/modules/qd/__init__.py deleted file mode 100644 index 871512b..0000000 --- a/modules/qd/__init__.py +++ /dev/null @@ -1,224 +0,0 @@ -# coding=utf-8 - -import re -import imp -from datetime import datetime - -nemubotversion = 3.0 - -from . import GameUpdater -from . import QDWrapper -from . import Score - -channels = "#nemutest #42sh #ykar #epitagueule" -LASTSEEN = dict () -temps = dict () - -SCORES = None - -def load(context): - global DATAS, SCORES, CONF - DATAS.setIndex("name", "player") - SCORES = QDWrapper.QDWrapper(DATAS) - GameUpdater.SCORES = SCORES - GameUpdater.CONF = CONF - GameUpdater.save = save - GameUpdater.getUser = getUser - -def reload(): - imp.reload(GameUpdater) - imp.reload(QDWrapper) - imp.reload(Score) - - -def help_tiny (): - """Line inserted in the response to the command !help""" - return "42 game!" - -def help_full (): - return "!42: display scores\n!42 help: display the performed calculate\n!42 manche: display information about current round\n!42 /who/: show the /who/'s scores" - - -def parseanswer (msg): - if msg.cmd[0] == "42" or msg.cmd[0] == "score" or msg.cmd[0] == "scores": - global SCORES - if len(msg.cmd) > 2 and msg.is_owner and ((msg.cmd[1] == "merge" and len(msg.cmd) > 3) or msg.cmd[1] == "oupstriche"): - if msg.cmd[2] in SCORES and (len(msg.cmd) <= 3 or msg.cmd[3] in SCORES): - if msg.cmd[1] == "merge": - SCORES[msg.cmd[2]].merge (SCORES[msg.cmd[3]]) - del SCORES[msg.cmd[3]] - msg.send_chn ("%s a été correctement fusionné avec %s."%(msg.cmd[3], msg.cmd[2])) - elif msg.cmd[1] == "oupstriche": - SCORES[msg.cmd[2]].oupsTriche() - else: - if msg.cmd[2] not in SCORES: - msg.send_chn ("%s n'est pas un joueur connu."%msg.cmd[2]) - elif msg.cmd[3] not in SCORES: - msg.send_chn ("%s n'est pas un joueur connu."%msg.cmd[3]) - elif len(msg.cmd) > 1 and (msg.cmd[1] == "help" or msg.cmd[1] == "aide"): - msg.send_chn ("Formule : \"42\" * 2 + great * 5 + leet * 13.37 + (pi + 1) * 3.1415 * (not_found + 1) + tententen * 10 + \"23\" - (bad + 1) * 10 * (triche * 5 + 1) + 7") - elif len(msg.cmd) > 1 and (msg.cmd[1] == "manche" or msg.cmd[1] == "round"): - manche = DATAS.getNode("manche") - msg.send_chn ("Nous sommes dans la %de manche, gagnée par %s avec %d points et commencée par %s le %s." % (manche.getInt("number"), manche["winner"], manche.getInt("winner_score"), manche["who"], manche.getDate("date"))) - #elif msg.channel == "#nemutest": - else: - phrase = "" - - if len(msg.cmd) > 1: - if msg.cmd[1] in SCORES: - phrase += " " + msg.cmd[1] + ": " + SCORES[msg.cmd[1]].details() - else: - phrase = " %s n'a encore jamais joué,"%(msg.cmd[1]) - else: - for nom, scr in sorted(SCORES.items(), key=rev, reverse=True): - score = scr.score() - if score != 0: - if phrase == "": - phrase = " *%s.%s: %d*,"%(nom[0:1], nom[1:len(nom)], score) - else: - phrase += " %s.%s: %d,"%(nom[0:1], nom[1:len(nom)], score) - - msg.send_chn ("Scores :%s" % (phrase[0:len(phrase)-1])) - return True - else: - return False - - -def win(msg): - global SCORES - who = msg.nick - - manche = DATAS.getNode("manche") - - maxi_scor = 0 - maxi_name = None - - for player in DATAS.index.keys(): - scr = SCORES[player].score() - if scr > maxi_scor: - maxi_scor = scr - maxi_name = player - - for player in DATAS.index.keys(): - scr = SCORES[player].score() - if scr > maxi_scor / 3: - del SCORES[player] - else: - DATAS.index[player]["great"] = 0 - SCORES.flush() - - if who != maxi_name: - msg.send_chn ("Félicitations %s, tu remportes cette manche terminée par %s, avec un score de %d !"%(maxi_name, who, maxi_scor)) - else: - msg.send_chn ("Félicitations %s, tu remportes cette manche avec %d points !"%(maxi_name, maxi_scor)) - - manche.setAttribute("number", manche.getInt("number") + 1) - manche.setAttribute("winner", maxi_name) - manche.setAttribute("winner_score", maxi_scor) - manche.setAttribute("who", who) - manche.setAttribute("date", datetime.now()) - - print ("Nouvelle manche !") - save() - - -def parseask (msg): - if len(GameUpdater.DELAYED) > 0: - if msg.nick in GameUpdater.DELAYED: - GameUpdater.DELAYED[msg.nick].msg = msg.content - GameUpdater.DELAYED[msg.nick].delayEvnt.set() - return True - return False - - - -def rev (tupl): - (k, v) = tupl - return (v.score(), k) - - -def getUser(name): - global SCORES - if name not in SCORES: - SCORES[name] = Score.Score() - return SCORES[name] - - -def parselisten (msg): - if len(GameUpdater.DELAYED) > 0 and msg.nick in GameUpdater.DELAYED and GameUpdater.DELAYED[msg.nick].good(msg.content): - msg.send_chn("%s: n'oublie pas le nemubot: devant ta réponse pour qu'elle soit prise en compte !" % msg.nick) - - bfrseen = None - if msg.realname in LASTSEEN: - bfrseen = LASTSEEN[msg.realname] - LASTSEEN[msg.realname] = datetime.now() - -# if msg.channel == "#nemutest" and msg.nick not in GameUpdater.DELAYED: - if msg.channel != "#nemutest" and msg.nick not in GameUpdater.DELAYED: - - if re.match("^(42|quarante[- ]?deux).{,2}$", msg.content.strip().lower()): - if msg.time.minute == 10 and msg.time.second == 10 and msg.time.hour == 10: - getUser(msg.nick).playTen() - getUser(msg.nick).playGreat() - elif msg.time.minute == 42: - if msg.time.second == 0: - getUser(msg.nick).playGreat() - getUser(msg.nick).playFtt() - else: - getUser(msg.nick).playBad() - - if re.match("^(23|vingt[ -]?trois).{,2}$", msg.content.strip().lower()): - if msg.time.minute == 23: - if msg.time.second == 0: - getUser(msg.nick).playGreat() - getUser(msg.nick).playTwt() - else: - getUser(msg.nick).playBad() - - if re.match("^(10){3}.{,2}$", msg.content.strip().lower()): - if msg.time.minute == 10 and msg.time.hour == 10: - if msg.time.second == 10: - getUser(msg.nick).playGreat() - getUser(msg.nick).playTen() - else: - getUser(msg.nick).playBad() - - if re.match("^0?12345.{,2}$", msg.content.strip().lower()): - if msg.time.hour == 1 and msg.time.minute == 23 and (msg.time.second == 45 or (msg.time.second == 46 and msg.time.microsecond < 330000)): - getUser(msg.nick).playSuite() - else: - getUser(msg.nick).playBad() - - if re.match("^[1l][e3]{2}[t7] ?t?ime.{,2}$", msg.content.strip().lower()): - if msg.time.hour == 13 and msg.time.minute == 37: - if msg.time.second == 0: - getUser(msg.nick).playGreat() - getUser(msg.nick).playLeet() - else: - getUser(msg.nick).playBad() - - if re.match("^(pi|3.14) ?time.{,2}$", msg.content.strip().lower()): - if msg.time.hour == 3 and msg.time.minute == 14: - if msg.time.second == 15 or msg.time.second == 16: - getUser(msg.nick).playGreat() - getUser(msg.nick).playPi() - else: - getUser(msg.nick).playBad() - - if re.match("^(404( ?time)?|time ?not ?found).{,2}$", msg.content.strip().lower()): - if msg.time.hour == 4 and msg.time.minute == 4: - if msg.time.second == 0 or msg.time.second == 4: - getUser(msg.nick).playGreat() - getUser(msg.nick).playNotfound() - else: - getUser(msg.nick).playBad() - - if getUser(msg.nick).isWinner(): - print ("Nous avons un vainqueur ! Nouvelle manche :p") - win(msg) - return True - elif getUser(msg.nick).hasChanged(): - gu = GameUpdater.GameUpdater(msg, bfrseen) - gu.start() - return True - return False diff --git a/modules/reddit.py b/modules/reddit.py index 11f1253..3500aac 100644 --- a/modules/reddit.py +++ b/modules/reddit.py @@ -4,7 +4,7 @@ import json import re import urllib -nemubotversion = 3.3 +nemubotversion = 3.4 from hooks import hook diff --git a/modules/rnd.py b/modules/rnd.py index 9be87b9..812b88e 100644 --- a/modules/rnd.py +++ b/modules/rnd.py @@ -4,7 +4,7 @@ import random from hooks import hook -nemubotversion = 3.3 +nemubotversion = 3.4 @hook("cmd_hook", "choice") def cmd_choice(msg): diff --git a/modules/sap.py b/modules/sap.py index 8f18c70..139a5ee 100644 --- a/modules/sap.py +++ b/modules/sap.py @@ -8,7 +8,7 @@ from hooks import hook from tools import web from tools.web import striphtml -nemubotversion = 3.3 +nemubotversion = 3.4 def help_tiny (): return "Find information about an SAP transaction codes" diff --git a/modules/sleepytime.py b/modules/sleepytime.py index 4a3b78f..681ed42 100644 --- a/modules/sleepytime.py +++ b/modules/sleepytime.py @@ -7,7 +7,7 @@ from datetime import timedelta from hooks import hook -nemubotversion = 3.3 +nemubotversion = 3.4 def help_tiny(): """Line inserted in the response to the command !help""" diff --git a/modules/sms.py b/modules/sms.py index e2c5f6a..8fc8fb6 100644 --- a/modules/sms.py +++ b/modules/sms.py @@ -9,7 +9,7 @@ import urllib.parse from hooks import hook -nemubotversion = 3.3 +nemubotversion = 3.4 def load(context): global DATAS diff --git a/modules/soutenance.xml b/modules/soutenance.xml deleted file mode 100644 index 957423b..0000000 --- a/modules/soutenance.xml +++ /dev/null @@ -1,5 +0,0 @@ -<?xml version="1.0" ?> -<nemubotmodule name="soutenance"> - <server ip="www.acu.epita.fr" url="/intra/sout_liste.html" /> - <message type="cmd" name="soutenance" call="ask_soutenance" /> -</nemubotmodule> diff --git a/modules/soutenance/Delayed.py b/modules/soutenance/Delayed.py deleted file mode 100644 index 8cf47c5..0000000 --- a/modules/soutenance/Delayed.py +++ /dev/null @@ -1,13 +0,0 @@ -# coding=utf-8 - -import threading - -class Delayed: - def __init__(self, name): - self.name = name - self.res = None - self.evt = threading.Event() - - def wait(self, timeout): - self.evt.clear() - self.evt.wait(timeout) diff --git a/modules/soutenance/SiteSoutenances.py b/modules/soutenance/SiteSoutenances.py deleted file mode 100644 index 63833b7..0000000 --- a/modules/soutenance/SiteSoutenances.py +++ /dev/null @@ -1,179 +0,0 @@ -# coding=utf-8 - -from datetime import datetime -from datetime import timedelta -import http.client -import re -import threading -import time - -from response import Response - -from .Soutenance import Soutenance - -class SiteSoutenances(threading.Thread): - def __init__(self, datas): - self.souts = list() - self.updated = datetime.now() - self.datas = datas - threading.Thread.__init__(self) - - def getPage(self): - conn = http.client.HTTPSConnection(CONF.getNode("server")["ip"], timeout=10) - try: - conn.request("GET", CONF.getNode("server")["url"]) - - res = conn.getresponse() - page = res.read() - except: - print ("[%s] impossible de récupérer la page %s."%(s, p)) - return "" - conn.close() - return page - - def parsePage(self, page): - save = False - for line in page.split("\n"): - if re.match("</tr>", line) is not None: - save = False - elif re.match("<tr.*>", line) is not None: - save = True - last = Soutenance() - self.souts.append(last) - elif save: - result = re.match("<td[^>]+>(.*)</td>", line) - if last.hour is None: - try: - last.hour = datetime.fromtimestamp(time.mktime(time.strptime(result.group(1), "%Y-%m-%d %H:%M"))) - except ValueError: - continue - elif last.rank == 0: - last.rank = int (result.group(1)) - elif last.login == None: - last.login = result.group(1) - elif last.state == None: - last.state = result.group(1) - elif last.assistant == None: - last.assistant = result.group(1) - elif last.start == None: - try: - last.start = datetime.fromtimestamp(time.mktime(time.strptime(result.group(1), "%Y-%m-%d %H:%M"))) - except ValueError: - last.start = None - elif last.end == None: - try: - last.end = datetime.fromtimestamp(time.mktime(time.strptime(result.group(1), "%Y-%m-%d %H:%M"))) - except ValueError: - last.end = None - - def gen_response(self, req, msg): - """Generate a text response on right server and channel""" - return Response(req["sender"], msg, req["channel"], server=req["server"]) - - def res_next(self, req): - soutenance = self.findLast() - if soutenance is None: - return self.gen_response(req, "Il ne semble pas y avoir de soutenance pour le moment.") - else: - if soutenance.start > soutenance.hour: - avre = "%s de *retard*"%msg.just_countdown(soutenance.start - soutenance.hour, 4) - else: - avre = "%s *d'avance*"%msg.just_countdown(soutenance.hour - soutenance.start, 4) - self.gen_response(req, "Actuellement à la soutenance numéro %d, commencée il y a %s avec %s."%(soutenance.rank, msg.just_countdown(datetime.now () - soutenance.start, 4), avre)) - - def res_assistants(self, req): - assistants = self.findAssistants() - if len(assistants) > 0: - return self.gen_response(req, "Les %d assistants faisant passer les soutenances sont : %s." % (len(assistants), ', '.join(assistants.keys()))) - else: - return self.gen_response(req, "Il ne semble pas y avoir de soutenance pour le moment.") - - def res_soutenance(self, req): - name = req["user"] - - if name == "acu" or name == "yaka" or name == "acus" or name == "yakas" or name == "assistant" or name == "assistants": - return self.res_assistants(req) - elif name == "next": - return self.res_next(req) - - soutenance = self.findClose(name) - if soutenance is None: - return self.gen_response(req, "Pas d'horaire de soutenance pour %s."%name) - else: - if soutenance.state == "En cours": - return self.gen_response(req, "%s est actuellement en soutenance avec %s. Elle était prévue à %s, position %d."%(name, soutenance.assistant, soutenance.hour, soutenance.rank)) - elif soutenance.state == "Effectue": - return self.gen_response(req, "%s a passé sa soutenance avec %s. Elle a duré %s."%(name, soutenance.assistant, msg.just_countdown(soutenance.end - soutenance.start, 4))) - elif soutenance.state == "Retard": - return self.gen_response(req, "%s était en retard à sa soutenance de %s."%(name, soutenance.hour)) - else: - last = self.findLast() - if last is not None: - if soutenance.hour + (last.start - last.hour) > datetime.now (): - return self.gen_response(req, "Soutenance de %s : %s, position %d ; estimation du passage : dans %s."%(name, soutenance.hour, soutenance.rank, msg.just_countdown((soutenance.hour - datetime.now ()) + (last.start - last.hour)))) - else: - return self.gen_response(req, "Soutenance de %s : %s, position %d ; passage imminent."%(name, soutenance.hour, soutenance.rank)) - else: - return self.gen_response(req, "Soutenance de %s : %s, position %d."%(name, soutenance.hour, soutenance.rank)) - - def res_list(self, req): - name = req["user"] - - souts = self.findAll(name) - if souts is None: - self.gen_response(req, "Pas de soutenance prévues pour %s."%name) - else: - first = True - for s in souts: - if first: - self.gen_response(req, "Soutenance(s) de %s : - %s (position %d) ;"%(name, s.hour, s.rank)) - first = False - else: - self.gen_response(req, " %s - %s (position %d) ;"%(len(name)*' ', s.hour, s.rank)) - - def run(self): - self.parsePage(self.getPage().decode()) - res = list() - for u in self.datas.getNodes("request"): - res.append(self.res_soutenance(u)) - return res - - def needUpdate(self): - if self.findLast() is not None and datetime.now () - self.updated > timedelta(minutes=2): - return True - elif datetime.now () - self.updated < timedelta(hours=1): - return False - else: - return True - - def findAssistants(self): - h = dict() - for s in self.souts: - if s.assistant is not None and s.assistant != "": - h[s.assistant] = (s.start, s.end) - return h - - def findLast(self): - close = None - for s in self.souts: - if (s.state != "En attente" and s.start is not None and (close is None or close.rank < s.rank or close.hour.day > s.hour.day)) and (close is None or s.hour - close.hour < timedelta(seconds=2499)): - close = s - return close - - def findAll(self, login): - ss = list() - for s in self.souts: - if s.login == login: - ss.append(s) - return ss - - def findClose(self, login): - ss = self.findAll(login) - close = None - for s in ss: - if close is not None: - print (close.hour) - print (s.hour) - if close is None or (close.hour < s.hour and close.hour.day >= datetime.datetime().day): - close = s - return close diff --git a/modules/soutenance/Soutenance.py b/modules/soutenance/Soutenance.py deleted file mode 100644 index e2a0882..0000000 --- a/modules/soutenance/Soutenance.py +++ /dev/null @@ -1,11 +0,0 @@ -# coding=utf-8 - -class Soutenance: - def __init__(self): - self.hour = None - self.rank = 0 - self.login = None - self.state = None - self.assistant = None - self.start = None - self.end = None diff --git a/modules/soutenance/__init__.py b/modules/soutenance/__init__.py deleted file mode 100644 index 61b3aa6..0000000 --- a/modules/soutenance/__init__.py +++ /dev/null @@ -1,48 +0,0 @@ -# coding=utf-8 - -import time -import re -import threading -from datetime import date -from datetime import datetime - -from . import SiteSoutenances - -nemubotversion = 3.3 - -def help_tiny(): - """Line inserted in the response to the command !help""" - return "EPITA ING1 defenses module" - -def help_full(): - return "!soutenance: gives information about current defenses state\n!soutenance <who>: gives the date of the next defense of /who/.\n!soutenances <who>: gives all defense dates of /who/" - -def load(context): - global CONF - SiteSoutenances.CONF = CONF - -def ask_soutenance(msg): - req = ModuleState("request") - if len(msg.cmds) > 1: - req.setAttribute("user", msg.cmds[1]) - else: - req.setAttribute("user", "next") - req.setAttribute("server", msg.server) - req.setAttribute("channel", msg.channel) - req.setAttribute("sender", msg.sender) - - #An instance of this module is already running? - if not DATAS.hasAttribute("_running") or DATAS["_running"].needUpdate(): - DATAS.addChild(req) - site = SiteSoutenances.SiteSoutenances(DATAS) - DATAS.setAttribute("_running", site) - - res = site.run() - - for n in DATAS.getNodes("request"): - DATAS.delChild(n) - - return res - else: - site = DATAS["_running"] - return site.res_soutenance(req) diff --git a/modules/spell/__init__.py b/modules/spell/__init__.py index b20072b..23bdda8 100644 --- a/modules/spell/__init__.py +++ b/modules/spell/__init__.py @@ -8,7 +8,7 @@ from hooks import hook from .pyaspell import Aspell from .pyaspell import AspellError -nemubotversion = 3.3 +nemubotversion = 3.4 def help_tiny(): return "Check words spelling" diff --git a/modules/syno.py b/modules/syno.py index 9a37ddf..9916bc5 100644 --- a/modules/syno.py +++ b/modules/syno.py @@ -8,7 +8,7 @@ from urllib.parse import quote from hooks import hook from tools import web -nemubotversion = 3.3 +nemubotversion = 3.4 def help_tiny(): return "Find french synonyms" diff --git a/modules/translate.py b/modules/translate.py index fb8f649..42b302a 100644 --- a/modules/translate.py +++ b/modules/translate.py @@ -7,7 +7,7 @@ import json from urllib.parse import quote from urllib.request import urlopen -nemubotversion = 3.3 +nemubotversion = 3.4 import xmlparser diff --git a/modules/velib.py b/modules/velib.py index f001778..d21c4f7 100644 --- a/modules/velib.py +++ b/modules/velib.py @@ -4,7 +4,7 @@ import re from tools import web -nemubotversion = 3.3 +nemubotversion = 3.4 def load(context): global DATAS diff --git a/modules/watchWebsite/__init__.py b/modules/watchWebsite/__init__.py index 9e7845a..dad1604 100644 --- a/modules/watchWebsite/__init__.py +++ b/modules/watchWebsite/__init__.py @@ -15,7 +15,7 @@ from hooks import hook from .atom import Atom -nemubotversion = 3.3 +nemubotversion = 3.4 def help_tiny (): """Line inserted in the response to the command !help""" diff --git a/modules/weather.py b/modules/weather.py index ca0f236..895afce 100644 --- a/modules/weather.py +++ b/modules/weather.py @@ -6,7 +6,7 @@ import re from urllib.parse import quote from urllib.request import urlopen -nemubotversion = 3.3 +nemubotversion = 3.4 def load(context): global DATAS diff --git a/modules/whereis.xml b/modules/whereis.xml deleted file mode 100644 index 90b2c2f..0000000 --- a/modules/whereis.xml +++ /dev/null @@ -1,28 +0,0 @@ -<?xml version="1.0" ?> -<nemubotmodule name="whereis"> - <server url="ns-server.epita.fr" port="4242" /> - <sm name="adm" ip="10.2." /> - <sm name="vpn" ip="10.10." /> - <sm name="wifi" ip="10.41" /> - <sm name="sm02" ip="10.200.2" /> - <sm name="sm14" ip="10.200.14." /> - <sm name="sm15" ip="10.200.15." /> - <sm name="épimac" ip="10.223.3." /> - <sm name="wcube" ip="10.223.4." /> - <sm name="cycom" ip="10.223.5" /> - <sm name="eptv" ip="10.223.6" /> - <sm name="prologin" ip="10.223.7" /> - <sm name="assos" ip="10.223" /> - <sm name="gistre" ip="10.226.7." /> - <sm name="astek" ip="10.224.1." /> - <sm name="acu" ip="10.224.2." /> - <sm name="lse" ip="10.224.4." /> - <sm name="eip" ip="10.224.8." /> - <sm name="evolutek" ip="10.224.16." /> - <sm name="mslab" ip="10.224.18." /> - <sm name="bocal" ip="10.242" /> - <sm name="pasteur" ip="10.247" /> - <sm name="srlab" ip="10.248" /> - <sm name="midlab" ip="10.249" /> - <sm name="cisco" ip="10.250" /> -</nemubotmodule> \ No newline at end of file diff --git a/modules/whereis/Delayed.py b/modules/whereis/Delayed.py deleted file mode 100644 index 45826f4..0000000 --- a/modules/whereis/Delayed.py +++ /dev/null @@ -1,5 +0,0 @@ -# coding=utf-8 - -class Delayed: - def __init__(self): - self.names = dict() diff --git a/modules/whereis/UpdatedStorage.py b/modules/whereis/UpdatedStorage.py deleted file mode 100644 index de09848..0000000 --- a/modules/whereis/UpdatedStorage.py +++ /dev/null @@ -1,57 +0,0 @@ -# coding=utf-8 - -import socket -from datetime import datetime -from datetime import timedelta - -from .User import User - -class UpdatedStorage: - def __init__(self, url, port): - sock = connect_to_ns(url, port) - self.users = dict() - if sock != None: - users = list_users(sock) - if users is not None: - for l in users: - u = User(l) - if u.login not in self.users: - self.users[u.login] = list() - self.users[u.login].append(u) - self.lastUpdate = datetime.now () - else: - self.users = None - sock.close() - else: - self.users = None - - def update(self): - if datetime.now () - self.lastUpdate < timedelta(minutes=10): - return self - else: - return None - - -def connect_to_ns(server, port): - try: - s = socket.socket() - s.settimeout(3) - s.connect((server, port)) - except socket.error: - return None - s.recv(8192) - return s - - -def list_users(sock): - try: - sock.send('list_users\n'.encode()) - buf = '' - while True: - tmp = sock.recv(8192).decode() - buf += tmp - if '\nrep 002' in tmp or tmp == '': - break - return buf.split('\n')[:-2] - except socket.error: - return None diff --git a/modules/whereis/User.py b/modules/whereis/User.py deleted file mode 100644 index d4b48b4..0000000 --- a/modules/whereis/User.py +++ /dev/null @@ -1,35 +0,0 @@ -# coding=utf-8 - -class User(object): - def __init__(self, line): - fields = line.split() - self.login = fields[1] - self.ip = fields[2] - self.location = fields[8] - self.promo = fields[9] - - @property - def sm(self): - for sm in CONF.getNodes("sm"): - if self.ip.startswith(sm["ip"]): - return sm["name"] - return None - - @property - def poste(self): - if self.sm is None: - if self.ip.startswith('10.'): - return 'quelque part sur le PIE (%s)'%self.ip - else: - return "chez lui" - else: - if self.ip.startswith('10.247') or self.ip.startswith('10.248') or self.ip.startswith('10.249') or self.ip.startswith('10.250'): - return "en " + self.sm + " rangée " + self.ip.split('.')[2] + " poste " + self.ip.split('.')[3] - else: - return "en " + self.sm - - def __cmp__(self, other): - return cmp(self.login, other.login) - - def __hash__(self): - return hash(self.login) diff --git a/modules/whereis/__init__.py b/modules/whereis/__init__.py deleted file mode 100644 index 57ebb73..0000000 --- a/modules/whereis/__init__.py +++ /dev/null @@ -1,206 +0,0 @@ -# coding=utf-8 - -import re -import sys -import socket -import time -import _thread -import threading -from datetime import datetime -from datetime import date -from datetime import timedelta -from urllib.parse import unquote - -from module_state import ModuleState - -from . import User -from .UpdatedStorage import UpdatedStorage -from .Delayed import Delayed - -nemubotversion = 3.0 - -THREAD = None -search = list() - -def help_tiny (): - """Line inserted in the response to the command !help""" - return "Find a user on the PIE" - -def help_full (): - return "!whereis <who>: gives the position of /who/.\n!whereare <who> [<other who> ...]: gives the position of these <who>.\n!peoplein <sm>: gives the number of people in this /sm/.\n!ip <who>: gets the IP adress of /who/.\n!whoison <location>: gives the name or the number (if > 15) of people at this /location/.\n!whoisin <sm>: gives the name or the number of people in this /sm/" - -def load(): - global CONF - User.CONF = CONF - -datas = None - -def startWhereis(msg): - global datas, THREAD, search - if datas is not None: - datas = datas.update () - if datas is None: - datas = UpdatedStorage(CONF.getNode("server")["url"], CONF.getNode("server").getInt("port")) - if datas is None or datas.users is None: - msg.send_chn("Hmm c'est embarassant, serait-ce la fin du monde ou juste netsoul qui est mort ?") - return - - if msg.cmd[0] == "peoplein": - peoplein(msg) - elif msg.cmd[0] == "whoison" or msg.cmd[0] == "whoisin": - whoison(msg) - else: - whereis_msg(msg) - - THREAD = None - if len(search) > 0: - startWhereis(search.pop()) - -def peoplein(msg): - if len(msg.cmd) > 1: - for sm in msg.cmd: - sm = sm.lower() - if sm == "peoplein": - continue - else: - count = 0 - for userC in datas.users: - for user in datas.users[userC]: - usersm = user.sm - if usersm is not None and usersm.lower() == sm: - count += 1 - if count > 1: - sOrNot = "s" - else: - sOrNot = "" - msg.send_chn ("Il y a %d personne%s en %s." % (count, sOrNot, sm)) - -def whoison(msg): - if len(msg.cmd) > 1: - for pb in msg.cmd: - pc = pb.lower() - if pc == "whoison" or pc == "whoisin": - continue - else: - found = list() - for userC in datas.users: - for user in datas.users[userC]: - if (msg.cmd[0] == "whoison" and (user.ip[:len(pc)] == pc or user.location.lower() == pc)) or (msg.cmd[0] == "whoisin" and user.sm == pc): - found.append(user.login) - if len(found) > 0: - if len(found) <= 15: - if pc == "whoisin": - msg.send_chn ("En %s, il y a %s" % (pb, ", ".join(found))) - else: - msg.send_chn ("%s correspond à %s" % (pb, ", ".join(found))) - else: - msg.send_chn ("%s: %d personnes" % (pb, len(found))) - else: - msg.send_chn ("%s: personne ne match ta demande :(" % (msg.nick)) - -DELAYED = dict() -delayEvnt = threading.Event() - -def whereis_msg(msg): - names = list() - for name in msg.cmd: - if name == "whereis" or name == "whereare" or name == "ouest" or name == "ousont" or name == "ip": - if len(msg.cmd) >= 2: - continue - else: - name = msg.nick - else: - names.append(name) - pasla = whereis(msg, names) - if len(pasla) > 0: - global DELAYED - DELAYED[msg] = Delayed() - for name in pasla: - DELAYED[msg].names[name] = None - #msg.srv.send_msg_prtn ("~whois %s" % name) - msg.srv.send_msg_prtn ("~whois %s" % " ".join(pasla)) - startTime = datetime.now() - names = list() - while len(DELAYED[msg].names) > 0 and startTime + timedelta(seconds=4) > datetime.now(): - delayEvnt.clear() - delayEvnt.wait(2) - rem = list() - for name in DELAYED[msg].names.keys(): - if DELAYED[msg].names[name] is not None: - pasla = whereis(msg, (DELAYED[msg].names[name],)) - if len(pasla) != 0: - names.append(pasla[0]) - rem.append(name) - for r in rem: - del DELAYED[msg].names[r] - for name in DELAYED[msg].names.keys(): - if DELAYED[msg].names[name] is None: - names.append(name) - else: - names.append(DELAYED[msg].names[name]) - if len(names) > 1: - msg.send_chn ("%s ne sont pas connectés sur le PIE." % (", ".join(names))) - else: - for name in names: - msg.send_chn ("%s n'est pas connecté sur le PIE." % name) - - -def whereis(msg, names): - pasla = list() - - for name in names: - if name in datas.users: - if msg.cmd[0] == "ip": - if len(datas.users[name]) == 1: - msg.send_chn ("L'ip de %s est %s." %(name, datas.users[name][0].ip)) - else: - out = "" - for local in datas.users[name]: - out += ", " + local.ip - msg.send_chn ("%s est connecté à plusieurs endroits : %s." %(name, out[2:])) - else: - if len(datas.users[name]) == 1: - msg.send_chn ("%s est %s (%s)." %(name, datas.users[name][0].poste, unquote(datas.users[name][0].location))) - else: - out = "" - for local in datas.users[name]: - out += ", " + local.poste + " (" + unquote(local.location) + ")" - msg.send_chn ("%s est %s." %(name, out[2:])) - else: - pasla.append(name) - - return pasla - - -def parseanswer (msg): - global datas, THREAD, search - if msg.cmd[0] == "whereis" or msg.cmd[0] == "whereare" or msg.cmd[0] == "ouest" or msg.cmd[0] == "ousont" or msg.cmd[0] == "ip" or msg.cmd[0] == "peoplein" or msg.cmd[0] == "whoison" or msg.cmd[0] == "whoisin": - if len(msg.cmd) > 10: - msg.send_snd ("Demande moi moins de personnes à la fois dans ton !%s" % msg.cmd[0]) - return True - - if THREAD is None: - THREAD = _thread.start_new_thread (startWhereis, (msg,)) - else: - search.append(msg) - return True - return False - -def parseask (msg): - if len(DELAYED) > 0 and msg.nick == msg.srv.partner: - treat = False - for part in msg.content.split(';'): - if part is None: - continue - for d in DELAYED.keys(): - nKeys = list() - for n in DELAYED[d].names.keys(): - nKeys.append(n) - for n in nKeys: - if DELAYED[d].names[n] is None and part.find(n) >= 0: - result = re.match(".* est (.*[^.])\.?", part) - if result is not None: - DELAYED[d].names[n] = result.group(1) - delayEvnt.set() - return treat - return False diff --git a/modules/worldcup.py b/modules/worldcup.py index b5df61b..a1628d2 100644 --- a/modules/worldcup.py +++ b/modules/worldcup.py @@ -6,7 +6,7 @@ import re from urllib.parse import quote from urllib.request import urlopen -nemubotversion = 3.3 +nemubotversion = 3.4 API_URL="http://worldcup.sfg.io/%s" diff --git a/modules/ycc.py b/modules/ycc.py index 878effa..779cb84 100644 --- a/modules/ycc.py +++ b/modules/ycc.py @@ -7,7 +7,7 @@ from urllib.request import urlopen from hooks import hook -nemubotversion = 3.3 +nemubotversion = 3.4 def help_tiny(): """Line inserted in the response to the command !help""" diff --git a/modules/youtube.py b/modules/youtube.py deleted file mode 100644 index f28ef77..0000000 --- a/modules/youtube.py +++ /dev/null @@ -1,51 +0,0 @@ -# coding=utf-8 - -import re -import http.client - -idAtom = "http://musik.p0m.fr/atom.php?nemubot" -URLS = dict () - -def load_module(datas_path): - """Load this module""" - global URLS - URLS = dict () - -def save_module(): - """Save the dates""" - return - -def help_tiny (): - """Line inserted in the response to the command !help""" - return "music extractor" - -def help_full (): - return "To launch a convertion task, juste paste a youtube link (or compatible service) and wait for nemubot answer!" - -def parseanswer(msg): - return False - - -def parseask(msg): - return False - -def parselisten (msg): - global URLS - matches = [".*(http://(www\.)?youtube.com/watch\?v=([a-zA-Z0-9_-]{11})).*", - ".*(http://(www\.)?youtu.be/([a-zA-Z0-9_-]{11})).*"] - for m in matches: - res = re.match (m, msg.content) - if res is not None: - #print ("seen : %s"%res.group(1)) - URLS[res.group(1)] = msg - conn = http.client.HTTPConnection("musik.p0m.fr", timeout=10) - conn.request("GET", "/?nemubot&a=add&url=%s"%(res.group (1))) - conn.getresponse() - conn.close() - return True - return False - -def send_global (origin, msg): - if origin in URLS: - URLS[origin].send_chn (msg) - del URLS[origin] From 85981b4d99bafdd219fbe3066ac3b9b9f9f59185 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 13 Aug 2014 17:04:11 +0200 Subject: [PATCH 075/674] Remove dead code: credits --- credits.py | 43 ------------------------------------------- message.py | 27 --------------------------- 2 files changed, 70 deletions(-) delete mode 100644 credits.py diff --git a/credits.py b/credits.py deleted file mode 100644 index fc0978e..0000000 --- a/credits.py +++ /dev/null @@ -1,43 +0,0 @@ -# coding=utf-8 - -from datetime import datetime -from datetime import timedelta -import random - -BANLIST = [] - -class Credits: - def __init__ (self, name): - self.name = name - self.credits = 5 - self.randsec = timedelta(seconds=random.randint(0, 55)) - self.lastmessage = datetime.now() + self.randsec - self.iask = True - - def ask(self): - if self.name in BANLIST: - return False - - now = datetime.now() + self.randsec - if self.lastmessage.minute == now.minute and (self.lastmessage.second == now.second or self.lastmessage.second == now.second - 1): - print("\033[1;36mAUTOBAN\033[0m %s: too low time between messages" % self.name) - #BANLIST.append(self.name) - self.credits -= self.credits / 2 #Une alternative - return False - - self.iask = True - return self.credits > 0 or self.lastmessage.minute != now.minute - - def speak(self): - if self.iask: - self.iask = False - now = datetime.now() + self.randsec - if self.lastmessage.minute != now.minute: - self.credits = min (15, self.credits + 5) - self.lastmessage = now - - self.credits -= 1 - return self.credits > -3 - - def to_string(self): - print ("%s: %d ; reset: %d" % (self.name, self.credits, self.randsec.seconds)) diff --git a/message.py b/message.py index 1a158ac..c6a5d8c 100644 --- a/message.py +++ b/message.py @@ -21,25 +21,11 @@ import re import shlex import time -import credits -from credits import Credits from response import Response import xmlparser -CREDITS = {} filename = "" -def load(config_file): - global CREDITS, filename - CREDITS = dict () - filename = config_file - credits.BANLIST = xmlparser.parse_file(filename) - -def save(): - global filename - credits.BANLIST.save(filename) - - class Message: def __init__ (self, line, timestamp, private=False): self.raw = line @@ -133,19 +119,6 @@ class Message: #TODO: use encoding from config file self.content = self.content.decode('utf-8', 'replace') - def authorize_DEPRECATED(self): - """Is nemubot listening for the sender on this channel?""" - # TODO: deprecated - if self.srv.isDCC(self.sender): - return True - elif self.realname not in CREDITS: - CREDITS[self.realname] = Credits(self.realname) - elif self.content[0] == '`': - return True - elif not CREDITS[self.realname].ask(): - return False - return self.srv.accepted_channel(self.channel) - ############################## # # # Extraction/Format text # From 3839455f42d14d4bbf06921e8851dbe7dd859f8b Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 13 Aug 2014 17:11:33 +0200 Subject: [PATCH 076/674] Prepare server to incoming split --- bot.py | 4 ++-- consumer.py | 2 +- nemuspeak.py | 4 ++-- networkbot.py | 2 +- DCC.py => server/DCC.py | 0 IRCServer.py => server/IRC.py | 2 +- server.py => server/__init__.py | 0 7 files changed, 7 insertions(+), 7 deletions(-) rename DCC.py => server/DCC.py (100%) rename IRCServer.py => server/IRC.py (99%) rename server.py => server/__init__.py (100%) diff --git a/bot.py b/bot.py index 47363de..e87cd1a 100644 --- a/bot.py +++ b/bot.py @@ -27,8 +27,8 @@ import consumer import event import hooks from networkbot import NetworkBot -from IRCServer import IRCServer -from DCC import DCC +from server.IRC import IRCServer +from server.DCC import DCC import response ID_letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" diff --git a/consumer.py b/consumer.py index d2df9fb..009eebf 100644 --- a/consumer.py +++ b/consumer.py @@ -23,7 +23,7 @@ import traceback import sys import bot -from DCC import DCC +from server.DCC import DCC from message import Message import response import server diff --git a/nemuspeak.py b/nemuspeak.py index e1d2a9e..48d338f 100755 --- a/nemuspeak.py +++ b/nemuspeak.py @@ -27,7 +27,7 @@ else: import xmlparser as msf import message -import IRCServer +from server.IRC import IRCServer SMILEY = list() CORRECTIONS = list() @@ -111,7 +111,7 @@ def speak(endstate): talkEC = 1 -class Server(IRCServer.IRCServer): +class Server(IRCServer): def treat_msg(self, line, private = False): global stopSpk, talkEC, g_queue try: diff --git a/networkbot.py b/networkbot.py index 756ab3c..37814e8 100644 --- a/networkbot.py +++ b/networkbot.py @@ -22,7 +22,7 @@ import shlex import urllib.parse import zlib -from DCC import DCC +from server.DCC import DCC import hooks from response import Response diff --git a/DCC.py b/server/DCC.py similarity index 100% rename from DCC.py rename to server/DCC.py diff --git a/IRCServer.py b/server/IRC.py similarity index 99% rename from IRCServer.py rename to server/IRC.py index c208b44..6cec073 100644 --- a/IRCServer.py +++ b/server/IRC.py @@ -24,7 +24,7 @@ import threading import traceback from channel import Channel -from DCC import DCC +from server.DCC import DCC from hooks import Hook import message import server diff --git a/server.py b/server/__init__.py similarity index 100% rename from server.py rename to server/__init__.py From d0b1336d073d3cadb9c96265ccc244db32db8f73 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 14 Aug 2014 12:49:38 +0200 Subject: [PATCH 077/674] Use a logger --- .gitignore | 1 + bot.py | 39 +++++++++++++++++++++++---------------- channel.py | 6 +++--- consumer.py | 26 +++++++++++++++----------- hooks.py | 18 ++++++++++++------ importer.py | 24 ++++++++++++++++-------- message.py | 3 --- nemubot.py | 10 ++++++++-- prompt/__init__.py | 6 +++--- prompt/builtins.py | 11 +++++++---- response.py | 7 ++++++- server/DCC.py | 12 ++++++------ server/IRC.py | 26 +++++++++++++------------- server/__init__.py | 6 +++++- 14 files changed, 118 insertions(+), 77 deletions(-) diff --git a/.gitignore b/.gitignore index 50aca48..6e6afac 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *# *~ +*.log TAGS *.py[cod] __pycache__ diff --git a/bot.py b/bot.py index e87cd1a..3e60b8c 100644 --- a/bot.py +++ b/bot.py @@ -18,6 +18,7 @@ from datetime import datetime from datetime import timedelta +import logging from queue import Queue import threading import time @@ -31,6 +32,8 @@ from server.IRC import IRCServer from server.DCC import DCC import response +logger = logging.getLogger(__name__) + ID_letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" class Bot: @@ -38,6 +41,7 @@ class Bot: # Bot general informations self.version = 3.4 self.version_txt = "3.4-dev" + logger.info("Initiate nemubot v%s" % self.version_txt) # Save various informations self.ip = ip @@ -87,6 +91,7 @@ class Bot: msg.sender, "USERINFO %s" % self.realname) self.ctcp_capabilities["VERSION"] = lambda srv, msg: _ctcp_response( msg.sender, "VERSION nemubot v%s" % self.version_txt) + logger.debug("CTCP capabilities setup: %s" % ", ".join(self.ctcp_capabilities)) def _ctcp_clientinfo(self, srv, msg): """Response to CLIENTINFO CTCP message""" @@ -101,7 +106,7 @@ class Bot: srv.dcc_clients[conn.sender] = conn conn.send_dcc("Hello %s!" % conn.nick) else: - print ("DCC: unable to connect to %s:%s" % (ip, msg.cmds[4])) + logger.error("DCC: unable to connect to %s:%s" % (ip, msg.cmds[4])) def add_event(self, evt, eid=None, module_src=None): @@ -132,10 +137,12 @@ class Bot: if module_src is not None: module_src.REGISTERED_EVENTS.append(evt.id) + logger.info("New event registered: %s -> %s" % (evt.id, evt)) return evt.id def del_event(self, id, module_src=None): """Find and remove an event from list""" + logger.info("Removing event: %s from %s" % (id, module_src)) if len(self.events) > 0 and id == self.events[0].id: self.events.remove(self.events[0]) self.update_timer() @@ -158,8 +165,8 @@ class Bot: if self.event_timer is not None: self.event_timer.cancel() if len(self.events) > 0: - #print ("Update timer, next in", self.events[0].time_left.seconds, - # "seconds") + logger.debug("Update timer: next event in %d seconds" % + self.events[0].time_left.seconds) if datetime.now() + timedelta(seconds=5) >= self.events[0].current: while datetime.now() < self.events[0].current: time.sleep(0.6) @@ -168,13 +175,13 @@ class Bot: self.event_timer = threading.Timer( self.events[0].time_left.seconds + 1, self.end_timer) self.event_timer.start() - #else: - # print ("Update timer: no timer left") + else: + logger.debug("Update timer: no timer left") def end_timer(self): """Function called at the end of the timer""" #print ("end timer") - while len(self.events)>0 and datetime.now() >= self.events[0].current: + while len(self.events) > 0 and datetime.now() >= self.events[0].current: #print ("end timer: while") evt = self.events.pop(0) self.cnsr_queue.put_nowait(consumer.EventConsumer(evt)) @@ -225,7 +232,7 @@ class Bot: return False - def unload_module(self, name, verb=False): + def unload_module(self, name): """Unload a module""" if name in self.modules: self.modules[name].print_debug("Unloading module %s" % name) @@ -240,7 +247,7 @@ class Bot: self.del_event(e) # Remove from the dict del self.modules[name] - print(" Module `%s' successfully unloaded." % name) + logger.info("Module `%s' successfully unloaded." % name) return True return False @@ -274,18 +281,18 @@ class Bot: if self.network[bot].srv == srv: self.network[bot].send_cmd(cmd, data) - def quit(self, verb=False): + def quit(self): """Save and unload modules and disconnect servers""" if self.event_timer is not None: - if verb: print ("Stop the event timer...") + logger.info("Stop the event timer...") self.event_timer.cancel() - if verb: print ("Save and unload all modules...") + logger.info("Save and unload all modules...") k = list(self.modules.keys()) for mod in k: - self.unload_module(mod, verb) + self.unload_module(mod) - if verb: print ("Close all servers connection...") + logger.info("Close all servers connection...") k = list(self.servers.keys()) for srv in k: self.servers[srv].disconnect() @@ -436,13 +443,13 @@ class Bot: return srv.moremessages[msg.channel] elif msg.cmds[0] == "dcc": - print("dcctest for", msg.sender) + logger.debug("dcctest for " + msg.sender) srv.send_dcc("Hello %s!" % msg.nick, msg.sender) elif msg.cmds[0] == "pvdcctest": - print("dcctest") + logger.debug("dcctest") return Response(msg.sender, message="Test DCC") elif msg.cmds[0] == "dccsendtest": - print("dccsendtest") + logger.debug("dccsendtest") conn = DCC(srv, msg.sender) conn.send_file("bot_sample.xml") diff --git a/channel.py b/channel.py index 6a67d76..974713f 100644 --- a/channel.py +++ b/channel.py @@ -41,7 +41,7 @@ class Channel: def join(self, nick, level = 0): """Someone join the channel""" - #print ("%s arrive sur %s" % (nick, self.name)) + logger.debug("%s join %s" % (nick, self.name)) self.people[nick] = level def chtopic(self, newtopic): @@ -52,7 +52,7 @@ class Channel: def nick(self, oldnick, newnick): """Someone change his nick""" if oldnick in self.people: - #print ("%s change de nom pour %s sur %s" % (oldnick, newnick, self.name)) + logger.debug("%s switch nick to %s on %s" % (oldnick, newnick, self.name)) lvl = self.people[oldnick] del self.people[oldnick] self.people[newnick] = lvl @@ -60,7 +60,7 @@ class Channel: def part(self, nick): """Someone leave the channel""" if nick in self.people: - #print ("%s vient de quitter %s" % (nick, self.name)) + logger.debug("%s has left %s" % (nick, self.name)) del self.people[nick] def mode(self, msg): diff --git a/consumer.py b/consumer.py index 009eebf..ea63e1c 100644 --- a/consumer.py +++ b/consumer.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 <http://www.gnu.org/licenses/>. +import logging import queue import re import threading @@ -28,6 +29,8 @@ from message import Message import response import server +logger = logging.getLogger(__name__) + class MessageConsumer: """Store a message before treating""" def __init__(self, srv, raw, time, prvt, data): @@ -63,8 +66,8 @@ class MessageConsumer: res.server = context.servers[res.server] if (res.server is not None and not isinstance(res.server, server.Server)): - print ("\033[1;35mWarning:\033[0m the server defined in this " - "response doesn't exist: %s" % (res.server)) + logger.error("The server defined in this response doesn't " + "exist: %s" % res.server) res.server = None if res.server is None: res.server = self.srv @@ -77,8 +80,7 @@ class MessageConsumer: context.hooks.add_hook(res.type, res.hook, res.src) elif res is not None: - print ("\033[1;35mWarning:\033[0m unrecognized response type " - ": %s" % res) + logger.error("Unrecognized response type: %s" % res) def run(self, context): """Create, parse and treat the message""" @@ -90,11 +92,12 @@ class MessageConsumer: msg.private = msg.private or msg.channel == self.srv.nick res = self.treat_in(context, msg) except: - print ("\033[1;31mERROR:\033[0m occurred during the " - "processing of the message: %s" % self.raw) + logger.error("Error occurred during the processing of the message:" + " %s" % self.raw) exc_type, exc_value, exc_traceback = sys.exc_info() - traceback.print_exception(exc_type, exc_value, - exc_traceback) + logger.debug(traceback.format_exception(exc_type, + exc_value, + exc_traceback)) return # Send message @@ -116,10 +119,11 @@ class EventConsumer: try: self.evt.launch_check() except: - print ("\033[1;31mError:\033[0m during event end") + logger.error("Error during event end") exc_type, exc_value, exc_traceback = sys.exc_info() - traceback.print_exception(exc_type, exc_value, - exc_traceback) + logger.debug(traceback.format_exception(exc_type, + exc_value, + exc_traceback)) if self.evt.next is not None: context.add_event(self.evt, self.evt.id) diff --git a/hooks.py b/hooks.py index f9002e0..48c47ac 100644 --- a/hooks.py +++ b/hooks.py @@ -16,11 +16,14 @@ # 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 re from response import Response from exception import IRCException +logger = logging.getLogger(__name__) + class MessagesHook: def __init__(self, context, bot): self.context = context @@ -51,16 +54,19 @@ class MessagesHook: def add_hook(self, store, hook, module_src=None): """Insert in the right place a hook into the given store""" + logger.info("Adding hook '%s' to store '%s' from module '%s'" % (hook, store, module_src)) if module_src is None: - print ("\033[1;35mWarning:\033[0m No source module was passed to " - "add_hook function, please fix it in order to be " - "compatible with unload feature") + logger.warn("No source module was passed to add_hook function, " + "please fix it in order to be compatible with unload " + "feature") if store in self.context.hooks_cache: + logger.debug("Cleaning hooks cache for " + store) del self.context.hooks_cache[store] if not hasattr(self, store): - print ("\033[1;35mWarning:\033[0m unrecognized hook store") + # TODO: raise custom exception, this is a user problem, not internal one! + logger.error("Unrecognized hook store: " + store) return attr = getattr(self, store) @@ -75,7 +81,7 @@ class MessagesHook: elif isinstance(attr, list): attr.append(hook) else: - print ("\033[1;32mWarning:\033[0m unrecognized hook store type") + logger.critical("Unrecognized hook store type: " + type(attr)) return if module_src is not None and hasattr(module_src, "REGISTERED_HOOKS"): module_src.REGISTERED_HOOKS.append((store, hook)) @@ -147,7 +153,7 @@ class MessagesHook: del self.context.hooks_cache[store] if not hasattr(self, store): - print ("Warning: unrecognized hook store type") + logger.warn("unrecognized hook store type") return attr = getattr(self, store) diff --git a/importer.py b/importer.py index d7d0c80..4b435dc 100644 --- a/importer.py +++ b/importer.py @@ -19,6 +19,7 @@ from importlib.abc import Finder from importlib.abc import SourceLoader import imp +import logging import os import sys @@ -28,6 +29,8 @@ import hooks import response import xmlparser +logger = logging.getLogger(__name__) + class ModuleFinder(Finder): def __init__(self, context, prompt): self.context = context @@ -143,22 +146,26 @@ class ModuleLoader(SourceLoader): # Set module common functions and datas module.__LOADED__ = True + module.logger = logging.getLogger("module/" + fullname) def prnt(*args): print("[%s]" % module.name, *args) + module.logger.info(*args) def prnt_dbg(*args): if module.DEBUG: print("{%s}" % module.name, *args) + module.logger.debug(*args) def mod_save(): - module.print_debug("Saving DATAS...") - module.DATAS.save(self.context.datas_path + "/" + module.name + ".xml") + fpath = self.context.datas_path + "/" + module.name + ".xml" + module.print_debug("Saving DATAS to " + fpath) + module.DATAS.save(fpath) def send_response(server, res): if server in self.context.servers: return self.context.servers[server].send_response(res, None) else: - print("\033[1;35mWarning:\033[0m Try to send a message to the unknown server: %s" % server) + module.logger.error("Try to send a message to the unknown server: %s" % server) return False def add_hook(store, hook): @@ -210,9 +217,9 @@ class ModuleLoader(SourceLoader): mod.MODS[md.name] = md break if depend["name"] not in module.MODS: - print ("\033[1;31mERROR:\033[0m in module `%s', module " - "`%s' require by this module but is not loaded." - % (module.name, depend["name"])) + logger.error("In module `%s', module `%s' require by this " + "module but is not loaded." % (module.name, + depend["name"])) return # Add the module to the global modules list @@ -225,8 +232,9 @@ class ModuleLoader(SourceLoader): # Register hooks register_hooks(module, self.context, self.prompt) - print (" Module `%s' successfully loaded." % module.name) + logger.info("Module '%s' successfully loaded." % module.name) else: + logger.error("An error occurs while importing `%s'." % module.name) raise ImportError("An error occurs while importing `%s'." % module.name) return module @@ -236,7 +244,7 @@ def add_cap_hook(prompt, module, cmd): if hasattr(module, cmd["call"]): prompt.add_cap_hook(cmd["name"], getattr(module, cmd["call"])) else: - print ("Warning: In module `%s', no function `%s' defined for `%s' " + logger.warn("In module `%s', no function `%s' defined for `%s' " "command hook." % (module.name, cmd["call"], cmd["name"])) def register_hooks(module, context, prompt): diff --git a/message.py b/message.py index c6a5d8c..aa2b6e0 100644 --- a/message.py +++ b/message.py @@ -76,10 +76,8 @@ class Message: self.channel = words[3] self.content = self.pickWords(words[4:]) else: - #print (line) self.content = self.pickWords(words[3:]) else: - print (line) if self.cmd == 'PRIVMSG': self.channel = words[2].decode() self.content = b' '.join(words[3:]) @@ -247,7 +245,6 @@ class Message: minute = result.group(9) second = result.group(11) - print ("Chaîne reconnue : %s/%s/%s %s:%s:%s"%(day, month, year, hour, minute, second)) if year == None: year = date.today().year if hour == None: diff --git a/nemubot.py b/nemubot.py index 4e77007..66fc396 100755 --- a/nemubot.py +++ b/nemubot.py @@ -17,9 +17,10 @@ # 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 sys -import os import imp +import logging +import os +import sys import traceback import bot @@ -28,6 +29,11 @@ from prompt.builtins import load_file import importer if __name__ == "__main__": + # Setup loggin interface + logging.basicConfig(filename='nemubot.log', level=logging.DEBUG) + logging.Formatter('%(asctime)s %(name)s %(levelname)s %(message)s') + logger = logging.getLogger(__name__) + # Create bot context context = bot.Bot(0, "FIXME") diff --git a/prompt/__init__.py b/prompt/__init__.py index 62c8dc3..6504744 100644 --- a/prompt/__init__.py +++ b/prompt/__init__.py @@ -67,7 +67,7 @@ class Prompt: (f,d) = self.HOOKS_CAPS[toks[0]] return f(d, toks, context, self) else: - print ("Unknown command: `%s'" % toks[0]) + print("Unknown command: `%s'" % toks[0]) return "" def getPS1(self): @@ -88,7 +88,7 @@ class Prompt: line = sys.stdin.readline() if len(line) <= 0: line = "quit" - print ("quit") + print("quit") cmds = self.lex_cmd(line.strip()) for toks in cmds: try: @@ -97,7 +97,7 @@ class Prompt: exc_type, exc_value, exc_traceback = sys.exc_info() traceback.print_exception(exc_type, exc_value, exc_traceback) except KeyboardInterrupt: - print ("") + print("") return ret != "quit" diff --git a/prompt/builtins.py b/prompt/builtins.py index 6e7aa5a..4e0b0d7 100644 --- a/prompt/builtins.py +++ b/prompt/builtins.py @@ -16,9 +16,12 @@ # 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 xmlparser +logger = logging.getLogger(__name__) + def end(toks, context, prompt): """Quit the prompt for reload or exit""" if toks[0] == "refresh": @@ -66,11 +69,11 @@ def load_file(filename, context): if context.addServer(server, config["nick"], config["owner"], config["realname"], server.hasAttribute("ssl")): - print (" Server `%s:%s' successfully added." - % (server["server"], server["port"])) + logger.info("Server `%s:%s' successfully added." + % (server["server"], server["port"])) else: - print (" Server `%s:%s' already added, skiped." - % (server["server"], server["port"])) + logger.warn("Server `%s:%s' already added, skiped." + % (server["server"], server["port"])) # Load files asked by the configuration file for load in config.getNodes("load"): diff --git a/response.py b/response.py index 9fda7f8..0716180 100644 --- a/response.py +++ b/response.py @@ -16,9 +16,12 @@ # 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 traceback import sys +logger = logging.getLogger(__name__) + class Response: def __init__(self, sender, message=None, channel=None, nick=None, server=None, nomore="No more message", title=None, more="(suite) ", count=None, @@ -54,7 +57,9 @@ class Response: if sender is None or sender.find("!") < 0: if sender is not None: exc_type, exc_value, exc_traceback = sys.exc_info() - traceback.print_exception(exc_type, "\033[1;35mWarning:\033[0m bad sender provided in Response, it will be ignored.", exc_traceback) + logger.warn(traceback.format_exception(exc_type, + "Bad sender provided in Response, it will be ignored.", + exc_traceback)) self.sender = None else: self.sender = sender diff --git a/server/DCC.py b/server/DCC.py index 5dc46ea..2a0b741 100644 --- a/server/DCC.py +++ b/server/DCC.py @@ -55,7 +55,7 @@ class DCC(server.Server): self.port = self.foundPort() if self.port is None: - print ("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.") @@ -81,7 +81,7 @@ class DCC(server.Server): self.s = socket.socket() try: self.s.connect((host, port)) - print ('Accepted user from', host, port, "for", self.sender) + self.logger.info("Accepted user from %s:%d for %s" % (host, port, self.sender)) self.connected = True self.stop = False except: @@ -106,7 +106,7 @@ class DCC(server.Server): self.setError("Une erreur s'est produite durant la tentative" " d'ouverture d'une session DCC.") return False - print ('Listen on', self.port, "for", 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, @@ -117,7 +117,7 @@ class DCC(server.Server): s.listen(1) #Waiting for the client (self.s, addr) = s.accept() - print ('Connected by', addr) + self.logger.info("Connected by %d" % addr) self.connected = True return True @@ -151,7 +151,7 @@ class DCC(server.Server): except RuntimeError: pass else: - print("File not found `%s'" % filename) + self.logger.error("File not found `%s'" % filename) def run(self): self.stopping.clear() @@ -204,7 +204,7 @@ class DCC(server.Server): if self.realname in self.srv.dcc_clients: del self.srv.dcc_clients[self.realname] - print ("Closing connection with", self.nick) + self.logger.info("Closing connection with " + self.nick) self.stopping.set() if self.closing_event is not None: self.closing_event() diff --git a/server/IRC.py b/server/IRC.py index 6cec073..2029340 100644 --- a/server/IRC.py +++ b/server/IRC.py @@ -43,10 +43,10 @@ class IRCServer(server.Server): realname -- string used as realname on this server ssl -- require SSL? """ - server.Server.__init__(self) - self.node = node + server.Server.__init__(self) + self.nick = nick self.owner = owner self.realname = realname @@ -173,7 +173,7 @@ class IRCServer(server.Server): self.s.connect((self.host, self.port)) #Connect to server except socket.error as e: self.s = None - print ("\033[1;31mError:\033[0m Unable to connect to %s:%d: %s" + self.logger.critical("Unable to connect to %s:%d: %s" % (self.host, self.port, os.strerror(e.errno))) return self.stopping.clear() @@ -185,10 +185,10 @@ class IRCServer(server.Server): self.realname)).encode()) raw = self.s.recv(1024) if not raw: - print ("Unable to connect to %s:%d" % (self.host, self.port)) + self.logger.critical("Unable to connect to %s:%d" % (self.host, self.port)) return self.connected = True - print ("Connection to %s:%d completed" % (self.host, self.port)) + self.logger.info("Connection to %s:%d completed" % (self.host, self.port)) if len(self.channels) > 0: for chn in self.channels.keys(): @@ -214,7 +214,7 @@ class IRCServer(server.Server): self.connected = False if self.closing_event is not None: self.closing_event() - print ("Server `%s' successfully stopped." % self.id) + self.logger.info("Server `%s' successfully stopped." % self.id) self.stopping.set() # Rearm Thread threading.Thread.__init__(self) @@ -255,17 +255,17 @@ class IRCServer(server.Server): """Send a message without checks or format""" #TODO: add something for post message treatment here if channel == self.nick: - print ("\033[1;35mWarning:\033[0m Nemubot talks to himself: %s" % msg) - traceback.print_stack() + self.logger.warn("Nemubot talks to himself: %s" % msg) + self.logger.debug(traceback.print_stack()) if line is not None and channel is not None: if self.s is None: - print ("\033[1;35mWarning:\033[0m Attempt to send message on a non connected server: %s: %s" % (self.id, line)) - traceback.print_stack() + self.logger.warn("Attempt to send message on a non connected server: %s: %s" % (self.id, line)) + self.logger.debug(traceback.format_stack()) elif len(line) < 442: - self.s.send (("%s %s :%s%s" % (cmd, channel, line, endl)).encode ()) + self.s.send(("%s %s :%s%s" % (cmd, channel, line, endl)).encode ()) else: - print ("\033[1;35mWarning:\033[0m Message truncated due to size (%d ; max : 442) : %s" % (len(line), line)) - traceback.print_stack() + self.logger.warn("Message truncated due to size (%d ; max : 442) : %s" % (len(line), line)) + self.logger.debug(traceback.format_stack()) self.s.send (("%s %s :%s%s" % (cmd, channel, line[0:442]+"<…>", endl)).encode ()) def send_msg_usr(self, user, msg): diff --git a/server/__init__.py b/server/__init__.py index e16bd57..590a194 100644 --- a/server/__init__.py +++ b/server/__init__.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 <http://www.gnu.org/licenses/>. +import logging import socket import threading @@ -29,6 +30,8 @@ class Server(threading.Thread): self.moremessages = dict() + self.logger = logging.getLogger(__name__ + "/" + self.id) + threading.Thread.__init__(self) def isDCC(self, to=None): @@ -142,7 +145,7 @@ class Server(threading.Thread): self.connected = False #Send a message in order to close the socket try: - self.s.send(("Bye!\r\n" % self.nick).encode ()) + self.s.send(("Bye!\r\n").encode ()) except: pass self.stopping.wait() @@ -155,6 +158,7 @@ class Server(threading.Thread): self._receive_action = receive_action if not self.connected: self.stop = False + self.logger.info("Entering main loop for server") try: self.start() except RuntimeError: From 46c8048b53cce114df8a71731d1aaf4070083d66 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 15 Aug 2014 23:32:19 +0200 Subject: [PATCH 078/674] New mapquest module: can geocode (will help #45) --- modules/mapquest.py | 49 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 modules/mapquest.py diff --git a/modules/mapquest.py b/modules/mapquest.py new file mode 100644 index 0000000..17e7eb8 --- /dev/null +++ b/modules/mapquest.py @@ -0,0 +1,49 @@ +# coding=utf-8 + +import json +import re +from urllib.parse import quote +from urllib.request import urlopen + +nemubotversion = 3.4 + +def load(context): + if not CONF or not CONF.hasNode("mapquestapi") or not CONF.getNode("mapquestapi").hasAttribute("key"): + print ("You need a MapQuest API key in order to use this " + "module. Add it to the module configuration file:\n<mapquestapi" + " key=\"XXXXXXXXXXXXXXXX\" />\nRegister at " + "http://developer.mapquest.com/") + return None + + from hooks import Hook + add_hook("cmd_hook", Hook(cmd_geocode, "geocode")) + + +def help_tiny (): + """Line inserted in the response to the command !help""" + return "The mapquest module" + +def help_full (): + return "!geocode /place/: get coordinate of /place/." + + +def geocode(location): + raw = urlopen("http://open.mapquestapi.com/geocoding/v1/address?key=%s&location=%s" % (CONF.getNode("mapquestapi")["key"], quote(location))) + obj = json.loads(raw.read().decode()) + + if "results" in obj and "locations" in obj["results"][0]: + for loc in obj["results"][0]["locations"]: + yield loc + +def where(loc): + return re.sub(" +", " ", "%s %s %s %s %s" % (loc["street"], loc["adminArea5"], loc["adminArea4"], loc["adminArea3"], loc["adminArea1"])).strip() + +def cmd_geocode(msg): + if len(msg.cmds) < 2: + raise IRCException("indicate a name") + + locname = ' '.join(msg.cmds[1:]) + res = Response(msg.sender, channel=msg.channel, nick=msg.nick, nomore="No more geocode", count=" (%s more geocode)") + for loc in geocode(locname): + res.append_message("%s is at %s,%s (%s precision)" % (where(loc), loc["latLng"]["lat"], loc["latLng"]["lng"], loc["geocodeQuality"].lower())) + return res From 0a96627d6a3dcf74c378dc31d9f226ba9b1739ba Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 16 Aug 2014 01:26:45 +0200 Subject: [PATCH 079/674] Weather module use mapquest module to found city location --- modules/weather.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/modules/weather.py b/modules/weather.py index 895afce..3833756 100644 --- a/modules/weather.py +++ b/modules/weather.py @@ -6,6 +6,8 @@ import re from urllib.parse import quote from urllib.request import urlopen +import mapquest + nemubotversion = 3.4 def load(context): @@ -106,7 +108,15 @@ def treat_coord(msg): coords = list() coords.append(DATAS.index[j]["lat"]) coords.append(DATAS.index[j]["long"]) - return coords, specific + return j, coords, specific + + else: + geocode = [x for x in mapquest.geocode(j)] + if len(geocode): + coords = list() + coords.append(geocode[0]["latLng"]["lat"]) + coords.append(geocode[0]["latLng"]["lng"]) + return mapquest.where(geocode[0]), coords, specific raise IRCException("Je ne sais pas où se trouve %s." % " ".join(msg.cmds[1:])) @@ -137,7 +147,7 @@ def cmd_coordinates(msg): return Response(msg.sender, "Les coordonnées de %s sont %s,%s" % (msg.cmds[1], coords["lat"], coords["long"]), channel=msg.channel) def cmd_alert(msg): - coords, specific = treat_coord(msg) + loc, coords, specific = treat_coord(msg) wth = get_json_weather(coords) res = Response(msg.sender, channel=msg.channel, nomore="No more weather alert", count=" (%d more alerts)") @@ -149,7 +159,7 @@ def cmd_alert(msg): return res def cmd_weather(msg): - coords, specific = treat_coord(msg) + loc, coords, specific = treat_coord(msg) wth = get_json_weather(coords) res = Response(msg.sender, channel=msg.channel, nomore="No more weather information") From 17c29e386cc37562d8ba4077c65dbcc51f0441ea Mon Sep 17 00:00:00 2001 From: Max <max@23.tf> Date: Mon, 18 Aug 2014 17:49:37 +0200 Subject: [PATCH 080/674] Added imdb id support as a fallback to imdb title checkout feature. --- modules/imdb.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/imdb.py b/modules/imdb.py index e7c89a0..f0d4cc9 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -25,9 +25,13 @@ def cmd_imdb(msg): data = json.loads(response.read().decode()) if "Error" in data: - raise IRCException(data["Error"]) + url = "http://www.omdbapi.com/?i=%s" % urllib.parse.quote(' '.join(msg.cmds[1:])) + response = urllib.request.urlopen(url) + data = json.loads(response.read().decode()) + if "Error" in data: + raise IRCException(data["Error"]) - elif "Response" in data and data["Response"] == "True": + if "Response" in data and data["Response"] == "True": res = Response(msg.sender, channel=msg.channel, title="%s (%s)" % (data['Title'], data['Year']), nomore="No more information, more at http://www.imdb.com/title/%s" % data['imdbID']) From 82156543aa1e46e42c42e82e88f2a27d60577132 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 21 Aug 2014 15:16:25 +0200 Subject: [PATCH 081/674] Birthday module: fixes #63 --- modules/birthday.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/modules/birthday.py b/modules/birthday.py index bc9efcd..f58697c 100644 --- a/modules/birthday.py +++ b/modules/birthday.py @@ -86,27 +86,31 @@ def cmd_age(msg): return True def parseask(msg): - if re.match("^.*(date de naissance|birthday|geburtstag|née? |nee? le|born on).*$", msg.content, re.I) is not None: + res = re.match(r"^(\S+)\s*('s|suis|est|is|was|were)?\s+(birthday|geburtstag|née? |nee? le|born on).*$", msg.content, re.I) + if res is not None: try: extDate = msg.extractDate() if extDate is None or extDate.year > datetime.now().year: return Response(msg.sender, - "ta date de naissance ne paraît pas valide...", + "la date de naissance ne paraît pas valide...", msg.channel, msg.nick) else: - if msg.nick.lower() in DATAS.index: + 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 + if nick.lower() in DATAS.index: DATAS.index[msg.nick.lower()]["born"] = extDate else: ms = ModuleState("birthday") - ms.setAttribute("name", msg.nick.lower()) + ms.setAttribute("name", nick.lower()) ms.setAttribute("born", extDate) DATAS.addChild(ms) save() return Response(msg.sender, - "ok, c'est noté, ta date de naissance est le %s" - % extDate.strftime("%A %d %B %Y à %H:%M"), + "ok, c'est noté, %s est né le %s" + % (nick, extDate.strftime("%A %d %B %Y à %H:%M")), msg.channel, msg.nick) except: - raise IRCException("ta date de naissance ne paraît pas valide.") + raise IRCException("la date de naissance ne paraît pas valide.") From 2100afed66a807657f16a2983e1a12c7fb207586 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 25 Aug 2014 12:06:21 +0200 Subject: [PATCH 082/674] Remove 0x01 of CTCP messages only one time, even if parse_content is called multiple time --- message.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/message.py b/message.py index aa2b6e0..af4785f 100644 --- a/message.py +++ b/message.py @@ -60,6 +60,9 @@ class Message: # Check for CTCP request self.ctcp = len(words[3]) > 1 and (words[3][0] == 0x01 or words[3][1] == 0x01) self.content = self.pickWords(words[3:]) + # If CTCP, remove 0x01 + if self.ctcp: + self.content = self.content[1:len(self.content)-1] elif self.cmd == '353' and len(words) > 3: for i in range(2, len(words)): if words[i][0] == 58: @@ -88,10 +91,6 @@ class Message: def parse_content(self): """Parse or reparse the message content""" - # If CTCP, remove 0x01 - if self.ctcp: - self.content = self.content[1:len(self.content)-1] - # Split content by words try: self.cmds = shlex.split(self.content) From 0e5562fa019563d01b5c19ae91b71703e20c905b Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 26 Aug 2014 07:06:23 +0200 Subject: [PATCH 083/674] New module: github --- modules/github.py | 123 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 modules/github.py diff --git a/modules/github.py b/modules/github.py new file mode 100644 index 0000000..23b6b3e --- /dev/null +++ b/modules/github.py @@ -0,0 +1,123 @@ +# coding=utf-8 + +import json +import re +import urllib.error +from urllib.parse import quote +from urllib.request import urlopen + +from hooks import hook + +nemubotversion = 3.4 + +def help_tiny (): + """Line inserted in the response to the command !help""" + return "The GitHub module" + +def help_full (): + return "!github /repo/: Display information about /repo/.\n!github_user /user/: Display information about /user/." + + + +def info_repos(repo): + raw = urlopen("https://api.github.com/search/repositories?q=%s" % quote(repo), timeout=10) + return json.loads(raw.read().decode()) + +def info_user(username): + raw = urlopen("https://api.github.com/users/%s" % quote(username), timeout=10) + user = json.loads(raw.read().decode()) + + raw = urlopen("https://api.github.com/users/%s/repos?sort=updated" % quote(username), timeout=10) + user["repos"] = json.loads(raw.read().decode()) + + return user + +def info_issue(repo, issue=None): + rp = info_repos(repo) + if rp["items"]: + fullname = rp["items"][0]["full_name"] + else: + fullname = repo + + try: + if issue is not None: + raw = urlopen("https://api.github.com/repos/%s/issues/%s" % (quote(fullname), quote(issue)), timeout=10) + return [ json.loads(raw.read().decode()) ] + else: + raw = urlopen("https://api.github.com/repos/%s/issues?sort=updated" % quote(fullname), timeout=10) + return json.loads(raw.read().decode()) + except urllib.error.HTTPError: + raise IRCException("Repository not found") + + +@hook("cmd_hook", "github") +def cmd_github(msg): + if len(msg.cmds) < 2: + raise IRCException("indicate a repository name to search") + + repos = info_repos(" ".join(msg.cmds[1:])) + + res = Response(msg.sender, channel=msg.channel, nomore="No more repository", count=" (%d more repo)") + + for repo in repos["items"]: + homepage = "" + if repo["homepage"] is not None: + homepage = repo["homepage"] + " - " + res.append_message("Repository %s: %s%s Main language: %s; %d forks; %d stars; %d watchers; %d opened_issues; view it at %s" % (repo["full_name"], homepage, repo["description"], repo["language"], repo["forks"], repo["stargazers_count"], repo["watchers_count"], repo["open_issues_count"], repo["html_url"])) + + return res + +@hook("cmd_hook", "github_user") +def cmd_github(msg): + if len(msg.cmds) < 2: + raise IRCException("indicate a user name to search") + + res = Response(msg.sender, channel=msg.channel, nomore="No more user") + + user = info_user(" ".join(msg.cmds[1:])) + + if "login" in user: + if user["repos"]: + kf = " Known for: " + ", ".join([repo["name"] for repo in user["repos"]]) + else: + kf = "" + if "name" in user: + name = user["name"] + else: + name = user["login"] + res.append_message("User %s: %d public repositories; %d public gists; %d followers; %d following; view it at %s.%s" % (name, user["public_repos"], user["public_gists"], user["followers"], user["following"], user["html_url"], kf)) + else: + raise IRCException("User not found") + + return res + + +@hook("cmd_hook", "github_issue") +def cmd_github(msg): + if len(msg.cmds) < 2: + raise IRCException("indicate a user name to search") + + repo = " ".join(msg.cmds[1:]) + li = re.match("^(?P<repo>.*)\s+(?:#(?P<issue>[0-9]+))$", repo) + ri = re.match("^(?:#(?P<issue>[0-9]+))\s+(?P<repo>.*)$", repo) + + if li is not None: + issue = li.group("issue") + repo = li.group("repo") + count = None + elif ri is not None: + issue = ri.group("issue") + repo = ri.group("repo") + count = None + else: + issue = None + count = " (%d more issues)" + + res = Response(msg.sender, channel=msg.channel, nomore="No more issue", count=count) + + issues = info_issue(repo, issue) + + for issue in issues: + res.append_message("%s%s issue #%d: \x03\x02%s\x03\x02 opened by %s on %s: %s" % (issue["state"][0].upper(), issue["state"][1:], issue["number"], issue["title"], issue["user"]["login"], issue["created_at"], issue["body"].replace("\n", " "))) + + return res From 84d3ee262cb3ead6b007adbebfab2425594b9eb5 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 26 Aug 2014 16:38:30 +0200 Subject: [PATCH 084/674] IMDB module: detect IMDBid --- modules/imdb.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/modules/imdb.py b/modules/imdb.py index f0d4cc9..2324cd6 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -1,7 +1,8 @@ # coding=utf-8 -import urllib.request import json +import re +import urllib.request from hooks import hook @@ -18,20 +19,22 @@ def cmd_imdb(msg): if len(msg.cmds) < 2: raise IRCException("precise a movie/serie title!") - url = "http://www.omdbapi.com/?t=%s" % urllib.parse.quote(' '.join(msg.cmds[1:])) + title = ' '.join(msg.cmds[1:]) + + if re.match("^tt[0-9]{7}$", title) is not None: + url = "http://www.omdbapi.com/?i=%s" % urllib.parse.quote(title) + else: + url = "http://www.omdbapi.com/?t=%s" % urllib.parse.quote(title) + print_debug(url) response = urllib.request.urlopen(url) data = json.loads(response.read().decode()) if "Error" in data: - url = "http://www.omdbapi.com/?i=%s" % urllib.parse.quote(' '.join(msg.cmds[1:])) - response = urllib.request.urlopen(url) - data = json.loads(response.read().decode()) - if "Error" in data: - raise IRCException(data["Error"]) + raise IRCException(data["Error"]) - if "Response" in data and data["Response"] == "True": + elif "Response" in data and data["Response"] == "True": res = Response(msg.sender, channel=msg.channel, title="%s (%s)" % (data['Title'], data['Year']), nomore="No more information, more at http://www.imdb.com/title/%s" % data['imdbID']) From e5ec487d29d2b90c07d7da7c091b2e004ca48a0a Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 27 Aug 2014 07:57:00 +0200 Subject: [PATCH 085/674] Improve logging system --- bot.py | 18 +++++++++--------- channel.py | 9 ++++++--- consumer.py | 19 +++++-------------- hooks.py | 10 +++++----- importer.py | 16 ++++++++-------- nemubot.py | 27 +++++++++++++++++---------- prompt/builtins.py | 10 +++++----- response.py | 7 ++----- server/DCC.py | 10 +++++----- server/IRC.py | 19 ++++++++----------- server/__init__.py | 2 +- xmlparser/node.py | 8 ++++---- 12 files changed, 75 insertions(+), 80 deletions(-) diff --git a/bot.py b/bot.py index 3e60b8c..1e24087 100644 --- a/bot.py +++ b/bot.py @@ -32,7 +32,7 @@ from server.IRC import IRCServer from server.DCC import DCC import response -logger = logging.getLogger(__name__) +logger = logging.getLogger("nemubot.bot") ID_letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" @@ -41,7 +41,7 @@ class Bot: # Bot general informations self.version = 3.4 self.version_txt = "3.4-dev" - logger.info("Initiate nemubot v%s" % self.version_txt) + logger.info("Initiate nemubot v%s", self.version_txt) # Save various informations self.ip = ip @@ -91,7 +91,7 @@ class Bot: msg.sender, "USERINFO %s" % self.realname) self.ctcp_capabilities["VERSION"] = lambda srv, msg: _ctcp_response( msg.sender, "VERSION nemubot v%s" % self.version_txt) - logger.debug("CTCP capabilities setup: %s" % ", ".join(self.ctcp_capabilities)) + logger.debug("CTCP capabilities setup: %s", ", ".join(self.ctcp_capabilities)) def _ctcp_clientinfo(self, srv, msg): """Response to CLIENTINFO CTCP message""" @@ -106,7 +106,7 @@ class Bot: srv.dcc_clients[conn.sender] = conn conn.send_dcc("Hello %s!" % conn.nick) else: - logger.error("DCC: unable to connect to %s:%s" % (ip, msg.cmds[4])) + logger.error("DCC: unable to connect to %s:%s", ip, msg.cmds[4]) def add_event(self, evt, eid=None, module_src=None): @@ -137,12 +137,12 @@ class Bot: if module_src is not None: module_src.REGISTERED_EVENTS.append(evt.id) - logger.info("New event registered: %s -> %s" % (evt.id, evt)) + logger.info("New event registered: %s -> %s", evt.id, evt) return evt.id def del_event(self, id, module_src=None): """Find and remove an event from list""" - logger.info("Removing event: %s from %s" % (id, module_src)) + logger.info("Removing event: %s from %s", id, module_src) if len(self.events) > 0 and id == self.events[0].id: self.events.remove(self.events[0]) self.update_timer() @@ -165,7 +165,7 @@ class Bot: if self.event_timer is not None: self.event_timer.cancel() if len(self.events) > 0: - logger.debug("Update timer: next event in %d seconds" % + logger.debug("Update timer: next event in %d seconds", self.events[0].time_left.seconds) if datetime.now() + timedelta(seconds=5) >= self.events[0].current: while datetime.now() < self.events[0].current: @@ -247,7 +247,7 @@ class Bot: self.del_event(e) # Remove from the dict del self.modules[name] - logger.info("Module `%s' successfully unloaded." % name) + logger.info("Module `%s' successfully unloaded.", name) return True return False @@ -443,7 +443,7 @@ class Bot: return srv.moremessages[msg.channel] elif msg.cmds[0] == "dcc": - logger.debug("dcctest for " + msg.sender) + logger.debug("dcctest for %s", msg.sender) srv.send_dcc("Hello %s!" % msg.nick, msg.sender) elif msg.cmds[0] == "pvdcctest": logger.debug("dcctest") diff --git a/channel.py b/channel.py index 974713f..c6266e8 100644 --- a/channel.py +++ b/channel.py @@ -16,12 +16,15 @@ # 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 Channel: def __init__(self, name, password=None): self.name = name self.password = password self.people = dict() self.topic = "" + self.logger = logging.getLogger("nemubot.channel." + name) def treat(self, cmd, msg): if cmd == "353": @@ -41,7 +44,7 @@ class Channel: def join(self, nick, level = 0): """Someone join the channel""" - logger.debug("%s join %s" % (nick, self.name)) + self.logger.debug("%s join", nick) self.people[nick] = level def chtopic(self, newtopic): @@ -52,7 +55,7 @@ class Channel: def nick(self, oldnick, newnick): """Someone change his nick""" if oldnick in self.people: - logger.debug("%s switch nick to %s on %s" % (oldnick, newnick, self.name)) + self.logger.debug("%s switch nick to %s on", oldnick, newnick) lvl = self.people[oldnick] del self.people[oldnick] self.people[newnick] = lvl @@ -60,7 +63,7 @@ class Channel: def part(self, nick): """Someone leave the channel""" if nick in self.people: - logger.debug("%s has left %s" % (nick, self.name)) + self.logger.debug("%s has left", nick) del self.people[nick] def mode(self, msg): diff --git a/consumer.py b/consumer.py index ea63e1c..0efe37b 100644 --- a/consumer.py +++ b/consumer.py @@ -29,7 +29,7 @@ from message import Message import response import server -logger = logging.getLogger(__name__) +logger = logging.getLogger("nemubot.consumer") class MessageConsumer: """Store a message before treating""" @@ -67,7 +67,7 @@ class MessageConsumer: if (res.server is not None and not isinstance(res.server, server.Server)): logger.error("The server defined in this response doesn't " - "exist: %s" % res.server) + "exist: %s", res.server) res.server = None if res.server is None: res.server = self.srv @@ -80,7 +80,7 @@ class MessageConsumer: context.hooks.add_hook(res.type, res.hook, res.src) elif res is not None: - logger.error("Unrecognized response type: %s" % res) + logger.error("Unrecognized response type: %s", res) def run(self, context): """Create, parse and treat the message""" @@ -92,12 +92,7 @@ class MessageConsumer: msg.private = msg.private or msg.channel == self.srv.nick res = self.treat_in(context, msg) except: - logger.error("Error occurred during the processing of the message:" - " %s" % self.raw) - exc_type, exc_value, exc_traceback = sys.exc_info() - logger.debug(traceback.format_exception(exc_type, - exc_value, - exc_traceback)) + logger.exception("Error occurred during the processing of the message: %s", self.raw) return # Send message @@ -119,11 +114,7 @@ class EventConsumer: try: self.evt.launch_check() except: - logger.error("Error during event end") - exc_type, exc_value, exc_traceback = sys.exc_info() - logger.debug(traceback.format_exception(exc_type, - exc_value, - exc_traceback)) + logger.exception("Error during event end") if self.evt.next is not None: context.add_event(self.evt, self.evt.id) diff --git a/hooks.py b/hooks.py index 48c47ac..25f1019 100644 --- a/hooks.py +++ b/hooks.py @@ -22,7 +22,7 @@ import re from response import Response from exception import IRCException -logger = logging.getLogger(__name__) +logger = logging.getLogger("nemubot.hooks") class MessagesHook: def __init__(self, context, bot): @@ -54,19 +54,19 @@ class MessagesHook: def add_hook(self, store, hook, module_src=None): """Insert in the right place a hook into the given store""" - logger.info("Adding hook '%s' to store '%s' from module '%s'" % (hook, store, module_src)) + logger.info("Adding hook '%s' to store '%s' from module '%s'", hook, store, module_src) if module_src is None: logger.warn("No source module was passed to add_hook function, " "please fix it in order to be compatible with unload " "feature") if store in self.context.hooks_cache: - logger.debug("Cleaning hooks cache for " + store) + logger.debug("Cleaning hooks cache for %s", store) del self.context.hooks_cache[store] if not hasattr(self, store): # TODO: raise custom exception, this is a user problem, not internal one! - logger.error("Unrecognized hook store: " + store) + logger.error("Unrecognized hook store: %s", store) return attr = getattr(self, store) @@ -81,7 +81,7 @@ class MessagesHook: elif isinstance(attr, list): attr.append(hook) else: - logger.critical("Unrecognized hook store type: " + type(attr)) + logger.critical("Unrecognized hook store type: %s", type(attr)) return if module_src is not None and hasattr(module_src, "REGISTERED_HOOKS"): module_src.REGISTERED_HOOKS.append((store, hook)) diff --git a/importer.py b/importer.py index 4b435dc..7795547 100644 --- a/importer.py +++ b/importer.py @@ -29,7 +29,7 @@ import hooks import response import xmlparser -logger = logging.getLogger(__name__) +logger = logging.getLogger("nemubot.importer") class ModuleFinder(Finder): def __init__(self, context, prompt): @@ -146,7 +146,7 @@ class ModuleLoader(SourceLoader): # Set module common functions and datas module.__LOADED__ = True - module.logger = logging.getLogger("module/" + fullname) + module.logger = logging.getLogger("nemubot.module." + fullname) def prnt(*args): print("[%s]" % module.name, *args) @@ -165,7 +165,7 @@ class ModuleLoader(SourceLoader): if server in self.context.servers: return self.context.servers[server].send_response(res, None) else: - module.logger.error("Try to send a message to the unknown server: %s" % server) + module.logger.error("Try to send a message to the unknown server: %s", server) return False def add_hook(store, hook): @@ -218,8 +218,8 @@ class ModuleLoader(SourceLoader): break if depend["name"] not in module.MODS: logger.error("In module `%s', module `%s' require by this " - "module but is not loaded." % (module.name, - depend["name"])) + "module but is not loaded.", module.name, + depend["name"]) return # Add the module to the global modules list @@ -232,9 +232,9 @@ class ModuleLoader(SourceLoader): # Register hooks register_hooks(module, self.context, self.prompt) - logger.info("Module '%s' successfully loaded." % module.name) + logger.info("Module '%s' successfully loaded.", module.name) else: - logger.error("An error occurs while importing `%s'." % module.name) + logger.error("An error occurs while importing `%s'.", module.name) raise ImportError("An error occurs while importing `%s'." % module.name) return module @@ -245,7 +245,7 @@ def add_cap_hook(prompt, module, cmd): prompt.add_cap_hook(cmd["name"], getattr(module, cmd["call"])) else: logger.warn("In module `%s', no function `%s' defined for `%s' " - "command hook." % (module.name, cmd["call"], cmd["name"])) + "command hook.", module.name, cmd["call"], cmd["name"]) def register_hooks(module, context, prompt): """Register all available hooks""" diff --git a/nemubot.py b/nemubot.py index 66fc396..997d6c2 100755 --- a/nemubot.py +++ b/nemubot.py @@ -30,9 +30,19 @@ import importer if __name__ == "__main__": # Setup loggin interface - logging.basicConfig(filename='nemubot.log', level=logging.DEBUG) - logging.Formatter('%(asctime)s %(name)s %(levelname)s %(message)s') - logger = logging.getLogger(__name__) + logger = logging.getLogger("nemubot") + + formatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s %(message)s') + + ch = logging.StreamHandler() + ch.setFormatter(formatter) + ch.setLevel(logging.INFO) + logger.addHandler(ch) + + fh = logging.FileHandler('./nemubot.log') + fh.setFormatter(formatter) + fh.setLevel(logging.DEBUG) + logger.addHandler(fh) # Create bot context context = bot.Bot(0, "FIXME") @@ -68,14 +78,11 @@ if __name__ == "__main__": prmpt = prompt.hotswap(prmpt) # Reload all other modules bot.reload() - print ("\033[1;32mContext reloaded\033[0m, now in Nemubot %s" % - context.version_txt) + print("\033[1;32mContext reloaded\033[0m, now in Nemubot %s" % + context.version_txt) except: - print ("\033[1;31mUnable to reload the prompt due to errors.\033[0" - "m Fix them before trying to reload the prompt.") - exc_type, exc_value, exc_traceback = sys.exc_info() - sys.stderr.write (traceback.format_exception_only(exc_type, - exc_value)[0]) + logger.exception("\033[1;31mUnable to reload the prompt due to errors.\033[0" + "m Fix them before trying to reload the prompt.") print ("\nWaiting for other threads shuts down...") diff --git a/prompt/builtins.py b/prompt/builtins.py index 4e0b0d7..ac6ab64 100644 --- a/prompt/builtins.py +++ b/prompt/builtins.py @@ -20,7 +20,7 @@ import logging import os import xmlparser -logger = logging.getLogger(__name__) +logger = logging.getLogger("nemubot.prompt.builtins") def end(toks, context, prompt): """Quit the prompt for reload or exit""" @@ -69,11 +69,11 @@ def load_file(filename, context): if context.addServer(server, config["nick"], config["owner"], config["realname"], server.hasAttribute("ssl")): - logger.info("Server `%s:%s' successfully added." - % (server["server"], server["port"])) + logger.info("Server `%s:%s' successfully added.", + server["server"], server["port"]) else: - logger.warn("Server `%s:%s' already added, skiped." - % (server["server"], server["port"])) + logger.warn("Server `%s:%s' already added, skiped.", + server["server"], server["port"]) # Load files asked by the configuration file for load in config.getNodes("load"): diff --git a/response.py b/response.py index 0716180..cd44405 100644 --- a/response.py +++ b/response.py @@ -20,7 +20,7 @@ import logging import traceback import sys -logger = logging.getLogger(__name__) +logger = logging.getLogger("nemubot.response") class Response: def __init__(self, sender, message=None, channel=None, nick=None, server=None, @@ -56,10 +56,7 @@ class Response: def set_sender(self, sender): if sender is None or sender.find("!") < 0: if sender is not None: - exc_type, exc_value, exc_traceback = sys.exc_info() - logger.warn(traceback.format_exception(exc_type, - "Bad sender provided in Response, it will be ignored.", - exc_traceback)) + logger.warn("Bad sender provided in Response, it will be ignored.", stack_info=True) self.sender = None else: self.sender = sender diff --git a/server/DCC.py b/server/DCC.py index 2a0b741..c5f8798 100644 --- a/server/DCC.py +++ b/server/DCC.py @@ -81,7 +81,7 @@ class DCC(server.Server): 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: @@ -106,7 +106,7 @@ class DCC(server.Server): 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, @@ -117,7 +117,7 @@ class DCC(server.Server): 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 @@ -151,7 +151,7 @@ class DCC(server.Server): 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() @@ -204,7 +204,7 @@ class DCC(server.Server): if self.realname in self.srv.dcc_clients: del self.srv.dcc_clients[self.realname] - self.logger.info("Closing connection with " + 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/server/IRC.py b/server/IRC.py index 2029340..f42ecc6 100644 --- a/server/IRC.py +++ b/server/IRC.py @@ -173,8 +173,8 @@ class IRCServer(server.Server): self.s.connect((self.host, self.port)) #Connect to server except socket.error as e: self.s = None - self.logger.critical("Unable to connect to %s:%d: %s" - % (self.host, self.port, os.strerror(e.errno))) + self.logger.critical("Unable to connect to %s:%d: %s", + self.host, self.port, os.strerror(e.errno)) return self.stopping.clear() @@ -185,10 +185,10 @@ class IRCServer(server.Server): self.realname)).encode()) raw = self.s.recv(1024) if not raw: - self.logger.critical("Unable to connect to %s:%d" % (self.host, self.port)) + self.logger.critical("Unable to connect to %s:%d", self.host, self.port) return self.connected = True - self.logger.info("Connection to %s:%d completed" % (self.host, self.port)) + self.logger.info("Connection to %s:%d completed", self.host, self.port) if len(self.channels) > 0: for chn in self.channels.keys(): @@ -214,7 +214,7 @@ class IRCServer(server.Server): self.connected = False if self.closing_event is not None: self.closing_event() - self.logger.info("Server `%s' successfully stopped." % self.id) + self.logger.info("Server `%s' successfully stopped.", self.id) self.stopping.set() # Rearm Thread threading.Thread.__init__(self) @@ -255,17 +255,14 @@ class IRCServer(server.Server): """Send a message without checks or format""" #TODO: add something for post message treatment here if channel == self.nick: - self.logger.warn("Nemubot talks to himself: %s" % msg) - self.logger.debug(traceback.print_stack()) + self.logger.warn("Nemubot talks to himself: %s", msg, stack_info=True) if line is not None and channel is not None: if self.s is None: - self.logger.warn("Attempt to send message on a non connected server: %s: %s" % (self.id, line)) - self.logger.debug(traceback.format_stack()) + self.logger.warn("Attempt to send message on a non connected server: %s: %s", self.id, line, stack_info=True) elif len(line) < 442: self.s.send(("%s %s :%s%s" % (cmd, channel, line, endl)).encode ()) else: - self.logger.warn("Message truncated due to size (%d ; max : 442) : %s" % (len(line), line)) - self.logger.debug(traceback.format_stack()) + self.logger.warn("Message truncated due to size (%d ; max : 442) : %s", len(line), line, stack_info=True) self.s.send (("%s %s :%s%s" % (cmd, channel, line[0:442]+"<…>", endl)).encode ()) def send_msg_usr(self, user, msg): diff --git a/server/__init__.py b/server/__init__.py index 590a194..a1ac635 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -30,7 +30,7 @@ class Server(threading.Thread): self.moremessages = dict() - self.logger = logging.getLogger(__name__ + "/" + self.id) + self.logger = logging.getLogger("nemubot.server." + self.id) threading.Thread.__init__(self) diff --git a/xmlparser/node.py b/xmlparser/node.py index 254dc31..1203cb6 100644 --- a/xmlparser/node.py +++ b/xmlparser/node.py @@ -3,10 +3,13 @@ import xml.sax from datetime import datetime from datetime import date +import logging import sys import time import traceback +logger = logging.getLogger("nemubot.xmlparser.node") + class ModuleState: """Tiny tree representation of an XML file""" @@ -192,10 +195,7 @@ class ModuleState: gen.endElement(self.name) except: - print ("\033[1;31mERROR:\033[0m occurred when saving the " - "following XML node: %s with %s" % (self.name, attrs)) - exc_type, exc_value, exc_traceback = sys.exc_info() - traceback.print_exception(exc_type, exc_value, exc_traceback) + 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""" From cd7843e16e3439192a8217a8fde27f17cba2c59a Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 28 Aug 2014 01:39:31 +0200 Subject: [PATCH 086/674] Replace help_tiny by module docstring --- bot.py | 2 +- modules/alias.py | 6 ++---- modules/birthday.py | 7 ++----- modules/bonneannee.py | 2 ++ modules/conjugaison.py | 5 ++--- modules/ddg/__init__.py | 2 ++ modules/events.py | 6 ++---- modules/github.py | 6 ++---- modules/imdb.py | 5 ++--- modules/networking.py | 6 ++---- modules/nextstop/__init__.py | 6 ++---- modules/reddit.py | 6 ++---- modules/rnd.py | 2 ++ modules/sap.py | 5 ++--- modules/sleepytime.py | 6 ++---- modules/sms.py | 6 ++---- modules/spell/__init__.py | 5 ++--- modules/syno.py | 5 ++--- modules/translate.py | 6 ++---- modules/velib.py | 6 ++---- modules/watchWebsite/__init__.py | 6 ++---- modules/weather.py | 6 ++---- modules/worldcup.py | 6 ++---- modules/ycc.py | 6 ++---- 24 files changed, 47 insertions(+), 77 deletions(-) diff --git a/bot.py b/bot.py index 1e24087..7aeaf01 100644 --- a/bot.py +++ b/bot.py @@ -608,7 +608,7 @@ def _help_msg(sndr, modules, cmd): 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, modules[im].help_tiny ()) for im in modules if hasattr(modules[im], "help_tiny")]) + message=["\x03\x02%s\x03\x02 (%s)" % (im, modules[im].__doc__) for im in modules if modules[im].__doc__]) return res def hotswap(bak): diff --git a/modules/alias.py b/modules/alias.py index 8b8eea6..7d83d3b 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -1,5 +1,7 @@ # coding=utf-8 +"""Create alias of commands""" + import re import sys from datetime import datetime @@ -19,10 +21,6 @@ def load(context): DATAS.getNode("variables").setIndex("name") -def help_tiny (): - """Line inserted in the response to the command !help""" - return "alias module" - def help_full (): return "TODO" diff --git a/modules/birthday.py b/modules/birthday.py index f58697c..cfb2ffa 100644 --- a/modules/birthday.py +++ b/modules/birthday.py @@ -1,5 +1,7 @@ # coding=utf-8 +"""People birthdays and ages""" + import re import sys from datetime import datetime @@ -15,11 +17,6 @@ def load(context): DATAS.setIndex("name", "birthday") -def help_tiny(): - """Line inserted in the response to the command !help""" - return "People birthdays and ages" - - def help_full(): return "!anniv /who/: gives the remaining time before the anniversary of /who/\n!age /who/: gives the age of /who/\nIf /who/ is not given, gives the remaining time before your anniversary.\n\n To set yout birthday, say it to nemubot :)" diff --git a/modules/bonneannee.py b/modules/bonneannee.py index 2d08038..4f2868d 100644 --- a/modules/bonneannee.py +++ b/modules/bonneannee.py @@ -1,5 +1,7 @@ # coding=utf-8 +"""Wishes Happy New Year when the time comes""" + from datetime import datetime from hooks import hook diff --git a/modules/conjugaison.py b/modules/conjugaison.py index f8838df..9d9c492 100644 --- a/modules/conjugaison.py +++ b/modules/conjugaison.py @@ -1,5 +1,7 @@ # coding=utf-8 +"""Find french conjugaison""" + import re import traceback import sys @@ -28,9 +30,6 @@ d = defaultdict(list) for k, v in s: d[k].append(v) -def help_tiny(): - return "Find french conjugaison" - def help_full(): return "!conjugaison <tens> <verb>: give the conjugaison for <verb> in <tens>." diff --git a/modules/ddg/__init__.py b/modules/ddg/__init__.py index 6896df4..18f01c9 100644 --- a/modules/ddg/__init__.py +++ b/modules/ddg/__init__.py @@ -1,5 +1,7 @@ # coding=utf-8 +"""Search around various search engine or knowledges database""" + import imp from hooks import hook diff --git a/modules/events.py b/modules/events.py index 193cca7..5a745fb 100644 --- a/modules/events.py +++ b/modules/events.py @@ -1,5 +1,7 @@ # coding=utf-8 +"""Create countdowns and reminders""" + import imp import re import sys @@ -14,10 +16,6 @@ nemubotversion = 3.4 from event import ModuleEvent from hooks import Hook, hook -def help_tiny (): - """Line inserted in the response to the command !help""" - return "events manager" - def help_full (): return "This module store a lot of events: ny, we, " + (", ".join(DATAS.index.keys())) + "\n!eventslist: gets list of timer\n!start /something/: launch a timer" diff --git a/modules/github.py b/modules/github.py index 23b6b3e..220c407 100644 --- a/modules/github.py +++ b/modules/github.py @@ -1,5 +1,7 @@ # coding=utf-8 +"""Repositories, users or issues on GitHub""" + import json import re import urllib.error @@ -10,10 +12,6 @@ from hooks import hook nemubotversion = 3.4 -def help_tiny (): - """Line inserted in the response to the command !help""" - return "The GitHub module" - def help_full (): return "!github /repo/: Display information about /repo/.\n!github_user /user/: Display information about /user/." diff --git a/modules/imdb.py b/modules/imdb.py index 2324cd6..26b40a3 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -1,5 +1,7 @@ # coding=utf-8 +"""Show many information about a movie or serie""" + import json import re import urllib.request @@ -8,9 +10,6 @@ from hooks import hook nemubotversion = 3.4 -def help_tiny (): - return "Show many information about a movie or serie" - def help_full (): return "Search a movie title with: !imdbs <approximative title> ; View movie details with !imdb <title>" diff --git a/modules/networking.py b/modules/networking.py index 7536063..5572320 100644 --- a/modules/networking.py +++ b/modules/networking.py @@ -1,5 +1,7 @@ # coding=utf-8 +"""Various network tools (w3m, w3c validator, curl, traceurl, ...)""" + import datetime import http.client import json @@ -21,10 +23,6 @@ def load(context): else: add_hook("cmd_hook", Hook(cmd_whois, "netwhois")) -def help_tiny(): - """Line inserted in the response to the command !help""" - return "The networking module" - def help_full(): return "!traceurl /url/: Follow redirections from /url/." diff --git a/modules/nextstop/__init__.py b/modules/nextstop/__init__.py index 6875239..36838ca 100644 --- a/modules/nextstop/__init__.py +++ b/modules/nextstop/__init__.py @@ -1,5 +1,7 @@ # coding=utf-8 +"""Informe les usagers des prochains passages des transports en communs de la RATP""" + import http.client import re from xml.dom.minidom import parseString @@ -13,10 +15,6 @@ def load(context): DATAS.setIndex("name", "station") -def help_tiny (): - """Line inserted in the response to the command !help""" - return "Informe les usagers des prochains passages des transports en communs de la 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." diff --git a/modules/reddit.py b/modules/reddit.py index 3500aac..62b01ab 100644 --- a/modules/reddit.py +++ b/modules/reddit.py @@ -1,5 +1,7 @@ # coding=utf-8 +"""Get information about subreddit""" + import json import re import urllib @@ -8,10 +10,6 @@ nemubotversion = 3.4 from hooks import hook -def help_tiny(): - """Line inserted in the response to the command !help""" - return "The subreddit module" - def help_full(): return "!subreddit /subreddit/: Display information on the subreddit." diff --git a/modules/rnd.py b/modules/rnd.py index 812b88e..fc28b01 100644 --- a/modules/rnd.py +++ b/modules/rnd.py @@ -1,5 +1,7 @@ # coding=utf-8 +"""Help to make choice""" + import random from hooks import hook diff --git a/modules/sap.py b/modules/sap.py index 139a5ee..5b802d5 100644 --- a/modules/sap.py +++ b/modules/sap.py @@ -1,5 +1,7 @@ # coding=utf-8 +"""Find information about an SAP transaction codes""" + import urllib.request import json import re @@ -10,9 +12,6 @@ from tools.web import striphtml nemubotversion = 3.4 -def help_tiny (): - return "Find information about an SAP transaction codes" - def help_full (): return "!tcode <transaction code|keywords>" diff --git a/modules/sleepytime.py b/modules/sleepytime.py index 681ed42..cf45af6 100644 --- a/modules/sleepytime.py +++ b/modules/sleepytime.py @@ -1,5 +1,7 @@ # coding=utf-8 +"""as http://sleepyti.me/, give you the best time to go to bed""" + import re import imp from datetime import datetime @@ -9,10 +11,6 @@ from hooks import hook nemubotversion = 3.4 -def help_tiny(): - """Line inserted in the response to the command !help""" - return "as http://sleepyti.me/, give you the best time to go to bed" - def help_full(): return "If you would like to sleep soon, use !sleepytime to know the best time to wake up; use !sleepytime hh:mm if you want to wake up at hh:mm" diff --git a/modules/sms.py b/modules/sms.py index 8fc8fb6..a0cfeba 100644 --- a/modules/sms.py +++ b/modules/sms.py @@ -1,5 +1,7 @@ # coding=utf-8 +"""Send SMS using SMS API (currently only Free Mobile)""" + import re import socket import time @@ -15,10 +17,6 @@ def load(context): global DATAS DATAS.setIndex("name", "phone") -def help_tiny(): - """Line inserted in the response to the command !help""" - return "Send SMS using SMS API (currently only Free Mobile)" - def help_full(): return "!sms /who/[,/who/[,...]] message: send a SMS to /who/." diff --git a/modules/spell/__init__.py b/modules/spell/__init__.py index 23bdda8..5426f73 100644 --- a/modules/spell/__init__.py +++ b/modules/spell/__init__.py @@ -1,5 +1,7 @@ # coding=utf-8 +"""Check words spelling""" + import re from urllib.parse import quote @@ -10,9 +12,6 @@ from .pyaspell import AspellError nemubotversion = 3.4 -def help_tiny(): - return "Check words spelling" - def help_full(): return "!spell [<lang>] <word>: give the correct spelling of <word> in <lang=fr>." diff --git a/modules/syno.py b/modules/syno.py index 9916bc5..548d89f 100644 --- a/modules/syno.py +++ b/modules/syno.py @@ -1,5 +1,7 @@ # coding=utf-8 +"""Find french synonyms""" + import re import traceback import sys @@ -10,9 +12,6 @@ from tools import web nemubotversion = 3.4 -def help_tiny(): - return "Find french synonyms" - def help_full(): return "!syno <word>: give a list of synonyms for <word>." diff --git a/modules/translate.py b/modules/translate.py index 42b302a..8191d3b 100644 --- a/modules/translate.py +++ b/modules/translate.py @@ -1,5 +1,7 @@ # coding=utf-8 +"""Translation module""" + import http.client import re import socket @@ -30,10 +32,6 @@ def load(context): add_hook("cmd_hook", Hook(cmd_translate, "translate")) -def help_tiny(): - """Line inserted in the response to the command !help""" - return "Translation module" - def help_full(): return "!translate [lang] <term>[ <term>[...]]: Found translation of <term> from/to english to/from <lang>. Data © WordReference.com" diff --git a/modules/velib.py b/modules/velib.py index d21c4f7..3a4cfe8 100644 --- a/modules/velib.py +++ b/modules/velib.py @@ -1,5 +1,7 @@ # coding=utf-8 +"""Gets information about velib stations""" + import re from tools import web @@ -15,10 +17,6 @@ def load(context): # station_status) # context.add_event(evt) -def help_tiny (): - """Line inserted in the response to the command !help""" - return "Gets information about velib stations" - def help_full (): return "!velib /number/ ...: gives available bikes and slots at the station /number/." diff --git a/modules/watchWebsite/__init__.py b/modules/watchWebsite/__init__.py index dad1604..1e749dd 100644 --- a/modules/watchWebsite/__init__.py +++ b/modules/watchWebsite/__init__.py @@ -1,5 +1,7 @@ # coding=utf-8 +"""Alert on changes on websites""" + from datetime import datetime from datetime import timedelta import http.client @@ -17,10 +19,6 @@ from .atom import Atom nemubotversion = 3.4 -def help_tiny (): - """Line inserted in the response to the command !help""" - return "Alert on changes on websites" - def help_full (): return "This module is autonomous you can't interract with it." diff --git a/modules/weather.py b/modules/weather.py index 3833756..cbb11b7 100644 --- a/modules/weather.py +++ b/modules/weather.py @@ -1,5 +1,7 @@ # coding=utf-8 +"""The weather module""" + import datetime import json import re @@ -27,10 +29,6 @@ def load(context): add_hook("cmd_hook", Hook(cmd_coordinates, "coordinates")) -def help_tiny (): - """Line inserted in the response to the command !help""" - return "The weather module" - def help_full (): return "!weather /city/: Display the current weather in /city/." diff --git a/modules/worldcup.py b/modules/worldcup.py index a1628d2..c32b133 100644 --- a/modules/worldcup.py +++ b/modules/worldcup.py @@ -1,5 +1,7 @@ # coding=utf-8 +"""The 2014 football worldcup module""" + import datetime import json import re @@ -19,10 +21,6 @@ def load(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, intervalle=30)) -def help_tiny (): - """Line inserted in the response to the command !help""" - return "The 2014 football worldcup module" - def help_full (): return "!worldcup: do something." diff --git a/modules/ycc.py b/modules/ycc.py index 779cb84..fb2208e 100644 --- a/modules/ycc.py +++ b/modules/ycc.py @@ -1,5 +1,7 @@ # coding=utf-8 +"""URL reducer module""" + import re from urllib.parse import urlparse from urllib.parse import quote @@ -9,10 +11,6 @@ from hooks import hook nemubotversion = 3.4 -def help_tiny(): - """Line inserted in the response to the command !help""" - return "Gets YCC urls" - def help_full(): return "!ycc [<url>]: with an argument, reduce the given <url> thanks to ycc.fr; without argument, reduce the last URL said on the current channel." From eae0adbb43f38b72259efaec78273d9d3f66d182 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 28 Aug 2014 02:04:33 +0200 Subject: [PATCH 087/674] IMDB module: reworking, handle year precision --- modules/imdb.py | 122 +++++++++++++++++++++++++++++++----------------- 1 file changed, 79 insertions(+), 43 deletions(-) diff --git a/modules/imdb.py b/modules/imdb.py index 26b40a3..c859381 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -10,63 +10,99 @@ from hooks import hook nemubotversion = 3.4 -def help_full (): - return "Search a movie title with: !imdbs <approximative title> ; View movie details with !imdb <title>" +def help_full(): + return "Search a movie title with: !imdbs <approximative title> ; View movie details with !imdb <title>" -@hook("cmd_hook", "imdb", help="View movie details with !imdb <title>") +def get_movie(title=None, year=None, imdbid=None, fullplot=True, tomatoes=False): + """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&" + + print_debug(url) + + # Make the request + response = urllib.request.urlopen(url) + data = json.loads(response.read().decode()) + + # Return data + if "Error" in data: + raise IRCException(data["Error"]) + + elif "Response" in data and data["Response"] == "True": + return data + + else: + raise IRCException("An error occurs during movie search") + +def find_movies(title): + """Find existing movies matching a approximate title""" + + # Built URL + url = "http://www.omdbapi.com/?s=%s" % urllib.parse.quote(title) + print_debug(url) + + # Make the request + raw = urllib.request.urlopen(url) + data = json.loads(raw.read().decode()) + + # Return data + if "Error" in data: + raise IRCException(data["Error"]) + + elif "Search" in data: + return data + + else: + raise IRCException("An error occurs during movie search") + + +@hook("cmd_hook", "imdb") def cmd_imdb(msg): + """View movie details with !imdb <title>""" if len(msg.cmds) < 2: raise IRCException("precise a movie/serie title!") title = ' '.join(msg.cmds[1:]) if re.match("^tt[0-9]{7}$", title) is not None: - url = "http://www.omdbapi.com/?i=%s" % urllib.parse.quote(title) + data = get_movie(imdbid=title) else: - url = "http://www.omdbapi.com/?t=%s" % urllib.parse.quote(title) + 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)) + else: + data = get_movie(title=title) - print_debug(url) + res = Response(msg.sender, channel=msg.channel, + title="%s (%s)" % (data['Title'], data['Year']), + nomore="No more information, more at http://www.imdb.com/title/%s" % data['imdbID']) - response = urllib.request.urlopen(url) - data = json.loads(response.read().decode()) + res.append_message("\x02rating\x0F: %s (%s votes); \x02plot\x0F: %s" % + (data['imdbRating'], data['imdbVotes'], data['Plot'])) - if "Error" in data: - raise IRCException(data["Error"]) + res.append_message("%s \x02from\x0F %s \x02released on\x0F %s; \x02genre:\x0F %s; \x02directed by:\x0F %s; \x02writed by:\x0F %s; \x02main actors:\x0F %s" + % (data['Type'], data['Country'], data['Released'], data['Genre'], data['Director'], data['Writer'], data['Actors'])) + return res - elif "Response" in data and data["Response"] == "True": - res = Response(msg.sender, 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 \x02from\x0F %s \x02released on\x0F %s; \x02genre:\x0F %s; \x02directed by:\x0F %s; \x02writed by:\x0F %s; \x02main actors:\x0F %s" - % (data['Type'], data['Country'], data['Released'], data['Genre'], data['Director'], data['Writer'], data['Actors'])) - return res - - else: - raise IRCException("An error occurs during movie search") - - -@hook("cmd_hook", "imdbs", help="!imdbs <approximative title> to search a movie title") +@hook("cmd_hook", "imdbs") def cmd_search(msg): - url = "http://www.omdbapi.com/?s=%s" % urllib.parse.quote(' '.join(msg.cmds[1:])) - print_debug(url) + """!imdbs <approximative title> to search a movie title""" - raw = urllib.request.urlopen(url) - data = json.loads(raw.read().decode()) + data = find_movies(' '.join(msg.cmds[1:])) - if "Error" in data: - raise IRCException(data["Error"]) + movies = list() + for m in data['Search']: + movies.append("\x02%s\x0F (%s of %s)" % (m['Title'], m['Type'], m['Year'])) - elif "Search" in data: - movies = list() - - for m in data['Search']: - movies.append("\x02%s\x0F (%s of %s)" % (m['Title'], m['Type'], m['Year'])) - - return Response(msg.sender, movies, title="Titles found", channel=msg.channel) - - else: - raise IRCException("An error occurs during movie search") + return Response(msg.sender, movies, title="Titles found", channel=msg.channel) From fdd4847f71c86e1f9bea091f2fc6a29169b769f5 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 28 Aug 2014 11:35:37 +0200 Subject: [PATCH 088/674] In config: nick, owner and realname can be overwrited in server node --- bot.py | 2 +- prompt/builtins.py | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/bot.py b/bot.py index 7aeaf01..c3f35e7 100644 --- a/bot.py +++ b/bot.py @@ -190,7 +190,7 @@ class Bot: self.update_timer() - def addServer(self, node, nick, owner, realname, ssl=False): + def add_server(self, node, nick, owner, realname, ssl=False): """Add a new server to the context""" srv = IRCServer(node, nick, owner, realname, ssl) srv.add_hook = lambda h: self.hooks.add_hook("irc_hook", h, self) diff --git a/prompt/builtins.py b/prompt/builtins.py index ac6ab64..4dd294f 100644 --- a/prompt/builtins.py +++ b/prompt/builtins.py @@ -62,18 +62,20 @@ def load_file(filename, context): config = xmlparser.parse_file(filename) # This is a true nemubot configuration file, load it! - if (config.getName() == "nemubotconfig" - or config.getName() == "config"): + if (config.getName() == "botconfig" + or config.getName() == "nemubotconfig"): # Preset each server in this file for server in config.getNodes("server"): - if context.addServer(server, config["nick"], - config["owner"], config["realname"], + nick = server["nick"] if server.hasAttribute("nick") else config["nick"] + owner = server["owner"] if server.hasAttribute("owner") else config["owner"] + realname = server["realname"] if server.hasAttribute("realname") else config["realname"] + if context.add_server(server, nick, owner, realname, server.hasAttribute("ssl")): - logger.info("Server `%s:%s' successfully added.", - server["server"], server["port"]) + print("Server `%s:%s' successfully added." % + (server["server"], server["port"])) else: - logger.warn("Server `%s:%s' already added, skiped.", - server["server"], server["port"]) + print("Server `%s:%s' already added, skiped." % + (server["server"], server["port"])) # Load files asked by the configuration file for load in config.getNodes("load"): @@ -88,7 +90,7 @@ def load_file(filename, context): print (" Can't load `%s'; this is not a valid nemubot " "configuration file." % filename) - # Unexisting file, assume a name was passed, import the module! + # Unexisting file, assume a name was passed, import the module! else: __import__(filename) From 97143a01824f2706ca027f8b883e6cd818003b14 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 28 Aug 2014 12:26:02 +0200 Subject: [PATCH 089/674] Centralize configuration: there is no more XML files for module, juste one bot configuration file, also containing module configuration; fixes #56 --- bot.py | 1 + bot_sample.xml | 23 ++++++++++------------ importer.py | 43 ++++++++++-------------------------------- modules/cmd_server.py | 12 ++++++++++++ modules/cmd_server.xml | 16 ---------------- prompt/builtins.py | 14 ++++++++------ 6 files changed, 41 insertions(+), 68 deletions(-) delete mode 100644 modules/cmd_server.xml diff --git a/bot.py b/bot.py index c3f35e7..da73b0a 100644 --- a/bot.py +++ b/bot.py @@ -52,6 +52,7 @@ class Bot: # Keep global context: servers and modules self.servers = dict() self.modules = dict() + self.modules_configuration = dict() # Context paths self.modules_path = mp diff --git a/bot_sample.xml b/bot_sample.xml index 5343a87..37f334f 100644 --- a/bot_sample.xml +++ b/bot_sample.xml @@ -10,19 +10,16 @@ </server> --> - <load path="cmd_server" /> + <!-- + <module name="ddg"> + <wfaapi key="YOUR-APIKEY" /> + </module> + --> - <load path="alias" /> - <load path="birthday" /> - <load path="ycc" /> - <load path="velib" /> - <load path="watchWebsite" /> - <load path="events" /> - <load path="sleepytime" /> - <load path="spell" /> - <load path="syno" /> - <load path="man" /> - <load path="reddit" /> - <load path="sap" /> + <module name="cmd_server" /> + + <module name="alias" /> + <module name="ycc" /> + <module name="events" /> </nemubotconfig> diff --git a/importer.py b/importer.py index 7795547..240af79 100644 --- a/importer.py +++ b/importer.py @@ -42,28 +42,22 @@ class ModuleFinder(Finder): if path is None: for mpath in self.context.modules_path: #print ("looking for", fullname, "in", mpath) - if os.path.isfile(mpath + fullname + ".xml"): - return ModuleLoader(self.context, self.prompt, fullname, - mpath, mpath + fullname + ".xml") - elif (os.path.isfile(mpath + fullname + ".py") or - os.path.isfile(mpath + fullname + "/__init__.py")): + if (os.path.isfile(mpath + fullname + ".py") or + os.path.isfile(mpath + fullname + "/__init__.py")): return ModuleLoader(self.context, self.prompt, - fullname, mpath, None) + fullname, mpath) #print ("not found") return None class ModuleLoader(SourceLoader): - def __init__(self, context, prompt, fullname, path, config_path): + def __init__(self, context, prompt, fullname, path): self.context = context self.prompt = prompt self.name = fullname - self.config_path = config_path - if config_path is not None: - self.config = xmlparser.parse_file(config_path) - if self.config.hasAttribute("name"): - self.name = self.config["name"] + if self.name in self.context.modules_configuration: + self.config = self.context.modules_configuration[self.name] else: self.config = None @@ -251,25 +245,8 @@ def register_hooks(module, context, prompt): """Register all available hooks""" # Register decorated functions for s, h in hooks.last_registered: - context.hooks.add_hook(s, h, module) + if s == "prompt_cmd": + prompt.add_cap_hook(h.name, h.call) + else: + context.hooks.add_hook(s, h, module) hooks.last_registered = [] - - if module.CONF is not None: - # Register command hooks - if module.CONF.hasNode("command"): - for cmd in module.CONF.getNodes("command"): - if cmd.hasAttribute("name") and cmd.hasAttribute("call"): - add_cap_hook(prompt, module, cmd) - - # Register message hooks - if module.CONF.hasNode("message"): - for msg in module.CONF.getNodes("message"): - context.hooks.register_hook(module, msg) - - # Register legacy hooks - if hasattr(module, "parseanswer"): - context.hooks.add_hook("cmd_default", hooks.Hook(module.parseanswer), module) - if hasattr(module, "parseask"): - context.hooks.add_hook("ask_default", hooks.Hook(module.parseask), module) - if hasattr(module, "parselisten"): - context.hooks.add_hook("msg_default", hooks.Hook(module.parselisten), module) diff --git a/modules/cmd_server.py b/modules/cmd_server.py index b898546..b781088 100644 --- a/modules/cmd_server.py +++ b/modules/cmd_server.py @@ -20,6 +20,7 @@ import traceback import sys from networkbot import NetworkBot +from hooks import hook nemubotversion = 3.4 NODATA = True @@ -33,6 +34,7 @@ def getserver(toks, context, prompt): else: return (None, toks) +@hook("prompt_cmd", "close") def close(data, toks, context, prompt): """Disconnect and forget (remove from the servers list) the server""" if len(toks) > 1: @@ -48,6 +50,7 @@ def close(data, toks, context, prompt): prompt.selectedServer = None return +@hook("prompt_cmd", "connect") def connect(data, toks, context, prompt): """Make the connexion to a server""" if len(toks) > 1: @@ -62,6 +65,7 @@ def connect(data, toks, context, prompt): else: print (" Please SELECT a server or give its name in argument.") +@hook("prompt_cmd", "disconnect") def disconnect(data, toks, context, prompt): """Close the connection to a server""" if len(toks) > 1: @@ -78,6 +82,7 @@ def disconnect(data, toks, context, prompt): else: print (" Please SELECT a server or give its name in argument.") +@hook("prompt_cmd", "discover") def discover(data, toks, context, prompt): """Discover a new bot on a server""" (srv, toks) = getserver(toks, context, prompt) @@ -91,6 +96,7 @@ def discover(data, toks, context, prompt): else: print (" Please SELECT a server or give its name in first argument.") +@hook("prompt_cmd", "hotswap") def hotswap(data, toks, context, prompt): """Reload a server class""" if len(toks) > 1: @@ -107,6 +113,7 @@ def hotswap(data, toks, context, prompt): else: print (" Please SELECT a server or give its name in argument.") +@hook("prompt_cmd", "join") def join(data, toks, context, prompt): """Join or leave a channel""" rd = 1 @@ -136,6 +143,7 @@ def join(data, toks, context, prompt): srv.leave(toks[rd]) return +@hook("prompt_cmd", "save") def save_mod(data, toks, context, prompt): """Force save module data""" if len(toks) < 2: @@ -150,6 +158,7 @@ def save_mod(data, toks, context, prompt): print ("save: no module named `%s´" % mod) return +@hook("prompt_cmd", "send") def send(data, toks, context, prompt): """Send a message on a channel""" rd = 1 @@ -190,6 +199,7 @@ def send(data, toks, context, prompt): srv.send_msg_final(chan, toks[rd]) return "done" +@hook("prompt_cmd", "zap") def zap(data, toks, context, prompt): """Hard change connexion state""" if len(toks) > 1: @@ -203,6 +213,7 @@ def zap(data, toks, context, prompt): else: print (" Please SELECT a server or give its name in argument.") +@hook("prompt_cmd", "top") def top(data, toks, context, prompt): """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)) @@ -216,6 +227,7 @@ def top(data, toks, context, prompt): print("################ Stack trace for thread %u ################" % th.ident) traceback.print_stack(sys._current_frames()[th.ident]) +@hook("prompt_cmd", "netstat") def netstat(data, toks, context, prompt): """Display sockets in use and many other things""" if len(context.network) > 0: diff --git a/modules/cmd_server.xml b/modules/cmd_server.xml deleted file mode 100644 index f87789a..0000000 --- a/modules/cmd_server.xml +++ /dev/null @@ -1,16 +0,0 @@ -<?xml version="1.0" ?> -<nemubotmodule name="cmd_server"> - <command name="close" call="close" /> - <command name="connect" call="connect" /> - <command name="discover" call="discover" /> - <command name="disconnect" call="disconnect" /> - <command name="hotswap" call="hotswap" /> - <command name="join" call="join" /> - <command name="leave" call="join" /> - <command name="part" call="join" /> - <command name="save" call="save_mod" /> - <command name="send" call="send" /> - <command name="zap" call="zap" /> - <command name="top" call="top" /> - <command name="netstat" call="netstat" /> -</nemubotmodule> diff --git a/prompt/builtins.py b/prompt/builtins.py index 4dd294f..dd5308a 100644 --- a/prompt/builtins.py +++ b/prompt/builtins.py @@ -77,13 +77,15 @@ def load_file(filename, context): print("Server `%s:%s' already added, skiped." % (server["server"], server["port"])) - # Load files asked by the configuration file - for load in config.getNodes("load"): - load_file(load["path"], context) + # Load module and their configuration + for mod in config.getNodes("module"): + context.modules_configuration[mod["name"]] = mod + if not mod.hasAttribute("autoload") or (mod["autoload"].lower() != "false" and mod["autoload"].lower() != "off"): + __import__(mod["name"]) - # This is a nemubot module configuration file, load the module - elif config.getName() == "nemubotmodule": - __import__(config["name"]) + # Load files asked by the configuration file + for load in config.getNodes("include"): + load_file(load["path"], context) # Other formats else: From 039c5789875ff7f5f3178430045d8b468b33b0a4 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 28 Aug 2014 12:43:22 +0200 Subject: [PATCH 090/674] New builtin IRC command: !next, similar to !more --- bot.py | 12 ++++++++++++ response.py | 4 +--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/bot.py b/bot.py index da73b0a..14f8e04 100644 --- a/bot.py +++ b/bot.py @@ -443,6 +443,18 @@ class Bot: if msg.channel in srv.moremessages: return srv.moremessages[msg.channel] + elif msg.cmds[0] == "next": + ret = None + if msg.channel == srv.nick: + if msg.sender in srv.moremessages: + ret = srv.moremessages[msg.sender] + else: + if msg.channel in srv.moremessages: + ret = srv.moremessages[msg.channel] + if ret is not None: + ret.pop() + return ret + elif msg.cmds[0] == "dcc": logger.debug("dcctest for %s", msg.sender) srv.send_dcc("Hello %s!" % msg.nick, msg.sender) diff --git a/response.py b/response.py index cd44405..b9c30c0 100644 --- a/response.py +++ b/response.py @@ -100,6 +100,7 @@ class Response: def pop(self): self.messages.pop(0) + self.elt = 0 if isinstance(self.rawtitle, list): self.rawtitle.pop(0) if len(self.rawtitle) <= 0: @@ -136,13 +137,11 @@ class Response: msg += e + ", " self.elt += 1 self.pop() - self.elt = 0 return msg[:len(msg)-2] else: if len(elts) <= 432: self.pop() - self.elt = 0 if self.count is not None: return msg + elts + (self.count % len(self.messages)) else: @@ -164,7 +163,6 @@ class Response: msg += w + " " self.elt += len(w) + 1 self.pop() - self.elt = 0 return msg import hooks From da32ee649098aee82b2ffa06c95cbe24c5ca1c5e Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 28 Aug 2014 14:28:56 +0200 Subject: [PATCH 091/674] GitHub module: add command !github_commit --- modules/github.py | 83 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 67 insertions(+), 16 deletions(-) diff --git a/modules/github.py b/modules/github.py index 220c407..e073cd4 100644 --- a/modules/github.py +++ b/modules/github.py @@ -47,6 +47,23 @@ def info_issue(repo, issue=None): except urllib.error.HTTPError: raise IRCException("Repository not found") +def info_commit(repo, commit=None): + rp = info_repos(repo) + if rp["items"]: + fullname = rp["items"][0]["full_name"] + else: + fullname = repo + + try: + if commit is not None: + raw = urlopen("https://api.github.com/repos/%s/commits/%s" % (quote(fullname), quote(commit)), timeout=10) + return [ json.loads(raw.read().decode()) ] + else: + raw = urlopen("https://api.github.com/repos/%s/commits" % quote(fullname), timeout=10) + return json.loads(raw.read().decode()) + except urllib.error.HTTPError: + raise IRCException("Repository not found") + @hook("cmd_hook", "github") def cmd_github(msg): @@ -93,29 +110,63 @@ def cmd_github(msg): @hook("cmd_hook", "github_issue") def cmd_github(msg): if len(msg.cmds) < 2: - raise IRCException("indicate a user name to search") + raise IRCException("indicate a repository to view its issues") + + issue = None + if len(msg.cmds) > 2: + li = re.match("^#?([0-9]+)$", msg.cmds[1]) + ri = re.match("^#?([0-9]+)$", msg.cmds[-1]) + if li is not None: + issue = msg.cmds[1] + del msg.cmds[1] + elif ri is not None: + issue = msg.cmds[-1] + del msg.cmds[-1] repo = " ".join(msg.cmds[1:]) - li = re.match("^(?P<repo>.*)\s+(?:#(?P<issue>[0-9]+))$", repo) - ri = re.match("^(?:#(?P<issue>[0-9]+))\s+(?P<repo>.*)$", repo) - - if li is not None: - issue = li.group("issue") - repo = li.group("repo") - count = None - elif ri is not None: - issue = ri.group("issue") - repo = ri.group("repo") - count = None - else: - issue = None - count = " (%d more issues)" + count = " (%d more issues)" if issue is None else None res = Response(msg.sender, channel=msg.channel, nomore="No more issue", count=count) issues = info_issue(repo, issue) for issue in issues: - res.append_message("%s%s issue #%d: \x03\x02%s\x03\x02 opened by %s on %s: %s" % (issue["state"][0].upper(), issue["state"][1:], issue["number"], issue["title"], issue["user"]["login"], issue["created_at"], issue["body"].replace("\n", " "))) + res.append_message("%s%s issue #%d: \x03\x02%s\x03\x02 opened by %s on %s: %s" % + (issue["state"][0].upper(), + issue["state"][1:], + issue["number"], + issue["title"], + issue["user"]["login"], + issue["created_at"], + issue["body"].replace("\n", " "))) + return res + + +@hook("cmd_hook", "github_commit") +def cmd_github(msg): + if len(msg.cmds) < 2: + raise IRCException("indicate a repository to view its commits") + + commit = None + if len(msg.cmds) > 2: + if re.match("^[a-fA-F0-9]+$", msg.cmds[1]): + commit = msg.cmds[1] + del msg.cmds[1] + elif re.match("^[a-fA-F0-9]+$", msg.cmds[-1]): + commit = msg.cmds[-1] + del msg.cmds[-1] + + repo = " ".join(msg.cmds[1:]) + + count = " (%d more commits)" if commit is None else None + res = Response(msg.sender, channel=msg.channel, nomore="No more commit", count=count) + + commits = info_commit(repo, commit) + + for commit in commits: + res.append_message("Commit %s by %s on %s: %s" % (commit["sha"][:10], + commit["commit"]["author"]["name"], + commit["commit"]["author"]["date"], + commit["commit"]["message"].replace("\n", " "))) return res From 4b9a6305d439945a90fc1b19e26a092592632905 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 28 Aug 2014 18:05:21 +0200 Subject: [PATCH 092/674] Legacy hooks now need to be explicitely declared --- modules/alias.py | 2 +- modules/birthday.py | 1 + modules/events.py | 2 ++ modules/reddit.py | 2 +- modules/sms.py | 1 + modules/weather.py | 3 +++ modules/ycc.py | 1 + 7 files changed, 10 insertions(+), 2 deletions(-) diff --git a/modules/alias.py b/modules/alias.py index 7d83d3b..6eb8501 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -171,7 +171,7 @@ def treat_alias(msg, hooks_cache): return False return False - +@hook("ask_default") def parseask(msg): global ALIAS if re.match(".*(set|cr[ée]{2}|nouvel(le)?) alias.*", msg.content) is not None: diff --git a/modules/birthday.py b/modules/birthday.py index cfb2ffa..1473091 100644 --- a/modules/birthday.py +++ b/modules/birthday.py @@ -82,6 +82,7 @@ def cmd_age(msg): " Quand est-il né ?" % name, msg.channel, msg.nick) return True +@hook("ask_default") 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.content, re.I) if res is not None: diff --git a/modules/events.py b/modules/events.py index 5a745fb..e14b3d2 100644 --- a/modules/events.py +++ b/modules/events.py @@ -166,6 +166,7 @@ def liste(msg): else: return Response(msg.sender, "Compteurs connus : %s." % ", ".join(DATAS.index.keys()), channel=msg.channel) +@hook("cmd_default") def parseanswer(msg): if msg.cmds[0] in DATAS.index: res = Response(msg.sender, channel=msg.channel) @@ -185,6 +186,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") def parseask(msg): if RGXP_ask.match(msg.content) is not None: name = re.match("^.*!([^ \"'@!]+).*$", msg.content) diff --git a/modules/reddit.py b/modules/reddit.py index 62b01ab..7789540 100644 --- a/modules/reddit.py +++ b/modules/reddit.py @@ -54,7 +54,7 @@ def cmd_subreddit(msg): return all_res - +@hook("msg_default") def parselisten(msg): global LAST_SUBS diff --git a/modules/sms.py b/modules/sms.py index a0cfeba..48e9da2 100644 --- a/modules/sms.py +++ b/modules/sms.py @@ -76,6 +76,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") def parseask(msg): if msg.content.find("Free") >= 0 and ( msg.content.find("API") >= 0 or msg.content.find("api") >= 0) and ( diff --git a/modules/weather.py b/modules/weather.py index cbb11b7..dab0509 100644 --- a/modules/weather.py +++ b/modules/weather.py @@ -8,6 +8,8 @@ import re from urllib.parse import quote from urllib.request import urlopen +from hooks import hook + import mapquest nemubotversion = 3.4 @@ -208,6 +210,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") def parseask(msg): res = gps_ask.match(msg.content) if res is not None: diff --git a/modules/ycc.py b/modules/ycc.py index fb2208e..d653ccb 100644 --- a/modules/ycc.py +++ b/modules/ycc.py @@ -51,6 +51,7 @@ def cmd_ycc(msg): res.append(gen_response(False, msg, url)) return res +@hook("msg_default") def parselisten(msg): global LAST_URLS try: From a8fe4c51593312d11794f84340583c25b5593d75 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 29 Aug 2014 11:46:11 +0200 Subject: [PATCH 093/674] Remove legacy and never used response.Hook --- consumer.py | 3 --- response.py | 10 ---------- 2 files changed, 13 deletions(-) diff --git a/consumer.py b/consumer.py index 0efe37b..3839e72 100644 --- a/consumer.py +++ b/consumer.py @@ -76,9 +76,6 @@ class MessageConsumer: if context.treat_post(res): res.server.send_response(res, self.data) - elif isinstance(res, response.Hook): - context.hooks.add_hook(res.type, res.hook, res.src) - elif res is not None: logger.error("Unrecognized response type: %s", res) diff --git a/response.py b/response.py index b9c30c0..3df4fc1 100644 --- a/response.py +++ b/response.py @@ -164,13 +164,3 @@ class Response: self.elt += len(w) + 1 self.pop() return msg - -import hooks -class Hook: - def __init__(self, TYPE, call, name=None, data=None, regexp=None, - channels=list(), server=None, end=None, call_end=None, - SRC=None): - self.hook = hooks.Hook(call, name, data, regexp, channels, - server, end, call_end) - self.type = TYPE - self.src = SRC From 8d1919a36b91f399f5be85cdcd7be259d7e26147 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 29 Aug 2014 11:53:32 +0200 Subject: [PATCH 094/674] Backport part of v4 Bot class --- bot.py | 234 ++++++++++++++++++++++++++++++++++------------------ importer.py | 12 +-- nemubot.py | 6 +- 3 files changed, 164 insertions(+), 88 deletions(-) diff --git a/bot.py b/bot.py index 14f8e04..002f2d7 100644 --- a/bot.py +++ b/bot.py @@ -20,12 +20,16 @@ from datetime import datetime from datetime import timedelta import logging from queue import Queue +import re import threading import time -import re +import uuid -import consumer -import event +__version__ = '3.4.dev0' +__author__ = 'nemunaire' + +from consumer import Consumer, EventConsumer, MessageConsumer +from event import ModuleEvent import hooks from networkbot import NetworkBot from server.IRC import IRCServer @@ -34,18 +38,29 @@ import response logger = logging.getLogger("nemubot.bot") -ID_letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - class Bot: - def __init__(self, ip, realname, mp=list()): - # Bot general informations - self.version = 3.4 - self.version_txt = "3.4-dev" - logger.info("Initiate nemubot v%s", self.version_txt) + + """Class containing the bot context and ensuring key goals""" + + def __init__(self, ip="127.0.0.1", modules_paths=list(), data_path="./datas/"): + """Initialize the bot context + + Keyword arguments: + ip -- The external IP of the bot (default: 127.0.0.1) + modules_paths -- Paths to all directories where looking for module + data_path -- Path to directory where store bot context data + """ + + logger.info("Initiate nemubot v%s", __version__) + + # External IP for accessing this bot + self.ip = ip + + # Context paths + self.modules_paths = modules_paths + self.data_path = data_path # Save various informations - self.ip = ip - self.realname = realname self.ctcp_capabilities = dict() self.init_ctcp_capabilities() @@ -54,10 +69,6 @@ class Bot: self.modules = dict() self.modules_configuration = dict() - # Context paths - self.modules_path = mp - self.datas_path = './datas/' - # Events self.events = list() self.event_timer = None @@ -85,13 +96,13 @@ class Bot: self.ctcp_capabilities["CLIENTINFO"] = self._ctcp_clientinfo self.ctcp_capabilities["DCC"] = self._ctcp_dcc self.ctcp_capabilities["NEMUBOT"] = lambda srv, msg: _ctcp_response( - msg.sender, "NEMUBOT %f" % self.version) + msg.sender, "NEMUBOT %s" % __version__) self.ctcp_capabilities["TIME"] = lambda srv, msg: _ctcp_response( msg.sender, "TIME %s" % (datetime.now())) self.ctcp_capabilities["USERINFO"] = lambda srv, msg: _ctcp_response( - msg.sender, "USERINFO %s" % self.realname) + msg.sender, "USERINFO %s" % srv.realname) self.ctcp_capabilities["VERSION"] = lambda srv, msg: _ctcp_response( - msg.sender, "VERSION nemubot v%s" % self.version_txt) + msg.sender, "VERSION nemubot v%s" % __version__) logger.debug("CTCP capabilities setup: %s", ", ".join(self.ctcp_capabilities)) def _ctcp_clientinfo(self, srv, msg): @@ -110,43 +121,85 @@ class Bot: logger.error("DCC: unable to connect to %s:%s", ip, msg.cmds[4]) - def add_event(self, evt, eid=None, module_src=None): - """Register an event and return its identifiant for futur update""" - if eid is None: - # Find an ID - now = datetime.now() - evt.id = "%d%c%d%d%c%d%d%c%d" % (now.year, ID_letters[now.microsecond % 52], - now.month, now.day, ID_letters[now.microsecond % 42], - now.hour, now.minute, ID_letters[now.microsecond % 32], - now.second) - else: - evt.id = eid + # Events methods - # Add the event in place + def add_event(self, evt, eid=None, module_src=None): + """Register an event and return its identifiant for futur update + + Return: + None if the event is not in the queue (eg. if it has been executed during the call) or + returns the event ID. + + Argument: + evt -- The event object to add + + Keyword arguments: + eid -- The desired event ID (object or string UUID) + module_src -- The module to which the event is attached to + """ + + # Generate the event id if no given + if eid is None: + eid = uuid.uuid1() + + # Fill the id field of the event + if type(eid) is uuid.UUID: + evt.id = str(eid) + else: + # Ok, this is quite useless... + try: + evt.id = str(uuid.UUID(eid)) + except ValueError: + evt.id = eid + + # Add the event in its place t = evt.current - i = -1 + i = 0 # sentinel for i in range(0, len(self.events)): if self.events[i].current > t: - i -= 1 break - self.events.insert(i + 1, evt) - if i == -1: - self.update_timer() - if len(self.events) <= 0 or self.events[i+1] != evt: + self.events.insert(i, evt) + + if i == 0: + # First event changed, reset timer + self._update_event_timer() + if len(self.events) <= 0 or self.events[i] != evt: + # Our event has been executed and removed from queue return None + # Register the event in the source module if module_src is not None: module_src.REGISTERED_EVENTS.append(evt.id) + evt.module_src = module_src logger.info("New event registered: %s -> %s", evt.id, evt) return evt.id - def del_event(self, id, module_src=None): - """Find and remove an event from list""" - logger.info("Removing event: %s from %s", id, module_src) + + def del_event(self, evt, module_src=None): + """Find and remove an event from list + + Return: + True if the event has been found and removed, False else + + Argument: + evt -- The ModuleEvent object to remove or just the event identifier + + Keyword arguments: + module_src -- The module to which the event is attached to (ignored if evt is a ModuleEvent) + """ + + logger.info("Removing event: %s from %s", evt, module_src) + + if type(evt) is ModuleEvent: + id = evt.id + module_src = evt.module_src + else: + id = evt + if len(self.events) > 0 and id == self.events[0].id: self.events.remove(self.events[0]) - self.update_timer() + self._update_event_timer() if module_src is not None: module_src.REGISTERED_EVENTS.remove(evt.id) return True @@ -160,35 +213,52 @@ class Bot: return True return False - def update_timer(self): - """Relaunch the timer to end with the closest event""" + + def _update_event_timer(self): + """(Re)launch the timer to end with the closest event""" + # Reset the timer if this is the first item if self.event_timer is not None: self.event_timer.cancel() + if len(self.events) > 0: logger.debug("Update timer: next event in %d seconds", self.events[0].time_left.seconds) if datetime.now() + timedelta(seconds=5) >= self.events[0].current: while datetime.now() < self.events[0].current: time.sleep(0.6) - self.end_timer() + self._end_event_timer() else: self.event_timer = threading.Timer( - self.events[0].time_left.seconds + 1, self.end_timer) + self.events[0].time_left.seconds + 1, self._end_event_timer) self.event_timer.start() else: logger.debug("Update timer: no timer left") - def end_timer(self): - """Function called at the end of the timer""" - #print ("end timer") - while len(self.events) > 0 and datetime.now() >= self.events[0].current: - #print ("end timer: while") - evt = self.events.pop(0) - self.cnsr_queue.put_nowait(consumer.EventConsumer(evt)) - self.update_consumers() - self.update_timer() + def _end_event_timer(self): + """Function called at the end of the event timer""" + + while len(self.events) > 0 and datetime.now() >= self.events[0].current: + evt = self.events.pop(0) + self.cnsr_queue.put_nowait(EventConsumer(evt)) + self._launch_consumers() + + self._update_event_timer() + + + # Consumers methods + + def _launch_consumers(self): + """Launch new consumer threads if necessary""" + + while self.cnsr_queue.qsize() > self.cnsr_thrd_size: + # Next launch if two more items in queue + self.cnsr_thrd_size += 2 + + c = Consumer(self) + self.cnsr_thrd.append(c) + c.start() def add_server(self, node, nick, owner, realname, ssl=False): @@ -207,6 +277,21 @@ class Bot: return False + # Modules methods + + def add_modules_path(self, path): + """Add a path to the modules_path array, used by module loader""" + # The path must end by / char + if path[len(path)-1] != "/": + path = path + "/" + + if path not in self.modules_paths: + self.modules_paths.append(path) + return True + + return False + + def add_module(self, module): """Add a module to the context, if already exists, unload the old one before""" @@ -220,19 +305,6 @@ class Bot: return True - def add_modules_path(self, path): - """Add a path to the modules_path array, used by module loader""" - # The path must end by / char - if path[len(path)-1] != "/": - path = path + "/" - - if path not in self.modules_path: - self.modules_path.append(path) - return True - - return False - - def unload_module(self, name): """Unload a module""" if name in self.modules: @@ -252,22 +324,14 @@ class Bot: return True return False - def update_consumers(self): - """Launch new consumer thread if necessary""" - if self.cnsr_queue.qsize() > self.cnsr_thrd_size: - c = consumer.Consumer(self) - self.cnsr_thrd.append(c) - c.start() - self.cnsr_thrd_size += 2 - def receive_message(self, srv, raw_msg, private=False, data=None): """Queued the message for treatment""" #print (raw_msg) - self.cnsr_queue.put_nowait(consumer.MessageConsumer(srv, raw_msg, datetime.now(), private, data)) + self.cnsr_queue.put_nowait(MessageConsumer(srv, raw_msg, datetime.now(), private, data)) # Launch a new thread if necessary - self.update_consumers() + self._launch_consumers() def add_networkbot(self, srv, dest, dcc=None): @@ -298,7 +362,7 @@ class Bot: for srv in k: self.servers[srv].disconnect() -# Hooks cache + # Hooks cache def create_cache(self, name): if name not in self.hooks_cache: @@ -349,7 +413,7 @@ class Bot: return self.hooks_cache[name] -# Treatment + # Treatment def check_rest_times(self, store, hook): """Remove from store the hook if it has been executed given time""" @@ -416,6 +480,8 @@ class Bot: return self.treat_ask(msg, srv) def treat_prvmsg(self, msg, srv): + msg.is_owner = msg.nick == srv.owner + # First, treat CTCP if msg.ctcp: if msg.cmds[0] in self.ctcp_capabilities: @@ -625,7 +691,15 @@ def _help_msg(sndr, modules, cmd): return res def hotswap(bak): - return Bot(bak.servers, bak.modules, bak.modules_path) + new = Bot(str(bak.ip), bak.modules_paths, bak.data_path) + new.ctcp_capabilities = bak.ctcp_capabilities + new.servers = bak.servers + new.modules = bak.modules + new.modules_configuration = bak.modules_configuration + new.events = bak.events + new.hooks = bak.hooks + new.network = bak.network + return new def reload(): import imp diff --git a/importer.py b/importer.py index 240af79..26bc67e 100644 --- a/importer.py +++ b/importer.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 <http://www.gnu.org/licenses/>. +from distutils.version import LooseVersion from importlib.abc import Finder from importlib.abc import SourceLoader import imp @@ -23,6 +24,7 @@ import logging import os import sys +from bot import __version__ import event import exception import hooks @@ -40,7 +42,7 @@ class ModuleFinder(Finder): #print ("looking for", fullname, "in", path) # Search only for new nemubot modules (packages init) if path is None: - for mpath in self.context.modules_path: + for mpath in self.context.modules_paths: #print ("looking for", fullname, "in", mpath) if (os.path.isfile(mpath + fullname + ".py") or os.path.isfile(mpath + fullname + "/__init__.py")): @@ -134,11 +136,11 @@ class ModuleLoader(SourceLoader): if not hasattr(module, "nemubotversion"): raise ImportError("Module `%s' is not a nemubot module."%self.name) # Check module version - if module.nemubotversion != self.context.version: + if LooseVersion(__version__) < LooseVersion(str(module.nemubotversion)): raise ImportError("Module `%s' is not compatible with this " "version." % self.name) - # Set module common functions and datas + # Set module common functions and data module.__LOADED__ = True module.logger = logging.getLogger("nemubot.module." + fullname) @@ -151,7 +153,7 @@ class ModuleLoader(SourceLoader): module.logger.debug(*args) def mod_save(): - fpath = self.context.datas_path + "/" + module.name + ".xml" + fpath = self.context.data_path + "/" + module.name + ".xml" module.print_debug("Saving DATAS to " + fpath) module.DATAS.save(fpath) @@ -189,7 +191,7 @@ class ModuleLoader(SourceLoader): module.del_event = del_event if not hasattr(module, "NODATA"): - module.DATAS = xmlparser.parse_file(self.context.datas_path + module.DATAS = xmlparser.parse_file(self.context.data_path + module.name + ".xml") module.save = mod_save else: diff --git a/nemubot.py b/nemubot.py index 997d6c2..9d17ef8 100755 --- a/nemubot.py +++ b/nemubot.py @@ -45,7 +45,7 @@ if __name__ == "__main__": logger.addHandler(fh) # Create bot context - context = bot.Bot(0, "FIXME") + context = bot.Bot() # Load the prompt prmpt = prompt.Prompt() @@ -66,7 +66,7 @@ if __name__ == "__main__": else: load_file(arg, context) - print ("Nemubot v%s ready, my PID is %i!" % (context.version_txt, + print ("Nemubot v%s ready, my PID is %i!" % (bot.__version__, os.getpid())) while prmpt.run(context): try: @@ -79,7 +79,7 @@ if __name__ == "__main__": # Reload all other modules bot.reload() print("\033[1;32mContext reloaded\033[0m, now in Nemubot %s" % - context.version_txt) + bot.__version__) except: logger.exception("\033[1;31mUnable to reload the prompt due to errors.\033[0" "m Fix them before trying to reload the prompt.") From b4800643e15c955644a9757a46de476e804cc49e Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 29 Aug 2014 12:03:49 +0200 Subject: [PATCH 095/674] Fix list function in prompt --- prompt/builtins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prompt/builtins.py b/prompt/builtins.py index dd5308a..7633e46 100644 --- a/prompt/builtins.py +++ b/prompt/builtins.py @@ -41,12 +41,12 @@ def liste(toks, context, prompt): if l == "server" or l == "servers": for srv in context.servers.keys(): print (" - %s ;" % srv) - else: + 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) - else: + if len(context.modules) == 0: print (" > No module loaded") elif l in prompt.HOOKS_LIST: (f,d) = prompt.HOOKS_LIST[l] From 0e26450d8f0f7b832605f87c69d76a1275d57579 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 29 Aug 2014 12:25:25 +0200 Subject: [PATCH 096/674] Rework CTCP responses and implement FINGER, PING and SOURCE --- bot.py | 60 +++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/bot.py b/bot.py index 002f2d7..36ef80d 100644 --- a/bot.py +++ b/bot.py @@ -92,34 +92,50 @@ class Bot: def init_ctcp_capabilities(self): """Reset existing CTCP capabilities to default one""" - self.ctcp_capabilities["ACTION"] = lambda srv, msg: print ("ACTION receive") - self.ctcp_capabilities["CLIENTINFO"] = self._ctcp_clientinfo - self.ctcp_capabilities["DCC"] = self._ctcp_dcc + + def _ctcp_clientinfo(srv, msg): + """Response to CLIENTINFO CTCP message""" + return _ctcp_response(msg.sender, + " ".join(self.ctcp_capabilities.keys())) + + def _ctcp_dcc(srv, msg): + """Response to DCC CTCP message""" + try: + ip = srv.toIP(int(msg.cmds[3])) + port = int(msg.cmds[4]) + conn = DCC(srv, msg.sender) + except: + return _ctcp_response(msg.sender, "ERRMSG invalid parameters provided as DCC CTCP request") + + logger.info("Receive DCC connection request from %s to %s:%d", conn.sender, ip, port) + + if conn.accept_user(ip, port): + srv.dcc_clients[conn.sender] = conn + conn.send_dcc("Hello %s!" % conn.nick) + else: + logger.error("DCC: unable to connect to %s:%d", ip, port) + return _ctcp_response(msg.sender, "ERRMSG unable to connect to %s:%d" % (ip, port)) + + self.ctcp_capabilities["ACTION"] = lambda srv, msg: print ("ACTION receive: %s" % msg.content) + self.ctcp_capabilities["CLIENTINFO"] = _ctcp_clientinfo + self.ctcp_capabilities["DCC"] = _ctcp_dcc + self.ctcp_capabilities["FINGER"] = lambda srv, msg: _ctcp_response( + msg.sender, "VERSION nemubot v%s" % __version__) self.ctcp_capabilities["NEMUBOT"] = lambda srv, msg: _ctcp_response( - msg.sender, "NEMUBOT %s" % __version__) + msg.sender, "NEMUBOT %s" % __version__) + self.ctcp_capabilities["PING"] = lambda srv, msg: _ctcp_response( + msg.sender, "PING %s" % " ".join(msg.cmds[1:])) + self.ctcp_capabilities["SOURCE"] = lambda srv, msg: _ctcp_response( + msg.sender, "SOURCE https://github.com/nemunaire/nemubot") self.ctcp_capabilities["TIME"] = lambda srv, msg: _ctcp_response( - msg.sender, "TIME %s" % (datetime.now())) + msg.sender, "TIME %s" % (datetime.now())) self.ctcp_capabilities["USERINFO"] = lambda srv, msg: _ctcp_response( - msg.sender, "USERINFO %s" % srv.realname) + msg.sender, "USERINFO %s" % srv.realname) self.ctcp_capabilities["VERSION"] = lambda srv, msg: _ctcp_response( - msg.sender, "VERSION nemubot v%s" % __version__) + msg.sender, "VERSION nemubot v%s" % __version__) + logger.debug("CTCP capabilities setup: %s", ", ".join(self.ctcp_capabilities)) - def _ctcp_clientinfo(self, srv, msg): - """Response to CLIENTINFO CTCP message""" - return _ctcp_response(msg.sndr, - " ".join(self.ctcp_capabilities.keys())) - - def _ctcp_dcc(self, srv, msg): - """Response to DCC CTCP message""" - ip = srv.toIP(int(msg.cmds[3])) - conn = DCC(srv, msg.sender) - if conn.accept_user(ip, int(msg.cmds[4])): - srv.dcc_clients[conn.sender] = conn - conn.send_dcc("Hello %s!" % conn.nick) - else: - logger.error("DCC: unable to connect to %s:%s", ip, msg.cmds[4]) - # Events methods From 0a163212590f27ab3b9fbfde29690273efe65f2c Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 29 Aug 2014 16:33:45 +0200 Subject: [PATCH 097/674] (wip) reworking of the Message class; backported from v4 --- bot.py | 62 ++++++++--------- channel.py | 30 ++++---- consumer.py | 12 ++-- exception.py | 2 +- message.py | 162 +++++++++++++++++++++++--------------------- modules/alias.py | 8 +-- modules/birthday.py | 2 +- modules/events.py | 6 +- modules/reddit.py | 2 +- modules/sms.py | 10 +-- modules/weather.py | 2 +- modules/ycc.py | 2 +- response.py | 24 ++++--- server/IRC.py | 21 +++--- server/__init__.py | 20 +++--- 15 files changed, 186 insertions(+), 179 deletions(-) diff --git a/bot.py b/bot.py index 36ef80d..2ac55b1 100644 --- a/bot.py +++ b/bot.py @@ -116,7 +116,7 @@ class Bot: logger.error("DCC: unable to connect to %s:%d", ip, port) return _ctcp_response(msg.sender, "ERRMSG unable to connect to %s:%d" % (ip, port)) - self.ctcp_capabilities["ACTION"] = lambda srv, msg: print ("ACTION receive: %s" % msg.content) + self.ctcp_capabilities["ACTION"] = lambda srv, msg: print ("ACTION receive: %s" % msg.text) self.ctcp_capabilities["CLIENTINFO"] = _ctcp_clientinfo self.ctcp_capabilities["DCC"] = _ctcp_dcc self.ctcp_capabilities["FINGER"] = lambda srv, msg: _ctcp_response( @@ -298,8 +298,8 @@ class Bot: def add_modules_path(self, path): """Add a path to the modules_path array, used by module loader""" # The path must end by / char - if path[len(path)-1] != "/": - path = path + "/" + if path[-1] != "/": + path += "/" if path not in self.modules_paths: self.modules_paths.append(path) @@ -444,9 +444,9 @@ class Bot: def treat_pre(self, msg, srv): """Treat a message before all other treatment""" # Treat all messages starting with 'nemubot:' as distinct commands - if msg.cmd == "PRIVMSG" and msg.content.find("%s:"%srv.nick) == 0: + if msg.cmd == "PRIVMSG" and msg.text.find("%s:"%srv.nick) == 0: # Remove the bot name - msg.content = msg.content[len(srv.nick)+1:].strip() + msg.text = msg.text[len(srv.nick)+1:].strip() msg.parse_content() msg.private = True @@ -487,30 +487,28 @@ class Bot: def treat_prvmsg_ask(self, msg, srv): # Treat ping if re.match("^ *(m[' ]?entends?[ -]+tu|h?ear me|do you copy|ping)", - msg.content, re.I) is not None: + msg.text, re.I) is not None: return response.Response(msg.sender, message="pong", - channel=msg.channel, nick=msg.nick) + channel=msg.receivers, nick=msg.nick) # Ask hooks else: return self.treat_ask(msg, srv) def treat_prvmsg(self, msg, srv): - msg.is_owner = msg.nick == srv.owner - # First, treat CTCP - if msg.ctcp: + if msg.is_ctcp: if msg.cmds[0] in self.ctcp_capabilities: return self.ctcp_capabilities[msg.cmds[0]](srv, msg) else: return _ctcp_response(msg.sender, "ERRMSG Unknown or unimplemented CTCP request") # Owner commands - if len(msg.content) > 1 and msg.content[0] == '`' and msg.nick == srv.owner: + if len(msg.text) > 1 and msg.text[0] == '`' and msg.nick == srv.owner: #TODO: owner commands pass - elif len(msg.content) > 1 and msg.content[0] == '!': + elif len(msg.text) > 1 and msg.text[0] == '!': # Remove the ! msg.cmds[0] = msg.cmds[0][1:] @@ -518,21 +516,21 @@ class Bot: return _help_msg(msg.sender, self.modules, msg.cmds) elif msg.cmds[0] == "more": - if msg.channel == srv.nick: + if msg.receivers == srv.nick: if msg.sender in srv.moremessages: return srv.moremessages[msg.sender] else: - if msg.channel in srv.moremessages: - return srv.moremessages[msg.channel] + if msg.receivers in srv.moremessages: + return srv.moremessages[msg.receivers] elif msg.cmds[0] == "next": ret = None - if msg.channel == srv.nick: + if msg.receivers == srv.nick: if msg.sender in srv.moremessages: ret = srv.moremessages[msg.sender] else: - if msg.channel in srv.moremessages: - ret = srv.moremessages[msg.channel] + if msg.receivers in srv.moremessages: + ret = srv.moremessages[msg.receivers] if ret is not None: ret.pop() return ret @@ -569,7 +567,7 @@ class Bot: if msg.cmds[0] in cmd_hook: (hks, lvl, store, bot) = cmd_hook[msg.cmds[0]] for h in hks: - if h.is_matching(msg.cmds[0], channel=msg.channel, server=srv) and (msg.private or lvl == 0 or bot.nick not in srv.channels[msg.channel].people): + if h.is_matching(msg.cmds[0], channel=msg.receivers, server=srv) and (msg.private or lvl == 0 or bot.nick not in srv.channels[msg.receivers].people): res = h.run(msg, strcmp=msg.cmds[0]) if res is not None and res != False: treated.append(res) @@ -578,7 +576,7 @@ class Bot: # Then, treat regexp based hook cmd_rgxp = self.create_cache("cmd_rgxp") for hook, lvl, store, bot in cmd_rgxp: - if hook.is_matching(msg.cmds[0], msg.channel, server=srv) and (msg.private or lvl == 0 or bot.nick not in srv.channels[msg.channel].people): + if hook.is_matching(msg.cmds[0], msg.receivers, server=srv) and (msg.private or lvl == 0 or bot.nick not in srv.channels[msg.receivers].people): res = hook.run(msg) if res is not None and res != False: treated.append(res) @@ -602,11 +600,11 @@ class Bot: # First, treat simple hook ask_hook = self.create_cache("ask_hook") - if msg.content in ask_hook: - hks, lvl, store, bot = ask_hook[msg.content] + if msg.text in ask_hook: + hks, lvl, store, bot = ask_hook[msg.text] for h in hks: - if h.is_matching(msg.content, channel=msg.channel, server=srv) and (msg.private or lvl == 0 or bot.nick not in srv.channels[msg.channel].people): - res = h.run(msg, strcmp=msg.content) + if h.is_matching(msg.text, channel=msg.receivers, server=srv) and (msg.private or lvl == 0 or bot.nick not in srv.channels[msg.receivers].people): + res = h.run(msg, strcmp=msg.text) if res is not None and res != False: treated.append(res) self.check_rest_times(store, h) @@ -614,8 +612,8 @@ class Bot: # Then, treat regexp based hook ask_rgxp = self.create_cache("ask_rgxp") for hook, lvl, store, bot in ask_rgxp: - if hook.is_matching(msg.content, channel=msg.channel, server=srv) and (msg.private or lvl == 0 or bot.nick not in srv.channels[msg.channel].people): - res = hook.run(msg, strcmp=msg.content) + if hook.is_matching(msg.text, channel=msg.receivers, server=srv) and (msg.private or lvl == 0 or bot.nick not in srv.channels[msg.receivers].people): + res = hook.run(msg, strcmp=msg.text) if res is not None and res != False: treated.append(res) self.check_rest_times(store, hook) @@ -638,11 +636,11 @@ class Bot: # First, treat simple hook msg_hook = self.create_cache("msg_hook") - if msg.content in msg_hook: - hks, lvl, store, bot = msg_hook[msg.content] + if msg.text in msg_hook: + hks, lvl, store, bot = msg_hook[msg.text] for h in hks: - if h.is_matching(msg.content, channel=msg.channel, server=srv) and (msg.private or lvl == 0 or bot.nick not in srv.channels[msg.channel].people): - res = h.run(msg, strcmp=msg.content) + if h.is_matching(msg.text, channel=msg.receivers, server=srv) and (msg.private or lvl == 0 or bot.nick not in srv.channels[msg.receivers].people): + res = h.run(msg, strcmp=msg.text) if res is not None and res != False: treated.append(res) self.check_rest_times(store, h) @@ -650,8 +648,8 @@ class Bot: # Then, treat regexp based hook msg_rgxp = self.create_cache("msg_rgxp") for hook, lvl, store, bot in msg_rgxp: - if hook.is_matching(msg.content, channel=msg.channel, server=srv) and (msg.private or lvl == 0 or bot.nick not in srv.channels[msg.channel].people): - res = hook.run(msg, strcmp=msg.content) + if hook.is_matching(msg.text, channel=msg.receivers, server=srv) and (msg.private or lvl == 0 or bot.nick not in srv.channels[msg.receivers].people): + res = hook.run(msg, strcmp=msg.text) if res is not None and res != False: treated.append(res) self.check_rest_times(store, hook) diff --git a/channel.py b/channel.py index c6266e8..fb570a2 100644 --- a/channel.py +++ b/channel.py @@ -36,11 +36,11 @@ class Channel: elif cmd == "JOIN": self.join(msg.nick) elif cmd == "NICK": - self.nick(msg.nick, msg.content) + self.nick(msg.nick, msg.text) elif cmd == "PART" or cmd == "QUIT": self.part(msg.nick) elif cmd == "TOPIC": - self.topic = self.content + self.topic = self.text def join(self, nick, level = 0): """Someone join the channel""" @@ -67,31 +67,31 @@ class Channel: del self.people[nick] def mode(self, msg): - if msg.content[0] == "-k": + if msg.text[0] == "-k": self.password = "" - elif msg.content[0] == "+k": - if len(msg.content) > 1: - self.password = ' '.join(msg.content[1:])[1:] + elif msg.text[0] == "+k": + if len(msg.text) > 1: + self.password = ' '.join(msg.text[1:])[1:] else: - self.password = msg.content[1] - elif msg.content[0] == "+o": + self.password = msg.text[1] + elif msg.text[0] == "+o": self.people[msg.nick] |= 4 - elif msg.content[0] == "-o": + elif msg.text[0] == "-o": self.people[msg.nick] &= ~4 - elif msg.content[0] == "+h": + elif msg.text[0] == "+h": self.people[msg.nick] |= 2 - elif msg.content[0] == "-h": + elif msg.text[0] == "-h": self.people[msg.nick] &= ~2 - elif msg.content[0] == "+v": + elif msg.text[0] == "+v": self.people[msg.nick] |= 1 - elif msg.content[0] == "-v": + elif msg.text[0] == "-v": self.people[msg.nick] &= ~1 def parse332(self, msg): - self.topic = msg.content + self.topic = msg.text def parse353(self, msg): - for p in msg.content: + for p in msg.text: p = p.decode() if p[0] == "@": level = 4 diff --git a/consumer.py b/consumer.py index 3839e72..9ad8304 100644 --- a/consumer.py +++ b/consumer.py @@ -32,7 +32,9 @@ import server logger = logging.getLogger("nemubot.consumer") class MessageConsumer: + """Store a message before treating""" + def __init__(self, srv, raw, time, prvt, data): self.srv = srv self.raw = raw @@ -44,10 +46,10 @@ class MessageConsumer: def treat_in(self, context, msg): """Treat the input message""" if msg.cmd == "PING": - self.srv.send_pong(msg.content) - else: - # TODO: Manage credits - if msg.channel is None or self.srv.accepted_channel(msg.channel): + self.srv.send_pong(msg.params[0]) + elif hasattr(msg, "receivers"): + msg.receivers = [ receiver for receiver in msg.receivers if self.srv.accepted_channel(receiver) ] + if msg.receivers: # All messages context.treat_pre(msg, self.srv) @@ -86,7 +88,7 @@ class MessageConsumer: msg.server = self.srv.id if msg.cmd == "PRIVMSG": msg.is_owner = (msg.nick == self.srv.owner) - msg.private = msg.private or msg.channel == self.srv.nick + msg.private = msg.private or (len(msg.receivers) == 1 and msg.receivers[0] == self.srv.nick) res = self.treat_in(context, msg) except: logger.exception("Error occurred during the processing of the message: %s", self.raw) diff --git a/exception.py b/exception.py index 7acfaf8..aae19c9 100644 --- a/exception.py +++ b/exception.py @@ -26,4 +26,4 @@ class IRCException(Exception): self.personnal = personnal def fill_response(self, msg): - return Response(msg.sender, self.message, channel=msg.channel, nick=(msg.nick if self.personnal else None)) + return Response(msg.sender, self.message, channel=msg.receivers, nick=(msg.nick if self.personnal else None)) diff --git a/message.py b/message.py index af4785f..c5bef6b 100644 --- a/message.py +++ b/message.py @@ -22,99 +22,107 @@ import shlex import time from response import Response -import xmlparser -filename = "" +mgx = re.compile(b'''^(?:@(?P<tags>[^ ]+)\ )? + (?::(?P<prefix> + (?P<nick>[a-zA-Z][^!@ ]*) + (?: !(?P<user>[^@ ]+))? + (?:@(?P<host>[^ ]+))? + )\ )? + (?P<command>(?:[a-zA-Z]+|[0-9]{3})) + (?P<params>(?:\ [^:][^ ]*)*)(?:\ :(?P<trailing>.*))? + $''', re.X) class Message: - def __init__ (self, line, timestamp, private=False): - self.raw = line - self.time = timestamp - self.channel = None - self.content = b'' - self.ctcp = False - line = line.rstrip() #remove trailing 'rn' - - words = line.split(b' ') - if words[0][0] == 58: #58 is : in ASCII table - self.sender = words[0][1:].decode() - self.cmd = words[1].decode() - else: - self.cmd = words[0].decode() - self.sender = None - - if self.cmd == 'PING': - self.content = words[1] - elif 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 - self.sender = self.nick + "!" + self.realname - - if len(words) > 2: - self.channel = self.pickWords(words[2:]).decode() - - if self.cmd == 'PRIVMSG': - # Check for CTCP request - self.ctcp = len(words[3]) > 1 and (words[3][0] == 0x01 or words[3][1] == 0x01) - self.content = self.pickWords(words[3:]) - # If CTCP, remove 0x01 - if self.ctcp: - self.content = self.content[1:len(self.content)-1] - elif self.cmd == '353' and len(words) > 3: - for i in range(2, len(words)): - if words[i][0] == 58: - self.content = words[i:] - #Remove the first : - self.content[0] = self.content[0][1:] - self.channel = words[i-1].decode() - break - elif self.cmd == 'NICK': - self.content = self.pickWords(words[2:]) - elif self.cmd == 'MODE': - self.content = words[3:] - elif self.cmd == '332': - self.channel = words[3] - self.content = self.pickWords(words[4:]) - else: - self.content = self.pickWords(words[3:]) - else: - if self.cmd == 'PRIVMSG': - self.channel = words[2].decode() - self.content = b' '.join(words[3:]) - self.decode() - if self.cmd == 'PRIVMSG': - self.parse_content() + def __init__ (self, raw_line, timestamp, private=False): + self.raw = raw_line self.private = private + self.tags = { 'time': timestamp } + self.params = list() + + p = mgx.match(raw_line.rstrip()) + + # 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"): + for param in p.group("params").strip().split(b' '): + self.params.append(param) + + if p.group("trailing"): + self.params.append(p.group("trailing")) + + # Special commands + if self.cmd == 'PRIVMSG': + self.receivers = self.decode(self.params[0]).split(',') + + # If CTCP, remove 0x01 + if len(self.params[1]) > 1 and (self.params[1][0] == 0x01 or self.params[1][1] == 0x01): + self.is_ctcp = True + self.text = self.decode(self.params[1][1:len(self.params[1])-1]) + else: + self.is_ctcp = False + self.text = self.decode(self.params[1]) + + # Split content by words + self.parse_content() + + elif self.cmd == '353': # RPL_NAMREPLY + self.receivers = [ self.decode(self.params[0]) ] + self.nicks = self.decode(self.params[1]).split(" ") + + elif self.cmd == '332': + self.receivers = [ self.decode(self.params[0]) ] + self.topic = self.decode(self.params[1]).split(" ") + + else: + for i in range(0, len(self.params)-1): + self.params[i] = self.decode(self.params[i]) + + + # TODO: here for legacy content + @property + def sender(self): + return self.prefix + @property + def channel(self): + return self.receivers[0] + def parse_content(self): """Parse or reparse the message content""" # Split content by words try: - self.cmds = shlex.split(self.content) + self.cmds = shlex.split(self.text) except ValueError: - self.cmds = self.content.split(' ') + self.cmds = self.text.split(' ') - def pickWords(self, words): - """Parse last argument of a line: can be a single word or a sentence starting with :""" - if len(words) > 0 and len(words[0]) > 0: - if words[0][0] == 58: - return b' '.join(words[0:])[1:] - else: - return words[0] - else: - return b'' - def decode(self): + def decode(self, s): """Decode the content string usign a specific encoding""" - if isinstance(self.content, bytes): + if isinstance(s, bytes): try: - self.content = self.content.decode() + s = s.decode() except UnicodeDecodeError: #TODO: use encoding from config file - self.content = self.content.decode('utf-8', 'replace') + s = s.decode('utf-8', 'replace') + return s ############################## # # diff --git a/modules/alias.py b/modules/alias.py index 6eb8501..a9621fb 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -157,7 +157,7 @@ def treat_alias(msg, hooks_cache): and msg.cmds[0][0] == "!" and msg.cmds[0][1:] in DATAS.getNode("aliases").index and msg.cmds[0][1:] not in hooks_cache("cmd_hook")): - msg.content = msg.content.replace(msg.cmds[0], + msg.text = msg.text.replace(msg.cmds[0], DATAS.getNode("aliases").index[msg.cmds[0][1:]]["origin"], 1) msg.parse_content() @@ -166,7 +166,7 @@ def treat_alias(msg, hooks_cache): return True else: - msg.content = replace_variables(msg.content, msg) + msg.text = replace_variables(msg.text, msg) msg.parse_content() return False return False @@ -174,8 +174,8 @@ def treat_alias(msg, hooks_cache): @hook("ask_default") def parseask(msg): global ALIAS - if re.match(".*(set|cr[ée]{2}|nouvel(le)?) alias.*", msg.content) is not None: - result = re.match(".*alias !?([^ ]+) (pour|=|:) (.+)$", msg.content) + if re.match(".*(set|cr[ée]{2}|nouvel(le)?) alias.*", msg.text) is not None: + result = re.match(".*alias !?([^ ]+) (pour|=|:) (.+)$", msg.text) if result.group(1) in DATAS.getNode("aliases").index or result.group(3).find("alias") >= 0: return Response(msg.sender, "Cet alias est déjà défini.") else: diff --git a/modules/birthday.py b/modules/birthday.py index 1473091..2ae5b9e 100644 --- a/modules/birthday.py +++ b/modules/birthday.py @@ -84,7 +84,7 @@ def cmd_age(msg): @hook("ask_default") 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.content, re.I) + 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: try: extDate = msg.extractDate() diff --git a/modules/events.py b/modules/events.py index e14b3d2..0026b85 100644 --- a/modules/events.py +++ b/modules/events.py @@ -188,14 +188,14 @@ RGXP_ask = re.compile(r"^.*((create|new)\s+(a|an|a\s*new|an\s*other)?\s*(events? @hook("ask_default") def parseask(msg): - if RGXP_ask.match(msg.content) is not None: - name = re.match("^.*!([^ \"'@!]+).*$", msg.content) + 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.") if name.group(1) in DATAS.index: raise IRCException("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.content, re.I) + 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 = msg.extractDate() if extDate is None or extDate == "": diff --git a/modules/reddit.py b/modules/reddit.py index 7789540..82bed8a 100644 --- a/modules/reddit.py +++ b/modules/reddit.py @@ -59,7 +59,7 @@ def parselisten(msg): global LAST_SUBS try: - urls = re.findall("www.reddit.com(/\w/\w+/?)", msg.content) + urls = re.findall("www.reddit.com(/\w/\w+/?)", msg.text) for url in urls: if msg.channel not in LAST_SUBS: LAST_SUBS[msg.channel] = list() diff --git a/modules/sms.py b/modules/sms.py index 48e9da2..8db48ef 100644 --- a/modules/sms.py +++ b/modules/sms.py @@ -78,11 +78,11 @@ apikey_ask = re.compile(r"(clef|key|password|mot de passe?)\s+(?:est|is)?\s+(?P< @hook("ask_default") def parseask(msg): - if msg.content.find("Free") >= 0 and ( - msg.content.find("API") >= 0 or msg.content.find("api") >= 0) and ( - msg.content.find("SMS") >= 0 or msg.content.find("sms") >= 0): - resuser = apiuser_ask.search(msg.content) - reskey = apikey_ask.search(msg.content) + 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 resuser is not None and reskey is not None: apiuser = resuser.group("user") apikey = reskey.group("key") diff --git a/modules/weather.py b/modules/weather.py index dab0509..dfcb583 100644 --- a/modules/weather.py +++ b/modules/weather.py @@ -212,7 +212,7 @@ gps_ask = re.compile(r"^\s*(?P<city>.*\w)\s*(?:(?:se|est)\s+(?:trouve|situ[ée]* @hook("ask_default") def parseask(msg): - res = gps_ask.match(msg.content) + res = gps_ask.match(msg.text) if res is not None: city_name = res.group("city").lower() gps_lat = res.group("lat").replace(",", ".") diff --git a/modules/ycc.py b/modules/ycc.py index d653ccb..9ea0d15 100644 --- a/modules/ycc.py +++ b/modules/ycc.py @@ -55,7 +55,7 @@ def cmd_ycc(msg): def parselisten(msg): global LAST_URLS try: - urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?[^ ]+)", msg.content) + urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?[^ ]+)", msg.text) for url in urls: o = urlparse(url) if o.scheme != "": diff --git a/response.py b/response.py index 3df4fc1..4533c86 100644 --- a/response.py +++ b/response.py @@ -32,7 +32,7 @@ class Response: self.server = server self.messages = list() self.alone = True - self.ctcp = ctcp + self.is_ctcp = ctcp if message is not None: self.append_message(message, shown_first_count=shown_first_count) self.elt = 0 # Next element to display @@ -106,12 +106,18 @@ class Response: if len(self.rawtitle) <= 0: self.rawtitle = None + def treat_ctcp(self, content): + if self.is_ctcp: + return "\x01" + content + "\x01" + else: + return content + def get_message(self): if self.alone and len(self.messages) > 1: self.alone = False if self.empty: - return self.nomore + return self.treat_ctcp(self.nomore) msg = "" if self.channel is not None and self.nick is not None: @@ -132,35 +138,35 @@ class Response: if len(msg) + len(e) > 430: msg += "[…]" self.alone = False - return msg + return self.treat_ctcp(msg) else: msg += e + ", " self.elt += 1 self.pop() - return msg[:len(msg)-2] + return self.treat_ctcp(msg[:len(msg)-2]) else: if len(elts) <= 432: self.pop() if self.count is not None: - return msg + elts + (self.count % len(self.messages)) + return self.treat_ctcp(msg + elts + (self.count % len(self.messages))) else: - return msg + elts + return self.treat_ctcp(msg + elts) else: words = elts.split(' ') if len(words[0]) > 432 - len(msg): self.elt += 432 - len(msg) - return msg + elts[:self.elt] + "[…]" + return self.treat_ctcp(msg + elts[:self.elt] + "[…]") for w in words: if len(msg) + len(w) > 431: msg += "[…]" self.alone = False - return msg + return self.treat_ctcp(msg) else: msg += w + " " self.elt += len(w) + 1 self.pop() - return msg + return self.treat_ctcp(msg) diff --git a/server/IRC.py b/server/IRC.py index f42ecc6..16bcabd 100644 --- a/server/IRC.py +++ b/server/IRC.py @@ -120,21 +120,16 @@ class IRCServer(server.Server): self.channels[chan].treat(msg.cmd, msg) def evt_channel(self, msg, srv): - if msg.channel is not None: - if msg.channel in self.channels: - self.channels[msg.channel].treat(msg.cmd, msg) + if msg.receivers is not None: + for receiver in msg.receivers: + if receiver in self.channels: + self.channels[receiver].treat(msg.cmd, msg) def accepted_channel(self, chan, sender=None): """Return True if the channel (or the user) is authorized""" - if self.allow_all: - return True - elif self.listen_nick: - return (chan in self.channels and (sender is None or sender in - self.channels[chan].people) - ) or chan == self.nick - else: - return chan in self.channels and (sender is None or sender - in self.channels[chan].people) + return (self.allow_all or + (chan in self.channels and (sender is None or sender in self.channels[chan].people)) or + (self.listen_nick and chan == self.nick)) def join(self, chan, password=None, force=False): """Join a channel""" @@ -255,7 +250,7 @@ class IRCServer(server.Server): """Send a message without checks or format""" #TODO: add something for post message treatment here if channel == self.nick: - self.logger.warn("Nemubot talks to himself: %s", msg, stack_info=True) + self.logger.warn("Nemubot talks to himself: %s", line, stack_info=True) if line is not None and channel is not None: if self.s is None: self.logger.warn("Attempt to send message on a non connected server: %s: %s", self.id, line, stack_info=True) diff --git a/server/__init__.py b/server/__init__.py index a1ac635..8fffe1d 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -73,22 +73,20 @@ class Server(threading.Thread): def send_response(self, res, origin): """Analyse a Response and send it""" - # TODO: how to send a CTCP message to a different person - if res.ctcp: - self.send_ctcp(res.sender, res.get_message()) + if type(res.channel) != list: + res.channel = [ res.channel ] - elif res.channel is not None and res.channel != self.nick: - self.send_msg(res.channel, res.get_message()) + for channel in res.channel: + if channel != self.nick: + self.send_msg(channel, res.get_message()) + else: + channel = res.sender + self.send_msg_usr(channel, res.get_message(), "NOTICE" if res.is_ctcp else "PRIVMSG") if not res.alone: if hasattr(self, "send_bot"): self.send_bot("NOMORE %s" % res.channel) - self.moremessages[res.channel] = res - elif res.sender is not None: - self.send_msg_usr(res.sender, res.get_message()) - - if not res.alone: - self.moremessages[res.sender] = res + self.moremessages[channel] = res def send_ctcp(self, to, msg, cmd="NOTICE", endl="\r\n"): """Send a message as CTCP response""" From 28c1ad088b6177f9f86f9f40cd07cac8356ddeb6 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 29 Aug 2014 17:13:28 +0200 Subject: [PATCH 098/674] Move countdown to a separate module in tools --- message.py | 79 ------------------------------------------- modules/birthday.py | 7 ++-- modules/bonneannee.py | 13 +++---- modules/events.py | 13 +++---- tools/countdown.py | 79 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 97 insertions(+), 94 deletions(-) create mode 100644 tools/countdown.py diff --git a/message.py b/message.py index c5bef6b..bfe5612 100644 --- a/message.py +++ b/message.py @@ -19,7 +19,6 @@ from datetime import datetime import re import shlex -import time from response import Response @@ -130,84 +129,6 @@ class Message: # # ############################## - def just_countdown (self, delta, resolution = 5): - sec = delta.seconds - hours, remainder = divmod(sec, 3600) - minutes, seconds = divmod(remainder, 60) - an = int(delta.days / 365.25) - days = delta.days % 365.25 - - sentence = "" - force = False - - if resolution > 0 and (force or an > 0): - force = True - sentence += " %i an"%(an) - - if an > 1: - sentence += "s" - if resolution > 2: - sentence += "," - elif resolution > 1: - sentence += " et" - - if resolution > 1 and (force or days > 0): - force = True - sentence += " %i jour"%(days) - - if days > 1: - sentence += "s" - if resolution > 3: - sentence += "," - elif resolution > 2: - sentence += " et" - - if resolution > 2 and (force or hours > 0): - force = True - sentence += " %i heure"%(hours) - if hours > 1: - sentence += "s" - if resolution > 4: - sentence += "," - elif resolution > 3: - sentence += " et" - - if resolution > 3 and (force or minutes > 0): - force = True - sentence += " %i minute"%(minutes) - if minutes > 1: - sentence += "s" - if resolution > 4: - sentence += " et" - - if resolution > 4 and (force or seconds > 0): - force = True - sentence += " %i seconde"%(seconds) - if seconds > 1: - sentence += "s" - return sentence[1:] - - - def countdown_format (self, date, msg_before, msg_after, timezone = None): - """Replace in a text %s by a sentence incidated the remaining time before/after an event""" - if timezone != None: - os.environ['TZ'] = timezone - time.tzset() - - #Calculate time before the date - if datetime.now() > date: - sentence_c = msg_after - delta = datetime.now() - date - else: - sentence_c = msg_before - delta = date - datetime.now() - - if timezone != None: - os.environ['TZ'] = "Europe/Paris" - - return sentence_c % self.just_countdown(delta) - - def extractDate (self): """Parse a message to extract a time and date""" msgl = self.content.lower () diff --git a/modules/birthday.py b/modules/birthday.py index 2ae5b9e..81a6546 100644 --- a/modules/birthday.py +++ b/modules/birthday.py @@ -8,6 +8,7 @@ from datetime import datetime from datetime import date from hooks import hook +from tools.countdown import countdown_format from xmlparser.node import ModuleState nemubotversion = 3.4 @@ -48,7 +49,7 @@ def cmd_anniv(msg): if (tyd.day == datetime.today().day and tyd.month == datetime.today().month): - return Response(msg.sender, msg.countdown_format( + return Response(msg.sender, countdown_format( DATAS.index[name].getDate("born"), "", "C'est aujourd'hui l'anniversaire de %s !" " Il a %s. Joyeux anniversaire :)" % (name, "%s")), @@ -57,7 +58,7 @@ def cmd_anniv(msg): if tyd < datetime.today(): tyd = datetime(date.today().year + 1, tyd.month, tyd.day) - return Response(msg.sender, msg.countdown_format(tyd, + return Response(msg.sender, countdown_format(tyd, "Il reste %s avant l'anniversaire de %s !" % ("%s", name), ""), msg.channel) @@ -73,7 +74,7 @@ def cmd_age(msg): name = matches[0] d = DATAS.index[name].getDate("born") - return Response(msg.sender, msg.countdown_format(d, + return Response(msg.sender, countdown_format(d, "%s va naître dans %s." % (name, "%s"), "%s a %s." % (name, "%s")), msg.channel) diff --git a/modules/bonneannee.py b/modules/bonneannee.py index 4f2868d..7d5d061 100644 --- a/modules/bonneannee.py +++ b/modules/bonneannee.py @@ -5,6 +5,7 @@ from datetime import datetime from hooks import hook +from tools.countdown import countdown_format nemubotversion = 3.4 @@ -29,9 +30,9 @@ def bonneannee(): @hook("cmd_hook", str(yrn), yrn) def cmd_newyear(msg, yr): return Response(msg.sender, - msg.countdown_format(datetime(yr, 1, 1, 0, 0, 1), - "Il reste %s avant la nouvelle année.", - "Nous faisons déjà la fête depuis %s !"), + countdown_format(datetime(yr, 1, 1, 0, 0, 1), + "Il reste %s avant la nouvelle année.", + "Nous faisons déjà la fête depuis %s !"), channel=msg.channel) @hook("cmd_rgxp", data=yrn, regexp="^[0-9]{4}$") @@ -42,7 +43,7 @@ def cmd_timetoyear(msg, cur): return None return Response(msg.sender, - msg.countdown_format(datetime(yr, 1, 1, 0, 0, 1), - "Il reste %s avant %d." % ("%s", yr), - "Le premier janvier %d est passé depuis %s !" % (yr, "%s")), + countdown_format(datetime(yr, 1, 1, 0, 0, 1), + "Il reste %s avant %d." % ("%s", yr), + "Le premier janvier %d est passé depuis %s !" % (yr, "%s")), channel=msg.channel) diff --git a/modules/events.py b/modules/events.py index 0026b85..3ef9f89 100644 --- a/modules/events.py +++ b/modules/events.py @@ -15,6 +15,7 @@ nemubotversion = 3.4 from event import ModuleEvent from hooks import Hook, hook +from tools.countdown import countdown_format, countdown def help_full (): return "This module store a lot of events: ny, we, " + (", ".join(DATAS.index.keys())) + "\n!eventslist: gets list of timer\n!start /something/: launch a timer" @@ -43,7 +44,7 @@ def cmd_gouter(msg): ndate = datetime.today() ndate = datetime(ndate.year, ndate.month, ndate.day, 16, 42) return Response(msg.sender, - msg.countdown_format(ndate, + countdown_format(ndate, "Le goûter aura lieu dans %s, préparez vos biscuits !", "Nous avons %s de retard pour le goûter :("), channel=msg.channel) @@ -53,7 +54,7 @@ def cmd_we(msg): ndate = datetime.today() + timedelta(5 - datetime.today().weekday()) ndate = datetime(ndate.year, ndate.month, ndate.day, 0, 0, 1) return Response(msg.sender, - msg.countdown_format(ndate, + countdown_format(ndate, "Il reste %s avant le week-end, courage ;)", "Youhou, on est en week-end depuis %s."), channel=msg.channel) @@ -141,7 +142,7 @@ def end_countdown(msg): if msg.cmds[1] in DATAS.index: if DATAS.index[msg.cmds[1]]["proprio"] == msg.nick or (msg.cmds[0] == "forceend" and msg.is_owner): - duration = msg.just_countdown(datetime.now() - DATAS.index[msg.cmds[1]].getDate("start")) + duration = countdown(datetime.now() - DATAS.index[msg.cmds[1]].getDate("start")) del_event(DATAS.index[msg.cmds[1]]["id"]) DATAS.delChild(DATAS.index[msg.cmds[1]]) save() @@ -177,11 +178,11 @@ def parseanswer(msg): if DATAS.index[msg.cmds[0]].name == "strend": if DATAS.index[msg.cmds[0]].hasAttribute("end"): - res.append_message("%s commencé il y a %s et se terminera dans %s." % (msg.cmds[0], msg.just_countdown(datetime.now() - DATAS.index[msg.cmds[0]].getDate("start")), msg.just_countdown(DATAS.index[msg.cmds[0]].getDate("end") - datetime.now()))) + res.append_message("%s commencé il y a %s et se terminera dans %s." % (msg.cmds[0], countdown(datetime.now() - DATAS.index[msg.cmds[0]].getDate("start")), countdown(DATAS.index[msg.cmds[0]].getDate("end") - datetime.now()))) else: - res.append_message("%s commencé il y a %s." % (msg.cmds[0], msg.just_countdown(datetime.now() - DATAS.index[msg.cmds[0]].getDate("start")))) + res.append_message("%s commencé il y a %s." % (msg.cmds[0], countdown(datetime.now() - DATAS.index[msg.cmds[0]].getDate("start")))) else: - res.append_message(msg.countdown_format(DATAS.index[msg.cmds[0]].getDate("start"), DATAS.index[msg.cmds[0]]["msg_before"], DATAS.index[msg.cmds[0]]["msg_after"])) + res.append_message(countdown_format(DATAS.index[msg.cmds[0]].getDate("start"), DATAS.index[msg.cmds[0]]["msg_before"], DATAS.index[msg.cmds[0]]["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) diff --git a/tools/countdown.py b/tools/countdown.py new file mode 100644 index 0000000..ed73e49 --- /dev/null +++ b/tools/countdown.py @@ -0,0 +1,79 @@ +from datetime import datetime +import time + +def countdown(delta, resolution=5): + sec = delta.seconds + hours, remainder = divmod(sec, 3600) + minutes, seconds = divmod(remainder, 60) + an = int(delta.days / 365.25) + days = delta.days % 365.25 + + sentence = "" + force = False + + if resolution > 0 and (force or an > 0): + force = True + sentence += " %i an"%(an) + + if an > 1: + sentence += "s" + if resolution > 2: + sentence += "," + elif resolution > 1: + sentence += " et" + + if resolution > 1 and (force or days > 0): + force = True + sentence += " %i jour"%(days) + + if days > 1: + sentence += "s" + if resolution > 3: + sentence += "," + elif resolution > 2: + sentence += " et" + + if resolution > 2 and (force or hours > 0): + force = True + sentence += " %i heure"%(hours) + if hours > 1: + sentence += "s" + if resolution > 4: + sentence += "," + elif resolution > 3: + sentence += " et" + + if resolution > 3 and (force or minutes > 0): + force = True + sentence += " %i minute"%(minutes) + if minutes > 1: + sentence += "s" + if resolution > 4: + sentence += " et" + + if resolution > 4 and (force or seconds > 0): + force = True + sentence += " %i seconde"%(seconds) + if seconds > 1: + sentence += "s" + return sentence[1:] + + +def countdown_format(date, msg_before, msg_after, timezone=None): + """Replace in a text %s by a sentence incidated the remaining time before/after an event""" + if timezone != None: + os.environ['TZ'] = timezone + time.tzset() + + #Calculate time before the date + if datetime.now() > date: + sentence_c = msg_after + delta = datetime.now() - date + else: + sentence_c = msg_before + delta = date - datetime.now() + + if timezone != None: + os.environ['TZ'] = "Europe/Paris" + + return sentence_c % countdown(delta) From 81593a493b1b3e98f3932e2fc584c8b5e456354f Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 30 Aug 2014 19:15:14 +0200 Subject: [PATCH 099/674] (wip) use select instead of a thread by server. Currently read and write seems to work properly, we lost some code (like more, next, ...) that never should be in server part --- bot.py | 33 +++++- consumer.py | 33 +++--- nemubot.py | 1 + prompt/builtins.py | 3 +- response.py | 7 ++ server/IRC.py | 282 ++++----------------------------------------- server/__init__.py | 188 ++++++++---------------------- server/socket.py | 77 +++++++++++++ 8 files changed, 205 insertions(+), 419 deletions(-) create mode 100644 server/socket.py diff --git a/bot.py b/bot.py index 2ac55b1..980f1bb 100644 --- a/bot.py +++ b/bot.py @@ -21,6 +21,7 @@ from datetime import timedelta import logging from queue import Queue import re +from select import select import threading import time import uuid @@ -38,7 +39,7 @@ import response logger = logging.getLogger("nemubot.bot") -class Bot: +class Bot(threading.Thread): """Class containing the bot context and ensuring key goals""" @@ -51,6 +52,8 @@ class Bot: data_path -- Path to directory where store bot context data """ + threading.Thread.__init__(self) + logger.info("Initiate nemubot v%s", __version__) # External IP for accessing this bot @@ -90,6 +93,22 @@ class Bot: self) + def run(self): + from server import _rlist, _wlist, _xlist + + self.stop = False + while not self.stop: + rl, wl, xl = select(_rlist, _wlist, _xlist, 0.1) + + for x in xl: + x.exception() + for w in wl: + w.write_select() + for r in rl: + for i in r.read(): + self.receive_message(r, i) + + def init_ctcp_capabilities(self): """Reset existing CTCP capabilities to default one""" @@ -277,17 +296,16 @@ class Bot: c.start() - def add_server(self, node, nick, owner, realname, ssl=False): + def add_server(self, node, nick, owner, realname): """Add a new server to the context""" - srv = IRCServer(node, nick, owner, realname, ssl) + srv = IRCServer(node, nick, owner, realname) srv.add_hook = lambda h: self.hooks.add_hook("irc_hook", h, self) srv.add_networkbot = self.add_networkbot srv.send_bot = lambda d: self.send_networkbot(srv, d) - srv.register_hooks() + #srv.register_hooks() if srv.id not in self.servers: self.servers[srv.id] = srv - if srv.autoconnect: - srv.launch(self.receive_message) + srv.open() return True else: return False @@ -378,6 +396,9 @@ class Bot: for srv in k: self.servers[srv].disconnect() + self.stop = True + + # Hooks cache def create_cache(self, name): diff --git a/consumer.py b/consumer.py index 9ad8304..5d41891 100644 --- a/consumer.py +++ b/consumer.py @@ -46,9 +46,8 @@ class MessageConsumer: def treat_in(self, context, msg): """Treat the input message""" if msg.cmd == "PING": - self.srv.send_pong(msg.params[0]) + self.srv.write("%s :%s" % ("PONG", msg.params[0])) elif hasattr(msg, "receivers"): - msg.receivers = [ receiver for receiver in msg.receivers if self.srv.accepted_channel(receiver) ] if msg.receivers: # All messages context.treat_pre(msg, self.srv) @@ -62,21 +61,29 @@ class MessageConsumer: if r is not None: self.treat_out(context, r) elif isinstance(res, response.Response): - # Define the destination server - if (res.server is not None and - isinstance(res.server, str) and res.server in context.servers): - res.server = context.servers[res.server] - if (res.server is not None and - not isinstance(res.server, server.Server)): + # Define the destination server + to_server = None + if 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] + + if to_server is None: logger.error("The server defined in this response doesn't " "exist: %s", res.server) - res.server = None - if res.server is None: - res.server = self.srv + return False # Sent the message only if treat_post authorize it if context.treat_post(res): - res.server.send_response(res, self.data) + if type(res.channel) != list: + res.channel = [ res.channel ] + for channel in res.channel: + if channel != to_server.nick: + to_server.write("%s %s :%s" % ("PRIVMSG", channel, res.get_message())) + else: + channel = res.sender + to_server.write("%s %s :%s" % ("NOTICE" if res.is_ctcp else "PRIVMSG", channel, res.get_message())) elif res is not None: logger.error("Unrecognized response type: %s", res) @@ -98,7 +105,7 @@ class MessageConsumer: self.treat_out(context, res) # Inform that the message has been treated - self.srv.msg_treated(self.data) + #self.srv.msg_treated(self.data) diff --git a/nemubot.py b/nemubot.py index 9d17ef8..f04899c 100755 --- a/nemubot.py +++ b/nemubot.py @@ -68,6 +68,7 @@ if __name__ == "__main__": print ("Nemubot v%s ready, my PID is %i!" % (bot.__version__, os.getpid())) + context.start() while prmpt.run(context): try: # Reload context diff --git a/prompt/builtins.py b/prompt/builtins.py index 7633e46..f23400d 100644 --- a/prompt/builtins.py +++ b/prompt/builtins.py @@ -69,8 +69,7 @@ def load_file(filename, context): nick = server["nick"] if server.hasAttribute("nick") else config["nick"] owner = server["owner"] if server.hasAttribute("owner") else config["owner"] realname = server["realname"] if server.hasAttribute("realname") else config["realname"] - if context.add_server(server, nick, owner, realname, - server.hasAttribute("ssl")): + if context.add_server(server, nick, owner, realname): print("Server `%s:%s' successfully added." % (server["server"], server["port"])) else: diff --git a/response.py b/response.py index 4533c86..781c4d8 100644 --- a/response.py +++ b/response.py @@ -42,6 +42,13 @@ class Response: self.set_sender(sender) self.count = count + @property + def receivers(self): + if type(self.channel) is list: + return self.channel + else: + return [ self.channel ] + @property def content(self): #FIXME: error when messages in self.messages are list! diff --git a/server/IRC.py b/server/IRC.py index 16bcabd..f05c2c8 100644 --- a/server/IRC.py +++ b/server/IRC.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -# Nemubot is a modulable IRC bot, built around XML configuration files. -# Copyright (C) 2012 Mercier Pierre-Olivier +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2014 nemunaire # # 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 @@ -16,272 +16,32 @@ # 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 errno -import os -import ssl -import socket -import threading -import traceback - -from channel import Channel -from server.DCC import DCC -from hooks import Hook -import message import server -import xmlparser +from server.socket import SocketServer -class IRCServer(server.Server): - """Class to interact with an IRC server""" +class IRCServer(SocketServer): - def __init__(self, node, nick, owner, realname, ssl=False): - """Initialize an IRC server - - Arguments: - node -- server node from XML configuration - nick -- nick used by the bot on this server - owner -- nick used by the bot owner on this server - realname -- string used as realname on this server - ssl -- require SSL? - """ - self.node = node - - server.Server.__init__(self) + def __init__(self, node, nick, owner, realname): + SocketServer.__init__(self, + node["host"], + node["port"], + node["password"], + node.hasAttribute("ssl") and node["ssl"].lower() == "true") self.nick = nick self.owner = owner self.realname = realname - self.ssl = ssl + self.id = "TODO" - # Listen private messages? - self.listen_nick = True - - self.dcc_clients = dict() - - self.channels = dict() - for chn in self.node.getNodes("channel"): - chan = Channel(chn["name"], chn["password"]) - self.channels[chan.name] = chan - - - @property - def host(self): - """Return the server hostname""" - if self.node is not None and self.node.hasAttribute("server"): - return self.node["server"] - else: - return "localhost" - - @property - def port(self): - """Return the connection port used on this server""" - if self.node is not None and self.node.hasAttribute("port"): - return self.node.getInt("port") - else: - return "6667" - - @property - def password(self): - """Return the password used to connect to this server""" - if self.node is not None and self.node.hasAttribute("password"): - return self.node["password"] - else: - return None - - @property - def allow_all(self): - """If True, treat message from all channels, not only listed one""" - return (self.node is not None and self.node.hasAttribute("allowall") - and self.node["allowall"] == "true") - - @property - def autoconnect(self): - """Autoconnect the server when added""" - if self.node is not None and self.node.hasAttribute("autoconnect"): - value = self.node["autoconnect"].lower() - return value != "no" and value != "off" and value != "false" - else: - return False - - @property - def id(self): - """Gives the server identifiant""" - return self.host + ":" + str(self.port) - - def register_hooks(self): - self.add_hook(Hook(self.evt_channel, "JOIN")) - self.add_hook(Hook(self.evt_channel, "PART")) - self.add_hook(Hook(self.evt_server, "NICK")) - self.add_hook(Hook(self.evt_server, "QUIT")) - self.add_hook(Hook(self.evt_channel, "332")) - self.add_hook(Hook(self.evt_channel, "353")) - - def evt_server(self, msg, srv): - for chan in self.channels: - self.channels[chan].treat(msg.cmd, msg) - - def evt_channel(self, msg, srv): - if msg.receivers is not None: - for receiver in msg.receivers: - if receiver in self.channels: - self.channels[receiver].treat(msg.cmd, msg) - - def accepted_channel(self, chan, sender=None): - """Return True if the channel (or the user) is authorized""" - return (self.allow_all or - (chan in self.channels and (sender is None or sender in self.channels[chan].people)) or - (self.listen_nick and chan == self.nick)) - - def join(self, chan, password=None, force=False): - """Join a channel""" - if force or (chan is not None and - self.connected and chan not in self.channels): - self.channels[chan] = Channel(chan, password) - if password is not None: - self.s.send(("JOIN %s %s\r\n" % (chan, password)).encode()) - else: - self.s.send(("JOIN %s\r\n" % chan).encode()) + def _open(self): + if SocketServer._open(self): + if self.password is not None: + self.write("PASS :" + self.password) + self.write("NICK :" + self.nick) + self.write("USER %s %s bla :%s" % (self.nick, self.host, self.realname)) return True - else: - return False + return False - def leave(self, chan): - """Leave a channel""" - if chan is not None and self.connected and chan in self.channels: - if isinstance(chan, list): - for c in chan: - self.leave(c) - else: - self.s.send(("PART %s\r\n" % self.channels[chan].name).encode()) - del self.channels[chan] - return True - else: - return False - -# Main loop - def run(self): - if not self.connected: - self.s = socket.socket() #Create the socket - if self.ssl: - ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1) - self.s = ctx.wrap_socket(self.s) - try: - self.s.connect((self.host, self.port)) #Connect to server - except socket.error as e: - self.s = None - self.logger.critical("Unable to connect to %s:%d: %s", - self.host, self.port, os.strerror(e.errno)) - return - self.stopping.clear() - - if self.password != None: - self.s.send(b"PASS " + self.password.encode () + b"\r\n") - self.s.send(("NICK %s\r\n" % self.nick).encode ()) - self.s.send(("USER %s %s bla :%s\r\n" % (self.nick, self.host, - self.realname)).encode()) - raw = self.s.recv(1024) - if not raw: - self.logger.critical("Unable to connect to %s:%d", self.host, self.port) - return - self.connected = True - self.logger.info("Connection to %s:%d completed", self.host, self.port) - - if len(self.channels) > 0: - for chn in self.channels.keys(): - self.join(self.channels[chn].name, - self.channels[chn].password, force=True) - - - readbuffer = b'' #Here we store all the messages from server - try: - while not self.stop: - readbuffer = readbuffer + raw - temp = readbuffer.split(b'\n') - readbuffer = temp.pop() - - for line in temp: - self.treat_msg(line) - raw = self.s.recv(1024) #recieve server messages - except socket.error: - pass - - if self.connected: - self.s.close() - self.connected = False - if self.closing_event is not None: - self.closing_event() - self.logger.info("Server `%s' successfully stopped.", self.id) - self.stopping.set() - # Rearm Thread - threading.Thread.__init__(self) - - -# Overwritted methods - - def disconnect(self): - """Close the socket with the server and all DCC client connections""" - #Close all DCC connection - clts = [c for c in self.dcc_clients] - for clt in clts: - self.dcc_clients[clt].disconnect() - return server.Server.disconnect(self) - - - -# Abstract methods - - def send_pong(self, cnt): - """Send a PONG command to the server with argument cnt""" - self.s.send(("PONG %s\r\n" % cnt).encode()) - - def msg_treated(self, origin): - """Do nothing; here for implement abstract class""" - pass - - def send_dcc(self, msg, to): - """Send a message through DCC connection""" - if msg is not None and to is not None: - realname = to.split("!")[1] - if realname not in self.dcc_clients.keys(): - d = DCC(self, to) - self.dcc_clients[realname] = d - self.dcc_clients[realname].send_dcc(msg) - - def send_msg_final(self, channel, line, cmd="PRIVMSG", endl="\r\n"): - """Send a message without checks or format""" - #TODO: add something for post message treatment here - if channel == self.nick: - self.logger.warn("Nemubot talks to himself: %s", line, stack_info=True) - if line is not None and channel is not None: - if self.s is None: - self.logger.warn("Attempt to send message on a non connected server: %s: %s", self.id, line, stack_info=True) - elif len(line) < 442: - self.s.send(("%s %s :%s%s" % (cmd, channel, line, endl)).encode ()) - else: - self.logger.warn("Message truncated due to size (%d ; max : 442) : %s", len(line), line, stack_info=True) - self.s.send (("%s %s :%s%s" % (cmd, channel, line[0:442]+"<…>", endl)).encode ()) - - def send_msg_usr(self, user, msg): - """Send a message to a user instead of a channel""" - if user is not None and user[0] != "#": - realname = user.split("!")[1] - if realname in self.dcc_clients or user in self.dcc_clients: - self.send_dcc(msg, user) - else: - for line in msg.split("\n"): - if line != "": - self.send_msg_final(user.split('!')[0], msg) - - def send_msg(self, channel, msg, cmd="PRIVMSG", endl="\r\n"): - """Send a message to a channel""" - if self.accepted_channel(channel): - server.Server.send_msg(self, channel, msg, cmd, endl) - - def send_msg_verified(self, sender, channel, msg, cmd = "PRIVMSG", endl = "\r\n"): - """Send a message to a channel, only if the source user is on this channel too""" - if self.accepted_channel(channel, sender): - self.send_msg_final(channel, msg, cmd, endl) - - def send_global(self, msg, cmd="PRIVMSG", endl="\r\n"): - """Send a message to all channels on this server""" - for channel in self.channels.keys(): - self.send_msg(channel, msg, cmd, endl) + def _close(self): + self.write("QUIT") + SocketServer._close(self) diff --git a/server/__init__.py b/server/__init__.py index 8fffe1d..9f423c2 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -# Nemubot is a modulable IRC bot, built around XML configuration files. -# Copyright (C) 2012 Mercier Pierre-Olivier +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2014 nemunaire # # 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 @@ -16,156 +16,70 @@ # 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 socket -import threading +import queue -class Server(threading.Thread): - def __init__(self, socket = None): - self.stop = False - self.stopping = threading.Event() - self.s = socket - self.connected = self.s is not None - self.closing_event = None +# Lists for select +_rlist = [] +_wlist = [] +_xlist = [] - self.moremessages = dict() +# Extends from IOBase in order to be compatible with select function +class AbstractServer(io.IOBase): - self.logger = logging.getLogger("nemubot.server." + self.id) + """An abstract server: handle communication with an IM server""" - threading.Thread.__init__(self) + def __init__(self, send_callback=None): + """Initialize an abstract server - def isDCC(self, to=None): - return to is not None and to in self.dcc_clients + Keyword argument: + send_callback -- Callback when developper want to send a message + """ - @property - def ip(self): - """Convert common IP representation to little-endian integer representation""" - sum = 0 - if self.node.hasAttribute("ip"): - ip = self.node["ip"] + self.logger = logging.getLogger("nemubot.server.TODO") + self._sending_queue = queue.Queue() + if send_callback is not None: + self._send_callback = send_callback else: - #TODO: find the external IP - ip = "0.0.0.0" - for b in ip.split("."): - sum = 256 * sum + int(b) - return sum + self._send_callback = self._write_select - def toIP(self, input): - """Convert little-endian int to IPv4 adress""" - ip = "" - for i in range(0,4): - mod = input % 256 - ip = "%d.%s" % (mod, ip) - input = (input - mod) / 256 - return ip[:len(ip) - 1] - @property - def id(self): - """Gives the server identifiant""" - raise NotImplemented() + def open(self): + """Generic open function that register the server un _rlist in case of successful _open""" + if self._open(): + _rlist.append(self) - def accepted_channel(self, msg, sender=None): - return True - def msg_treated(self, origin): - """Action done on server when a message was treated""" - raise NotImplemented() + def close(self): + """Generic close function that register the server un _{r,w,x}list in case of successful _close""" + if self._close(): + if self in _rlist: + _rlist.remove(self) + if self in _wlist: + _wlist.remove(self) + if self in _xlist: + _xlist.remove(self) - def send_response(self, res, origin): - """Analyse a Response and send it""" - if type(res.channel) != list: - res.channel = [ res.channel ] - for channel in res.channel: - if channel != self.nick: - self.send_msg(channel, res.get_message()) - else: - channel = res.sender - self.send_msg_usr(channel, res.get_message(), "NOTICE" if res.is_ctcp else "PRIVMSG") + def write(self, message): + """Send a message to the server using send_callback""" + self._send_callback(message) - if not res.alone: - if hasattr(self, "send_bot"): - self.send_bot("NOMORE %s" % res.channel) - self.moremessages[channel] = res + def write_select(self): + """Internal function used by the select function""" + try: + while not self._sending_queue.empty(): + self._write(self._sending_queue.get_nowait()) + _wlist.remove(self) - def send_ctcp(self, to, msg, cmd="NOTICE", endl="\r\n"): - """Send a message as CTCP response""" - if msg is not None and to is not None: - for line in msg.split("\n"): - if line != "": - self.send_msg_final(to.split("!")[0], "\x01" + line + "\x01", cmd, endl) + except queue.Empty: + pass - def send_dcc(self, msg, to): - """Send a message through DCC connection""" - raise NotImplemented() - - def send_msg_final(self, channel, msg, cmd="PRIVMSG", endl="\r\n"): - """Send a message without checks or format""" - raise NotImplemented() - - def send_msg_usr(self, user, msg): - """Send a message to a user instead of a channel""" - raise NotImplemented() - - def send_msg(self, channel, msg, cmd="PRIVMSG", endl="\r\n"): - """Send a message to a channel""" - if msg is not None: - for line in msg.split("\n"): - if line != "": - self.send_msg_final(channel, line, cmd, endl) - - def send_msg_verified(self, sender, channel, msg, cmd="PRIVMSG", endl="\r\n"): - """A more secure way to send messages""" - raise NotImplemented() - - def send_global(self, msg, cmd="PRIVMSG", endl="\r\n"): - """Send a message to all channels on this server""" - raise NotImplemented() - - def disconnect(self): - """Close the socket with the server""" - if self.connected: - self.stop = True - try: - self.s.shutdown(socket.SHUT_RDWR) - except socket.error: - pass - - self.stopping.wait() - return True - else: - return False - - def kill(self): - """Just stop the main loop, don't close the socket directly""" - if self.connected: - self.stop = True - self.connected = False - #Send a message in order to close the socket - try: - self.s.send(("Bye!\r\n").encode ()) - except: - pass - self.stopping.wait() - return True - else: - return False - - def launch(self, receive_action, verb=True): - """Connect to the server if it is no yet connected""" - self._receive_action = receive_action - if not self.connected: - self.stop = False - self.logger.info("Entering main loop for server") - try: - self.start() - except RuntimeError: - pass - elif verb: - print (" Already connected.") - - def treat_msg(self, line, private=False): - self._receive_action(self, line, private) - - def run(self): - raise NotImplemented() + def _write_select(self, message): + """Send a message to the server safely through select""" + self._sending_queue.put(self.format(message)) + self.logger.debug("Message '%s' appended to Queue", message) + if self not in _wlist: + _wlist.append(self) diff --git a/server/socket.py b/server/socket.py new file mode 100644 index 0000000..21d3d0c --- /dev/null +++ b/server/socket.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- + +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2014 nemunaire +# +# 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 ssl +import socket + +from server import AbstractServer + +class SocketServer(AbstractServer): + + def __init__(self, host, port=6667, password=None, ssl=False): + AbstractServer.__init__(self) + self.host = host + self.port = int(port) + self.password = password + self.ssl = ssl + + self.socket = None + self.readbuffer = b'' + + def fileno(self): + return self.socket.fileno() if self.socket else None + + def _open(self): + # Create the socket + self.socket = socket.socket() + + # Wrap the socket for SSL + if self.ssl: + ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1) + self.socket = ctx.wrap_socket(self.socket) + + try: + self.socket.connect((self.host, self.port)) #Connect to server + self.logger.info("Connected to %s:%d", self.host, self.port) + except socket.error as e: + self.socket = None + self.logger.critical("Unable to connect to %s:%d: %s", + self.host, self.port, os.strerror(e.errno)) + return False + + return True + + def _close(self): + if self.socket is not None: + self.socket.shutdown(SHUT_RDWR) + self.socket.close() + self.socket = None + + def _write(self, cnt): + self.socket.send(cnt) + + def format(self, txt): + return txt.encode() + b'\r\n' + + def read(self): + raw = self.socket.recv(1024) + temp = (self.readbuffer + raw).split(b'\r\n') + self.readbuffer = temp.pop() + + for line in temp: + yield line From dcce36eb7cfdd25677f40a6d9ca1502960540b81 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 30 Aug 2014 20:22:14 +0200 Subject: [PATCH 100/674] Implement more and next features as module instead of part of core --- bot.py | 20 ----------------- modules/more.py | 59 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 20 deletions(-) create mode 100644 modules/more.py diff --git a/bot.py b/bot.py index 980f1bb..cce1a4e 100644 --- a/bot.py +++ b/bot.py @@ -536,26 +536,6 @@ class Bot(threading.Thread): if msg.cmds[0] == "help": return _help_msg(msg.sender, self.modules, msg.cmds) - elif msg.cmds[0] == "more": - if msg.receivers == srv.nick: - if msg.sender in srv.moremessages: - return srv.moremessages[msg.sender] - else: - if msg.receivers in srv.moremessages: - return srv.moremessages[msg.receivers] - - elif msg.cmds[0] == "next": - ret = None - if msg.receivers == srv.nick: - if msg.sender in srv.moremessages: - ret = srv.moremessages[msg.sender] - else: - if msg.receivers in srv.moremessages: - ret = srv.moremessages[msg.receivers] - if ret is not None: - ret.pop() - return ret - elif msg.cmds[0] == "dcc": logger.debug("dcctest for %s", msg.sender) srv.send_dcc("Hello %s!" % msg.nick, msg.sender) diff --git a/modules/more.py b/modules/more.py new file mode 100644 index 0000000..202e91a --- /dev/null +++ b/modules/more.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2014 nemunaire +# +# 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/>. + +"""Progressive display of very long messages""" + +from hooks import hook + +nemubotversion = 3.4 + +SERVERS = dict() + +@hook("all_post") +def parseresponse(res): + # TODO: handle inter-bot communication NOMORE + # TODO: check that the response is not the one already saved + if not res.alone: + if res.server not in SERVERS: + SERVERS[res.server] = dict() + for receiver in res.receivers: + SERVERS[res.server][receiver] = res + return True + + +@hook("cmd_hook", "more") +def cmd_more(msg): + """Display next chunck of the message""" + res = list() + if msg.server in SERVERS: + for receiver in msg.receivers: + if receiver in SERVERS[msg.server]: + res.append(SERVERS[msg.server][receiver]) + return res + + +@hook("cmd_hook", "next") +def cmd_next(msg): + """Display the next information include in the message""" + res = list() + if msg.server in SERVERS: + for receiver in msg.receivers: + if receiver in SERVERS[msg.server]: + SERVERS[msg.server][receiver].pop() + res.append(SERVERS[msg.server][receiver]) + return res From 038590c6590b4117368037aa5aca1939b152a5ed Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 31 Aug 2014 10:51:44 +0200 Subject: [PATCH 101/674] Server disconnection works properly --- bot.py | 2 +- nemubot.py | 2 -- server/__init__.py | 3 ++- server/socket.py | 3 ++- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bot.py b/bot.py index cce1a4e..f9ebf10 100644 --- a/bot.py +++ b/bot.py @@ -394,7 +394,7 @@ class Bot(threading.Thread): logger.info("Close all servers connection...") k = list(self.servers.keys()) for srv in k: - self.servers[srv].disconnect() + self.servers[srv].close() self.stop = True diff --git a/nemubot.py b/nemubot.py index f04899c..a958de4 100755 --- a/nemubot.py +++ b/nemubot.py @@ -87,6 +87,4 @@ if __name__ == "__main__": print ("\nWaiting for other threads shuts down...") - # Indeed, the server socket is waiting for receiving some data - sys.exit(0) diff --git a/server/__init__.py b/server/__init__.py index 9f423c2..3162c3f 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -70,9 +70,10 @@ class AbstractServer(io.IOBase): def write_select(self): """Internal function used by the select function""" try: + _wlist.remove(self) while not self._sending_queue.empty(): self._write(self._sending_queue.get_nowait()) - _wlist.remove(self) + self._sending_queue.task_done() except queue.Empty: pass diff --git a/server/socket.py b/server/socket.py index 21d3d0c..e432abf 100644 --- a/server/socket.py +++ b/server/socket.py @@ -57,8 +57,9 @@ class SocketServer(AbstractServer): return True def _close(self): + self._sending_queue.join() if self.socket is not None: - self.socket.shutdown(SHUT_RDWR) + self.socket.shutdown(socket.SHUT_RDWR) self.socket.close() self.socket = None From 29819c7874ca22769a44c58f4e8e451422b8e489 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 31 Aug 2014 11:08:34 +0200 Subject: [PATCH 102/674] Fix private and CTCP responses --- consumer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/consumer.py b/consumer.py index 5d41891..b559dd7 100644 --- a/consumer.py +++ b/consumer.py @@ -79,10 +79,10 @@ class MessageConsumer: if type(res.channel) != list: res.channel = [ res.channel ] for channel in res.channel: - if channel != to_server.nick: + if channel is not None and channel != to_server.nick: to_server.write("%s %s :%s" % ("PRIVMSG", channel, res.get_message())) else: - channel = res.sender + channel = res.sender.split("!")[0] to_server.write("%s %s :%s" % ("NOTICE" if res.is_ctcp else "PRIVMSG", channel, res.get_message())) elif res is not None: From 3bc53bb4ef5076b305fa9195ea5693434a541f52 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 1 Sep 2014 19:21:54 +0200 Subject: [PATCH 103/674] Introducing new hooks manager Currently, the manager use a naive implementation, this is mainly for architectural testing purpose. --- bot.py | 219 ++------------------------------------------- consumer.py | 191 +++++++++++++++++++++++++++++---------- hooks.py | 157 +++----------------------------- hooksmanager.py | 107 ++++++++++++++++++++++ importer.py | 29 +++++- message.py | 7 +- modules/alias.py | 34 +++---- modules/more.py | 2 +- modules/reddit.py | 5 +- modules/ycc.py | 5 +- response.py | 8 ++ server/DCC.py | 2 +- server/IRC.py | 11 +++ server/__init__.py | 4 + 14 files changed, 343 insertions(+), 438 deletions(-) create mode 100644 hooksmanager.py diff --git a/bot.py b/bot.py index f9ebf10..8030b6b 100644 --- a/bot.py +++ b/bot.py @@ -32,6 +32,7 @@ __author__ = 'nemunaire' from consumer import Consumer, EventConsumer, MessageConsumer from event import ModuleEvent import hooks +from hooksmanager import HooksManager from networkbot import NetworkBot from server.IRC import IRCServer from server.DCC import DCC @@ -77,7 +78,11 @@ class Bot(threading.Thread): self.event_timer = None # Own hooks - self.hooks = hooks.MessagesHook(self, self) + self.hooks = HooksManager() + def in_ping(msg): + if re.match("^ *(m[' ]?entends?[ -]+tu|h?ear me|do you copy|ping)", msg.text, re.I) is not None: + return response.Response(msg.sender, message="pong", channel=msg.receivers, nick=msg.nick) + self.hooks.add_hook(hooks.Hook(in_ping), "in", "PRIVMSG") # Other known bots, making a bots network self.network = dict() @@ -88,10 +93,6 @@ class Bot(threading.Thread): self.cnsr_thrd = list() self.cnsr_thrd_size = -1 - self.hooks.add_hook("irc_hook", - hooks.Hook(self.treat_prvmsg, "PRIVMSG"), - self) - def run(self): from server import _rlist, _wlist, _xlist @@ -299,9 +300,6 @@ class Bot(threading.Thread): def add_server(self, node, nick, owner, realname): """Add a new server to the context""" srv = IRCServer(node, nick, owner, realname) - srv.add_hook = lambda h: self.hooks.add_hook("irc_hook", h, self) - srv.add_networkbot = self.add_networkbot - srv.send_bot = lambda d: self.send_networkbot(srv, d) #srv.register_hooks() if srv.id not in self.servers: self.servers[srv.id] = srv @@ -348,7 +346,7 @@ class Bot(threading.Thread): self.modules[name].unload(self) # Remove registered hooks for (s, h) in self.modules[name].REGISTERED_HOOKS: - self.hooks.del_hook(s, h) + self.hooks.del_hook(h, s) # Remove registered events for e in self.modules[name].REGISTERED_EVENTS: self.del_event(e) @@ -361,7 +359,7 @@ class Bot(threading.Thread): def receive_message(self, srv, raw_msg, private=False, data=None): """Queued the message for treatment""" - #print (raw_msg) + #print("READ", raw_msg) self.cnsr_queue.put_nowait(MessageConsumer(srv, raw_msg, datetime.now(), private, data)) # Launch a new thread if necessary @@ -399,57 +397,6 @@ class Bot(threading.Thread): self.stop = True - # Hooks cache - - def create_cache(self, name): - if name not in self.hooks_cache: - if isinstance(self.hooks.__dict__[name], list): - self.hooks_cache[name] = list() - - # Start by adding locals hooks - for h in self.hooks.__dict__[name]: - tpl = (h, 0, self.hooks.__dict__[name], self.hooks.bot) - self.hooks_cache[name].append(tpl) - - # Now, add extermal hooks - level = 0 - while level == 0 or lvl_exist: - lvl_exist = False - for ext in self.network: - if len(self.network[ext].hooks) > level: - lvl_exist = True - for h in self.network[ext].hooks[level].__dict__[name]: - if h not in self.hooks_cache[name]: - self.hooks_cache[name].append((h, level + 1, - self.network[ext].hooks[level].__dict__[name], self.network[ext].hooks[level].bot)) - level += 1 - - elif isinstance(self.hooks.__dict__[name], dict): - self.hooks_cache[name] = dict() - - # Start by adding locals hooks - for h in self.hooks.__dict__[name]: - self.hooks_cache[name][h] = (self.hooks.__dict__[name][h], 0, - self.hooks.__dict__[name], - self.hooks.bot) - - # Now, add extermal hooks - level = 0 - while level == 0 or lvl_exist: - lvl_exist = False - for ext in self.network: - if len(self.network[ext].hooks) > level: - lvl_exist = True - for h in self.network[ext].hooks[level].__dict__[name]: - if h not in self.hooks_cache[name]: - self.hooks_cache[name][h] = (self.network[ext].hooks[level].__dict__[name][h], level + 1, self.network[ext].hooks[level].__dict__[name], self.network[ext].hooks[level].bot) - level += 1 - - else: - raise Exception(name + " hook type unrecognized") - - return self.hooks_cache[name] - # Treatment def check_rest_times(self, store, hook): @@ -462,48 +409,6 @@ class Bot(threading.Thread): elif isinstance(store, list): store.remove(hook) - def treat_pre(self, msg, srv): - """Treat a message before all other treatment""" - # Treat all messages starting with 'nemubot:' as distinct commands - if msg.cmd == "PRIVMSG" and msg.text.find("%s:"%srv.nick) == 0: - # Remove the bot name - msg.text = msg.text[len(srv.nick)+1:].strip() - msg.parse_content() - msg.private = True - - for h, lvl, store, bot in self.create_cache("all_pre"): - if h.is_matching(None, server=srv): - h.run(msg, self.create_cache) - self.check_rest_times(store, h) - - - def treat_post(self, res): - """Treat a message before send""" - for h, lvl, store, bot in self.create_cache("all_post"): - if h.is_matching(None, channel=res.channel, server=res.server): - c = h.run(res) - self.check_rest_times(store, h) - if not c: - return False - return True - - - def treat_irc(self, msg, srv): - """Treat all incoming IRC commands""" - treated = list() - - irc_hooks = self.create_cache("irc_hook") - if msg.cmd in irc_hooks: - (hks, lvl, store, bot) = irc_hooks[msg.cmd] - for h in hks: - if h.is_matching(msg.cmd, server=srv): - res = h.run(msg, srv, msg.cmd) - if res is not None and res != False: - treated.append(res) - self.check_rest_times(store, h) - - return treated - def treat_prvmsg_ask(self, msg, srv): # Treat ping @@ -559,114 +464,6 @@ class Bot(threading.Thread): return res - def treat_cmd(self, msg, srv): - """Treat a command message""" - treated = list() - - # First, treat simple hook - cmd_hook = self.create_cache("cmd_hook") - if msg.cmds[0] in cmd_hook: - (hks, lvl, store, bot) = cmd_hook[msg.cmds[0]] - for h in hks: - if h.is_matching(msg.cmds[0], channel=msg.receivers, server=srv) and (msg.private or lvl == 0 or bot.nick not in srv.channels[msg.receivers].people): - res = h.run(msg, strcmp=msg.cmds[0]) - if res is not None and res != False: - treated.append(res) - self.check_rest_times(store, h) - - # Then, treat regexp based hook - cmd_rgxp = self.create_cache("cmd_rgxp") - for hook, lvl, store, bot in cmd_rgxp: - if hook.is_matching(msg.cmds[0], msg.receivers, server=srv) and (msg.private or lvl == 0 or bot.nick not in srv.channels[msg.receivers].people): - res = hook.run(msg) - if res is not None and res != False: - treated.append(res) - self.check_rest_times(store, hook) - - # Finally, treat default hooks if not catched before - cmd_default = self.create_cache("cmd_default") - for hook, lvl, store, bot in cmd_default: - if treated: - break - res = hook.run(msg) - if res is not None and res != False: - treated.append(res) - self.check_rest_times(store, hook) - - return treated - - def treat_ask(self, msg, srv): - """Treat an ask message""" - treated = list() - - # First, treat simple hook - ask_hook = self.create_cache("ask_hook") - if msg.text in ask_hook: - hks, lvl, store, bot = ask_hook[msg.text] - for h in hks: - if h.is_matching(msg.text, channel=msg.receivers, server=srv) and (msg.private or lvl == 0 or bot.nick not in srv.channels[msg.receivers].people): - res = h.run(msg, strcmp=msg.text) - if res is not None and res != False: - treated.append(res) - self.check_rest_times(store, h) - - # Then, treat regexp based hook - ask_rgxp = self.create_cache("ask_rgxp") - for hook, lvl, store, bot in ask_rgxp: - if hook.is_matching(msg.text, channel=msg.receivers, server=srv) and (msg.private or lvl == 0 or bot.nick not in srv.channels[msg.receivers].people): - res = hook.run(msg, strcmp=msg.text) - if res is not None and res != False: - treated.append(res) - self.check_rest_times(store, hook) - - # Finally, treat default hooks if not catched before - ask_default = self.create_cache("ask_default") - for hook, lvl, store, bot in ask_default: - if treated: - break - res = hook.run(msg) - if res is not None and res != False: - treated.append(res) - self.check_rest_times(store, hook) - - return treated - - def treat_answer(self, msg, srv): - """Treat a normal message""" - treated = list() - - # First, treat simple hook - msg_hook = self.create_cache("msg_hook") - if msg.text in msg_hook: - hks, lvl, store, bot = msg_hook[msg.text] - for h in hks: - if h.is_matching(msg.text, channel=msg.receivers, server=srv) and (msg.private or lvl == 0 or bot.nick not in srv.channels[msg.receivers].people): - res = h.run(msg, strcmp=msg.text) - if res is not None and res != False: - treated.append(res) - self.check_rest_times(store, h) - - # Then, treat regexp based hook - msg_rgxp = self.create_cache("msg_rgxp") - for hook, lvl, store, bot in msg_rgxp: - if hook.is_matching(msg.text, channel=msg.receivers, server=srv) and (msg.private or lvl == 0 or bot.nick not in srv.channels[msg.receivers].people): - res = hook.run(msg, strcmp=msg.text) - if res is not None and res != False: - treated.append(res) - self.check_rest_times(store, hook) - - # Finally, treat default hooks if not catched before - msg_default = self.create_cache("msg_default") - for hook, lvl, store, bot in msg_default: - if len(treated) > 0: - break - res = hook.run(msg) - if res is not None and res != False: - treated.append(res) - self.check_rest_times(store, hook) - - return treated - def _ctcp_response(sndr, msg): return response.Response(sndr, msg, ctcp=True) diff --git a/consumer.py b/consumer.py index b559dd7..0287288 100644 --- a/consumer.py +++ b/consumer.py @@ -42,26 +42,151 @@ class MessageConsumer: self.prvt = prvt self.data = data + self.msgs = list() + self.responses = None - def treat_in(self, context, msg): - """Treat the input message""" - if msg.cmd == "PING": - self.srv.write("%s :%s" % ("PONG", msg.params[0])) - elif hasattr(msg, "receivers"): - if msg.receivers: - # All messages - context.treat_pre(msg, self.srv) - return context.treat_irc(msg, self.srv) + def first_treat(self, msg): + """Qualify a new message/response - def treat_out(self, context, res): - """Treat the output message""" - if isinstance(res, list): - for r in res: - if r is not None: self.treat_out(context, r) + Argument: + msg -- The Message or Response to qualify + """ - elif isinstance(res, response.Response): - # Define the destination server + if not hasattr(msg, "qual") or msg.qual is None: + # Assume this is a message with no particulariry + msg.qual = "def" + + # Define the source server if not already done + if not hasattr(msg, "server") or msg.server is None: + msg.server = self.srv.id + + if isinstance(msg, Message): + if msg.cmd == "PRIVMSG" or msg.cmd == "NOTICE": + msg.is_owner = (msg.nick == self.srv.owner) + msg.private = msg.private or (len(msg.receivers) == 1 and msg.receivers[0] == self.srv.nick) + if msg.private: + msg.qual = "ask" + + # Remove nemubot: + if msg.qual != "cmd" and msg.text.find(self.srv.nick) == 0 and len(msg.text) > len(self.srv.nick) + 2 and msg.text[len(self.srv.nick)] == ":": + msg.text = msg.text[len(self.srv.nick) + 1:].strip() + msg.qual = "ask" + + return msg + + + def pre_treat(self, hm): + """Modify input Messages + + Arguments: + hm -- Hooks manager + """ + + new_msg = list() + new_msg += self.msgs + self.msgs = list() + + while len(new_msg) > 0: + msg = new_msg.pop(0) + for h in hm.get_hooks("pre", msg.cmd, msg.qual): + if h.match(message=msg, server=self.srv): + res = h.run(msg) + if isinstance(res, list): + for i in xrange(len(res)): + if res[i] == msg: + res.pop(i) + break + new_msg += res + elif res is not None and res != msg: + new_msg.append(res) + msg = None + break + elif res is None or res == False: + msg = None + break + if msg is not None: + self.msgs.append(msg) + + + def in_treat(self, hm): + """Treat Messages and store responses + + Arguments: + hm -- Hooks manager + """ + + self.responses = list() + for msg in self.msgs: + for h in hm.get_hooks("in", msg.cmd, msg.qual): + if msg.cmd == "PING": + self.srv.write("%s :%s" % ("PONG", msg.params[0])) + + elif h.match(message=msg, server=self.srv): + res = h.run(msg) + if isinstance(res, list): + self.responses += res + elif res is not None: + self.responses.append(res) + + + def post_treat(self, hm): + """Modify output Messages + + Arguments: + hm -- Hooks manager + """ + + new_msg = list() + new_msg += self.responses + self.responses = list() + + while len(new_msg) > 0: + msg = self.first_treat(new_msg.pop(0)) + for h in hm.get_hooks("post"): + if h.match(message=msg, server=self.srv): + res = h.run(msg) + if isinstance(res, list): + for i in xrange(len(res)): + if res[i] == msg: + res.pop(i) + break + new_msg += res + elif res is not None and res != msg: + new_msg.append(res) + msg = None + break + elif res is None or res == False: + msg = None + break + if msg is not None: + self.responses.append(msg) + + + def run(self, context): + """Create, parse and treat the message""" + try: + # Parse and create the original message + msg = Message(self.raw, self.time, self.prvt) + self.first_treat(msg) + self.msgs.append(msg) + + # Run pre-treatment: from Message to [ Message ] + self.pre_treat(context.hooks) + + # Run in-treatment: from Message to [ Response ] + if len(self.msgs) > 0: + self.in_treat(context.hooks) + + # Run post-treatment: from Response to [ Response ] + if self.responses is not None and len(self.responses) > 0: + self.post_treat(context.hooks) + except: + logger.exception("Error occurred during the processing of the message: %s", self.raw) + return + + #return self.responses + for res in self.responses: to_server = None if res.server is None: to_server = self.srv @@ -75,39 +200,7 @@ class MessageConsumer: return False # Sent the message only if treat_post authorize it - if context.treat_post(res): - if type(res.channel) != list: - res.channel = [ res.channel ] - for channel in res.channel: - if channel is not None and channel != to_server.nick: - to_server.write("%s %s :%s" % ("PRIVMSG", channel, res.get_message())) - else: - channel = res.sender.split("!")[0] - to_server.write("%s %s :%s" % ("NOTICE" if res.is_ctcp else "PRIVMSG", channel, res.get_message())) - - elif res is not None: - logger.error("Unrecognized response type: %s", res) - - def run(self, context): - """Create, parse and treat the message""" - try: - msg = Message(self.raw, self.time, self.prvt) - msg.server = self.srv.id - if msg.cmd == "PRIVMSG": - msg.is_owner = (msg.nick == self.srv.owner) - msg.private = msg.private or (len(msg.receivers) == 1 and msg.receivers[0] == self.srv.nick) - res = self.treat_in(context, msg) - except: - logger.exception("Error occurred during the processing of the message: %s", self.raw) - return - - # Send message - self.treat_out(context, res) - - # Inform that the message has been treated - #self.srv.msg_treated(self.data) - - + to_server.send_response(res) class EventConsumer: """Store a event before treating""" diff --git a/hooks.py b/hooks.py index 25f1019..4d52135 100644 --- a/hooks.py +++ b/hooks.py @@ -24,151 +24,6 @@ from exception import IRCException logger = logging.getLogger("nemubot.hooks") -class MessagesHook: - def __init__(self, context, bot): - self.context = context - self.bot = bot - - # Store specials hooks - self.all_pre = list() # Treated before any parse - self.all_post = list() # Treated before send message to user - - # Store IRC commands hooks - self.irc_hook = dict() - - # Store direct hooks - self.cmd_hook = dict() - self.ask_hook = dict() - self.msg_hook = dict() - - # Store regexp hooks - self.cmd_rgxp = list() - self.ask_rgxp = list() - self.msg_rgxp = list() - - # Store default hooks (after other hooks if no match) - self.cmd_default = list() - self.ask_default = list() - self.msg_default = list() - - - def add_hook(self, store, hook, module_src=None): - """Insert in the right place a hook into the given store""" - logger.info("Adding hook '%s' to store '%s' from module '%s'", hook, store, module_src) - if module_src is None: - logger.warn("No source module was passed to add_hook function, " - "please fix it in order to be compatible with unload " - "feature") - - if store in self.context.hooks_cache: - logger.debug("Cleaning hooks cache for %s", store) - del self.context.hooks_cache[store] - - if not hasattr(self, store): - # TODO: raise custom exception, this is a user problem, not internal one! - logger.error("Unrecognized hook store: %s", store) - return - attr = getattr(self, store) - - if isinstance(attr, dict) and hook.name is not None: - if hook.name not in attr: - attr[hook.name] = list() - attr[hook.name].append(hook) - if hook.end is not None: - if hook.end not in attr: - attr[hook.end] = list() - attr[hook.end].append(hook) - elif isinstance(attr, list): - attr.append(hook) - else: - logger.critical("Unrecognized hook store type: %s", type(attr)) - return - if module_src is not None and hasattr(module_src, "REGISTERED_HOOKS"): - module_src.REGISTERED_HOOKS.append((store, hook)) - - def register_hook_attributes(self, store, module, node): - if node.hasAttribute("data"): - data = node["data"] - else: - data = None - if node.hasAttribute("name"): - self.add_hook(store + "_hook", Hook(getattr(module, node["call"]), - node["name"], data=data), - module) - elif node.hasAttribute("regexp"): - self.add_hook(store + "_rgxp", Hook(getattr(module, node["call"]), - regexp=node["regexp"], data=data), - module) - - def register_hook(self, module, node): - """Create a hook from configuration node""" - if node.name == "message" and node.hasAttribute("type"): - if node["type"] == "cmd" or node["type"] == "all": - self.register_hook_attributes("cmd", module, node) - - if node["type"] == "ask" or node["type"] == "all": - self.register_hook_attributes("ask", module, node) - - if (node["type"] == "msg" or node["type"] == "answer" or - node["type"] == "all"): - self.register_hook_attributes("answer", module, node) - - def clear(self): - for h in self.all_pre: - self.del_hook("all_pre", h) - for h in self.all_post: - self.del_hook("all_post", h) - - for l in self.irc_hook: - for h in self.irc_hook[l]: - self.del_hook("irc_hook", h) - - for l in self.cmd_hook: - for h in self.cmd_hook[l]: - self.del_hook("cmd_hook", h) - for l in self.ask_hook: - for h in self.ask_hook[l]: - self.del_hook("ask_hook", h) - for l in self.msg_hook: - for h in self.msg_hook[l]: - self.del_hook("msg_hook", h) - - for h in self.cmd_rgxp: - self.del_hook("cmd_rgxp", h) - for h in self.ask_rgxp: - self.del_hook("ask_rgxp", h) - for h in self.msg_rgxp: - self.del_hook("msg_rgxp", h) - - for h in self.cmd_default: - self.del_hook("cmd_default", h) - for h in self.ask_default: - self.del_hook("ask_default", h) - for h in self.msg_default: - self.del_hook("msg_default", h) - - def del_hook(self, store, hook, module_src=None): - """Remove a registered hook from a given store""" - if store in self.context.hooks_cache: - del self.context.hooks_cache[store] - - if not hasattr(self, store): - logger.warn("unrecognized hook store type") - return - attr = getattr(self, store) - - if isinstance(attr, dict) and hook.name is not None: - if hook.name in attr: - attr[hook.name].remove(hook) - if hook.end is not None and hook.end in attr: - attr[hook.end].remove(hook) - else: - attr.remove(hook) - - if module_src is not None: - module_src.REGISTERED_HOOKS.remove((store, hook)) - - class Hook: """Class storing hook informations""" def __init__(self, call, name=None, data=None, regexp=None, channels=list(), server=None, end=None, call_end=None, help=None): @@ -186,6 +41,18 @@ class Hook: self.channels = channels self.help = help + def match(self, message, channel=None, server=None): + if isinstance(message, Response): + return self.is_matching(None, channel, server) + elif message.qual == "cmd": + return self.is_matching(message.cmds[0], channel, server) + elif hasattr(message, "text"): + return self.is_matching(message.text, channel, server) + elif len(message.params) > 0: + return self.is_matching(message.params[0], channel, server) + else: + return self.is_matching(message.cmd, channel, server) + def is_matching(self, strcmp, channel=None, server=None): """Test if the current hook correspond to the message""" return (channel is None or len(self.channels) <= 0 or diff --git a/hooksmanager.py b/hooksmanager.py new file mode 100644 index 0000000..6db0989 --- /dev/null +++ b/hooksmanager.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- + +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2014 nemunaire +# +# 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 HooksManager: + + """Class to manage hooks""" + + def __init__(self): + """Initialize the manager""" + + self.hooks = dict() + + + def add_hook(self, hook, *triggers): + """Add a hook to the manager + + Argument: + hook -- a Hook instance + triggers -- string that trigger the hook + """ + + trigger = "_".join(triggers) + + if trigger not in self.hooks: + self.hooks[trigger] = list() + + #print("ADD hook: %s => %s" % (trigger, hook)) + self.hooks[trigger].append(hook) + + + def del_hook(self, hook=None, *triggers): + """Remove the given hook from the manager + + Return: + Boolean value reporting the deletion success + + Argument: + triggers -- trigger string to remove + + Keyword argument: + hook -- a Hook instance to remove from the trigger string + """ + + trigger = "_".join(triggers) + + if trigger in self.hooks: + if hook is None: + del self.hooks[trigger] + return True + else: + return self.hooks[trigger].remove(hook) + return False + + + def get_hooks(self, *triggers): + """Returns list of trigger hooks that match the given trigger string + + 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] + + #print("GET hooks: %s => %d" % (trigger, len(res))) + return res + + + def exec_hook(self, *triggers, **data): + """Trigger hooks that match the given trigger string + + Argument: + trigger -- the trigger string + + Keyword argument: + data -- Data to pass to the hook as argument + """ + + trigger = "_".join(triggers) + + for key in self.hooks: + if trigger.find(key) == 0: + for hook in self.hooks[key]: + hook.run(**data) diff --git a/importer.py b/importer.py index 26bc67e..a3554af 100644 --- a/importer.py +++ b/importer.py @@ -159,15 +159,19 @@ class ModuleLoader(SourceLoader): def send_response(server, res): if server in self.context.servers: - return self.context.servers[server].send_response(res, None) + return self.context.servers[server].send_response(res) else: module.logger.error("Try to send a message to the unknown server: %s", server) return False def add_hook(store, hook): - return self.context.hooks.add_hook(store, hook, module) + store = convert_legacy_store(store) + module.REGISTERED_HOOKS.append((store, hook)) + return self.context.hooks.add_hook(hook, store) def del_hook(store, hook): - return self.context.hooks.del_hook(store, hook) + store = convert_legacy_store(store) + module.REGISTERED_HOOKS.remove((store, hook)) + return self.context.hooks.del_hook(hook, store) def add_event(evt, eid=None): return self.context.add_event(evt, eid, module_src=module) def add_event_eid(evt, eid): @@ -236,6 +240,21 @@ class ModuleLoader(SourceLoader): return module +def convert_legacy_store(old): + if old == "cmd_hook" or old == "cmd_rgxp" or old == "cmd_default": + return "in_PRIVMSG_cmd" + elif old == "ask_hook" or old == "ask_rgxp" or old == "ask_default": + return "in_PRIVMSG_ask" + elif old == "msg_hook" or old == "msg_rgxp" or old == "msg_default": + return "in_PRIVMSG_def" + elif old == "all_post": + return "post" + elif old == "all_pre": + return "pre" + else: + print("UNKNOWN store:", old) + return old + def add_cap_hook(prompt, module, cmd): if hasattr(module, cmd["call"]): prompt.add_cap_hook(cmd["name"], getattr(module, cmd["call"])) @@ -250,5 +269,7 @@ def register_hooks(module, context, prompt): if s == "prompt_cmd": prompt.add_cap_hook(h.name, h.call) else: - context.hooks.add_hook(s, h, module) + s = convert_legacy_store(s) + module.REGISTERED_HOOKS.append((s, h)) + context.hooks.add_hook(h, s) hooks.last_registered = [] diff --git a/message.py b/message.py index bfe5612..9c2f44f 100644 --- a/message.py +++ b/message.py @@ -68,7 +68,7 @@ class Message: self.params.append(p.group("trailing")) # Special commands - if self.cmd == 'PRIVMSG': + if self.cmd == 'PRIVMSG' or self.cmd == 'NOTICE': self.receivers = self.decode(self.params[0]).split(',') # If CTCP, remove 0x01 @@ -106,6 +106,11 @@ class Message: def parse_content(self): """Parse or reparse the message content""" + # Remove ! + if self.text[0] == '!': + self.qual = "cmd" + self.text = self.text[1:].strip() + # Split content by words try: self.cmds = shlex.split(self.text) diff --git a/modules/alias.py b/modules/alias.py index a9621fb..7685f02 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -148,28 +148,22 @@ def treat_variables(res): res.messages[i] = replace_variables(", ".join(res.messages[i]), res) else: res.messages[i] = replace_variables(res.messages[i], res) - return True + return res -@hook("all_pre") -def treat_alias(msg, hooks_cache): - if msg.cmd == "PRIVMSG": - if len(msg.cmds) > 0 and (len(msg.cmds[0]) > 0 - and msg.cmds[0][0] == "!" - and msg.cmds[0][1:] in DATAS.getNode("aliases").index - and msg.cmds[0][1:] not in hooks_cache("cmd_hook")): - msg.text = msg.text.replace(msg.cmds[0], - DATAS.getNode("aliases").index[msg.cmds[0][1:]]["origin"], 1) +@hook("pre_PRIVMSG_cmd") +def treat_alias(msg): + if msg.cmds[0] in DATAS.getNode("aliases").index: + msg.text = msg.text.replace(msg.cmds[0], + DATAS.getNode("aliases").index[msg.cmds[0]]["origin"], 1) + msg.qual = "def" + msg.parse_content() - msg.parse_content() + return treat_alias(msg) - treat_alias(msg, hooks_cache) - return True - - else: - msg.text = replace_variables(msg.text, msg) - msg.parse_content() - return False - return False + else: + msg.text = replace_variables(msg.text, msg) + msg.parse_content() + return msg @hook("ask_default") def parseask(msg): @@ -187,4 +181,4 @@ def parseask(msg): res = Response(msg.sender, "Nouvel alias %s défini avec succès." % result.group(1)) save() return res - return False + return None diff --git a/modules/more.py b/modules/more.py index 202e91a..3956954 100644 --- a/modules/more.py +++ b/modules/more.py @@ -33,7 +33,7 @@ def parseresponse(res): SERVERS[res.server] = dict() for receiver in res.receivers: SERVERS[res.server][receiver] = res - return True + return res @hook("cmd_hook", "more") diff --git a/modules/reddit.py b/modules/reddit.py index 82bed8a..ac04856 100644 --- a/modules/reddit.py +++ b/modules/reddit.py @@ -67,9 +67,8 @@ def parselisten(msg): except: pass - return False + return msg @hook("all_post") def parseresponse(res): - parselisten(res) - return True + return parselisten(res) diff --git a/modules/ycc.py b/modules/ycc.py index 9ea0d15..1ed378b 100644 --- a/modules/ycc.py +++ b/modules/ycc.py @@ -66,9 +66,8 @@ def parselisten(msg): LAST_URLS[msg.channel].append(url) except: pass - return False + return msg @hook("all_post") def parseresponse(res): - parselisten(res) - return True + return parselisten(res) diff --git a/response.py b/response.py index 781c4d8..5529e92 100644 --- a/response.py +++ b/response.py @@ -69,6 +69,14 @@ class Response: self.sender = sender def append_message(self, message, title=None, shown_first_count=-1): + if type(message) is str: + message = message.split('\n') + if len(message) > 1: + for m in message: + self.append_message(m) + return + else: + message = message[0] if message is not None and len(message) > 0: if shown_first_count >= 0: self.messages.append(message[:shown_first_count]) diff --git a/server/DCC.py b/server/DCC.py index c5f8798..6f85b0b 100644 --- a/server/DCC.py +++ b/server/DCC.py @@ -31,7 +31,7 @@ import server #Store all used ports PORTS = list() -class DCC(server.Server): +class DCC(server.AbstractServer): def __init__(self, srv, dest, socket=None): server.Server.__init__(self) diff --git a/server/IRC.py b/server/IRC.py index f05c2c8..61c8172 100644 --- a/server/IRC.py +++ b/server/IRC.py @@ -42,6 +42,17 @@ class IRCServer(SocketServer): return True return False + def send_response(self, res): + if type(res.channel) != list: + res.channel = [ res.channel ] + for channel in res.channel: + if channel is not None and channel != self.nick: + self.write("%s %s :%s" % ("PRIVMSG", channel, res.get_message())) + else: + channel = res.sender.split("!")[0] + self.write("%s %s :%s" % ("NOTICE" if res.is_ctcp else "PRIVMSG", channel, res.get_message())) + + def _close(self): self.write("QUIT") SocketServer._close(self) diff --git a/server/__init__.py b/server/__init__.py index 3162c3f..67f1b2e 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -63,6 +63,10 @@ class AbstractServer(io.IOBase): _xlist.remove(self) + def send_response(self, res): + return NotImplemented + + def write(self, message): """Send a message to the server using send_callback""" self._send_callback(message) From 7387fabee1299e976a4ba7d01b567385065934e8 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 2 Sep 2014 21:19:08 +0200 Subject: [PATCH 104/674] New module mediawiki --- modules/ddg/Wikipedia.py | 56 ------------------------ modules/ddg/__init__.py | 51 ---------------------- modules/mediawiki.py | 92 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 107 deletions(-) delete mode 100644 modules/ddg/Wikipedia.py create mode 100644 modules/mediawiki.py diff --git a/modules/ddg/Wikipedia.py b/modules/ddg/Wikipedia.py deleted file mode 100644 index 5da4211..0000000 --- a/modules/ddg/Wikipedia.py +++ /dev/null @@ -1,56 +0,0 @@ -# coding=utf-8 - -import re -from urllib.parse import quote -import urllib.request - -import xmlparser - -class Wikipedia: - def __init__(self, terms, lang="fr", site="wikipedia.org", section=0): - self.terms = terms - self.lang = lang - self.curRT = section - - raw = urllib.request.urlopen(urllib.request.Request("http://" + self.lang + "." + site + "/w/api.php?format=xml&redirects&action=query&prop=revisions&rvprop=content&titles=%s" % (quote(terms)), headers={"User-agent": "Nemubot v3"})) - self.wres = xmlparser.parse_string(raw.read()) - if self.wres is None or not (self.wres.hasNode("query") and self.wres.getFirstNode("query").hasNode("pages") and self.wres.getFirstNode("query").getFirstNode("pages").hasNode("page") and self.wres.getFirstNode("query").getFirstNode("pages").getFirstNode("page").hasNode("revisions")): - self.wres = None - else: - self.wres = self.wres.getFirstNode("query").getFirstNode("pages").getFirstNode("page").getFirstNode("revisions").getFirstNode("rev").getContent() - self.wres = striplink(self.wres) - - @property - def nextRes(self): - if self.wres is not None: - for cnt in self.wres.split("\n"): - if self.curRT > 0: - self.curRT -= 1 - continue - - (c, u) = RGXP_s.subn(' ', cnt) - c = c.strip() - if c != "": - yield c - -RGXP_p = re.compile(r"(<!--.*-->|<ref[^>]*/>|<ref[^>]*>[^>]*</ref>|<dfn[^>]*>[^>]*</dfn>|\{\{[^{}]*\}\}|\[\[([^\[\]]*\[\[[^\]\[]*\]\])+[^\[\]]*\]\]|\{\{([^{}]*\{\{[^{}]*\}\}[^{}]*)+\}\}|\{\{([^{}]*\{\{([^{}]*\{\{[^{}]*\}\}[^{}]*)+\}\}[^{}]*)+\}\}|\[\[[^\]|]+(\|[^\]\|]+)*\]\])|#\* ''" + "\n", re.I) -RGXP_l = re.compile(r'\{\{(nobr|lang\|[^|}]+)\|([^}]+)\}\}', re.I) -RGXP_m = re.compile(r'\{\{pron\|([^|}]+)\|[^}]+\}\}', re.I) -RGXP_t = re.compile("==+ *([^=]+) *=+=\n+([^\n])", re.I) -RGXP_q = re.compile(r'\[\[([^\[\]|]+)\|([^\]|]+)]]', re.I) -RGXP_r = re.compile(r'\[\[([^\[\]|]+)\]\]', re.I) -RGXP_s = re.compile(r'\s+') - -def striplink(s): - s.replace("{{m}}", "masculin").replace("{{f}}", "feminin").replace("{{n}}", "neutre") - (s, n) = RGXP_m.subn(r"[\1]", s) - (s, n) = RGXP_l.subn(r"\2", s) - - (s, n) = RGXP_q.subn(r"\1", s) - (s, n) = RGXP_r.subn(r"\1", s) - - (s, n) = RGXP_p.subn('', s) - if s == "": return s - - (s, n) = RGXP_t.subn("\x03\x16" + r"\1" + " :\x03\x16 " + r"\2", s) - return s.replace("'''", "\x03\x02").replace("''", "\x03\x1f") diff --git a/modules/ddg/__init__.py b/modules/ddg/__init__.py index 18f01c9..81ed09e 100644 --- a/modules/ddg/__init__.py +++ b/modules/ddg/__init__.py @@ -11,7 +11,6 @@ nemubotversion = 3.4 from . import DDGSearch from . import UrbanDictionnary from . import WFASearch -from . import Wikipedia def load(context): global CONF @@ -98,53 +97,3 @@ def calculate(msg): return res else: return Response(msg.sender, s.error, msg.channel) - - -@hook("cmd_hook", "wikipedia") -def wikipedia(msg): - return wiki("wikipedia.org", 0, msg) - -@hook("cmd_hook", "wiktionary") -def wiktionary(msg): - return wiki("wiktionary.org", 1, msg) - -@hook("cmd_hook", "etymology") -def wiktionary(msg): - return wiki("wiktionary.org", 0, msg) - -def wiki(site, section, msg): - if len(msg.cmds) <= 1: - return Response(msg.sender, - "Indicate a term to search", - msg.channel, nick=msg.nick) - if len(msg.cmds) > 2 and len(msg.cmds[1]) < 4: - lang = msg.cmds[1] - extract = 2 - else: - lang = "fr" - extract = 1 - - s = Wikipedia.Wikipedia(' '.join(msg.cmds[extract:]), lang, site, section) - - res = Response(msg.sender, channel=msg.channel, nomore="No more results") - if site == "wiktionary.org": - tout = [result for result in s.nextRes if result.find("\x03\x16 :\x03\x16 ") != 0] - if len(tout) > 0: - defI=1 - for t in tout: - if t.find("# ") == 0: - t = t.replace("# ", "%d. " % defI) - defI += 1 - elif t.find("#* ") == 0: - t = t.replace("#* ", " * ") - res.append_message(t) - else: - for result in s.nextRes: - res.append_message(result) - - if len(res.messages) > 0: - return res - else: - return Response(msg.sender, - "No information about " + " ".join(msg.cmds[extract:]), - msg.channel) diff --git a/modules/mediawiki.py b/modules/mediawiki.py new file mode 100644 index 0000000..8bdb05f --- /dev/null +++ b/modules/mediawiki.py @@ -0,0 +1,92 @@ +# coding=utf-8 + +"""Use MediaWiki API to get pages""" + +import json +import re +import urllib.parse +import urllib.request + +from hooks import hook +from tools.web import striphtml + +nemubotversion = 3.4 + +def get_raw_page(site, term, ssl=False): + # Built IRL + 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)) + print_debug(url) + + # Make the request + raw = urllib.request.urlopen(url) + data = json.loads(raw.read().decode()) + + for k in data["query"]["pages"]: + return data["query"]["pages"][k]["revisions"][0]["*"] + +def get_unwikitextified(site, wikitext, ssl=False): + # Built IRL + url = "http%s://%s/w/api.php?format=json&action=expandtemplates&text=%s" % ( + "s" if ssl else "", site, urllib.parse.quote(wikitext)) + print_debug(url) + + # Make the request + raw = urllib.request.urlopen(url) + data = json.loads(raw.read().decode()) + + return data["expandtemplates"]["*"] + + +def strip_model(cnt): + # Strip models at begin and end: mostly useless + cnt = re.sub(r"^(({{([^{]|\s|({{(.|\s|{{.*?}})*?}})*?)*?}}|\[\[(.|\s|\[\[.*?\]\])*?\]\])\s*)+", "", cnt) + #cnt = re.sub(r"({{([^{]|\s|({{(.|\s|{{.*?}})*?}})*?)*?}}\s?)+$", "", cnt) + + # Strip HTML comments + cnt = re.sub(r"<!--.*?-->", "", cnt) + + # Strip ref + cnt = re.sub(r"<ref.*?/ref>", "", cnt) + return cnt + +def parse_wikitext(site, cnt, ssl=False): + for i,_,_,_ in re.findall(r"({{([^{]|\s|({{(.|\s|{{.*?}})*?}})*?)*?}})", cnt): + cnt = cnt.replace(i, get_unwikitextified(site, i, ssl), 1) + + # Strip [[...]] + cnt, _ = re.subn(r"\[\[([^]]*\|)?([^]]*?)\]\]", r"\2", cnt) + + # Strip HTML tags + cnt = striphtml(cnt) + + return cnt + +def irc_format(cnt): + cnt, _ = re.subn(r"(?P<title>==+)\s*(.*?)\s*(?P=title)\n+", "\x03\x16" + r"\2" + " :\x03\x16 ", cnt) + return cnt.replace("'''", "\x03\x02").replace("''", "\x03\x1f") + +def get_page(site, term, ssl=False): + return strip_model(get_raw_page(site, term, ssl)) + + +@hook("in_PRIVMSG_cmd", "mediawiki") +def cmd_mediawiki(msg): + """Read an article on a MediaWiki""" + if len(msg.cmds) < 3: + raise IRCException("indicate a domain and a term to search") + + return Response(msg.sender, + get_page(msg.cmds[1], " ".join(msg.cmds[2:])), + channel=msg.receivers) + + +@hook("in_PRIVMSG_cmd", "wikipedia") +def cmd_wikipedia(msg): + if len(msg.cmds) < 3: + raise IRCException("indicate a lang and a term to search") + + return Response(msg.sender, + get_page(msg.cmds[1] + ".wikipedia.org", " ".join(msg.cmds[2:])), + channel=msg.receivers, + line_treat=lambda line: irc_format(parse_wikitext(site, line, ssl))) From b0e457ffc9756551c39d8a1b8ad489d801b07d9a Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 3 Sep 2014 19:06:26 +0200 Subject: [PATCH 105/674] Fix return of parselisten functions --- modules/reddit.py | 9 +++++---- modules/ycc.py | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/modules/reddit.py b/modules/reddit.py index ac04856..b7ff786 100644 --- a/modules/reddit.py +++ b/modules/reddit.py @@ -56,6 +56,11 @@ def cmd_subreddit(msg): @hook("msg_default") def parselisten(msg): + parseresponse(msg) + return None + +@hook("all_post") +def parseresponse(msg): global LAST_SUBS try: @@ -68,7 +73,3 @@ def parselisten(msg): pass return msg - -@hook("all_post") -def parseresponse(res): - return parselisten(res) diff --git a/modules/ycc.py b/modules/ycc.py index 1ed378b..c450d0d 100644 --- a/modules/ycc.py +++ b/modules/ycc.py @@ -53,6 +53,11 @@ def cmd_ycc(msg): @hook("msg_default") def parselisten(msg): + parseresponse(msg) + return None + +@hook("all_post") +def parseresponse(msg): global LAST_URLS try: urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?[^ ]+)", msg.text) @@ -67,7 +72,3 @@ def parselisten(msg): except: pass return msg - -@hook("all_post") -def parseresponse(res): - return parselisten(res) From c32f1579eef53c180b6d7363ecc27ae7235811b1 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 4 Sep 2014 09:56:53 +0200 Subject: [PATCH 106/674] Fix PONG response when no registered input treatment --- consumer.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/consumer.py b/consumer.py index 0287288..da9e711 100644 --- a/consumer.py +++ b/consumer.py @@ -118,16 +118,19 @@ class MessageConsumer: self.responses = list() for msg in self.msgs: - for h in hm.get_hooks("in", msg.cmd, msg.qual): - if msg.cmd == "PING": - self.srv.write("%s :%s" % ("PONG", msg.params[0])) + # TODO: should be placed in server hooks + if msg.cmd == "PING": + self.srv.write("%s :%s" % ("PONG", msg.params[0])) - elif h.match(message=msg, server=self.srv): - res = h.run(msg) - if isinstance(res, list): - self.responses += res - elif res is not None: - self.responses.append(res) + else: + for h in hm.get_hooks("in", msg.cmd, msg.qual): + + if h.match(message=msg, server=self.srv): + res = h.run(msg) + if isinstance(res, list): + self.responses += res + elif res is not None: + self.responses.append(res) def post_treat(self, hm): From 85ec2dcd018650f7a6a92b26f1cd7c3d5137244f Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 4 Sep 2014 10:43:50 +0200 Subject: [PATCH 107/674] New callback _on_connect, called after 001 numeric reply reception: currently it joins channels --- consumer.py | 6 +++++- server/IRC.py | 10 ++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/consumer.py b/consumer.py index da9e711..4a94c2c 100644 --- a/consumer.py +++ b/consumer.py @@ -119,7 +119,11 @@ class MessageConsumer: self.responses = list() for msg in self.msgs: # TODO: should be placed in server hooks - if msg.cmd == "PING": + if msg.cmd == "001": + if hasattr(self.srv, "_on_connect"): + self.srv._on_connect() + + elif msg.cmd == "PING": self.srv.write("%s :%s" % ("PONG", msg.params[0])) else: diff --git a/server/IRC.py b/server/IRC.py index 61c8172..fe8bbb2 100644 --- a/server/IRC.py +++ b/server/IRC.py @@ -33,6 +33,16 @@ class IRCServer(SocketServer): self.realname = realname self.id = "TODO" + def _on_connect(): + # First, JOIN some channels + for chn in node.getNodes("channel"): + if chn["password"] is not None: + self.write("JOIN %s %s" % (chn["name"], chn["password"])) + else: + self.write("JOIN %s" % chn["name"]) + self._on_connect = _on_connect + + def _open(self): if SocketServer._open(self): if self.password is not None: From 028b7fd88db9d18af98cf9ba5468017c10ba96d4 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 5 Sep 2014 01:48:01 +0200 Subject: [PATCH 108/674] Fix PONG response: Messages params are not decoded --- consumer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/consumer.py b/consumer.py index 4a94c2c..025757e 100644 --- a/consumer.py +++ b/consumer.py @@ -124,7 +124,7 @@ class MessageConsumer: self.srv._on_connect() elif msg.cmd == "PING": - self.srv.write("%s :%s" % ("PONG", msg.params[0])) + self.srv.write("%s :%s" % ("PONG", msg.decode(msg.params[0]))) else: for h in hm.get_hooks("in", msg.cmd, msg.qual): From b63170244a2e3d3e45cf09b2eae7ea607be612fa Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 6 Sep 2014 21:30:07 +0200 Subject: [PATCH 109/674] IRC: capabilities negociation --- server/IRC.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/IRC.py b/server/IRC.py index fe8bbb2..3d3c7b2 100644 --- a/server/IRC.py +++ b/server/IRC.py @@ -31,6 +31,7 @@ class IRCServer(SocketServer): self.nick = nick self.owner = owner self.realname = realname + self.capabilities = list() if not node.hasAttribute("caps") or node["caps"].lower() != "no" else None self.id = "TODO" def _on_connect(): @@ -47,8 +48,12 @@ class IRCServer(SocketServer): if SocketServer._open(self): 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.nick, self.host, self.realname)) + if self.capabilities is not None: + self.write("CAP END") return True return False From 8efc92eddec388e25319c16727ec62919e8792aa Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 7 Sep 2014 23:51:20 +0200 Subject: [PATCH 110/674] Fix usage of parse_string --- tools/web.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/web.py b/tools/web.py index 7678a7d..a67091c 100644 --- a/tools/web.py +++ b/tools/web.py @@ -125,7 +125,7 @@ def getXML(url, timeout=15): if cnt is None: return None else: - return xmlparser.parse_string(cnt) + return xmlparser.parse_string(cnt.encode()) def getJSON(url, timeout=15): """Get content page and return JSON content""" From 8f17c0a977f04af8fdafef6b6122e8fc47fea416 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 7 Sep 2014 23:52:54 +0200 Subject: [PATCH 111/674] Velib module: fix use without xml configuration file --- modules/velib.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/velib.py b/modules/velib.py index 3a4cfe8..d8f9ea2 100644 --- a/modules/velib.py +++ b/modules/velib.py @@ -4,6 +4,7 @@ import re +from hooks import hook from tools import web nemubotversion = 3.4 @@ -60,6 +61,7 @@ def print_station_status(msg, station): channel=msg.channel, nick=msg.nick) raise IRCException("station %s inconnue." % station) +@hook("cmd_hook", "velib") def ask_stations(msg): """Hook entry from !velib""" global DATAS From 76399a110f6f87bac241808b424b42c1cf324132 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 7 Sep 2014 23:55:40 +0200 Subject: [PATCH 112/674] New module books: related to #65 --- modules/books.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 modules/books.py diff --git a/modules/books.py b/modules/books.py new file mode 100644 index 0000000..8a429c6 --- /dev/null +++ b/modules/books.py @@ -0,0 +1,39 @@ +# coding=utf-8 + +"""Looking for books""" + +import urllib.request + +from hooks import hook +from tools import web + +nemubotversion = 3.4 + +def load(context): + if not CONF or not CONF.hasNode("goodreadsapi") or not CONF.getNode("goodreadsapi").hasAttribute("key"): + print ("You need a Goodreads API key in order to use this " + "module. Add it to the module configuration file:\n<goodreadsapi" + " key=\"XXXXXXXXXXXXXXXX\" />\nGet one at " + "https://www.goodreads.com/api/keys") + return None + +def search_book(title): + response = web.getXML("https://www.goodreads.com/search.xml?key=%s&q=%s" % (CONF.getNode("goodreadsapi")["key"], urllib.parse.quote(title))) + if response is not None: + return response.getNode("search").getNode("results").getNodes("work") + else: + return [] + +@hook("cmd_hook", "book") +def cmd_book(msg): + if len(msg.cmds) < 2: + raise IRCException("please give me a title to search") + + title = " ".join(msg.cmds) + res = Response(msg.sender, channel=msg.channel, + title="%s" % (title), count=" (%d more books)") + + books = search_book(title) + for book in books: + res.append_message("%s, writed by %s" % (book.getNode("best_book").getNode("title").getContent(), book.getNode("best_book").getNode("author").getNode("name").getContent())) + return res From c13173e62c3593f9aa8ec64339931e5ecf464340 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 8 Sep 2014 00:21:10 +0200 Subject: [PATCH 113/674] Handle :***!znc@znc.in --- message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/message.py b/message.py index 9c2f44f..b05a489 100644 --- a/message.py +++ b/message.py @@ -24,7 +24,7 @@ from response import Response mgx = re.compile(b'''^(?:@(?P<tags>[^ ]+)\ )? (?::(?P<prefix> - (?P<nick>[a-zA-Z][^!@ ]*) + (?P<nick>[^!@ ]+) (?: !(?P<user>[^@ ]+))? (?:@(?P<host>[^ ]+))? )\ )? From 04bde604823b7f5e68d8eae8e9294626b6c8498f Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 8 Sep 2014 01:55:36 +0200 Subject: [PATCH 114/674] Fix message decoding: not all parameters was decoded --- consumer.py | 2 +- message.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/consumer.py b/consumer.py index 025757e..4a94c2c 100644 --- a/consumer.py +++ b/consumer.py @@ -124,7 +124,7 @@ class MessageConsumer: self.srv._on_connect() elif msg.cmd == "PING": - self.srv.write("%s :%s" % ("PONG", msg.decode(msg.params[0]))) + self.srv.write("%s :%s" % ("PONG", msg.params[0])) else: for h in hm.get_hooks("in", msg.cmd, msg.qual): diff --git a/message.py b/message.py index b05a489..b222a76 100644 --- a/message.py +++ b/message.py @@ -91,7 +91,7 @@ class Message: self.topic = self.decode(self.params[1]).split(" ") else: - for i in range(0, len(self.params)-1): + for i in range(0, len(self.params)): self.params[i] = self.decode(self.params[i]) From ee14682c4f5ba4746f16b13774610501b7cf5b7d Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 8 Sep 2014 02:25:26 +0200 Subject: [PATCH 115/674] extractDate function is now in a separate Python module --- message.py | 68 --------------------------------------------- modules/birthday.py | 3 +- modules/events.py | 3 +- tools/date.py | 55 ++++++++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 70 deletions(-) create mode 100644 tools/date.py diff --git a/message.py b/message.py index b222a76..20a120b 100644 --- a/message.py +++ b/message.py @@ -127,71 +127,3 @@ class Message: #TODO: use encoding from config file s = s.decode('utf-8', 'replace') return s - -############################## -# # -# Extraction/Format text # -# # -############################## - - def extractDate (self): - """Parse a message to extract a time and date""" - msgl = self.content.lower () - result = re.match("^[^0-9]+(([0-9]{1,4})[^0-9]+([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)([^0-9]+([0-9]{1,4}))?)[^0-9]+(([0-9]{1,2})[^0-9]*[h':]([^0-9]*([0-9]{1,2})([^0-9]*[m\":][^0-9]*([0-9]{1,2}))?)?)?.*$", msgl + " TXT") - if result is not None: - day = result.group(2) - if len(day) == 4: - year = day - day = 0 - month = result.group(3) - 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 day == 0: - day = result.group(5) - else: - year = result.group(5) - - hour = result.group(7) - minute = result.group(9) - second = result.group(11) - - if year == None: - year = date.today().year - if hour == None: - hour = 0 - if minute == None: - minute = 0 - if second == None: - second = 1 - else: - second = int (second) + 1 - if second > 59: - minute = int (minute) + 1 - second = 0 - - return datetime(int(year), int(month), int(day), int(hour), int(minute), int(second)) - else: - return None diff --git a/modules/birthday.py b/modules/birthday.py index 81a6546..c49bb82 100644 --- a/modules/birthday.py +++ b/modules/birthday.py @@ -9,6 +9,7 @@ from datetime import date from hooks import hook from tools.countdown import countdown_format +from tools.date import extractDate from xmlparser.node import ModuleState nemubotversion = 3.4 @@ -88,7 +89,7 @@ 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: try: - extDate = msg.extractDate() + extDate = extractDate(msg.text) if extDate is None or extDate.year > datetime.now().year: return Response(msg.sender, "la date de naissance ne paraît pas valide...", diff --git a/modules/events.py b/modules/events.py index 3ef9f89..51e2c67 100644 --- a/modules/events.py +++ b/modules/events.py @@ -15,6 +15,7 @@ nemubotversion = 3.4 from event import ModuleEvent from hooks import Hook, hook +from tools.date import extractDate from tools.countdown import countdown_format, countdown def help_full (): @@ -198,7 +199,7 @@ def parseask(msg): 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 = msg.extractDate() + extDate = extractDate(msg.text) if extDate is None or extDate == "": raise IRCException("la date de l'événement est invalide !") diff --git a/tools/date.py b/tools/date.py new file mode 100644 index 0000000..58182c9 --- /dev/null +++ b/tools/date.py @@ -0,0 +1,55 @@ +# Extraction/Format text + +from datetime import datetime +import re + +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<year>[0-9]{1,4}))? [^0-9]+ + (?:(?P<hour>[0-9]{1,2})[^0-9]*[h':] + (?:[^0-9]*(?P<minute>[0-9]{1,2}) + (?:[^0-9]*[m\":][^0-9]*(?P<second>[0-9]{1,2}))?)?)?.*? + $''', re.X) + +def extractDate(msg): + """Parse a message to extract a time and date""" + result = xtrdt.match(msg.lower()) + 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 + + year = result.group("year") + + if len(day) == 4: + day, year = year, day + + hour = result.group("hour") + minute = result.group("minute") + second = result.group("second") + + if year is None: year = date.today().year + if hour is None: hour = 0 + if minute is None: minute = 0 + if second is None: + second = 1 + else: + second = int(second) + 1 + if second > 59: + minute = int(minute) + 1 + second = 0 + + return datetime(int(year), int(month), int(day), int(hour), int(minute), int(second)) + else: + return None From cccee20cdf68fe9c291aff1563ac442ce40fb5ab Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 8 Sep 2014 02:26:07 +0200 Subject: [PATCH 116/674] Pick from v4 message tag parser --- message.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/message.py b/message.py index 20a120b..f469319 100644 --- a/message.py +++ b/message.py @@ -118,6 +118,17 @@ class Message: self.cmds = self.text.split(' ') + def add_tag(self, key, value=None): + """Add an IRCv3.2 Message Tags""" + # Treat special tags + if key == "time": + # TODO: this is UTC timezone, nemubot works with local timezone + value = datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%fZ") + + # Store tag + self.tags[key] = value + + def decode(self, s): """Decode the content string usign a specific encoding""" if isinstance(s, bytes): From 3ac40ac7032b26eeb8fe302f68e1b70ff7a59ed4 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 8 Sep 2014 02:26:50 +0200 Subject: [PATCH 117/674] Handle fd/socket exception in select --- server/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/__init__.py b/server/__init__.py index 67f1b2e..d3310d9 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -50,6 +50,7 @@ class AbstractServer(io.IOBase): """Generic open function that register the server un _rlist in case of successful _open""" if self._open(): _rlist.append(self) + _xlist.append(self) def close(self): @@ -88,3 +89,7 @@ class AbstractServer(io.IOBase): self.logger.debug("Message '%s' appended to Queue", message) if self not in _wlist: _wlist.append(self) + + def exception(self): + """Exception occurs in fd""" + print("Unhandle file descriptor exception on server " + self.id) From eba4a07ed12bde17e227948dcd995cc011aefa53 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 8 Sep 2014 02:28:34 +0200 Subject: [PATCH 118/674] Event module: use time tag instead of now() as event start time --- modules/events.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/events.py b/modules/events.py index 51e2c67..f7901c6 100644 --- a/modules/events.py +++ b/modules/events.py @@ -72,7 +72,7 @@ def start_countdown(msg): strnd["channel"] = msg.channel strnd["proprio"] = msg.nick strnd["sender"] = msg.sender - strnd["start"] = datetime.now() + strnd["start"] = msg.tags["time"] strnd["name"] = msg.cmds[1] DATAS.addChild(strnd) @@ -84,7 +84,7 @@ def start_countdown(msg): result3 = re.match("(.*[^0-9])?([0-2]?[0-9]):([0-5]?[0-9])(:([0-5]?[0-9]))?", msg.cmds[2]) if result2 is not None or result3 is not None: try: - now = datetime.now() + now = msg.tags["time"] if result3 is None or result3.group(5) is None: sec = 0 else: sec = int(result3.group(5)) if result3 is None or result3.group(3) is None: minu = 0 @@ -109,7 +109,7 @@ def start_countdown(msg): raise IRCException("Mauvais format de date pour l'événement %s. Il n'a pas été créé." % msg.cmds[1]) elif result1 is not None and len(result1) > 0: - strnd["end"] = datetime.now() + strnd["end"] = msg.tags["time"] for (t, g) in result1: if g is None or g == "" or g == "m" or g == "M": strnd["end"] += timedelta(minutes=int(t)) @@ -129,11 +129,11 @@ def start_countdown(msg): save() if "end" in strnd: return Response(msg.sender, "%s commencé le %s et se terminera le %s." % - (msg.cmds[1], datetime.now().strftime("%A %d %B %Y à %H:%M:%S"), + (msg.cmds[1], msg.tags["time"].strftime("%A %d %B %Y à %H:%M:%S"), strnd.getDate("end").strftime("%A %d %B %Y à %H:%M:%S"))) else: return Response(msg.sender, "%s commencé le %s"% (msg.cmds[1], - datetime.now().strftime("%A %d %B %Y à %H:%M:%S"))) + msg.tags["time"].strftime("%A %d %B %Y à %H:%M:%S"))) @hook("cmd_hook", "end") @hook("cmd_hook", "forceend") @@ -143,7 +143,7 @@ def end_countdown(msg): if msg.cmds[1] in DATAS.index: if DATAS.index[msg.cmds[1]]["proprio"] == msg.nick or (msg.cmds[0] == "forceend" and msg.is_owner): - duration = countdown(datetime.now() - DATAS.index[msg.cmds[1]].getDate("start")) + duration = countdown(msg.tags["time"] - DATAS.index[msg.cmds[1]].getDate("start")) del_event(DATAS.index[msg.cmds[1]]["id"]) DATAS.delChild(DATAS.index[msg.cmds[1]]) save() @@ -179,9 +179,9 @@ def parseanswer(msg): if DATAS.index[msg.cmds[0]].name == "strend": if DATAS.index[msg.cmds[0]].hasAttribute("end"): - res.append_message("%s commencé il y a %s et se terminera dans %s." % (msg.cmds[0], countdown(datetime.now() - DATAS.index[msg.cmds[0]].getDate("start")), countdown(DATAS.index[msg.cmds[0]].getDate("end") - datetime.now()))) + res.append_message("%s commencé il y a %s et se terminera dans %s." % (msg.cmds[0], countdown(msg.tags["time"] - DATAS.index[msg.cmds[0]].getDate("start")), countdown(DATAS.index[msg.cmds[0]].getDate("end") - msg.tags["time"]))) else: - res.append_message("%s commencé il y a %s." % (msg.cmds[0], countdown(datetime.now() - DATAS.index[msg.cmds[0]].getDate("start")))) + res.append_message("%s commencé il y a %s." % (msg.cmds[0], countdown(msg.tags["time"] - DATAS.index[msg.cmds[0]].getDate("start")))) else: res.append_message(countdown_format(DATAS.index[msg.cmds[0]].getDate("start"), DATAS.index[msg.cmds[0]]["msg_before"], DATAS.index[msg.cmds[0]]["msg_after"])) return res From 9b9c02fe29c77dc1f362a5d3691e422406758987 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 8 Sep 2014 02:30:18 +0200 Subject: [PATCH 119/674] Handle connection errors (like timeout) --- consumer.py | 3 +++ server/IRC.py | 4 ++-- server/socket.py | 10 ++++++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/consumer.py b/consumer.py index 4a94c2c..7c0128f 100644 --- a/consumer.py +++ b/consumer.py @@ -123,6 +123,9 @@ class MessageConsumer: if hasattr(self.srv, "_on_connect"): self.srv._on_connect() + elif msg.cmd == "ERROR": + self.srv.close() + elif msg.cmd == "PING": self.srv.write("%s :%s" % ("PONG", msg.params[0])) diff --git a/server/IRC.py b/server/IRC.py index 3d3c7b2..1abe8ef 100644 --- a/server/IRC.py +++ b/server/IRC.py @@ -69,5 +69,5 @@ class IRCServer(SocketServer): def _close(self): - self.write("QUIT") - SocketServer._close(self) + if self.socket is not None: self.write("QUIT") + return SocketServer._close(self) diff --git a/server/socket.py b/server/socket.py index e432abf..c3a4824 100644 --- a/server/socket.py +++ b/server/socket.py @@ -59,17 +59,23 @@ class SocketServer(AbstractServer): def _close(self): self._sending_queue.join() if self.socket is not None: - self.socket.shutdown(socket.SHUT_RDWR) - self.socket.close() + try: + self.socket.shutdown(socket.SHUT_RDWR) + self.socket.close() + except socket.error: + pass self.socket = None + return True def _write(self, cnt): + if self.socket is None: return self.socket.send(cnt) def format(self, txt): return txt.encode() + b'\r\n' def read(self): + if self.socket is None: return raw = self.socket.recv(1024) temp = (self.readbuffer + raw).split(b'\r\n') self.readbuffer = temp.pop() From 95db63bf47b6a59fd40b9f20d4504652db440372 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 8 Sep 2014 02:36:19 +0200 Subject: [PATCH 120/674] Capabilities negociation fully implemented --- consumer.py | 5 +++++ server/IRC.py | 22 +++++++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/consumer.py b/consumer.py index 7c0128f..99085f6 100644 --- a/consumer.py +++ b/consumer.py @@ -126,6 +126,11 @@ class MessageConsumer: elif msg.cmd == "ERROR": self.srv.close() + elif (msg.cmd == "CAP" and + hasattr(self.srv, "_on_caps_ls") and + self.srv._on_caps_ls(msg)): + pass + elif msg.cmd == "PING": self.srv.write("%s :%s" % ("PONG", msg.params[0])) diff --git a/server/IRC.py b/server/IRC.py index 1abe8ef..299d522 100644 --- a/server/IRC.py +++ b/server/IRC.py @@ -31,9 +31,16 @@ class IRCServer(SocketServer): self.nick = nick self.owner = owner self.realname = realname - self.capabilities = list() if not node.hasAttribute("caps") or node["caps"].lower() != "no" else None self.id = "TODO" + if node.hasAttribute("caps"): + if node["caps"].lower() == "no": + self.capabilities = None + else: + self.capabilities = node["caps"].split(",") + else: + self.capabilities = list() + def _on_connect(): # First, JOIN some channels for chn in node.getNodes("channel"): @@ -43,6 +50,17 @@ class IRCServer(SocketServer): self.write("JOIN %s" % chn["name"]) self._on_connect = _on_connect + def _on_caps_ls(msg): + if len(msg.params) != 3 or msg.params[1] != "LS": + return False + server_caps = msg.params[2].split(" ") + for cap in self.capabilities: + if cap not in server_caps: + self.capabilities.remove(cap) + self.write("CAP REQ :" + " ".join(self.capabilities)) + self.write("CAP END") + self._on_caps_ls = _on_caps_ls + def _open(self): if SocketServer._open(self): @@ -52,8 +70,6 @@ class IRCServer(SocketServer): self.write("CAP LS") self.write("NICK :" + self.nick) self.write("USER %s %s bla :%s" % (self.nick, self.host, self.realname)) - if self.capabilities is not None: - self.write("CAP END") return True return False From f51ad21e91e63c25b3032f1fe3dca73bd972c60f Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 8 Sep 2014 02:41:50 +0200 Subject: [PATCH 121/674] Don't send CAP REQ if there is no compatible capabilities --- server/IRC.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/IRC.py b/server/IRC.py index 299d522..b3d014b 100644 --- a/server/IRC.py +++ b/server/IRC.py @@ -57,7 +57,8 @@ class IRCServer(SocketServer): for cap in self.capabilities: if cap not in server_caps: self.capabilities.remove(cap) - self.write("CAP REQ :" + " ".join(self.capabilities)) + if len(self.capabilities) > 0: + self.write("CAP REQ :" + " ".join(self.capabilities)) self.write("CAP END") self._on_caps_ls = _on_caps_ls From d32a0cdc15d0df60cb503a5f5537a443189dd1cb Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 8 Sep 2014 02:42:37 +0200 Subject: [PATCH 122/674] Update configuration sample with capabilities --- bot_sample.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot_sample.xml b/bot_sample.xml index 37f334f..85d6f42 100644 --- a/bot_sample.xml +++ b/bot_sample.xml @@ -1,6 +1,6 @@ <nemubotconfig nick="nemubot" realname="nemubot clone" owner="someone"> - <server server="irc.rezosup.org" port="6667" autoconnect="true"> + <server server="irc.rezosup.org" port="6667" autoconnect="true" caps="znc.in/server-time-iso"> <channel name="#nemutest" /> </server> From 28c86d461aee8c4a9c998f50f54690c41782bd77 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 9 Sep 2014 06:59:57 +0200 Subject: [PATCH 123/674] Restore !help feature as standard command hook --- bot.py | 126 +++++++++++++++++---------------------------------------- 1 file changed, 37 insertions(+), 89 deletions(-) diff --git a/bot.py b/bot.py index 8030b6b..4881857 100644 --- a/bot.py +++ b/bot.py @@ -84,6 +84,43 @@ class Bot(threading.Thread): return response.Response(msg.sender, message="pong", channel=msg.receivers, nick=msg.nick) self.hooks.add_hook(hooks.Hook(in_ping), "in", "PRIVMSG") + def _help_msg(msg): + """Parse and response to help messages""" + cmd = msg.cmds + sndr = msg.sender + res = response.Response(sndr) + if len(cmd) > 1: + if cmd[1] in self.modules: + if len(cmd) > 2: + if hasattr(self.modules[cmd[1]], "HELP_cmd"): + res.append_message(self.modules[cmd[1]].HELP_cmd(cmd[2])) + else: + res.append_message("No help for command %s in module %s" % (cmd[2], cmd[1])) + elif hasattr(self.modules[cmd[1]], "help_full"): + res.append_message(self.modules[cmd[1]].help_full()) + else: + res.append_message("No help for module %s" % cmd[1]) + else: + res.append_message("No module named %s" % cmd[1]) + else: + res.append_message("Pour me demander quelque chose, commencez " + "votre message par mon nom ; je réagis " + "également à certaine commandes commençant par" + " !. Pour plus d'informations, envoyez le " + "message \"!more\".") + res.append_message("Mon code source est libre, publié sous " + "licence AGPL (http://www.gnu.org/licenses/). " + "Vous pouvez le consulter, le dupliquer, " + "envoyer des rapports de bogues ou bien " + "contribuer au projet sur GitHub : " + "http://github.com/nemunaire/nemubot/") + res.append_message(title="Pour plus de détails sur un module, " + "envoyez \"!help nomdumodule\". Voici la liste" + " de tous les modules disponibles localement", + message=["\x03\x02%s\x03\x02 (%s)" % (im, self.modules[im].__doc__) for im in self.modules if self.modules[im].__doc__]) + return res + self.hooks.add_hook(hooks.Hook(_help_msg, "help"), "in", "PRIVMSG", "cmd") + # Other known bots, making a bots network self.network = dict() self.hooks_cache = dict() @@ -410,98 +447,9 @@ class Bot(threading.Thread): store.remove(hook) - def treat_prvmsg_ask(self, msg, srv): - # Treat ping - if re.match("^ *(m[' ]?entends?[ -]+tu|h?ear me|do you copy|ping)", - msg.text, re.I) is not None: - return response.Response(msg.sender, message="pong", - channel=msg.receivers, nick=msg.nick) - - # Ask hooks - else: - return self.treat_ask(msg, srv) - - def treat_prvmsg(self, msg, srv): - # First, treat CTCP - if msg.is_ctcp: - if msg.cmds[0] in self.ctcp_capabilities: - return self.ctcp_capabilities[msg.cmds[0]](srv, msg) - else: - return _ctcp_response(msg.sender, "ERRMSG Unknown or unimplemented CTCP request") - - # Owner commands - if len(msg.text) > 1 and msg.text[0] == '`' and msg.nick == srv.owner: - #TODO: owner commands - pass - - elif len(msg.text) > 1 and msg.text[0] == '!': - # Remove the ! - msg.cmds[0] = msg.cmds[0][1:] - - if msg.cmds[0] == "help": - return _help_msg(msg.sender, self.modules, msg.cmds) - - elif msg.cmds[0] == "dcc": - logger.debug("dcctest for %s", msg.sender) - srv.send_dcc("Hello %s!" % msg.nick, msg.sender) - elif msg.cmds[0] == "pvdcctest": - logger.debug("dcctest") - return Response(msg.sender, message="Test DCC") - elif msg.cmds[0] == "dccsendtest": - logger.debug("dccsendtest") - conn = DCC(srv, msg.sender) - conn.send_file("bot_sample.xml") - - else: - return self.treat_cmd(msg, srv) - - else: - res = self.treat_answer(msg, srv) - - # Assume the message starts with nemubot: - if not res and msg.private: - return self.treat_prvmsg_ask(msg, srv) - return res - - def _ctcp_response(sndr, msg): return response.Response(sndr, msg, ctcp=True) - -def _help_msg(sndr, modules, cmd): - """Parse and response to help messages""" - res = response.Response(sndr) - if len(cmd) > 1: - if cmd[1] in modules: - if len(cmd) > 2: - if hasattr(modules[cmd[1]], "HELP_cmd"): - res.append_message(modules[cmd[1]].HELP_cmd(cmd[2])) - else: - res.append_message("No help for command %s in module %s" % (cmd[2], cmd[1])) - elif hasattr(modules[cmd[1]], "help_full"): - res.append_message(modules[cmd[1]].help_full()) - else: - res.append_message("No help for module %s" % cmd[1]) - else: - res.append_message("No module named %s" % cmd[1]) - else: - res.append_message("Pour me demander quelque chose, commencez " - "votre message par mon nom ; je réagis " - "également à certaine commandes commençant par" - " !. Pour plus d'informations, envoyez le " - "message \"!more\".") - res.append_message("Mon code source est libre, publié sous " - "licence AGPL (http://www.gnu.org/licenses/). " - "Vous pouvez le consulter, le dupliquer, " - "envoyer des rapports de bogues ou bien " - "contribuer au projet sur GitHub : " - "http://github.com/nemunaire/nemubot/") - res.append_message(title="Pour plus de détails sur un module, " - "envoyez \"!help nomdumodule\". Voici la liste" - " de tous les modules disponibles localement", - message=["\x03\x02%s\x03\x02 (%s)" % (im, modules[im].__doc__) for im in modules if modules[im].__doc__]) - return res - def hotswap(bak): new = Bot(str(bak.ip), bak.modules_paths, bak.data_path) new.ctcp_capabilities = bak.ctcp_capabilities From 1c847d11d6958620c8c3c0aae5d24090e81ca7c1 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 9 Sep 2014 07:00:17 +0200 Subject: [PATCH 124/674] allow_all doesn't exist anymore --- bot_sample.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot_sample.xml b/bot_sample.xml index 85d6f42..f02a20a 100644 --- a/bot_sample.xml +++ b/bot_sample.xml @@ -5,7 +5,7 @@ </server> <!-- - <server server="my_host.local" port="6667" password="secret" autoconnect="true" ip="10.69.42.23" ssl="on" allowall="true"> + <server server="my_host.local" port="6667" password="secret" autoconnect="true" ip="10.69.42.23" ssl="on"> <channel name="#nemutest" /> </server> --> From 5f86b35cf0d080e5a6b3336d9566233bc7eb793a Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 9 Sep 2014 07:02:41 +0200 Subject: [PATCH 125/674] Fix context reload feature --- bot.py | 32 +++++++++++++++++++++++++++++--- nemubot.py | 1 + 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/bot.py b/bot.py index 4881857..f5fc855 100644 --- a/bot.py +++ b/bot.py @@ -451,6 +451,7 @@ def _ctcp_response(sndr, msg): return response.Response(sndr, msg, ctcp=True) def hotswap(bak): + bak.stop = True new = Bot(str(bak.ip), bak.modules_paths, bak.data_path) new.ctcp_capabilities = bak.ctcp_capabilities new.servers = bak.servers @@ -470,26 +471,51 @@ def reload(): import consumer imp.reload(consumer) - import DCC - imp.reload(DCC) - import event imp.reload(event) + import exception + imp.reload(exception) + import hooks imp.reload(hooks) + import hooksmanager + imp.reload(hooksmanager) + import importer imp.reload(importer) import message imp.reload(message) + import prompt + imp.reload(prompt) import prompt.builtins imp.reload(prompt.builtins) + import response + imp.reload(response) + import server + rl,wl,xl = server._rlist,server._wlist,server._xlist imp.reload(server) + server._rlist,server._wlist,server._xlist = rl,wl,xl + import server.socket + imp.reload(server.socket) + import server.IRC + imp.reload(server.IRC) + + import tools + imp.reload(tools) + import tools.countdown + imp.reload(tools.countdown) + import tools.date + imp.reload(tools.date) + import tools.web + imp.reload(tools.web) + import tools.wrapper + imp.reload(tools.wrapper) import xmlparser imp.reload(xmlparser) diff --git a/nemubot.py b/nemubot.py index a958de4..e56297e 100755 --- a/nemubot.py +++ b/nemubot.py @@ -81,6 +81,7 @@ if __name__ == "__main__": bot.reload() print("\033[1;32mContext reloaded\033[0m, now in Nemubot %s" % bot.__version__) + context.start() except: logger.exception("\033[1;31mUnable to reload the prompt due to errors.\033[0" "m Fix them before trying to reload the prompt.") From 88219a773a310663d15bb9f7fc44b1b72deb83a8 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 9 Sep 2014 07:06:49 +0200 Subject: [PATCH 126/674] Prompt now uses Python readline feature without need of rlwrap --- prompt/__init__.py | 12 +++++------- prompt/builtins.py | 5 +++-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/prompt/__init__.py b/prompt/__init__.py index 6504744..9095991 100644 --- a/prompt/__init__.py +++ b/prompt/__init__.py @@ -18,6 +18,7 @@ import imp import os +import readline import shlex import sys import traceback @@ -81,14 +82,8 @@ class Prompt: """Launch the prompt""" ret = "" while ret != "quit" and ret != "reset" and ret != "refresh": - sys.stdout.write("\033[0;33m%s§\033[0m " % self.getPS1()) - sys.stdout.flush() - try: - line = sys.stdin.readline() - if len(line) <= 0: - line = "quit" - print("quit") + line = input("\033[0;33m%s§\033[0m " % self.getPS1()) cmds = self.lex_cmd(line.strip()) for toks in cmds: try: @@ -98,6 +93,9 @@ class Prompt: traceback.print_exception(exc_type, exc_value, exc_traceback) except KeyboardInterrupt: print("") + except EOFError: + ret = "quit" + print("quit") return ret != "quit" diff --git a/prompt/builtins.py b/prompt/builtins.py index f23400d..ded17ea 100644 --- a/prompt/builtins.py +++ b/prompt/builtins.py @@ -66,15 +66,16 @@ def load_file(filename, context): or config.getName() == "nemubotconfig"): # Preset each server in this file for server in config.getNodes("server"): + ip = server["ip"] if server.hasAttribute("ip") else config["ip"] nick = server["nick"] if server.hasAttribute("nick") else config["nick"] owner = server["owner"] if server.hasAttribute("owner") else config["owner"] realname = server["realname"] if server.hasAttribute("realname") else config["realname"] if context.add_server(server, nick, owner, realname): print("Server `%s:%s' successfully added." % - (server["server"], server["port"])) + (server["host"], server["port"])) else: print("Server `%s:%s' already added, skiped." % - (server["server"], server["port"])) + (server["host"], server["port"])) # Load module and their configuration for mod in config.getNodes("module"): From 4f19f08c9f70bf59df57e3425a8c2f3023cb4a6a Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 10 Sep 2014 12:19:25 +0200 Subject: [PATCH 127/674] WatchWebsite module: raw content can be display in response --- modules/watchWebsite/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/watchWebsite/__init__.py b/modules/watchWebsite/__init__.py index 1e749dd..688715a 100644 --- a/modules/watchWebsite/__init__.py +++ b/modules/watchWebsite/__init__.py @@ -93,7 +93,7 @@ def add_site(msg, diffType="diff"): alert["sender"] = msg.sender alert["server"] = msg.server alert["channel"] = msg.channel - alert["message"] = "%s a changé !" % url + alert["message"] = "{url} a changé !" if url not in DATAS.index: watch = ModuleState("watch") @@ -110,9 +110,9 @@ def add_site(msg, diffType="diff"): return Response(msg.sender, channel=msg.channel, nick=msg.nick, message="ce site est maintenant sous ma surveillance.") -def format_response(site, link='%s', title='%s', categ='%s'): +def format_response(site, link='%s', title='%s', categ='%s', content='%s'): for a in site.getNodes("alert"): - send_response(a["server"], Response(a["sender"], a["message"].format(url=site["url"], link=link, title=title, categ=categ), + send_response(a["server"], Response(a["sender"], a["message"].format(url=site["url"], link=link, title=title, categ=categ, content=content), channel=a["channel"], server=a["server"])) def alert_change(content, site): @@ -162,7 +162,7 @@ def alert_change(content, site): return #Stop here, no changes, so don't save else: # Just looking for any changes - format_response(site, link=site["url"]) + format_response(site, link=site["url"], content=content) site["lastcontent"] = content start_watching(site) save() From 4cbf73c45a3a53846c11d401d1112fd12fb40e64 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 10 Sep 2014 21:28:47 +0200 Subject: [PATCH 128/674] Rework on networking module --- modules/networking.py | 89 +++++++++++++++++++++---------------------- 1 file changed, 44 insertions(+), 45 deletions(-) diff --git a/modules/networking.py b/modules/networking.py index 5572320..e573a34 100644 --- a/modules/networking.py +++ b/modules/networking.py @@ -37,58 +37,57 @@ def cmd_w3m(msg): res.append_message(line.decode()) return res else: - raise IRCException("Veuillez indiquer une URL à visiter.") + raise IRCException("Indicate the URL to visit.") @hook("cmd_hook", "curl") def cmd_curl(msg): - if len(msg.cmds) > 1: - try: - req = web.getURLContent(" ".join(msg.cmds[1:])) - if req is not None: - res = Response(msg.sender, channel=msg.channel) - for m in req.split("\n"): - res.append_message(m) - return res - else: - return Response(msg.sender, "Une erreur est survenue lors de l'accès à cette URL", channel=msg.channel) - except socket.timeout: - return Response(msg.sender, "le délais d'attente a été dépassé durant l'accès à %s" % msg.cmds[1:], channel=msg.channel, nick=msg.nick) - except socket.error as e: - return Response(msg.sender, e.strerror, channel=msg.channel) - else: - return Response(msg.sender, "Veuillez indiquer une URL à visiter.", - channel=msg.channel) + if len(msg.cmds) < 2: + raise IRCException("Indicate the URL to visit.") + + try: + req = web.getURLContent(" ".join(msg.cmds[1:])) + if req is not None: + res = Response(msg.sender, channel=msg.channel) + for m in req.split("\n"): + res.append_message(m) + return res + else: + raise IRCException("An error occurs when trying to access the page") + except socket.timeout: + raise IRCException("The request timeout when trying to access the page") + except socket.error as e: + raise IRCException(e.strerror) @hook("cmd_hook", "curly") def cmd_curly(msg): - if len(msg.cmds) > 1: - url = msg.cmds[1] - o = urllib.parse.urlparse(url, "http") - if o.netloc == "": - raise IRCException("URL invalide") - if o.scheme == "http": - conn = http.client.HTTPConnection(o.netloc, port=o.port, timeout=5) - else: - conn = http.client.HTTPSConnection(o.netloc, port=o.port, timeout=5) - try: - conn.request("HEAD", o.path, None, {"User-agent": "Nemubot v3"}) - except socket.timeout: - raise IRCException("Délais d'attente dépassé") - except socket.gaierror: - print ("<tools.web> Unable to receive page %s from %s on %d." - % (o.path, o.netloc, o.port)) - raise IRCException("Une erreur innatendue est survenue") + if len(msg.cmds) < 2: + raise IRCException("Indicate the URL to visit.") - try: - res = conn.getresponse() - except http.client.BadStatusLine: - raise IRCException("Une erreur est survenue") - finally: - conn.close() - - return Response(msg.sender, "Entêtes de la page %s : HTTP/%s, statut : %d %s ; headers : %s" % (url, res.version, res.status, res.reason, ", ".join(["\x03\x02" + h + "\x03\x02: " + v for h, v in res.getheaders()])), channel=msg.channel) + url = msg.cmds[1] + o = urllib.parse.urlparse(url, "http") + if o.netloc == "": + raise IRCException("URL invalide") + if o.scheme == "http": + conn = http.client.HTTPConnection(o.netloc, port=o.port, timeout=5) else: - raise IRCException("Veuillez indiquer une URL à visiter.") + conn = http.client.HTTPSConnection(o.netloc, port=o.port, timeout=5) + try: + conn.request("HEAD", o.path, None, {"User-agent": "Nemubot v3"}) + except socket.timeout: + raise IRCException("Délais d'attente dépassé") + except socket.gaierror: + print ("<tools.web> Unable to receive page %s from %s on %d." + % (o.path, o.netloc, o.port)) + raise IRCException("Une erreur innatendue est survenue") + + try: + res = conn.getresponse() + except http.client.BadStatusLine: + raise IRCException("Une erreur est survenue") + finally: + conn.close() + + return Response(msg.sender, "Entêtes de la page %s : HTTP/%s, statut : %d %s ; headers : %s" % (url, res.version, res.status, res.reason, ", ".join(["\x03\x02" + h + "\x03\x02: " + v for h, v in res.getheaders()])), channel=msg.channel) @hook("cmd_hook", "traceurl") def cmd_traceurl(msg): @@ -99,7 +98,7 @@ def cmd_traceurl(msg): res.append(Response(msg.sender, trace, channel=msg.channel, title="TraceURL")) return res else: - return Response(msg.sender, "Indiquer une URL à tracer !", channel=msg.channel) + raise IRCException("Indiquer a URL to trace!") def extractdate(str): From d83b0d1b81b65d95cd139d78ecf4a602d8d2218a Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 10 Sep 2014 21:33:28 +0200 Subject: [PATCH 129/674] Report HTTP error to users --- tools/web.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/web.py b/tools/web.py index a67091c..020a616 100644 --- a/tools/web.py +++ b/tools/web.py @@ -25,6 +25,7 @@ from urllib.parse import quote from urllib.parse import urlparse from urllib.request import urlopen +from exception import IRCException import xmlparser def isURL(url): @@ -108,7 +109,7 @@ def getURLContent(url, timeout=15): else: charset = cha[0] except http.client.BadStatusLine: - return None + raise IRCException("Invalid HTTP response") finally: conn.close() @@ -117,7 +118,7 @@ def getURLContent(url, timeout=15): elif (res.status == http.client.FOUND or res.status == http.client.MOVED_PERMANENTLY) and res.getheader("Location") != url: return getURLContent(res.getheader("Location"), timeout) else: - return None + raise IRCException("A HTTP error occurs: %d - %s" % (res.status, http.client.responses[res.status])) def getXML(url, timeout=15): """Get content page and return XML parsed content""" From 877041bb123c63dcc305fb76201728e819e35b86 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 11 Sep 2014 17:23:07 +0200 Subject: [PATCH 130/674] Message parsing is now a server part --- bot.py | 4 ++-- consumer.py | 16 +++++----------- server/IRC.py | 7 +++++++ 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/bot.py b/bot.py index f5fc855..3f704f3 100644 --- a/bot.py +++ b/bot.py @@ -394,10 +394,10 @@ class Bot(threading.Thread): return False - def receive_message(self, srv, raw_msg, private=False, data=None): + def receive_message(self, srv, msg, private=False, data=None): """Queued the message for treatment""" #print("READ", raw_msg) - self.cnsr_queue.put_nowait(MessageConsumer(srv, raw_msg, datetime.now(), private, data)) + self.cnsr_queue.put_nowait(MessageConsumer(srv, msg)) # Launch a new thread if necessary self._launch_consumers() diff --git a/consumer.py b/consumer.py index 99085f6..679b8cc 100644 --- a/consumer.py +++ b/consumer.py @@ -35,14 +35,10 @@ class MessageConsumer: """Store a message before treating""" - def __init__(self, srv, raw, time, prvt, data): + def __init__(self, srv, msg): self.srv = srv - self.raw = raw - self.time = time - self.prvt = prvt - self.data = data - self.msgs = list() + self.msgs = [ msg ] self.responses = None @@ -181,10 +177,8 @@ class MessageConsumer: def run(self, context): """Create, parse and treat the message""" try: - # Parse and create the original message - msg = Message(self.raw, self.time, self.prvt) - self.first_treat(msg) - self.msgs.append(msg) + for msg in self.msgs: + self.first_treat(msg) # Run pre-treatment: from Message to [ Message ] self.pre_treat(context.hooks) @@ -197,7 +191,7 @@ class MessageConsumer: if self.responses is not None and len(self.responses) > 0: self.post_treat(context.hooks) except: - logger.exception("Error occurred during the processing of the message: %s", self.raw) + logger.exception("Error occurred during the processing of the message: %s", self.msgs[0].raw) return #return self.responses diff --git a/server/IRC.py b/server/IRC.py index b3d014b..3c4e092 100644 --- a/server/IRC.py +++ b/server/IRC.py @@ -16,6 +16,9 @@ # 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 + +from message import Message import server from server.socket import SocketServer @@ -88,3 +91,7 @@ class IRCServer(SocketServer): def _close(self): if self.socket is not None: self.write("QUIT") return SocketServer._close(self) + + def read(self): + for line in SocketServer.read(self): + yield Message(line, datetime.now()) From 8c52f75b6a51e510bb15cdd2841ff67bf745a027 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 11 Sep 2014 21:20:56 +0200 Subject: [PATCH 131/674] Prepare hooks to be used for other things than Message --- bot.py | 15 ++-- hooks.py | 106 ---------------------------- hooks/__init__.py | 76 ++++++++++++++++++++ hooksmanager.py => hooks/manager.py | 0 hooks/messagehook.py | 61 ++++++++++++++++ modules/conjugaison.py | 2 +- modules/events.py | 8 ++- modules/mapquest.py | 4 +- modules/networking.py | 5 +- modules/reddit.py | 2 +- modules/sleepytime.py | 4 +- modules/syno.py | 2 +- modules/translate.py | 4 +- modules/weather.py | 8 +-- modules/worldcup.py | 8 +-- modules/ycc.py | 2 +- 16 files changed, 171 insertions(+), 136 deletions(-) delete mode 100644 hooks.py create mode 100644 hooks/__init__.py rename hooksmanager.py => hooks/manager.py (100%) create mode 100644 hooks/messagehook.py diff --git a/bot.py b/bot.py index 3f704f3..786be74 100644 --- a/bot.py +++ b/bot.py @@ -31,8 +31,8 @@ __author__ = 'nemunaire' from consumer import Consumer, EventConsumer, MessageConsumer from event import ModuleEvent -import hooks -from hooksmanager import HooksManager +from hooks.messagehook import MessageHook +from hooks.manager import HooksManager from networkbot import NetworkBot from server.IRC import IRCServer from server.DCC import DCC @@ -82,7 +82,7 @@ class Bot(threading.Thread): def in_ping(msg): if re.match("^ *(m[' ]?entends?[ -]+tu|h?ear me|do you copy|ping)", msg.text, re.I) is not None: return response.Response(msg.sender, message="pong", channel=msg.receivers, nick=msg.nick) - self.hooks.add_hook(hooks.Hook(in_ping), "in", "PRIVMSG") + self.hooks.add_hook(MessageHook(in_ping), "in", "PRIVMSG") def _help_msg(msg): """Parse and response to help messages""" @@ -119,7 +119,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.hooks.add_hook(hooks.Hook(_help_msg, "help"), "in", "PRIVMSG", "cmd") + self.hooks.add_hook(MessageHook(_help_msg, "help"), "in", "PRIVMSG", "cmd") # Other known bots, making a bots network self.network = dict() @@ -479,9 +479,10 @@ def reload(): import hooks imp.reload(hooks) - - import hooksmanager - imp.reload(hooksmanager) + import hooks.manager + imp.reload(hooks.manager) + import hooks.messagehook + imp.reload(hooks.messagehook) import importer imp.reload(importer) diff --git a/hooks.py b/hooks.py deleted file mode 100644 index 4d52135..0000000 --- a/hooks.py +++ /dev/null @@ -1,106 +0,0 @@ -# -*- coding: utf-8 -*- - -# Nemubot is a modulable IRC bot, built around XML configuration files. -# Copyright (C) 2012 Mercier Pierre-Olivier -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import logging -import re - -from response import Response -from exception import IRCException - -logger = logging.getLogger("nemubot.hooks") - -class Hook: - """Class storing hook informations""" - def __init__(self, call, name=None, data=None, regexp=None, channels=list(), server=None, end=None, call_end=None, help=None): - self.name = name - self.end = end - self.call = call - if call_end is None: - self.call_end = self.call - else: - self.call_end = call_end - self.regexp = regexp - self.data = data - self.times = -1 - self.server = server - self.channels = channels - self.help = help - - def match(self, message, channel=None, server=None): - if isinstance(message, Response): - return self.is_matching(None, channel, server) - elif message.qual == "cmd": - return self.is_matching(message.cmds[0], channel, server) - elif hasattr(message, "text"): - return self.is_matching(message.text, channel, server) - elif len(message.params) > 0: - return self.is_matching(message.params[0], channel, server) - else: - return self.is_matching(message.cmd, channel, server) - - def is_matching(self, strcmp, channel=None, server=None): - """Test if the current hook correspond to the message""" - return (channel is None or len(self.channels) <= 0 or - channel in self.channels) and (server is None or - self.server is None or self.server == server) and ( - (self.name is None or strcmp == self.name) and ( - self.end is None or strcmp == self.end) and ( - self.regexp is None or re.match(self.regexp, strcmp))) - - def run(self, msg, data2=None, strcmp=None): - """Run the hook""" - if self.times != 0: - self.times -= 1 - - if (self.end is not None and strcmp is not None and - self.call_end is not None and strcmp == self.end): - call = self.call_end - self.times = 0 - else: - call = self.call - - try: - if self.data is None: - if data2 is None: - return call(msg) - elif isinstance(data2, dict): - return call(msg, **data2) - else: - return call(msg, data2) - elif isinstance(self.data, dict): - if data2 is None: - return call(msg, **self.data) - else: - return call(msg, data2, **self.data) - else: - if data2 is None: - return call(msg, self.data) - elif isinstance(data2, dict): - return call(msg, self.data, **data2) - else: - return call(msg, self.data, data2) - except IRCException as e: - return e.fill_response(msg) - -last_registered = [] - -def hook(store, *args, **kargs): - def sec(call): - last_registered.append((store, Hook(call, *args, **kargs))) - return call - return sec diff --git a/hooks/__init__.py b/hooks/__init__.py new file mode 100644 index 0000000..e1fa0cf --- /dev/null +++ b/hooks/__init__.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- + +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2014 nemunaire +# +# 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 exception import IRCException + +def call_game(call, *args, **kargs): + """TODO""" + l = list() + d = kargs + + for a in args: + if a is not None: + if isinstance(a, dict): + d.update(a) + else: + l.append(a) + + return call(*l, **d) + + +class AbstractHook: + + """Abstract class for Hook implementation""" + + def __init__(self, call, data=None, mtimes=-1, end_call=None): + self.call = call + self.data = data + + self.times = mtimes + self.end_call = end_call + + + def match(self, data1, server): + return NotImplemented + + + def run(self, data1, *args): + """Run the hook""" + self.times -= 1 + + try: + ret = call_game(self.call, data1, self.data, *args) + except IRCException as e: + ret = e.fill_response(data1) + finally: + if self.times == 0: + self.call_end(ret) + + return ret + + +from hooks.messagehook import MessageHook + +last_registered = [] + +def hook(store, *args, **kargs): + """Function used as a decorator for module loading""" + def sec(call): + last_registered.append((store, MessageHook(call, *args, **kargs))) + return call + return sec diff --git a/hooksmanager.py b/hooks/manager.py similarity index 100% rename from hooksmanager.py rename to hooks/manager.py diff --git a/hooks/messagehook.py b/hooks/messagehook.py new file mode 100644 index 0000000..2ae3572 --- /dev/null +++ b/hooks/messagehook.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- + +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2014 nemunaire +# +# 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 exception import IRCException +import hooks +from response import Response + +class MessageHook(hooks.AbstractHook): + + """Class storing hook information, specialized for a generic Message""" + + def __init__(self, call, name=None, data=None, regexp=None, + channels=list(), server=None, mtimes=-1, end_call=None): + + hooks.AbstractHook.__init__(self, call=call, data=data, + end_call=end_call, mtimes=mtimes) + + self.name = name + self.regexp = regexp + self.server = server + self.channels = channels + + + def match(self, message, server=None): + if isinstance(message, Response): + return self.is_matching(None, message.channel, server) + + elif message.qual == "cmd": + return self.is_matching(message.cmds[0], message.channel, server) + elif hasattr(message, "text"): + return self.is_matching(message.text, message.channel, server) + elif len(message.params) > 0: + return self.is_matching(message.params[0], message.channel, server) + else: + return self.is_matching(message.cmd, message.channel, server) + + + def is_matching(self, strcmp, channel=None, server=None): + """Test if the current hook correspond to the message""" + return (channel is None or len(self.channels) <= 0 or + channel in self.channels) and (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))) diff --git a/modules/conjugaison.py b/modules/conjugaison.py index 9d9c492..d5ae720 100644 --- a/modules/conjugaison.py +++ b/modules/conjugaison.py @@ -34,7 +34,7 @@ def help_full(): return "!conjugaison <tens> <verb>: give the conjugaison for <verb> in <tens>." -@hook("cmd_hook", "conjugaison", help="!conjugaison <tens> <verb>: give the conjugaison for <verb> in <tens>.") +@hook("cmd_hook", "conjugaison") def cmd_conjug(msg): if len(msg.cmds) < 3: raise IRCException("donne moi un temps et un verbe, et je te donnerai sa conjugaison!") diff --git a/modules/events.py b/modules/events.py index f7901c6..90191b1 100644 --- a/modules/events.py +++ b/modules/events.py @@ -14,7 +14,7 @@ import traceback nemubotversion = 3.4 from event import ModuleEvent -from hooks import Hook, hook +from hooks import hook from tools.date import extractDate from tools.countdown import countdown_format, countdown @@ -60,8 +60,9 @@ def cmd_we(msg): "Youhou, on est en week-end depuis %s."), channel=msg.channel) -@hook("cmd_hook", "start", help="!start /something/: launch a timer") +@hook("cmd_hook", "start") def start_countdown(msg): + """!start /something/: launch a timer""" if len(msg.cmds) < 2: raise IRCException("indique le nom d'un événement à chronométrer") if msg.cmds[1] in DATAS.index: @@ -154,8 +155,9 @@ def end_countdown(msg): else: return Response(msg.sender, "%s n'est pas un compteur connu."% (msg.cmds[1]), channel=msg.channel, nick=msg.nick) -@hook("cmd_hook", "eventslist", help="!eventslist: gets list of timer") +@hook("cmd_hook", "eventslist") def liste(msg): + """!eventslist: gets list of timer""" if len(msg.cmds) > 1: res = list() for user in msg.cmds[1:]: diff --git a/modules/mapquest.py b/modules/mapquest.py index 17e7eb8..e2487a6 100644 --- a/modules/mapquest.py +++ b/modules/mapquest.py @@ -15,8 +15,8 @@ def load(context): "http://developer.mapquest.com/") return None - from hooks import Hook - add_hook("cmd_hook", Hook(cmd_geocode, "geocode")) + from hooks.messagehook import MessageHook + add_hook("cmd_hook", MessageHook(cmd_geocode, "geocode")) def help_tiny (): diff --git a/modules/networking.py b/modules/networking.py index e573a34..b83e8f7 100644 --- a/modules/networking.py +++ b/modules/networking.py @@ -9,7 +9,7 @@ import socket import subprocess import urllib -from hooks import Hook, hook +from hooks import hook from tools import web nemubotversion = 3.4 @@ -21,7 +21,8 @@ def load(context): "<whoisxmlapi username=\"XX\" password=\"XXX\" />\nRegister at " "http://www.whoisxmlapi.com/newaccount.php") else: - add_hook("cmd_hook", Hook(cmd_whois, "netwhois")) + from hooks.messagehook import MessageHook + add_hook("cmd_hook", MessageHook(cmd_whois, "netwhois")) def help_full(): return "!traceurl /url/: Follow redirections from /url/." diff --git a/modules/reddit.py b/modules/reddit.py index b7ff786..39eb049 100644 --- a/modules/reddit.py +++ b/modules/reddit.py @@ -15,7 +15,7 @@ def help_full(): LAST_SUBS = dict() -@hook("cmd_hook", "subreddit", help="!subreddit /subreddit/: Display information on the subreddit.") +@hook("cmd_hook", "subreddit") def cmd_subreddit(msg): global LAST_SUBS if len(msg.cmds) <= 1: diff --git a/modules/sleepytime.py b/modules/sleepytime.py index cf45af6..b539a9e 100644 --- a/modules/sleepytime.py +++ b/modules/sleepytime.py @@ -12,9 +12,9 @@ from hooks import hook nemubotversion = 3.4 def help_full(): - return "If you would like to sleep soon, use !sleepytime to know the best time to wake up; use !sleepytime hh:mm if you want to wake up at hh:mm" + return "If you would like to sleep soon, use !sleepytime to know the best time to wake up; use !sleepytime hh:mm if you want to wake up at hh:mm" -@hook("cmd_hook", "sleepytime", help="If you would like to sleep soon, use !sleepytime to know the best time to wake up; use !sleepytime hh:mm if you want to wake up at hh:mm") +@hook("cmd_hook", "sleepytime") def cmd_sleep(msg): if len (msg.cmds) > 1 and re.match("[0-9]{1,2}[h':.,-]([0-9]{1,2})?[m'\":.,-]?", msg.cmds[1]) is not None: diff --git a/modules/syno.py b/modules/syno.py index 548d89f..81e392b 100644 --- a/modules/syno.py +++ b/modules/syno.py @@ -15,7 +15,7 @@ nemubotversion = 3.4 def help_full(): return "!syno <word>: give a list of synonyms for <word>." -@hook("cmd_hook", "synonymes", help="!syno <word>: give a list of synonyms for <word>.") +@hook("cmd_hook", "synonymes") def cmd_syno(msg): return go("synonymes", msg) diff --git a/modules/translate.py b/modules/translate.py index 8191d3b..6a19640 100644 --- a/modules/translate.py +++ b/modules/translate.py @@ -28,8 +28,8 @@ def load(context): else: URL = URL % CONF.getNode("wrapi")["key"] - from hooks import Hook - add_hook("cmd_hook", Hook(cmd_translate, "translate")) + from hooks.messagehook import MessageHook + add_hook("cmd_hook", MessageHook(cmd_translate, "translate")) def help_full(): diff --git a/modules/weather.py b/modules/weather.py index dfcb583..3f865c8 100644 --- a/modules/weather.py +++ b/modules/weather.py @@ -25,10 +25,10 @@ def load(context): "http://developer.forecast.io/") return None - from hooks import Hook - add_hook("cmd_hook", Hook(cmd_weather, "météo")) - add_hook("cmd_hook", Hook(cmd_alert, "alert")) - add_hook("cmd_hook", Hook(cmd_coordinates, "coordinates")) + from hooks.messagehook import MessageHook + add_hook("cmd_hook", MessageHook(cmd_weather, "météo")) + add_hook("cmd_hook", MessageHook(cmd_alert, "alert")) + add_hook("cmd_hook", MessageHook(cmd_coordinates, "coordinates")) def help_full (): diff --git a/modules/worldcup.py b/modules/worldcup.py index c32b133..04db62a 100644 --- a/modules/worldcup.py +++ b/modules/worldcup.py @@ -10,13 +10,11 @@ from urllib.request import urlopen nemubotversion = 3.4 +from hooks import hook + API_URL="http://worldcup.sfg.io/%s" def load(context): - from hooks import Hook - add_hook("cmd_hook", Hook(cmd_watch, "watch_worldcup")) - add_hook("cmd_hook", Hook(cmd_worldcup, "worldcup")) - from event import ModuleEvent 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, intervalle=30)) @@ -37,6 +35,7 @@ def start_watch(msg): save() raise IRCException("This channel is now watching world cup events!") +@hook("cmd_hook", "watch_worldcup") def cmd_watch(msg): global DATAS @@ -178,6 +177,7 @@ def get_matches(url): if is_valid(match): yield match +@hook("cmd_hook", "worldcup") def cmd_worldcup(msg): res = Response(msg.sender, channel=msg.channel, nomore="No more match to display", count=" (%d more matches)") nb = len(msg.cmds) diff --git a/modules/ycc.py b/modules/ycc.py index c450d0d..8c9a3b0 100644 --- a/modules/ycc.py +++ b/modules/ycc.py @@ -24,7 +24,7 @@ def gen_response(res, msg, srv): else: raise IRCException("mauvaise URL : %s" % srv) -@hook("cmd_hook", "ycc", help="!ycc [<url>]: with an argument, reduce the given <url> thanks to ycc.fr; without argument, reduce the last URL said on the current channel.") +@hook("cmd_hook", "ycc") def cmd_ycc(msg): if len(msg.cmds) == 1: global LAST_URLS From db22436e5d4132dea92f5f57bac0a2d615e24792 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 12 Sep 2014 08:12:55 +0200 Subject: [PATCH 132/674] Handle server related or specific stuff out of the pure core, in the server part (PING, CAP, CTCP requests, ...) --- bot.py | 55 ----------------------------- consumer.py | 32 ++++------------- server/IRC.py | 97 ++++++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 95 insertions(+), 89 deletions(-) diff --git a/bot.py b/bot.py index 786be74..7c5e557 100644 --- a/bot.py +++ b/bot.py @@ -64,10 +64,6 @@ class Bot(threading.Thread): self.modules_paths = modules_paths self.data_path = data_path - # Save various informations - self.ctcp_capabilities = dict() - self.init_ctcp_capabilities() - # Keep global context: servers and modules self.servers = dict() self.modules = dict() @@ -147,53 +143,6 @@ class Bot(threading.Thread): self.receive_message(r, i) - def init_ctcp_capabilities(self): - """Reset existing CTCP capabilities to default one""" - - def _ctcp_clientinfo(srv, msg): - """Response to CLIENTINFO CTCP message""" - return _ctcp_response(msg.sender, - " ".join(self.ctcp_capabilities.keys())) - - def _ctcp_dcc(srv, msg): - """Response to DCC CTCP message""" - try: - ip = srv.toIP(int(msg.cmds[3])) - port = int(msg.cmds[4]) - conn = DCC(srv, msg.sender) - except: - return _ctcp_response(msg.sender, "ERRMSG invalid parameters provided as DCC CTCP request") - - logger.info("Receive DCC connection request from %s to %s:%d", conn.sender, ip, port) - - if conn.accept_user(ip, port): - srv.dcc_clients[conn.sender] = conn - conn.send_dcc("Hello %s!" % conn.nick) - else: - logger.error("DCC: unable to connect to %s:%d", ip, port) - return _ctcp_response(msg.sender, "ERRMSG unable to connect to %s:%d" % (ip, port)) - - self.ctcp_capabilities["ACTION"] = lambda srv, msg: print ("ACTION receive: %s" % msg.text) - self.ctcp_capabilities["CLIENTINFO"] = _ctcp_clientinfo - self.ctcp_capabilities["DCC"] = _ctcp_dcc - self.ctcp_capabilities["FINGER"] = lambda srv, msg: _ctcp_response( - msg.sender, "VERSION nemubot v%s" % __version__) - self.ctcp_capabilities["NEMUBOT"] = lambda srv, msg: _ctcp_response( - msg.sender, "NEMUBOT %s" % __version__) - self.ctcp_capabilities["PING"] = lambda srv, msg: _ctcp_response( - msg.sender, "PING %s" % " ".join(msg.cmds[1:])) - self.ctcp_capabilities["SOURCE"] = lambda srv, msg: _ctcp_response( - msg.sender, "SOURCE https://github.com/nemunaire/nemubot") - self.ctcp_capabilities["TIME"] = lambda srv, msg: _ctcp_response( - msg.sender, "TIME %s" % (datetime.now())) - self.ctcp_capabilities["USERINFO"] = lambda srv, msg: _ctcp_response( - msg.sender, "USERINFO %s" % srv.realname) - self.ctcp_capabilities["VERSION"] = lambda srv, msg: _ctcp_response( - msg.sender, "VERSION nemubot v%s" % __version__) - - logger.debug("CTCP capabilities setup: %s", ", ".join(self.ctcp_capabilities)) - - # Events methods def add_event(self, evt, eid=None, module_src=None): @@ -447,13 +396,9 @@ class Bot(threading.Thread): store.remove(hook) -def _ctcp_response(sndr, msg): - return response.Response(sndr, msg, ctcp=True) - def hotswap(bak): bak.stop = True new = Bot(str(bak.ip), bak.modules_paths, bak.data_path) - new.ctcp_capabilities = bak.ctcp_capabilities new.servers = bak.servers new.modules = bak.modules new.modules_configuration = bak.modules_configuration diff --git a/consumer.py b/consumer.py index 679b8cc..a2390d3 100644 --- a/consumer.py +++ b/consumer.py @@ -114,31 +114,13 @@ class MessageConsumer: self.responses = list() for msg in self.msgs: - # TODO: should be placed in server hooks - if msg.cmd == "001": - if hasattr(self.srv, "_on_connect"): - self.srv._on_connect() - - elif msg.cmd == "ERROR": - self.srv.close() - - elif (msg.cmd == "CAP" and - hasattr(self.srv, "_on_caps_ls") and - self.srv._on_caps_ls(msg)): - pass - - elif msg.cmd == "PING": - self.srv.write("%s :%s" % ("PONG", msg.params[0])) - - else: - for h in hm.get_hooks("in", msg.cmd, msg.qual): - - if h.match(message=msg, server=self.srv): - res = h.run(msg) - if isinstance(res, list): - self.responses += res - elif res is not None: - self.responses.append(res) + for h in hm.get_hooks("in", msg.cmd, msg.qual): + if h.match(message=msg, server=self.srv): + res = h.run(msg) + if isinstance(res, list): + self.responses += res + elif res is not None: + self.responses.append(res) def post_treat(self, hm): diff --git a/server/IRC.py b/server/IRC.py index 3c4e092..ee90eec 100644 --- a/server/IRC.py +++ b/server/IRC.py @@ -18,6 +18,7 @@ from datetime import datetime +import bot from message import Message import server from server.socket import SocketServer @@ -31,10 +32,11 @@ class IRCServer(SocketServer): node["password"], node.hasAttribute("ssl") and node["ssl"].lower() == "true") - self.nick = nick - self.owner = owner + self.nick = nick + self.owner = owner self.realname = realname - self.id = "TODO" + self.id = nick + "@" + node["host"] + ":" + node["port"] + if node.hasAttribute("caps"): if node["caps"].lower() == "no": @@ -44,18 +46,75 @@ class IRCServer(SocketServer): else: self.capabilities = list() - def _on_connect(): + # Register CTCP capabilities + self.ctcp_capabilities = dict() + + def _ctcp_clientinfo(msg): + """Response to CLIENTINFO CTCP message""" + return _ctcp_response(msg.sender, + " ".join(self.ctcp_capabilities.keys())) + + def _ctcp_dcc(msg): + """Response to DCC CTCP message""" + try: + ip = srv.toIP(int(msg.cmds[3])) + port = int(msg.cmds[4]) + conn = DCC(srv, msg.sender) + except: + return _ctcp_response(msg.sender, "ERRMSG invalid parameters provided as DCC CTCP request") + + 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 _ctcp_response(msg.sender, "ERRMSG unable to connect to %s:%d" % (ip, port)) + + self.ctcp_capabilities["ACTION"] = lambda msg: print ("ACTION receive: %s" % msg.text) + self.ctcp_capabilities["CLIENTINFO"] = _ctcp_clientinfo + #self.ctcp_capabilities["DCC"] = _ctcp_dcc + self.ctcp_capabilities["FINGER"] = lambda msg: _ctcp_response( + msg.sender, "VERSION nemubot v%s" % bot.__version__) + self.ctcp_capabilities["NEMUBOT"] = lambda msg: _ctcp_response( + msg.sender, "NEMUBOT %s" % bot.__version__) + self.ctcp_capabilities["PING"] = lambda msg: _ctcp_response( + msg.sender, "PING %s" % " ".join(msg.cmds[1:])) + self.ctcp_capabilities["SOURCE"] = lambda msg: _ctcp_response( + msg.sender, "SOURCE https://github.com/nemunaire/nemubot") + self.ctcp_capabilities["TIME"] = lambda msg: _ctcp_response( + msg.sender, "TIME %s" % (datetime.now())) + self.ctcp_capabilities["USERINFO"] = lambda msg: _ctcp_response( + msg.sender, "USERINFO %s" % self.realname) + self.ctcp_capabilities["VERSION"] = lambda msg: _ctcp_response( + msg.sender, "VERSION nemubot v%s" % bot.__version__) + + self.logger.debug("CTCP capabilities setup: %s", ", ".join(self.ctcp_capabilities)) + + # Register hooks on some IRC CMD + self.hookscmd = dict() + + def _on_ping(msg): + self.write("%s :%s" % ("PONG", msg.params[0])) + self.hookscmd["PING"] = _on_ping + + def _on_connect(msg): # First, JOIN some channels for chn in node.getNodes("channel"): if chn["password"] is not None: self.write("JOIN %s %s" % (chn["name"], chn["password"])) else: self.write("JOIN %s" % chn["name"]) - self._on_connect = _on_connect + self.hookscmd["001"] = _on_connect - def _on_caps_ls(msg): + def _on_error(msg): + self.close() + self.hookscmd["ERROR"] = _on_error + + def _on_cap(msg): if len(msg.params) != 3 or msg.params[1] != "LS": - return False + return server_caps = msg.params[2].split(" ") for cap in self.capabilities: if cap not in server_caps: @@ -63,7 +122,7 @@ class IRCServer(SocketServer): if len(self.capabilities) > 0: self.write("CAP REQ :" + " ".join(self.capabilities)) self.write("CAP END") - self._on_caps_ls = _on_caps_ls + self.hookscmd["CAP"] = _on_cap def _open(self): @@ -94,4 +153,24 @@ class IRCServer(SocketServer): def read(self): for line in SocketServer.read(self): - yield Message(line, datetime.now()) + msg = Message(line, datetime.now()) + + if msg.cmd in self.hookscmd: + self.hookscmd[msg.cmd](msg) + + elif msg.cmd == "PRIVMSG" and msg.is_ctcp: + if msg.cmds[0] in self.ctcp_capabilities: + res = self.ctcp_capabilities[msg.cmds[0]](msg) + else: + res = _ctcp_response(msg.sender, "ERRMSG Unknown or unimplemented CTCP request") + if res is not None: + self.send_response(res) + + else: + yield msg + + +from response import Response + +def _ctcp_response(sndr, msg): + return Response(sndr, msg, ctcp=True) From 7dc3b55c34a205e717433169b3719ab7711dbc9b Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 13 Sep 2014 23:49:00 +0200 Subject: [PATCH 133/674] Parse most of IRC messages in server part instead of core --- message.py | 87 ++++++--------------------------- server/IRC.py | 122 ++++++++++++++++++++++++++++++++++++++++++----- server/socket.py | 5 +- 3 files changed, 127 insertions(+), 87 deletions(-) diff --git a/message.py b/message.py index f469319..ee50a0c 100644 --- a/message.py +++ b/message.py @@ -17,82 +17,45 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. from datetime import datetime -import re import shlex from response import Response -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 Message: - def __init__ (self, raw_line, timestamp, private=False): - self.raw = raw_line + def __init__ (self, orig, private=False): + self.cmd = orig.cmd + self.tags = orig.tags + self.params = orig.params self.private = private - self.tags = { 'time': timestamp } - self.params = list() - - p = mgx.match(raw_line.rstrip()) - - # 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"): - for param in p.group("params").strip().split(b' '): - self.params.append(param) - - if p.group("trailing"): - self.params.append(p.group("trailing")) + self.prefix = orig.prefix + self.nick = orig.nick # Special commands if self.cmd == 'PRIVMSG' or self.cmd == 'NOTICE': - self.receivers = self.decode(self.params[0]).split(',') + self.receivers = orig.decode(self.params[0]).split(',') # If CTCP, remove 0x01 if len(self.params[1]) > 1 and (self.params[1][0] == 0x01 or self.params[1][1] == 0x01): self.is_ctcp = True - self.text = self.decode(self.params[1][1:len(self.params[1])-1]) + self.text = orig.decode(self.params[1][1:len(self.params[1])-1]) else: self.is_ctcp = False - self.text = self.decode(self.params[1]) + self.text = orig.decode(self.params[1]) # Split content by words self.parse_content() elif self.cmd == '353': # RPL_NAMREPLY - self.receivers = [ self.decode(self.params[0]) ] - self.nicks = self.decode(self.params[1]).split(" ") + self.receivers = [ orig.decode(self.params[0]) ] + self.nicks = orig.decode(self.params[1]).split(" ") elif self.cmd == '332': - self.receivers = [ self.decode(self.params[0]) ] - self.topic = self.decode(self.params[1]).split(" ") + self.receivers = [ orig.decode(self.params[0]) ] + self.topic = orig.decode(self.params[1]).split(" ") else: for i in range(0, len(self.params)): - self.params[i] = self.decode(self.params[i]) + self.params[i] = orig.decode(self.params[i]) # TODO: here for legacy content @@ -116,25 +79,3 @@ class Message: self.cmds = shlex.split(self.text) except ValueError: self.cmds = self.text.split(' ') - - - def add_tag(self, key, value=None): - """Add an IRCv3.2 Message Tags""" - # Treat special tags - if key == "time": - # TODO: this is UTC timezone, nemubot works with local timezone - value = datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%fZ") - - # Store tag - self.tags[key] = value - - - def decode(self, s): - """Decode the content string usign a specific encoding""" - if isinstance(s, bytes): - try: - s = s.decode() - except UnicodeDecodeError: - #TODO: use encoding from config file - s = s.decode('utf-8', 'replace') - return s diff --git a/server/IRC.py b/server/IRC.py index ee90eec..0d7a46d 100644 --- a/server/IRC.py +++ b/server/IRC.py @@ -17,6 +17,8 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. from datetime import datetime +import re +import shlex import bot from message import Message @@ -96,7 +98,7 @@ class IRCServer(SocketServer): self.hookscmd = dict() def _on_ping(msg): - self.write("%s :%s" % ("PONG", msg.params[0])) + self.write(b"PONG :" + msg.params[0]) self.hookscmd["PING"] = _on_ping def _on_connect(msg): @@ -113,9 +115,9 @@ class IRCServer(SocketServer): self.hookscmd["ERROR"] = _on_error def _on_cap(msg): - if len(msg.params) != 3 or msg.params[1] != "LS": + if len(msg.params) != 3 or msg.params[1] != b"LS": return - server_caps = msg.params[2].split(" ") + server_caps = msg.params[2].decode().split(" ") for cap in self.capabilities: if cap not in server_caps: self.capabilities.remove(cap) @@ -153,24 +155,118 @@ class IRCServer(SocketServer): def read(self): for line in SocketServer.read(self): - msg = Message(line, datetime.now()) + msg = IRCMessage(line, datetime.now()) if msg.cmd in self.hookscmd: self.hookscmd[msg.cmd](msg) - elif msg.cmd == "PRIVMSG" and msg.is_ctcp: - if msg.cmds[0] in self.ctcp_capabilities: - res = self.ctcp_capabilities[msg.cmds[0]](msg) - else: - res = _ctcp_response(msg.sender, "ERRMSG Unknown or unimplemented CTCP request") - if res is not None: - self.send_response(res) - else: - yield msg + mes = msg.to_message() + mes.raw = msg.raw + + if mes.cmd == "PRIVMSG" and mes.is_ctcp: + if mes.cmds[0] in self.ctcp_capabilities: + res = self.ctcp_capabilities[mes.cmds[0]](mes) + else: + res = _ctcp_response(mes.sender, "ERRMSG Unknown or unimplemented CTCP request") + if res is not None: + self.send_response(res) + + else: + yield mes from response import Response def _ctcp_response(sndr, msg): return Response(sndr, msg, ctcp=True) + + +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 IRCMessage: + + def __init__(self, raw, timestamp): + self.raw = raw + self.tags = { 'time': timestamp } + 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"): + for param in p.group("params").strip().split(b' '): + self.params.append(param) + + if p.group("trailing"): + self.params.append(p.group("trailing")) + + + def add_tag(self, key, value=None): + """Add an IRCv3.2 Message Tags""" + # Treat special tags + if key == "time": + # TODO: this is UTC timezone, nemubot works with local timezone + value = datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%fZ") + + # Store tag + self.tags[key] = value + + + def decode(self, s): + """Decode the content string usign a specific encoding""" + if isinstance(s, bytes): + try: + s = s.decode() + except UnicodeDecodeError: + #TODO: use encoding from config file + s = s.decode('utf-8', 'replace') + return s + + + def to_message(self): + return Message(self) + + def to_irc_string(self, client=True): + res = ";".join(["@%s=%s" % (k,v) 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 += " " + " ".join(self.params[:-1]) + res += " :" + self.params[-1] + + return res diff --git a/server/socket.py b/server/socket.py index c3a4824..88ab044 100644 --- a/server/socket.py +++ b/server/socket.py @@ -72,7 +72,10 @@ class SocketServer(AbstractServer): self.socket.send(cnt) def format(self, txt): - return txt.encode() + b'\r\n' + if isinstance(txt, bytes): + return txt + b'\r\n' + else: + return txt.encode() + b'\r\n' def read(self): if self.socket is None: return From fa81fa581475eb5a782fb02d810af6a615682039 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 14 Sep 2014 23:53:51 +0200 Subject: [PATCH 134/674] Handle channel creation in IRC server --- message.py | 8 -------- server/IRC.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/message.py b/message.py index ee50a0c..4d16e23 100644 --- a/message.py +++ b/message.py @@ -45,14 +45,6 @@ class Message: # Split content by words self.parse_content() - elif self.cmd == '353': # RPL_NAMREPLY - self.receivers = [ orig.decode(self.params[0]) ] - self.nicks = orig.decode(self.params[1]).split(" ") - - elif self.cmd == '332': - self.receivers = [ orig.decode(self.params[0]) ] - self.topic = orig.decode(self.params[1]).split(" ") - else: for i in range(0, len(self.params)): self.params[i] = orig.decode(self.params[i]) diff --git a/server/IRC.py b/server/IRC.py index 0d7a46d..4cca4c8 100644 --- a/server/IRC.py +++ b/server/IRC.py @@ -21,6 +21,7 @@ import re import shlex import bot +from channel import Channel from message import Message import server from server.socket import SocketServer @@ -39,6 +40,8 @@ class IRCServer(SocketServer): self.realname = realname self.id = nick + "@" + node["host"] + ":" + node["port"] + #Keep a list of connected channels + self.channels = dict() if node.hasAttribute("caps"): if node["caps"].lower() == "no": @@ -97,10 +100,12 @@ class IRCServer(SocketServer): # 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, JOIN some channels for chn in node.getNodes("channel"): @@ -110,10 +115,12 @@ class IRCServer(SocketServer): 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 @@ -126,6 +133,36 @@ class IRCServer(SocketServer): 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.params[0].split(b","): + # Register the channel + chan = Channel(msg.decode(chname)) + self.channels[chname] = chan + self.hookscmd["JOIN"] = _on_join + # 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 + def _open(self): if SocketServer._open(self): From 73acc007620d01bb39cf8b356e3a3df4633b3f10 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 15 Sep 2014 01:07:43 +0200 Subject: [PATCH 135/674] Handle IRC PART command --- server/IRC.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/server/IRC.py b/server/IRC.py index 4cca4c8..02cbf16 100644 --- a/server/IRC.py +++ b/server/IRC.py @@ -142,6 +142,17 @@ class IRCServer(SocketServer): chan = Channel(msg.decode(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.nick == self.nick: + del self.channels[chname] + elif msg.nick in self.channels[chname].people: + del self.channels[chname].people[msg.nick] + 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 From edbfac2943647a3e797b799e861d0442ba19a26e Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 15 Sep 2014 07:55:35 +0200 Subject: [PATCH 136/674] Alias module: avoid infinite recursion if an alias substitute by the same command --- modules/alias.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/modules/alias.py b/modules/alias.py index 7685f02..aec5411 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -153,12 +153,18 @@ def treat_variables(res): @hook("pre_PRIVMSG_cmd") def treat_alias(msg): if msg.cmds[0] in DATAS.getNode("aliases").index: + oldcmd = msg.cmds[0] msg.text = msg.text.replace(msg.cmds[0], DATAS.getNode("aliases").index[msg.cmds[0]]["origin"], 1) + msg.qual = "def" msg.parse_content() - return treat_alias(msg) + # Avoid infinite recursion + if oldcmd == msg.cmds[0]: + return msg + else: + return treat_alias(msg) else: msg.text = replace_variables(msg.text, msg) From 1beed6751ba2ba7d0cd98c07a6351a38ca4c7f43 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 16 Sep 2014 20:20:37 +0200 Subject: [PATCH 137/674] Books module: can search books writen by someone and read description of a given book; closing #65 --- modules/books.py | 47 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/modules/books.py b/modules/books.py index 8a429c6..0768eaa 100644 --- a/modules/books.py +++ b/modules/books.py @@ -17,23 +17,62 @@ def load(context): "https://www.goodreads.com/api/keys") return None -def search_book(title): +def get_book(title): + response = web.getXML("https://www.goodreads.com/book/title.xml?key=%s&title=%s" % (CONF.getNode("goodreadsapi")["key"], urllib.parse.quote(title))) + if response is not None and response.hasNode("book"): + return response.getNode("book") + else: + return None + +def search_books(title): response = web.getXML("https://www.goodreads.com/search.xml?key=%s&q=%s" % (CONF.getNode("goodreadsapi")["key"], urllib.parse.quote(title))) - if response is not None: + if response is not None and response.hasNode("search"): return response.getNode("search").getNode("results").getNodes("work") else: return [] +def search_author(name): + response = web.getXML("https://www.goodreads.com/api/author_url/%s?key=%s" % (urllib.parse.quote(name), CONF.getNode("goodreadsapi")["key"])) + if response is not None and response.hasNode("author") and response.getNode("author").hasAttribute("id"): + response = web.getXML("https://www.goodreads.com/author/show/%s.xml?key=%s" % (urllib.parse.quote(response.getNode("author")["id"]), CONF.getNode("goodreadsapi")["key"])) + if response is not None and response.hasNode("author"): + return response.getNode("author") + return None + + @hook("cmd_hook", "book") def cmd_book(msg): if len(msg.cmds) < 2: raise IRCException("please give me a title to search") - title = " ".join(msg.cmds) + book = get_book(" ".join(msg.cmds[1:])) + if book is None: + raise IRCException("unable to find book named like this") + res = Response(msg.sender, 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()) + )) + return res + +@hook("cmd_hook", "search_books") +def cmd_books(msg): + if len(msg.cmds) < 2: + raise IRCException("please give me a title to search") + + title = " ".join(msg.cmds[1:]) res = Response(msg.sender, channel=msg.channel, title="%s" % (title), count=" (%d more books)") - books = search_book(title) + books = search_books(title) for book in books: res.append_message("%s, writed by %s" % (book.getNode("best_book").getNode("title").getContent(), book.getNode("best_book").getNode("author").getNode("name").getContent())) return res + +@hook("cmd_hook", "author_books") +def cmd_author(msg): + if len(msg.cmds) < 2: + raise IRCException("please give me an author to search") + + ath = search_author(" ".join(msg.cmds[1:])) + return Response(msg.sender, [b.getNode("title").getContent() for b in ath.getNode("books").getNodes("book")], channel=msg.channel, title=ath.getNode("name").getContent()) From a0a1ef8989a62a8ce3dd4d4578d0ad50addf0b4c Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 17 Sep 2014 06:59:40 +0200 Subject: [PATCH 138/674] Always parse the same number of arguments; empty string != None --- message.py | 2 +- server/IRC.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/message.py b/message.py index 4d16e23..8e32d6d 100644 --- a/message.py +++ b/message.py @@ -62,7 +62,7 @@ class Message: def parse_content(self): """Parse or reparse the message content""" # Remove ! - if self.text[0] == '!': + if len(self.text) > 1 and self.text[0] == '!': self.qual = "cmd" self.text = self.text[1:].strip() diff --git a/server/IRC.py b/server/IRC.py index 02cbf16..0373fa3 100644 --- a/server/IRC.py +++ b/server/IRC.py @@ -271,11 +271,11 @@ class IRCMessage: self.cmd = self.decode(p.group("command")) # Parse params - if p.group("params"): + if p.group("params") is not None: for param in p.group("params").strip().split(b' '): self.params.append(param) - if p.group("trailing"): + if p.group("trailing") is not None: self.params.append(p.group("trailing")) From 5e202063d498c05dfda91b44699b0f02368a36a1 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 18 Sep 2014 06:26:50 +0200 Subject: [PATCH 139/674] Improve stability: catch all kind of exception in main bot loop --- bot.py | 41 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/bot.py b/bot.py index 7c5e557..ce5aa82 100644 --- a/bot.py +++ b/bot.py @@ -132,15 +132,48 @@ class Bot(threading.Thread): self.stop = False while not self.stop: - rl, wl, xl = select(_rlist, _wlist, _xlist, 0.1) + try: + rl, wl, xl = select(_rlist, _wlist, _xlist, 0.1) + except: + logger.error("Something went wrong in select") + fnd_smth = False + # Looking for invalid server + for r in _rlist: + if not hasattr(r, "fileno") or not isinstance(r.fileno(), int): + _rlist.remove(r) + logger.error("Found invalid object in _rlist: " + r) + fnd_smth = True + for w in _wlist: + if not hasattr(r, "fileno") or not isinstance(w.fileno(), int): + _wlist.remove(w) + logger.error("Found invalid object in _wlist: " + w) + fnd_smth = True + for x in _xlist: + if not hasattr(r, "fileno") or not isinstance(x.fileno(), int): + _xlist.remove(x) + logger.error("Found invalid object in _xlist: " + x) + fnd_smth = True + if not fnd_smth: + logger.exception("Can't continue, sorry") + self.stop = True + continue for x in xl: - x.exception() + try: + x.exception() + except: + logger.exception("Uncatched exception on server exception") for w in wl: - w.write_select() + try: + w.write_select() + except: + logger.exception("Uncatched exception on server write") for r in rl: for i in r.read(): - self.receive_message(r, i) + try: + self.receive_message(r, i) + except: + logger.exception("Uncatched exception on server read") # Events methods From 772d68a34d52d9a9ecbfbd274af61dc1caecc0c7 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 18 Sep 2014 07:57:06 +0200 Subject: [PATCH 140/674] Response sender is not needed anymore, private channels are now better handled --- bot.py | 5 ++-- consumer.py | 11 +++++--- exception.py | 2 +- modules/alias.py | 30 ++++++++++----------- modules/birthday.py | 20 +++++++------- modules/bonneannee.py | 18 ++++++------- modules/books.py | 6 ++--- modules/conjugaison.py | 2 +- modules/ddg/__init__.py | 22 +++++++-------- modules/events.py | 27 +++++++++---------- modules/github.py | 8 +++--- modules/imdb.py | 4 +-- modules/man.py | 4 +-- modules/mapquest.py | 2 +- modules/mediawiki.py | 11 ++++---- modules/networking.py | 20 +++++++------- modules/nextstop/__init__.py | 15 +++++------ modules/reddit.py | 6 ++--- modules/rnd.py | 2 +- modules/sap.py | 2 +- modules/sleepytime.py | 6 ++--- modules/sms.py | 8 +++--- modules/spell/__init__.py | 10 +++---- modules/syno.py | 4 +-- modules/translate.py | 2 +- modules/velib.py | 3 +-- modules/watchWebsite/__init__.py | 7 +++-- modules/weather.py | 9 +++---- modules/worldcup.py | 4 +-- modules/ycc.py | 2 +- response.py | 25 +++++++---------- server/IRC.py | 46 +++++++++++++++++--------------- 32 files changed, 161 insertions(+), 182 deletions(-) diff --git a/bot.py b/bot.py index ce5aa82..478859c 100644 --- a/bot.py +++ b/bot.py @@ -77,14 +77,13 @@ class Bot(threading.Thread): self.hooks = HooksManager() def in_ping(msg): if re.match("^ *(m[' ]?entends?[ -]+tu|h?ear me|do you copy|ping)", msg.text, re.I) is not None: - return response.Response(msg.sender, message="pong", channel=msg.receivers, nick=msg.nick) + return response.Response(message="pong", channel=msg.receivers, nick=msg.nick) self.hooks.add_hook(MessageHook(in_ping), "in", "PRIVMSG") def _help_msg(msg): """Parse and response to help messages""" cmd = msg.cmds - sndr = msg.sender - res = response.Response(sndr) + res = response.Response() if len(cmd) > 1: if cmd[1] in self.modules: if len(cmd) > 2: diff --git a/consumer.py b/consumer.py index a2390d3..26904c6 100644 --- a/consumer.py +++ b/consumer.py @@ -26,7 +26,7 @@ import sys import bot from server.DCC import DCC from message import Message -import response +from response import Response import server logger = logging.getLogger("nemubot.consumer") @@ -118,8 +118,13 @@ class MessageConsumer: if h.match(message=msg, server=self.srv): res = h.run(msg) if isinstance(res, list): + for r in res: + if isinstance(r, Response): + r.set_sender(msg.sender) self.responses += res elif res is not None: + if isinstance(res, Response): + res.set_sender(msg.sender) self.responses.append(res) @@ -174,9 +179,7 @@ class MessageConsumer: self.post_treat(context.hooks) except: logger.exception("Error occurred during the processing of the message: %s", self.msgs[0].raw) - return - #return self.responses for res in self.responses: to_server = None if res.server is None: @@ -188,7 +191,7 @@ class MessageConsumer: if to_server is None: logger.error("The server defined in this response doesn't " "exist: %s", res.server) - return False + continue # Sent the message only if treat_post authorize it to_server.send_response(res) diff --git a/exception.py b/exception.py index aae19c9..d73ec99 100644 --- a/exception.py +++ b/exception.py @@ -26,4 +26,4 @@ class IRCException(Exception): self.personnal = personnal def fill_response(self, msg): - return Response(msg.sender, self.message, channel=msg.receivers, nick=(msg.nick if self.personnal else None)) + return Response(self.message, channel=msg.receivers, nick=(msg.nick if self.personnal else None)) diff --git a/modules/alias.py b/modules/alias.py index aec5411..d54c25c 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -51,10 +51,10 @@ def get_variable(name, msg=None): def cmd_set(msg): if len (msg.cmds) > 2: set_variable(msg.cmds[1], " ".join(msg.cmds[2:]), msg.nick) - res = Response(msg.sender, "Variable \$%s définie." % msg.cmds[1]) + res = Response("Variable \$%s définie." % msg.cmds[1]) save() return res - return Response(msg.sender, "!set prend au minimum deux arguments : le nom de la variable et sa valeur.") + return Response("!set prend au minimum deux arguments : le nom de la variable et sa valeur.") @hook("cmd_hook", "listalias") def cmd_listalias(msg): @@ -66,9 +66,9 @@ def cmd_listalias(msg): res.append("Alias créés par %s : %s" % (user, ", ".join(als))) else: res.append("%s n'a pas encore créé d'alias" % user) - return Response(msg.sender, " ; ".join(res), channel=msg.channel) + return Response(" ; ".join(res), channel=msg.channel) else: - return Response(msg.sender, "Alias connus : %s." % ", ".join(DATAS.getNode("aliases").index.keys()), channel=msg.channel) + return Response("Alias connus : %s." % ", ".join(DATAS.getNode("aliases").index.keys()), channel=msg.channel) @hook("cmd_hook", "listvars") def cmd_listvars(msg): @@ -80,9 +80,9 @@ def cmd_listvars(msg): res.append("Variables créées par %s : %s" % (user, ", ".join(als))) else: res.append("%s n'a pas encore créé de variable" % user) - return Response(msg.sender, " ; ".join(res), channel=msg.channel) + return Response(" ; ".join(res), channel=msg.channel) else: - return Response(msg.sender, "Variables connues : %s." % ", ".join(DATAS.getNode("variables").index.keys()), channel=msg.channel) + return Response("Variables connues : %s." % ", ".join(DATAS.getNode("variables").index.keys()), channel=msg.channel) @hook("cmd_hook", "alias") def cmd_alias(msg): @@ -92,15 +92,15 @@ def cmd_alias(msg): if alias[0] == "!": alias = alias[1:] if alias in DATAS.getNode("aliases").index: - res.append(Response(msg.sender, "!%s correspond à %s" % (alias, + res.append(Response("!%s correspond à %s" % (alias, DATAS.getNode("aliases").index[alias]["origin"]), channel=msg.channel)) else: - res.append(Response(msg.sender, "!%s n'est pas un alias" % alias, + res.append(Response("!%s n'est pas un alias" % alias, channel=msg.channel)) return res else: - return Response(msg.sender, "!alias prend en argument l'alias à étendre.", + return Response("!alias prend en argument l'alias à étendre.", channel=msg.channel) @hook("cmd_hook", "unalias") @@ -113,14 +113,14 @@ def cmd_unalias(msg): if alias in DATAS.getNode("aliases").index: if DATAS.getNode("aliases").index[alias]["creator"] == msg.nick or msg.is_owner: DATAS.getNode("aliases").delChild(DATAS.getNode("aliases").index[alias]) - res.append(Response(msg.sender, "%s a bien été supprimé" % alias, channel=msg.channel)) + res.append(Response("%s a bien été supprimé" % alias, channel=msg.channel)) else: - res.append(Response(msg.sender, "Vous n'êtes pas le createur de l'alias %s." % alias, channel=msg.channel)) + res.append(Response("Vous n'êtes pas le createur de l'alias %s." % alias, channel=msg.channel)) else: - res.append(Response(msg.sender, "%s n'est pas un alias" % alias, channel=msg.channel)) + res.append(Response("%s n'est pas un alias" % alias, channel=msg.channel)) return res else: - return Response(msg.sender, "!unalias prend en argument l'alias à supprimer.", channel=msg.channel) + return Response("!unalias prend en argument l'alias à supprimer.", channel=msg.channel) def replace_variables(cnt, msg=None): cnt = cnt.split(' ') @@ -177,14 +177,14 @@ def parseask(msg): if re.match(".*(set|cr[ée]{2}|nouvel(le)?) alias.*", msg.text) is not None: result = re.match(".*alias !?([^ ]+) (pour|=|:) (.+)$", msg.text) if result.group(1) in DATAS.getNode("aliases").index or result.group(3).find("alias") >= 0: - return Response(msg.sender, "Cet alias est déjà défini.") + raise IRCException("cet alias est déjà défini.") else: alias = ModuleState("alias") alias["alias"] = result.group(1) alias["origin"] = result.group(3) alias["creator"] = msg.nick DATAS.getNode("aliases").addChild(alias) - res = Response(msg.sender, "Nouvel alias %s défini avec succès." % result.group(1)) + res = Response("Nouvel alias %s défini avec succès." % result.group(1)) save() return res return None diff --git a/modules/birthday.py b/modules/birthday.py index c49bb82..5c90019 100644 --- a/modules/birthday.py +++ b/modules/birthday.py @@ -50,7 +50,7 @@ def cmd_anniv(msg): if (tyd.day == datetime.today().day and tyd.month == datetime.today().month): - return Response(msg.sender, countdown_format( + return Response(countdown_format( DATAS.index[name].getDate("born"), "", "C'est aujourd'hui l'anniversaire de %s !" " Il a %s. Joyeux anniversaire :)" % (name, "%s")), @@ -59,12 +59,12 @@ def cmd_anniv(msg): if tyd < datetime.today(): tyd = datetime(date.today().year + 1, tyd.month, tyd.day) - return Response(msg.sender, countdown_format(tyd, + return Response(countdown_format(tyd, "Il reste %s avant l'anniversaire de %s !" % ("%s", name), ""), msg.channel) else: - return Response(msg.sender, "désolé, je ne connais pas la date d'anniversaire" + return Response("désolé, je ne connais pas la date d'anniversaire" " de %s. Quand est-il né ?" % name, msg.channel, msg.nick) @@ -75,12 +75,12 @@ def cmd_age(msg): name = matches[0] d = DATAS.index[name].getDate("born") - return Response(msg.sender, countdown_format(d, - "%s va naître dans %s." % (name, "%s"), - "%s a %s." % (name, "%s")), + return Response(countdown_format(d, + "%s va naître dans %s." % (name, "%s"), + "%s a %s." % (name, "%s")), msg.channel) else: - return Response(msg.sender, "désolé, je ne connais pas l'âge de %s." + return Response("désolé, je ne connais pas l'âge de %s." " Quand est-il né ?" % name, msg.channel, msg.nick) return True @@ -91,8 +91,7 @@ def parseask(msg): try: extDate = extractDate(msg.text) if extDate is None or extDate.year > datetime.now().year: - return Response(msg.sender, - "la date de naissance ne paraît pas valide...", + return Response("la date de naissance ne paraît pas valide...", msg.channel, msg.nick) else: @@ -107,8 +106,7 @@ def parseask(msg): ms.setAttribute("born", extDate) DATAS.addChild(ms) save() - return Response(msg.sender, - "ok, c'est noté, %s est né le %s" + return Response("ok, c'est noté, %s est né le %s" % (nick, extDate.strftime("%A %d %B %Y à %H:%M")), msg.channel, msg.nick) diff --git a/modules/bonneannee.py b/modules/bonneannee.py index 7d5d061..0d8f180 100644 --- a/modules/bonneannee.py +++ b/modules/bonneannee.py @@ -19,18 +19,17 @@ def load(context): def bonneannee(): txt = "Bonne année %d !" % datetime.today().year print (txt) - send_response("localhost:2771", Response(None, txt, "#epitagueule")) - send_response("localhost:2771", Response(None, txt, "#yaka")) - send_response("localhost:2771", Response(None, txt, "#epita2014")) - send_response("localhost:2771", Response(None, txt, "#ykar")) - send_response("localhost:2771", Response(None, txt, "#42sh")) - send_response("localhost:2771", Response(None, txt, "#nemubot")) + send_response("localhost:2771", Response(txt, "#epitagueule")) + send_response("localhost:2771", Response(txt, "#yaka")) + send_response("localhost:2771", Response(txt, "#epita2014")) + send_response("localhost:2771", Response(txt, "#ykar")) + send_response("localhost:2771", Response(txt, "#42sh")) + send_response("localhost:2771", Response(txt, "#nemubot")) @hook("cmd_hook", "newyear") @hook("cmd_hook", str(yrn), yrn) def cmd_newyear(msg, yr): - return Response(msg.sender, - countdown_format(datetime(yr, 1, 1, 0, 0, 1), + return Response(countdown_format(datetime(yr, 1, 1, 0, 0, 1), "Il reste %s avant la nouvelle année.", "Nous faisons déjà la fête depuis %s !"), channel=msg.channel) @@ -42,8 +41,7 @@ def cmd_timetoyear(msg, cur): if yr == cur: return None - return Response(msg.sender, - countdown_format(datetime(yr, 1, 1, 0, 0, 1), + return Response(countdown_format(datetime(yr, 1, 1, 0, 0, 1), "Il reste %s avant %d." % ("%s", yr), "Le premier janvier %d est passé depuis %s !" % (yr, "%s")), channel=msg.channel) diff --git a/modules/books.py b/modules/books.py index 0768eaa..4544098 100644 --- a/modules/books.py +++ b/modules/books.py @@ -48,7 +48,7 @@ def cmd_book(msg): book = get_book(" ".join(msg.cmds[1:])) if book is None: raise IRCException("unable to find book named like this") - res = Response(msg.sender, channel=msg.channel) + 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()) @@ -61,7 +61,7 @@ def cmd_books(msg): raise IRCException("please give me a title to search") title = " ".join(msg.cmds[1:]) - res = Response(msg.sender, channel=msg.channel, + res = Response(channel=msg.channel, title="%s" % (title), count=" (%d more books)") books = search_books(title) @@ -75,4 +75,4 @@ def cmd_author(msg): raise IRCException("please give me an author to search") ath = search_author(" ".join(msg.cmds[1:])) - return Response(msg.sender, [b.getNode("title").getContent() for b in ath.getNode("books").getNodes("book")], channel=msg.channel, title=ath.getNode("name").getContent()) + return Response([b.getNode("title").getContent() for b in ath.getNode("books").getNodes("book")], channel=msg.channel, title=ath.getNode("name").getContent()) diff --git a/modules/conjugaison.py b/modules/conjugaison.py index d5ae720..85e481d 100644 --- a/modules/conjugaison.py +++ b/modules/conjugaison.py @@ -47,7 +47,7 @@ def cmd_conjug(msg): conjug = get_conjug(verb, tens) if len(conjug) > 0: - return Response(msg.sender, conjug, channel=msg.channel, + return Response(conjug, channel=msg.channel, title="Conjugaison de %s" % verb) else: raise IRCException("aucune conjugaison de '%s' n'a été trouvé" % verb) diff --git a/modules/ddg/__init__.py b/modules/ddg/__init__.py index 81ed09e..7117c04 100644 --- a/modules/ddg/__init__.py +++ b/modules/ddg/__init__.py @@ -26,13 +26,12 @@ def reload(): @hook("cmd_hook", "define") def define(msg): if len(msg.cmds) <= 1: - return Response(msg.sender, - "Indicate a term to define", + return Response("Indicate a term to define", msg.channel, nick=msg.nick) s = DDGSearch.DDGSearch(' '.join(msg.cmds[1:])) - res = Response(msg.sender, channel=msg.channel) + res = Response(channel=msg.channel) res.append_message(s.definition) @@ -41,13 +40,12 @@ def define(msg): @hook("cmd_hook", "search") def search(msg): if len(msg.cmds) <= 1: - return Response(msg.sender, - "Indicate a term to search", + return Response("Indicate a term to search", msg.channel, nick=msg.nick) s = DDGSearch.DDGSearch(' '.join(msg.cmds[1:])) - res = Response(msg.sender, channel=msg.channel, nomore="No more results", + res = Response(channel=msg.channel, nomore="No more results", count=" (%d more results)") res.append_message(s.redirect) @@ -64,13 +62,12 @@ def search(msg): @hook("cmd_hook", "urbandictionnary") def udsearch(msg): if len(msg.cmds) <= 1: - return Response(msg.sender, - "Indicate a term to search", + return Response("Indicate a term to search", msg.channel, nick=msg.nick) s = UrbanDictionnary.UrbanDictionnary(' '.join(msg.cmds[1:])) - res = Response(msg.sender, channel=msg.channel, nomore="No more results", + res = Response(channel=msg.channel, nomore="No more results", count=" (%d more definitions)") for d in s.definitions: @@ -82,18 +79,17 @@ def udsearch(msg): @hook("cmd_hook", "calculate") def calculate(msg): if len(msg.cmds) <= 1: - return Response(msg.sender, - "Indicate a calcul to compute", + return Response("Indicate a calcul to compute", msg.channel, nick=msg.nick) s = WFASearch.WFASearch(' '.join(msg.cmds[1:])) if s.success: - res = Response(msg.sender, channel=msg.channel, nomore="No more results") + 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(msg.sender, s.error, msg.channel) + return Response(s.error, msg.channel) diff --git a/modules/events.py b/modules/events.py index 90191b1..12cbb36 100644 --- a/modules/events.py +++ b/modules/events.py @@ -36,7 +36,7 @@ def load(context): def fini(d, strend): - send_response(strend["server"], Response(strend["sender"], "%s arrivé à échéance." % strend["name"], channel=strend["channel"], nick=strend["proprio"])) + send_response(strend["server"], Response("%s arrivé à échéance." % strend["name"], channel=strend["channel"], nick=strend["proprio"])) DATAS.delChild(DATAS.index[strend["name"]]) save() @@ -44,8 +44,7 @@ def fini(d, strend): def cmd_gouter(msg): ndate = datetime.today() ndate = datetime(ndate.year, ndate.month, ndate.day, 16, 42) - return Response(msg.sender, - countdown_format(ndate, + return Response(countdown_format(ndate, "Le goûter aura lieu dans %s, préparez vos biscuits !", "Nous avons %s de retard pour le goûter :("), channel=msg.channel) @@ -54,8 +53,7 @@ def cmd_gouter(msg): def cmd_we(msg): ndate = datetime.today() + timedelta(5 - datetime.today().weekday()) ndate = datetime(ndate.year, ndate.month, ndate.day, 0, 0, 1) - return Response(msg.sender, - countdown_format(ndate, + return Response(countdown_format(ndate, "Il reste %s avant le week-end, courage ;)", "Youhou, on est en week-end depuis %s."), channel=msg.channel) @@ -129,11 +127,11 @@ def start_countdown(msg): save() if "end" in strnd: - return Response(msg.sender, "%s commencé le %s et se terminera le %s." % + return Response("%s commencé le %s et se terminera le %s." % (msg.cmds[1], msg.tags["time"].strftime("%A %d %B %Y à %H:%M:%S"), strnd.getDate("end").strftime("%A %d %B %Y à %H:%M:%S"))) else: - return Response(msg.sender, "%s commencé le %s"% (msg.cmds[1], + return Response("%s commencé le %s"% (msg.cmds[1], msg.tags["time"].strftime("%A %d %B %Y à %H:%M:%S"))) @hook("cmd_hook", "end") @@ -148,12 +146,12 @@ def end_countdown(msg): del_event(DATAS.index[msg.cmds[1]]["id"]) DATAS.delChild(DATAS.index[msg.cmds[1]]) save() - return Response(msg.sender, "%s a duré %s." % (msg.cmds[1], duration), + return Response("%s a duré %s." % (msg.cmds[1], duration), channel=msg.channel, nick=msg.nick) else: raise IRCException("Vous ne pouvez pas terminer le compteur %s, créé par %s." % (msg.cmds[1], DATAS.index[msg.cmds[1]]["proprio"])) else: - return Response(msg.sender, "%s n'est pas un compteur connu."% (msg.cmds[1]), channel=msg.channel, nick=msg.nick) + return Response("%s n'est pas un compteur connu."% (msg.cmds[1]), channel=msg.channel, nick=msg.nick) @hook("cmd_hook", "eventslist") def liste(msg): @@ -166,14 +164,14 @@ def liste(msg): res.append("Compteurs créés par %s : %s" % (user, ", ".join(cmptr))) else: res.append("%s n'a pas créé de compteur" % user) - return Response(msg.sender, " ; ".join(res), channel=msg.channel) + return Response(" ; ".join(res), channel=msg.channel) else: - return Response(msg.sender, "Compteurs connus : %s." % ", ".join(DATAS.index.keys()), channel=msg.channel) + return Response("Compteurs connus : %s." % ", ".join(DATAS.index.keys()), channel=msg.channel) @hook("cmd_default") def parseanswer(msg): if msg.cmds[0] in DATAS.index: - res = Response(msg.sender, channel=msg.channel) + res = Response(channel=msg.channel) # Avoid message starting by ! which can be interpreted as command by other bots if msg.cmds[0][0] == "!": @@ -228,8 +226,7 @@ def parseask(msg): evt["msg_before"] = msg_before DATAS.addChild(evt) save() - return Response(msg.sender, - "Nouvel événement !%s ajouté avec succès." % name.group(1), + 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: @@ -242,7 +239,7 @@ def parseask(msg): evt["msg_before"] = texts.group (2) DATAS.addChild(evt) save() - return Response(msg.sender, "Nouvelle commande !%s ajoutée avec succès." % name.group(1)) + return Response("Nouvelle commande !%s ajoutée avec succès." % name.group(1)) else: raise IRCException("Veuillez indiquez les messages d'attente et d'après événement entre guillemets.") diff --git a/modules/github.py b/modules/github.py index e073cd4..a2b9dd6 100644 --- a/modules/github.py +++ b/modules/github.py @@ -72,7 +72,7 @@ def cmd_github(msg): repos = info_repos(" ".join(msg.cmds[1:])) - res = Response(msg.sender, channel=msg.channel, nomore="No more repository", count=" (%d more repo)") + res = Response(channel=msg.channel, nomore="No more repository", count=" (%d more repo)") for repo in repos["items"]: homepage = "" @@ -87,7 +87,7 @@ def cmd_github(msg): if len(msg.cmds) < 2: raise IRCException("indicate a user name to search") - res = Response(msg.sender, channel=msg.channel, nomore="No more user") + res = Response(channel=msg.channel, nomore="No more user") user = info_user(" ".join(msg.cmds[1:])) @@ -126,7 +126,7 @@ def cmd_github(msg): repo = " ".join(msg.cmds[1:]) count = " (%d more issues)" if issue is None else None - res = Response(msg.sender, channel=msg.channel, nomore="No more issue", count=count) + res = Response(channel=msg.channel, nomore="No more issue", count=count) issues = info_issue(repo, issue) @@ -159,7 +159,7 @@ def cmd_github(msg): repo = " ".join(msg.cmds[1:]) count = " (%d more commits)" if commit is None else None - res = Response(msg.sender, channel=msg.channel, nomore="No more commit", count=count) + res = Response(channel=msg.channel, nomore="No more commit", count=count) commits = info_commit(repo, commit) diff --git a/modules/imdb.py b/modules/imdb.py index c859381..106d553 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -84,7 +84,7 @@ def cmd_imdb(msg): else: data = get_movie(title=title) - res = Response(msg.sender, channel=msg.channel, + 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']) @@ -105,4 +105,4 @@ def cmd_search(msg): for m in data['Search']: movies.append("\x02%s\x0F (%s of %s)" % (m['Title'], m['Type'], m['Year'])) - return Response(msg.sender, movies, title="Titles found", channel=msg.channel) + return Response(movies, title="Titles found", channel=msg.channel) diff --git a/modules/man.py b/modules/man.py index 7ad888f..9993ac7 100644 --- a/modules/man.py +++ b/modules/man.py @@ -32,7 +32,7 @@ def cmd_man(msg): args.append(msg.cmds[1]) os.unsetenv("LANG") - res = Response(msg.sender, channel=msg.channel) + res = Response(channel=msg.channel) with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc: for line in proc.stdout.read().split(b"\n"): (line, n) = RGXP_s.subn(b'', line) @@ -50,7 +50,7 @@ def cmd_man(msg): def cmd_whatis(msg): args = ["whatis", " ".join(msg.cmds[1:])] - res = Response(msg.sender, channel=msg.channel) + res = Response(channel=msg.channel) with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc: for line in proc.stdout.read().split(b"\n"): (line, n) = RGXP_s.subn(b'', line) diff --git a/modules/mapquest.py b/modules/mapquest.py index e2487a6..17d9b8b 100644 --- a/modules/mapquest.py +++ b/modules/mapquest.py @@ -43,7 +43,7 @@ def cmd_geocode(msg): raise IRCException("indicate a name") locname = ' '.join(msg.cmds[1:]) - res = Response(msg.sender, channel=msg.channel, nick=msg.nick, nomore="No more geocode", count=" (%s more geocode)") + res = Response(channel=msg.channel, nick=msg.nick, nomore="No more geocode", count=" (%s more geocode)") for loc in geocode(locname): res.append_message("%s is at %s,%s (%s precision)" % (where(loc), loc["latLng"]["lat"], loc["latLng"]["lng"], loc["geocodeQuality"].lower())) return res diff --git a/modules/mediawiki.py b/modules/mediawiki.py index 8bdb05f..909de64 100644 --- a/modules/mediawiki.py +++ b/modules/mediawiki.py @@ -76,8 +76,7 @@ def cmd_mediawiki(msg): if len(msg.cmds) < 3: raise IRCException("indicate a domain and a term to search") - return Response(msg.sender, - get_page(msg.cmds[1], " ".join(msg.cmds[2:])), + return Response(get_page(msg.cmds[1], " ".join(msg.cmds[2:])), channel=msg.receivers) @@ -86,7 +85,7 @@ def cmd_wikipedia(msg): if len(msg.cmds) < 3: raise IRCException("indicate a lang and a term to search") - return Response(msg.sender, - get_page(msg.cmds[1] + ".wikipedia.org", " ".join(msg.cmds[2:])), - channel=msg.receivers, - line_treat=lambda line: irc_format(parse_wikitext(site, line, ssl))) + return Response(irc_format(parse_wikitext(get_page(msg.cmds[1] + ".wikipedia.org", " ".join(msg.cmds[2:])))), +# get_page(msg.cmds[1] + ".wikipedia.org", " ".join(msg.cmds[2:])), +# line_treat=lambda line: irc_format(parse_wikitext(site, line, ssl)), + channel=msg.receivers) diff --git a/modules/networking.py b/modules/networking.py index b83e8f7..26618d5 100644 --- a/modules/networking.py +++ b/modules/networking.py @@ -32,7 +32,7 @@ def cmd_w3m(msg): if len(msg.cmds) > 1: args = ["w3m", "-T", "text/html", "-dump"] args.append(msg.cmds[1]) - res = Response(msg.sender, channel=msg.channel) + res = Response(channel=msg.channel) with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc: for line in proc.stdout.read().split(b"\n"): res.append_message(line.decode()) @@ -48,7 +48,7 @@ def cmd_curl(msg): try: req = web.getURLContent(" ".join(msg.cmds[1:])) if req is not None: - res = Response(msg.sender, channel=msg.channel) + res = Response(channel=msg.channel) for m in req.split("\n"): res.append_message(m) return res @@ -88,7 +88,7 @@ def cmd_curly(msg): finally: conn.close() - return Response(msg.sender, "Entêtes de la page %s : HTTP/%s, statut : %d %s ; headers : %s" % (url, res.version, res.status, res.reason, ", ".join(["\x03\x02" + h + "\x03\x02: " + v for h, v in res.getheaders()])), channel=msg.channel) + return Response("Entêtes de la page %s : HTTP/%s, statut : %d %s ; headers : %s" % (url, res.version, res.status, res.reason, ", ".join(["\x03\x02" + h + "\x03\x02: " + v for h, v in res.getheaders()])), channel=msg.channel) @hook("cmd_hook", "traceurl") def cmd_traceurl(msg): @@ -96,7 +96,7 @@ def cmd_traceurl(msg): res = list() for url in msg.cmds[1:]: trace = traceURL(url) - res.append(Response(msg.sender, trace, channel=msg.channel, title="TraceURL")) + res.append(Response(trace, channel=msg.channel, title="TraceURL")) return res else: raise IRCException("Indiquer a URL to trace!") @@ -170,7 +170,7 @@ def cmd_whois(msg): whois = js["WhoisRecord"] - res = Response(msg.sender, channel=msg.channel, nomore="No more whois information") + res = Response(channel=msg.channel, nomore="No more whois information") 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"] + " " if "status" in whois else "", @@ -196,14 +196,14 @@ def cmd_isup(msg): raw = urllib.request.urlopen(req, timeout=10) isup = json.loads(raw.read().decode()) if "status_code" in isup and isup["status_code"] == 1: - res.append(Response(msg.sender, "%s est accessible (temps de reponse : %ss)" % (isup["domain"], isup["response_time"]), channel=msg.channel)) + res.append(Response("%s est accessible (temps de reponse : %ss)" % (isup["domain"], isup["response_time"]), channel=msg.channel)) else: - res.append(Response(msg.sender, "%s n'est pas accessible :(" % (isup["domain"]), channel=msg.channel)) + res.append(Response("%s n'est pas accessible :(" % (isup["domain"]), channel=msg.channel)) else: - res.append(Response(msg.sender, "%s n'est pas une URL valide" % url, channel=msg.channel)) + res.append(Response("%s n'est pas une URL valide" % url, channel=msg.channel)) return res else: - return Response(msg.sender, "Indiquer une URL à vérifier !", channel=msg.channel) + return Response("Indiquer une URL à vérifier !", channel=msg.channel) def traceURL(url, timeout=5, stack=None): """Follow redirections and return the redirections stack""" @@ -277,7 +277,7 @@ def cmd_w3c(msg): validator = json.loads(raw.read().decode()) - res = Response(msg.sender, channel=msg.channel, nomore="No more error") + res = Response(channel=msg.channel, nomore="No more error") res.append_message("%s: status: %s, %s warning(s), %s error(s)" % (validator["url"], headers["X-W3C-Validator-Status"], headers["X-W3C-Validator-Warnings"], headers["X-W3C-Validator-Errors"])) diff --git a/modules/nextstop/__init__.py b/modules/nextstop/__init__.py index 36838ca..cdd6d6c 100644 --- a/modules/nextstop/__init__.py +++ b/modules/nextstop/__init__.py @@ -24,17 +24,16 @@ def extractInformation(msg, transport, line, station=None): times = ratp.getNextStopsAtStation(transport, line, station) if len(times) > 0: (time, direction, stationname) = times[0] - return Response(msg.sender, message=["\x03\x02"+time+"\x03\x02 direction "+direction for time, direction, stationname in times], title="Prochains passages du %s ligne %s à l'arrêt %s" % + return Response(message=["\x03\x02"+time+"\x03\x02 direction "+direction for time, direction, stationname in times], title="Prochains passages du %s ligne %s à l'arrêt %s" % (transport, line, stationname), channel=msg.channel) else: - return Response(msg.sender, "La station `%s' ne semble pas exister sur le %s ligne %s." - % (station, transport, line), msg.channel) + raise IRCException("La station `%s' ne semble pas exister sur le %s ligne %s." + % (station, transport, line)) else: stations = ratp.getAllStations(transport, line) - if len(stations) > 0: - return Response(msg.sender, [s for s in stations], title="Stations", channel=msg.channel) - else: - return Response(msg.sender, "Aucune station trouvée.", msg.channel) + if len(stations) == 0: + raise IRCException("aucune station trouvée.") + return Response([s for s in stations], title="Stations", channel=msg.channel) def ask_ratp(msg): """Hook entry from !ratp""" @@ -44,5 +43,5 @@ def ask_ratp(msg): elif len(msg.cmds) == 3: return extractInformation(msg, msg.cmds[1], msg.cmds[2]) else: - return Response(msg.sender, "Mauvais usage, merci de spécifier un type de transport et une ligne, ou de consulter l'aide du module.", msg.channel, msg.nick) + raise IRCException("Mauvais usage, merci de spécifier un type de transport et une ligne, ou de consulter l'aide du module.") return False diff --git a/modules/reddit.py b/modules/reddit.py index 39eb049..f33d25a 100644 --- a/modules/reddit.py +++ b/modules/reddit.py @@ -42,15 +42,15 @@ def cmd_subreddit(msg): sbr = json.loads(raw.read().decode()) if "title" in sbr["data"]: - res = Response(msg.sender, channel=msg.channel, nomore="No more information") + res = Response(channel=msg.channel, nomore="No more information") res.append_message(("[NSFW] " if sbr["data"]["over18"] else "") + sbr["data"]["url"] + " " + sbr["data"]["title"] + ": " + sbr["data"]["public_description" if sbr["data"]["public_description"] != "" else "description"].replace("\n", " ") + " %s subscriber(s)" % sbr["data"]["subscribers"]) if sbr["data"]["public_description"] != "": res.append_message(sbr["data"]["description"].replace("\n", " ")) all_res.append(res) else: - all_res.append(Response(msg.sender, "/%s/%s doesn't exist" % (where, sub.group(2)), channel=msg.channel)) + all_res.append(Response("/%s/%s doesn't exist" % (where, sub.group(2)), channel=msg.channel)) else: - all_res.append(Response(msg.sender, "%s is not a valid subreddit" % osub, channel=msg.channel, nick=msg.nick)) + all_res.append(Response("%s is not a valid subreddit" % osub, channel=msg.channel, nick=msg.nick)) return all_res diff --git a/modules/rnd.py b/modules/rnd.py index fc28b01..62ead64 100644 --- a/modules/rnd.py +++ b/modules/rnd.py @@ -11,6 +11,6 @@ nemubotversion = 3.4 @hook("cmd_hook", "choice") def cmd_choice(msg): if len(msg.cmds) > 1: - return Response(msg.sender, random.choice(msg.cmds[1:]), channel=msg.channel, nick=msg.nick) + return Response(random.choice(msg.cmds[1:]), channel=msg.channel, nick=msg.nick) else: raise IRCException("indicate some terms to pick!") diff --git a/modules/sap.py b/modules/sap.py index 5b802d5..3065724 100644 --- a/modules/sap.py +++ b/modules/sap.py @@ -23,7 +23,7 @@ def cmd_tcode(msg): url = "http://www.tcodesearch.com/tcodes/search?q=%s" % urllib.parse.quote(msg.cmds[1]) page = web.getURLContent(url) - res = Response(msg.sender, channel=msg.channel, + res = Response(channel=msg.channel, nomore="No more transaction code", count=" (%d more tcodes)") if page is not None: diff --git a/modules/sleepytime.py b/modules/sleepytime.py index b539a9e..35ceece 100644 --- a/modules/sleepytime.py +++ b/modules/sleepytime.py @@ -30,8 +30,7 @@ def cmd_sleep(msg): for i in range(0,6): f.append(f[i] - timedelta(hours=1,minutes=30)) g.append(f[i+1].strftime("%H:%M")) - return Response(msg.sender, - "You should try to fall asleep at one of the following" + return Response("You should try to fall asleep at one of the following" " times: %s" % ', '.join(g), channel=msg.channel) # Just get awake times @@ -41,7 +40,6 @@ def cmd_sleep(msg): for i in range(0,6): f.append(f[i] + timedelta(hours=1,minutes=30)) g.append(f[i+1].strftime("%H:%M")) - return Response(msg.sender, - "If you head to bed right now, you should try to wake" + return Response("If you head to bed right now, you should try to wake" " up at one of the following times: %s" % ', '.join(g), channel=msg.channel) diff --git a/modules/sms.py b/modules/sms.py index 8db48ef..b41aa55 100644 --- a/modules/sms.py +++ b/modules/sms.py @@ -69,9 +69,9 @@ def cmd_sms(msg): fails.append( "%s: %s" % (u, test) ) if len(fails) > 0: - return Response(msg.sender, "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.nick) else: - return Response(msg.sender, "le SMS a bien été envoyé", msg.channel, msg.nick) + return Response("le SMS a bien été envoyé", msg.channel, msg.nick) 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) @@ -90,7 +90,7 @@ 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(msg.sender, "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.nick) if msg.nick in DATAS.index: DATAS.index[msg.nick]["user"] = apiuser @@ -103,5 +103,5 @@ def parseask(msg): ms.setAttribute("lastuse", 0) DATAS.addChild(ms) save() - return Response(msg.sender, "ok, c'est noté. Je t'ai envoyé un SMS pour tester ;)", + return Response("ok, c'est noté. Je t'ai envoyé un SMS pour tester ;)", msg.channel, msg.nick) diff --git a/modules/spell/__init__.py b/modules/spell/__init__.py index 5426f73..b56ee2b 100644 --- a/modules/spell/__init__.py +++ b/modules/spell/__init__.py @@ -33,7 +33,7 @@ def cmd_spell(msg): try: r = check_spell(word, lang) except AspellError: - return Response(msg.sender, "Je n'ai pas le dictionnaire `%s' :(" % lang, msg.channel, msg.nick) + 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) @@ -43,7 +43,7 @@ def cmd_spell(msg): else: add_score(msg.nick, "bad") strRes.append("aucune suggestion pour `%s'" % word) - return Response(msg.sender, strRes, channel=msg.channel, nick=msg.nick) + return Response(strRes, channel=msg.channel, nick=msg.nick) def add_score(nick, t): global DATAS @@ -66,13 +66,13 @@ def cmd_score(msg): if len(msg.cmds) > 1: for cmd in msg.cmds[1:]: if cmd in DATAS.index: - res.append(Response(msg.sender, "%s: %s" % (cmd, " ; ".join(["%s: %d" % (a, DATAS.index[cmd].getInt(a)) for a in DATAS.index[cmd].attributes.keys() if a != "name"])), channel=msg.channel)) + res.append(Response("%s: %s" % (cmd, " ; ".join(["%s: %d" % (a, DATAS.index[cmd].getInt(a)) for a in DATAS.index[cmd].attributes.keys() if a != "name"])), channel=msg.channel)) else: unknown.append(cmd) else: - return Response(msg.sender, "De qui veux-tu voir les scores ?", channel=msg.channel, nick=msg.nick) + return Response("De qui veux-tu voir les scores ?", channel=msg.channel, nick=msg.nick) if len(unknown) > 0: - res.append(Response(msg.sender, "%s inconnus" % ", ".join(unknown), channel=msg.channel)) + res.append(Response("%s inconnus" % ", ".join(unknown), channel=msg.channel)) return res diff --git a/modules/syno.py b/modules/syno.py index 81e392b..596f340 100644 --- a/modules/syno.py +++ b/modules/syno.py @@ -35,7 +35,7 @@ def go(what, msg): if what == "synonymes": if len(synos) > 0: - res = Response(msg.sender, best, channel=msg.channel, + res = Response(best, channel=msg.channel, title="Synonymes de %s" % word) res.append_message(synos) return res @@ -44,7 +44,7 @@ def go(what, msg): elif what == "antonymes": if len(anton) > 0: - res = Response(msg.sender, anton, channel=msg.channel, + res = Response(anton, channel=msg.channel, title="Antonymes de %s" % word) return res else: diff --git a/modules/translate.py b/modules/translate.py index 6a19640..1bcb0f3 100644 --- a/modules/translate.py +++ b/modules/translate.py @@ -68,7 +68,7 @@ def cmd_translate(msg): raise IRCException(wres["Note"]) else: - res = Response(msg.sender, channel=msg.channel, + res = Response(channel=msg.channel, count=" (%d more meanings)", nomore="No more translation") for k in sorted(wres.keys()): diff --git a/modules/velib.py b/modules/velib.py index d8f9ea2..ae8854a 100644 --- a/modules/velib.py +++ b/modules/velib.py @@ -55,8 +55,7 @@ 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(msg.sender, - "à 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) raise IRCException("station %s inconnue." % station) diff --git a/modules/watchWebsite/__init__.py b/modules/watchWebsite/__init__.py index 688715a..2a5b373 100644 --- a/modules/watchWebsite/__init__.py +++ b/modules/watchWebsite/__init__.py @@ -70,8 +70,7 @@ def del_site(msg): del_event(site["_evt_id"]) DATAS.delChild(site) save() - return Response(msg.sender, - "je ne surveille désormais plus cette URL.", + return Response("je ne surveille désormais plus cette URL.", channel=msg.channel, nick=msg.nick) raise IRCException("je ne surveillais pas cette URL !") @@ -107,12 +106,12 @@ def add_site(msg, diffType="diff"): DATAS.index[url].addChild(alert) save() - return Response(msg.sender, channel=msg.channel, nick=msg.nick, + return Response(channel=msg.channel, nick=msg.nick, message="ce site est maintenant sous ma surveillance.") def format_response(site, link='%s', title='%s', categ='%s', content='%s'): for a in site.getNodes("alert"): - send_response(a["server"], Response(a["sender"], a["message"].format(url=site["url"], link=link, title=title, categ=categ, content=content), + send_response(a["server"], Response(a["message"].format(url=site["url"], link=link, title=title, categ=categ, content=content), channel=a["channel"], server=a["server"])) def alert_change(content, site): diff --git a/modules/weather.py b/modules/weather.py index 3f865c8..91c8a5c 100644 --- a/modules/weather.py +++ b/modules/weather.py @@ -144,13 +144,13 @@ def cmd_coordinates(msg): raise IRCException("%s n'est pas une ville connue" % msg.cmds[1]) coords = DATAS.index[j] - return Response(msg.sender, "Les coordonnées de %s sont %s,%s" % (msg.cmds[1], coords["lat"], coords["long"]), channel=msg.channel) + return Response("Les coordonnées de %s sont %s,%s" % (msg.cmds[1], coords["lat"], coords["long"]), channel=msg.channel) def cmd_alert(msg): loc, coords, specific = treat_coord(msg) wth = get_json_weather(coords) - res = Response(msg.sender, channel=msg.channel, nomore="No more weather alert", count=" (%d more alerts)") + res = Response(channel=msg.channel, nomore="No more weather alert", count=" (%d more alerts)") if "alerts" in wth: for alert in wth["alerts"]: @@ -162,7 +162,7 @@ def cmd_weather(msg): loc, coords, specific = treat_coord(msg) wth = get_json_weather(coords) - res = Response(msg.sender, channel=msg.channel, nomore="No more weather information") + res = Response(channel=msg.channel, nomore="No more weather information") if "alerts" in wth: alert_msgs = list() @@ -228,6 +228,5 @@ def parseask(msg): ms.setAttribute("long", gps_long) DATAS.addChild(ms) save() - return Response(msg.sender, - "ok, j'ai bien noté les coordonnées de %s" % res.group("city"), + return Response("ok, j'ai bien noté les coordonnées de %s" % res.group("city"), msg.channel, msg.nick) diff --git a/modules/worldcup.py b/modules/worldcup.py index 04db62a..fc65daa 100644 --- a/modules/worldcup.py +++ b/modules/worldcup.py @@ -79,7 +79,7 @@ def current_match_new_action(match_str, osef): msg += " ; à la " + txt_event(events[0]) for n in DATAS.getChilds(): - send_response(n["server"], Response(n["sender"], msg, channel=n["channel"])) + send_response(n["server"], Response(msg, channel=n["channel"])) def is_int(s): try: @@ -179,7 +179,7 @@ def get_matches(url): @hook("cmd_hook", "worldcup") def cmd_worldcup(msg): - res = Response(msg.sender, channel=msg.channel, nomore="No more match to display", count=" (%d more matches)") + res = Response(channel=msg.channel, nomore="No more match to display", count=" (%d more matches)") nb = len(msg.cmds) url = None diff --git a/modules/ycc.py b/modules/ycc.py index 8c9a3b0..e38608b 100644 --- a/modules/ycc.py +++ b/modules/ycc.py @@ -20,7 +20,7 @@ def gen_response(res, msg, srv): if res is None: raise IRCException("la situation est embarassante, il semblerait que YCC soit down :(") elif isinstance(res, str): - return Response(msg.sender, "URL pour %s : %s" % (srv, res), msg.channel) + return Response("URL pour %s : %s" % (srv, res), msg.channel) else: raise IRCException("mauvaise URL : %s" % srv) diff --git a/response.py b/response.py index 5529e92..46563c7 100644 --- a/response.py +++ b/response.py @@ -23,9 +23,9 @@ import sys logger = logging.getLogger("nemubot.response") class Response: - def __init__(self, sender, message=None, channel=None, nick=None, server=None, - nomore="No more message", title=None, more="(suite) ", count=None, - ctcp=False, shown_first_count=-1): + def __init__(self, message=None, channel=None, nick=None, server=None, + nomore="No more message", title=None, more="(suite) ", + count=None, ctcp=False, shown_first_count=-1): self.nomore = nomore self.more = more self.rawtitle = title @@ -37,29 +37,22 @@ class Response: self.append_message(message, shown_first_count=shown_first_count) self.elt = 0 # Next element to display + self.sender = None self.channel = channel self.nick = nick - self.set_sender(sender) self.count = count @property def receivers(self): - if type(self.channel) is list: + if self.channel is None: + if self.nick is not None: + return [ self.nick ] + return [ self.sender.split("!")[0] ] + elif isinstance(self.channel, list): return self.channel else: return [ self.channel ] - @property - def content(self): - #FIXME: error when messages in self.messages are list! - try: - if self.title is not None: - return self.title + ", ".join(self.messages) - else: - return ", ".join(self.messages) - except: - return "" - def set_sender(self, sender): if sender is None or sender.find("!") < 0: if sender is not None: diff --git a/server/IRC.py b/server/IRC.py index 0373fa3..02f0ccb 100644 --- a/server/IRC.py +++ b/server/IRC.py @@ -56,8 +56,7 @@ class IRCServer(SocketServer): def _ctcp_clientinfo(msg): """Response to CLIENTINFO CTCP message""" - return _ctcp_response(msg.sender, - " ".join(self.ctcp_capabilities.keys())) + return _ctcp_response(" ".join(self.ctcp_capabilities.keys())) def _ctcp_dcc(msg): """Response to DCC CTCP message""" @@ -66,7 +65,7 @@ class IRCServer(SocketServer): port = int(msg.cmds[4]) conn = DCC(srv, msg.sender) except: - return _ctcp_response(msg.sender, "ERRMSG invalid parameters provided as DCC CTCP request") + return _ctcp_response("ERRMSG invalid parameters provided as DCC CTCP request") self.logger.info("Receive DCC connection request from %s to %s:%d", conn.sender, ip, port) @@ -75,25 +74,25 @@ class IRCServer(SocketServer): conn.send_dcc("Hello %s!" % conn.nick) else: self.logger.error("DCC: unable to connect to %s:%d", ip, port) - return _ctcp_response(msg.sender, "ERRMSG unable to connect to %s:%d" % (ip, port)) + return _ctcp_response("ERRMSG unable to connect to %s:%d" % (ip, port)) self.ctcp_capabilities["ACTION"] = lambda msg: print ("ACTION receive: %s" % msg.text) self.ctcp_capabilities["CLIENTINFO"] = _ctcp_clientinfo #self.ctcp_capabilities["DCC"] = _ctcp_dcc self.ctcp_capabilities["FINGER"] = lambda msg: _ctcp_response( - msg.sender, "VERSION nemubot v%s" % bot.__version__) + "VERSION nemubot v%s" % bot.__version__) self.ctcp_capabilities["NEMUBOT"] = lambda msg: _ctcp_response( - msg.sender, "NEMUBOT %s" % bot.__version__) + "NEMUBOT %s" % bot.__version__) self.ctcp_capabilities["PING"] = lambda msg: _ctcp_response( - msg.sender, "PING %s" % " ".join(msg.cmds[1:])) + "PING %s" % " ".join(msg.cmds[1:])) self.ctcp_capabilities["SOURCE"] = lambda msg: _ctcp_response( - msg.sender, "SOURCE https://github.com/nemunaire/nemubot") + "SOURCE https://github.com/nemunaire/nemubot") self.ctcp_capabilities["TIME"] = lambda msg: _ctcp_response( - msg.sender, "TIME %s" % (datetime.now())) + "TIME %s" % (datetime.now())) self.ctcp_capabilities["USERINFO"] = lambda msg: _ctcp_response( - msg.sender, "USERINFO %s" % self.realname) + "USERINFO %s" % self.realname) self.ctcp_capabilities["VERSION"] = lambda msg: _ctcp_response( - msg.sender, "VERSION nemubot v%s" % bot.__version__) + "VERSION nemubot v%s" % bot.__version__) self.logger.debug("CTCP capabilities setup: %s", ", ".join(self.ctcp_capabilities)) @@ -187,15 +186,11 @@ class IRCServer(SocketServer): return False def send_response(self, res): - if type(res.channel) != list: - res.channel = [ res.channel ] - for channel in res.channel: + for channel in res.receivers: if channel is not None and channel != self.nick: - self.write("%s %s :%s" % ("PRIVMSG", channel, res.get_message())) - else: - channel = res.sender.split("!")[0] self.write("%s %s :%s" % ("NOTICE" if res.is_ctcp else "PRIVMSG", channel, res.get_message())) - + else: + raise Exception("Trying to send a message to an undefined channel: %s" % channel) def _close(self): if self.socket is not None: self.write("QUIT") @@ -212,12 +207,19 @@ class IRCServer(SocketServer): mes = msg.to_message() mes.raw = msg.raw - if mes.cmd == "PRIVMSG" and mes.is_ctcp: + if hasattr(mes, "receivers"): + # Private message: prepare response + for i in range(len(mes.receivers)): + if mes.receivers[i] == self.nick: + mes.receivers[i] = mes.nick + + if (mes.cmd == "PRIVMSG" or mes.cmd == "NOTICE") and mes.is_ctcp: if mes.cmds[0] in self.ctcp_capabilities: res = self.ctcp_capabilities[mes.cmds[0]](mes) else: - res = _ctcp_response(mes.sender, "ERRMSG Unknown or unimplemented CTCP request") + res = _ctcp_response("ERRMSG Unknown or unimplemented CTCP request") if res is not None: + res.set_sender(mes.sender) self.send_response(res) else: @@ -226,8 +228,8 @@ class IRCServer(SocketServer): from response import Response -def _ctcp_response(sndr, msg): - return Response(sndr, msg, ctcp=True) +def _ctcp_response(msg): + return Response(msg, ctcp=True) mgx = re.compile(b'''^(?:@(?P<tags>[^ ]+)\ )? From 4d187f61e3d8a064ade0210a5294c42a5b461afb Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 18 Sep 2014 08:08:46 +0200 Subject: [PATCH 141/674] Fix exception during deletion of the first event in rare case --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 478859c..91ebc40 100644 --- a/bot.py +++ b/bot.py @@ -255,7 +255,7 @@ class Bot(threading.Thread): self.events.remove(self.events[0]) self._update_event_timer() if module_src is not None: - module_src.REGISTERED_EVENTS.remove(evt.id) + module_src.REGISTERED_EVENTS.remove(id) return True for evt in self.events: From dee6ec28fe11699d62bda3895e7d44b26f3d93f8 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 18 Sep 2014 08:22:59 +0200 Subject: [PATCH 142/674] Can deferred a treatment on each line responded (to save time at first fetch) --- modules/mediawiki.py | 9 +++++---- response.py | 7 ++++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/modules/mediawiki.py b/modules/mediawiki.py index 909de64..3da43ca 100644 --- a/modules/mediawiki.py +++ b/modules/mediawiki.py @@ -63,7 +63,7 @@ def parse_wikitext(site, cnt, ssl=False): return cnt def irc_format(cnt): - cnt, _ = re.subn(r"(?P<title>==+)\s*(.*?)\s*(?P=title)\n+", "\x03\x16" + r"\2" + " :\x03\x16 ", cnt) + cnt, _ = re.subn(r"(?P<title>==+)\s*(.*?)\s*(?P=title)\n*", "\x03\x16" + r"\2" + " :\x03\x16 ", cnt) return cnt.replace("'''", "\x03\x02").replace("''", "\x03\x1f") def get_page(site, term, ssl=False): @@ -85,7 +85,8 @@ def cmd_wikipedia(msg): if len(msg.cmds) < 3: raise IRCException("indicate a lang and a term to search") - return Response(irc_format(parse_wikitext(get_page(msg.cmds[1] + ".wikipedia.org", " ".join(msg.cmds[2:])))), -# get_page(msg.cmds[1] + ".wikipedia.org", " ".join(msg.cmds[2:])), -# line_treat=lambda line: irc_format(parse_wikitext(site, line, ssl)), + site = msg.cmds[1] + ".wikipedia.org" + + return Response(get_page(site, " ".join(msg.cmds[2:])), + line_treat=lambda line: irc_format(parse_wikitext(site, line)), channel=msg.receivers) diff --git a/response.py b/response.py index 46563c7..5990b16 100644 --- a/response.py +++ b/response.py @@ -25,9 +25,11 @@ logger = logging.getLogger("nemubot.response") class Response: def __init__(self, message=None, channel=None, nick=None, server=None, nomore="No more message", title=None, more="(suite) ", - count=None, ctcp=False, shown_first_count=-1): + count=None, ctcp=False, shown_first_count=-1, + line_treat=None): self.nomore = nomore self.more = more + self.line_treat = line_treat self.rawtitle = title self.server = server self.messages = list() @@ -127,6 +129,9 @@ class Response: if self.empty: return self.treat_ctcp(self.nomore) + if self.line_treat is not None and self.elt == 0: + self.messages[0] = self.line_treat(self.messages[0]) + msg = "" if self.channel is not None and self.nick is not None: msg += self.nick + ": " From ae3c46e693b29b9bf38e3320c49debf693511d9a Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 19 Sep 2014 01:38:53 +0200 Subject: [PATCH 143/674] Xmlparser: check the attribute type is storable --- tools/wrapper.py | 66 ----------------------------------------------- xmlparser/node.py | 5 +++- 2 files changed, 4 insertions(+), 67 deletions(-) delete mode 100644 tools/wrapper.py diff --git a/tools/wrapper.py b/tools/wrapper.py deleted file mode 100644 index 3f4f5e6..0000000 --- a/tools/wrapper.py +++ /dev/null @@ -1,66 +0,0 @@ -# coding=utf-8 - -# Nemubot is a modulable IRC bot, built around XML configuration files. -# Copyright (C) 2012 Mercier Pierre-Olivier -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -from xmlparser.node import ModuleState - -class Wrapper: - """Simulate a hash table - - """ - - def __init__(self): - self.stateName = "state" - self.attName = "name" - self.cache = dict() - - def items(self): - ret = list() - for k in self.DATAS.index.keys(): - ret.append((k, self[k])) - return ret - - def __contains__(self, i): - return i in self.DATAS.index - - def __getitem__(self, i): - return self.DATAS.index[i] - - def __setitem__(self, i, j): - ms = ModuleState(self.stateName) - ms.setAttribute(self.attName, i) - j.save(ms) - self.DATAS.addChild(ms) - self.DATAS.setIndex(self.attName, self.stateName) - - def __delitem__(self, i): - self.DATAS.delChild(self.DATAS.index[i]) - - def save(self, i): - if i in self.cache: - self.cache[i].save(self.DATAS.index[i]) - del self.cache[i] - - def flush(self): - """Remove all cached datas""" - self.cache = dict() - - def reset(self): - """Erase the list and flush the cache""" - for child in self.DATAS.getNodes(self.stateName): - self.DATAS.delChild(child) - self.flush() diff --git a/xmlparser/node.py b/xmlparser/node.py index 1203cb6..3eb6b2f 100644 --- a/xmlparser/node.py +++ b/xmlparser/node.py @@ -124,7 +124,10 @@ class ModuleState: def setAttribute(self, name, value): """DOM like method""" - self.attributes[name] = value + if isinstance(value, datetime) or isinstance(value, str) or isinstance(value, int) or isinstance(value, float): + self.attributes[name] = value + else: + raise TypeError("attributes must be primary type or datetime") def getContent(self): return self.content From 880b2950d3986a533b45cd96a5d7e36ff08f37c5 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 19 Sep 2014 01:42:37 +0200 Subject: [PATCH 144/674] RATP: clean and update module --- modules/nextstop/__init__.py | 51 ++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/modules/nextstop/__init__.py b/modules/nextstop/__init__.py index cdd6d6c..35227e7 100644 --- a/modules/nextstop/__init__.py +++ b/modules/nextstop/__init__.py @@ -2,46 +2,39 @@ """Informe les usagers des prochains passages des transports en communs de la RATP""" -import http.client -import re -from xml.dom.minidom import parseString - -from .external.src import ratp +from hooks import hook nemubotversion = 3.4 -def load(context): - global DATAS - DATAS.setIndex("name", "station") - +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." -def extractInformation(msg, transport, line, station=None): - if station is not None and station != "": - times = ratp.getNextStopsAtStation(transport, line, station) - if len(times) > 0: - (time, direction, stationname) = times[0] - return Response(message=["\x03\x02"+time+"\x03\x02 direction "+direction for time, direction, stationname in times], title="Prochains passages du %s ligne %s à l'arrêt %s" % - (transport, line, stationname), channel=msg.channel) - else: - raise IRCException("La station `%s' ne semble pas exister sur le %s ligne %s." - % (station, transport, line)) - else: - stations = ratp.getAllStations(transport, line) - if len(stations) == 0: - raise IRCException("aucune station trouvée.") - return Response([s for s in stations], title="Stations", channel=msg.channel) - +@hook("cmd_hook", "ratp") def ask_ratp(msg): """Hook entry from !ratp""" - global DATAS if len(msg.cmds) == 4: - return extractInformation(msg, msg.cmds[1], msg.cmds[2], msg.cmds[3]) + transport = msg.cmds[1] + line = msg.cmds[2] + station = msg.cmds[3] + 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)) + + (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.cmds) == 3: - return extractInformation(msg, msg.cmds[1], msg.cmds[2]) + stations = ratp.getAllStations(msg.cmds[1], msg.cmds[2]) + + if len(stations) == 0: + raise IRCException("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.") - return False From 8b819f097d64df39f5b1281654620799bd928700 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 19 Sep 2014 07:59:11 +0200 Subject: [PATCH 145/674] Mediawiki: display an error when the article doesn't exist --- modules/mediawiki.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/mediawiki.py b/modules/mediawiki.py index 3da43ca..8f6494b 100644 --- a/modules/mediawiki.py +++ b/modules/mediawiki.py @@ -23,7 +23,10 @@ def get_raw_page(site, term, ssl=False): data = json.loads(raw.read().decode()) for k in data["query"]["pages"]: - return data["query"]["pages"][k]["revisions"][0]["*"] + try: + return data["query"]["pages"][k]["revisions"][0]["*"] + except: + raise IRCException("article not found") def get_unwikitextified(site, wikitext, ssl=False): # Built IRL From b184b27d4ffbe6d4cb05b7e4095b5c2094975e5b Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 19 Sep 2014 19:28:48 +0200 Subject: [PATCH 146/674] Randomize the first fetch of watched pages; closing #33 --- modules/watchWebsite/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/watchWebsite/__init__.py b/modules/watchWebsite/__init__.py index 2a5b373..0ade8b3 100644 --- a/modules/watchWebsite/__init__.py +++ b/modules/watchWebsite/__init__.py @@ -6,6 +6,7 @@ from datetime import datetime from datetime import timedelta import http.client import hashlib +from random import randint import re import socket import sys @@ -27,7 +28,7 @@ def load(context): DATAS.setIndex("url", "watch") for site in DATAS.getNodes("watch"): if site.hasNode("alert"): - start_watching(site) + start_watching(site, randint(-30, 30)) else: print("No alert defined for this site: " + site["url"]) #DATAS.delChild(site) @@ -41,11 +42,11 @@ def getPageContent(url): except: return None -def start_watching(site): +def start_watching(site, offset=0): o = urlparse(site["url"], "http") print_debug("Add event for site: %s" % o.netloc) evt = ModuleEvent(func=getPageContent, cmp_data=site["lastcontent"], - func_data=site["url"], + func_data=site["url"], offset=offset, intervalle=site.getInt("time"), call=alert_change, call_data=site) site["_evt_id"] = add_event(evt) From 7a5c2d9786b2aaeeb1fd9bccfaec1fc12debb8c4 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 20 Sep 2014 00:14:19 +0200 Subject: [PATCH 147/674] Separate curl and w3m functions to use it from others modules --- modules/networking.py | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/modules/networking.py b/modules/networking.py index 26618d5..a5674aa 100644 --- a/modules/networking.py +++ b/modules/networking.py @@ -27,31 +27,27 @@ def load(context): def help_full(): return "!traceurl /url/: Follow redirections from /url/." +def w3m(url): + args = ["w3m", "-T", "text/html", "-dump"] + args.append(url) + with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc: + return proc.stdout.read().decode() + @hook("cmd_hook", "w3m") def cmd_w3m(msg): if len(msg.cmds) > 1: - args = ["w3m", "-T", "text/html", "-dump"] - args.append(msg.cmds[1]) res = Response(channel=msg.channel) - with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc: - for line in proc.stdout.read().split(b"\n"): - res.append_message(line.decode()) + for line in w3m(" ".join(msg.cmds[1:])).split("\n"): + res.append_message(line) return res else: raise IRCException("Indicate the URL to visit.") -@hook("cmd_hook", "curl") -def cmd_curl(msg): - if len(msg.cmds) < 2: - raise IRCException("Indicate the URL to visit.") - +def curl(url): try: - req = web.getURLContent(" ".join(msg.cmds[1:])) + req = web.getURLContent(url) if req is not None: - res = Response(channel=msg.channel) - for m in req.split("\n"): - res.append_message(m) - return res + return req else: raise IRCException("An error occurs when trying to access the page") except socket.timeout: @@ -59,6 +55,16 @@ def cmd_curl(msg): except socket.error as e: raise IRCException(e.strerror) +@hook("cmd_hook", "curl") +def cmd_curl(msg): + if len(msg.cmds) < 2: + raise IRCException("Indicate the URL to visit.") + + res = Response(channel=msg.channel) + for m in curl(" ".join(msg.cmds[1:])).split("\n"): + res.append_message(m) + return res + @hook("cmd_hook", "curly") def cmd_curly(msg): if len(msg.cmds) < 2: From a7830f709d4df6ba37727f42618efec025270edd Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 20 Sep 2014 00:15:06 +0200 Subject: [PATCH 148/674] WatchWebsite module: use w3m function from networking module --- modules/watchWebsite/__init__.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/modules/watchWebsite/__init__.py b/modules/watchWebsite/__init__.py index 0ade8b3..1da0ff8 100644 --- a/modules/watchWebsite/__init__.py +++ b/modules/watchWebsite/__init__.py @@ -12,10 +12,10 @@ import socket import sys import urllib.parse from urllib.parse import urlparse -from urllib.request import urlopen from hooks import hook +from networking import w3m from .atom import Atom nemubotversion = 3.4 @@ -33,19 +33,10 @@ def load(context): print("No alert defined for this site: " + site["url"]) #DATAS.delChild(site) -def getPageContent(url): - """Returns the content of the given url""" - print_debug("Get page %s" % url) - try: - raw = urlopen(url, timeout=10) - return raw.read().decode() - except: - return None - def start_watching(site, offset=0): o = urlparse(site["url"], "http") print_debug("Add event for site: %s" % o.netloc) - evt = ModuleEvent(func=getPageContent, cmp_data=site["lastcontent"], + evt = ModuleEvent(func=lambda url: w3m(url), cmp_data=site["lastcontent"], func_data=site["url"], offset=offset, intervalle=site.getInt("time"), call=alert_change, call_data=site) From 314d4107896ec883e608b6f08a180c19c334a324 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 21 Sep 2014 19:46:18 +0200 Subject: [PATCH 149/674] Logger identifier for server now depends on server identifier and is not global --- server/IRC.py | 2 +- server/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/IRC.py b/server/IRC.py index 02f0ccb..c913fdb 100644 --- a/server/IRC.py +++ b/server/IRC.py @@ -29,6 +29,7 @@ from server.socket import SocketServer class IRCServer(SocketServer): def __init__(self, node, nick, owner, realname): + self.id = nick + "@" + node["host"] + ":" + node["port"] SocketServer.__init__(self, node["host"], node["port"], @@ -38,7 +39,6 @@ class IRCServer(SocketServer): self.nick = nick self.owner = owner self.realname = realname - self.id = nick + "@" + node["host"] + ":" + node["port"] #Keep a list of connected channels self.channels = dict() diff --git a/server/__init__.py b/server/__init__.py index d3310d9..c10c90f 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -38,7 +38,7 @@ class AbstractServer(io.IOBase): send_callback -- Callback when developper want to send a message """ - self.logger = logging.getLogger("nemubot.server.TODO") + self.logger = logging.getLogger("nemubot.server." + self.id) self._sending_queue = queue.Queue() if send_callback is not None: self._send_callback = send_callback From 0ab51d79aef215688374655540e6dbc73921be1f Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 22 Sep 2014 17:48:41 +0200 Subject: [PATCH 150/674] Pong only on "ask" --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 91ebc40..612d0ba 100644 --- a/bot.py +++ b/bot.py @@ -78,7 +78,7 @@ class Bot(threading.Thread): def in_ping(msg): if re.match("^ *(m[' ]?entends?[ -]+tu|h?ear me|do you copy|ping)", msg.text, re.I) is not None: return response.Response(message="pong", channel=msg.receivers, nick=msg.nick) - self.hooks.add_hook(MessageHook(in_ping), "in", "PRIVMSG") + self.hooks.add_hook(MessageHook(in_ping), "in", "PRIVMSG", "ask") def _help_msg(msg): """Parse and response to help messages""" From 04eccbe250a01360d0f6077b63a3a0d7fee0e5ca Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 22 Sep 2014 17:48:59 +0200 Subject: [PATCH 151/674] Remove some legacy stuff --- bot.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/bot.py b/bot.py index 612d0ba..b54dc29 100644 --- a/bot.py +++ b/bot.py @@ -118,7 +118,6 @@ class Bot(threading.Thread): # Other known bots, making a bots network self.network = dict() - self.hooks_cache = dict() # Messages to be treated self.cnsr_queue = Queue() @@ -318,7 +317,6 @@ class Bot(threading.Thread): def add_server(self, node, nick, owner, realname): """Add a new server to the context""" srv = IRCServer(node, nick, owner, realname) - #srv.register_hooks() if srv.id not in self.servers: self.servers[srv.id] = srv srv.open() @@ -492,8 +490,6 @@ def reload(): imp.reload(tools.date) import tools.web imp.reload(tools.web) - import tools.wrapper - imp.reload(tools.wrapper) import xmlparser imp.reload(xmlparser) From acded35e1a74caff0dc0ecafa2c3a5c7bd7fd389 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 23 Sep 2014 22:27:53 +0200 Subject: [PATCH 152/674] Pick events class from v4 --- consumer.py | 10 ++- event.py | 103 +++++++++++++++++++------------ modules/bonneannee.py | 2 +- modules/events.py | 10 +-- modules/watchWebsite/__init__.py | 2 +- modules/worldcup.py | 4 +- 6 files changed, 82 insertions(+), 49 deletions(-) diff --git a/consumer.py b/consumer.py index 26904c6..f5274ce 100644 --- a/consumer.py +++ b/consumer.py @@ -205,11 +205,17 @@ class EventConsumer: def run(self, context): try: - self.evt.launch_check() + self.evt.check() except: logger.exception("Error during event end") + + # Reappend the event in the queue if it has next iteration if self.evt.next is not None: - context.add_event(self.evt, self.evt.id) + context.add_event(self.evt, eid=self.evt.id) + + # Or remove reference of this event + elif hasattr(self.evt, "module_src") and self.evt.module_src is not None: + self.evt.module_src.REGISTERED_EVENTS.remove(self.evt.id) diff --git a/event.py b/event.py index 89b10f3..fc90785 100644 --- a/event.py +++ b/event.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -# Nemubot is a modulable IRC bot, built around XML configuration files. -# Copyright (C) 2012 Mercier Pierre-Olivier +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2014 nemunaire # # 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 @@ -16,18 +16,36 @@ # 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 -from datetime import timedelta +from datetime import datetime, timedelta class ModuleEvent: - def __init__(self, func=None, func_data=None, check=None, cmp_data=None, - intervalle=60, offset=0, call=None, call_data=None, times=1): + + """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): + + """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 + 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) + """ + # What have we to check? self.func = func self.func_data = func_data # How detect a change? - self.check = check + self.cmp = cmp + self.cmp_data = None if cmp_data is not None: self.cmp_data = cmp_data elif self.func is not None: @@ -37,20 +55,19 @@ class ModuleEvent: self.cmp_data = self.func(**self.func_data) else: self.cmp_data = self.func(self.func_data) - else: - self.cmp_data = None - self.offset = timedelta(seconds=offset) # Time to wait before the first check - self.intervalle = timedelta(seconds=intervalle) - self.end = None - - # What should we call when + # 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 + self.offset = timedelta(seconds=offset) # Time to wait before the first check + self.interval = timedelta(seconds=interval) + self._end = None # Cache + # How many times do this event? self.times = times @@ -59,60 +76,68 @@ class ModuleEvent: def current(self): """Return the date of the near check""" if self.times != 0: - if self.end is None: - self.end = datetime.now() + self.offset + self.intervalle - return self.end + if self._end is None: + self._end = datetime.now() + self.offset + self.interval + return self._end return None + @property def next(self): """Return the date of the next check""" if self.times != 0: - if self.end is None: + if self._end is None: return self.current - elif self.end < datetime.now(): - self.end += self.intervalle - return self.end + elif self._end < datetime.now(): + 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() - return 99999 + return 99999 #TODO: 99999 is not a valid time to return - def launch_check(self): + + def check(self): + """Run a check and realized the event if this is time""" + + # Get initial data if self.func is None: - d = self.func_data + d_init = self.func_data elif self.func_data is None: - d = self.func() + d_init = self.func() elif isinstance(self.func_data, dict): - d = self.func(**self.func_data) + d_init = self.func(**self.func_data) else: - d = self.func(self.func_data) - #print ("do test with", d, self.cmp_data) + d_init = self.func(self.func_data) - if self.check is None: + # then compare with current data + if self.cmp is None: if self.cmp_data is None: - r = True + rlz = True else: - r = d != self.cmp_data + rlz = (d_init != self.cmp_data) elif self.cmp_data is None: - r = self.check(d) + rlz = self.cmp(d_init) elif isinstance(self.cmp_data, dict): - r = self.check(d, **self.cmp_data) + rlz = self.cmp(d_init, **self.cmp_data) else: - r = self.check(d, self.cmp_data) + rlz = self.cmp(d_init, self.cmp_data) - if r: + if rlz: self.times -= 1 + + # Call attended function if self.call_data is None: - if d is None: + if d_init is None: self.call() else: - self.call(d) + self.call(d_init) elif isinstance(self.call_data, dict): - self.call(d, **self.call_data) + self.call(d_init, **self.call_data) else: - self.call(d, self.call_data) + self.call(d_init, self.call_data) diff --git a/modules/bonneannee.py b/modules/bonneannee.py index 0d8f180..c5bff46 100644 --- a/modules/bonneannee.py +++ b/modules/bonneannee.py @@ -14,7 +14,7 @@ yrn = datetime.today().year + 1 def load(context): d = datetime(yrn, 1, 1, 0, 0, 0) - datetime.now() - add_event(ModuleEvent(intervalle=0, offset=d.total_seconds(), call=bonneannee)) + add_event(ModuleEvent(interval=0, offset=d.total_seconds(), call=bonneannee)) def bonneannee(): txt = "Bonne année %d !" % datetime.today().year diff --git a/modules/events.py b/modules/events.py index 12cbb36..78a1236 100644 --- a/modules/events.py +++ b/modules/events.py @@ -29,7 +29,7 @@ def load(context): for evt in DATAS.index.keys(): if DATAS.index[evt].hasAttribute("end"): event = ModuleEvent(call=fini, call_data=dict(strend=DATAS.index[evt])) - event.end = DATAS.index[evt].getDate("end") + event._end = DATAS.index[evt].getDate("end") idt = context.add_event(event) if idt is not None: DATAS.index[evt]["id"] = idt @@ -101,7 +101,7 @@ def start_countdown(msg): strnd["end"] = datetime(now.year, now.month, now.day, hou, minu, sec) else: strnd["end"] = datetime(now.year, now.month, now.day + 1, hou, minu, sec) - evt.end = strnd.getDate("end") + evt._end = strnd.getDate("end") strnd["id"] = add_event(evt) except: DATAS.delChild(strnd) @@ -122,8 +122,10 @@ def start_countdown(msg): strnd["end"] += timedelta(days=int(t)*365) else: strnd["end"] += timedelta(seconds=int(t)) - evt.end = strnd.getDate("end") - strnd["id"] = add_event(evt) + evt._end = strnd.getDate("end") + eid = add_event(evt) + if eid is not None: + strnd["id"] = eid save() if "end" in strnd: diff --git a/modules/watchWebsite/__init__.py b/modules/watchWebsite/__init__.py index 1da0ff8..efbcf52 100644 --- a/modules/watchWebsite/__init__.py +++ b/modules/watchWebsite/__init__.py @@ -38,7 +38,7 @@ def start_watching(site, offset=0): print_debug("Add event for site: %s" % o.netloc) evt = ModuleEvent(func=lambda url: w3m(url), cmp_data=site["lastcontent"], func_data=site["url"], offset=offset, - intervalle=site.getInt("time"), + interval=site.getInt("time"), call=alert_change, call_data=site) site["_evt_id"] = add_event(evt) diff --git a/modules/worldcup.py b/modules/worldcup.py index fc65daa..b4f42a8 100644 --- a/modules/worldcup.py +++ b/modules/worldcup.py @@ -16,7 +16,7 @@ API_URL="http://worldcup.sfg.io/%s" def load(context): from event import ModuleEvent - 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, intervalle=30)) + 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)) def help_full (): @@ -66,7 +66,7 @@ def cmd_watch(msg): def current_match_new_action(match_str, osef): global DATAS - add_event(ModuleEvent(func=lambda url: urlopen(url).read().decode(), func_data=API_URL % "matches/current?by_date=DESC", call=current_match_new_action, intervalle=30)) + 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) From cdaad47b136da1e04e0b84be3be621bf8117e9ed Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 24 Sep 2014 10:53:16 +0200 Subject: [PATCH 153/674] Ensure multiline message after line_treat are collapsed to one line --- modules/mediawiki.py | 1 + response.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/mediawiki.py b/modules/mediawiki.py index 8f6494b..47e282c 100644 --- a/modules/mediawiki.py +++ b/modules/mediawiki.py @@ -80,6 +80,7 @@ def cmd_mediawiki(msg): raise IRCException("indicate a domain and a term to search") return Response(get_page(msg.cmds[1], " ".join(msg.cmds[2:])), + line_treat=lambda line: irc_format(parse_wikitext(msg.cmds[1], line)), channel=msg.receivers) diff --git a/response.py b/response.py index 5990b16..fb83bf9 100644 --- a/response.py +++ b/response.py @@ -130,7 +130,7 @@ class Response: return self.treat_ctcp(self.nomore) if self.line_treat is not None and self.elt == 0: - self.messages[0] = self.line_treat(self.messages[0]) + self.messages[0] = self.line_treat(self.messages[0]).replace("\n", " ").strip() msg = "" if self.channel is not None and self.nick is not None: From 5be1e9741111374a6c18ccaf63c43508ad61c44f Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 24 Sep 2014 15:56:46 +0200 Subject: [PATCH 154/674] Calculate message size based on raw size, not on UTF-8 characters number --- response.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/response.py b/response.py index fb83bf9..6365fc1 100644 --- a/response.py +++ b/response.py @@ -159,7 +159,7 @@ class Response: return self.treat_ctcp(msg[:len(msg)-2]) else: - if len(elts) <= 432: + if len(elts.encode()) <= 432: self.pop() if self.count is not None: return self.treat_ctcp(msg + elts + (self.count % len(self.messages))) @@ -169,12 +169,12 @@ class Response: else: words = elts.split(' ') - if len(words[0]) > 432 - len(msg): - self.elt += 432 - len(msg) + if len(words[0].encode()) > 432 - len(msg.encode()): + self.elt += 432 - len(msg.encode()) return self.treat_ctcp(msg + elts[:self.elt] + "[…]") for w in words: - if len(msg) + len(w) > 431: + if len(msg.encode()) + len(w.encode()) > 431: msg += "[…]" self.alone = False return self.treat_ctcp(msg) From 8f620b97566075cf71ca1f1e1e12d62ab2b6b9f8 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 24 Sep 2014 15:57:21 +0200 Subject: [PATCH 155/674] XMLparser: precise unhandled type --- xmlparser/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xmlparser/node.py b/xmlparser/node.py index 3eb6b2f..4a674e4 100644 --- a/xmlparser/node.py +++ b/xmlparser/node.py @@ -127,7 +127,7 @@ class ModuleState: if isinstance(value, datetime) or isinstance(value, str) or isinstance(value, int) or isinstance(value, float): self.attributes[name] = value else: - raise TypeError("attributes must be primary type or datetime") + raise TypeError("attributes must be primary type or datetime (here %s)" % type(value)) def getContent(self): return self.content From 41da1c0780aa9d0262df910d20a794dfde808602 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 26 Sep 2014 18:00:22 +0200 Subject: [PATCH 156/674] Response class is now part of 'more' module. This commit prepare the new message flow based on protocol independent messages. This commit changes the module API: you need to import the Response class manually at the begining of our module. --- bot.py | 9 +- consumer.py | 26 +++-- exception.py | 4 +- hooks/messagehook.py | 7 +- importer.py | 4 +- message.py | 2 - modules/alias.py | 2 + modules/birthday.py | 2 + modules/bonneannee.py | 2 + modules/books.py | 2 + modules/conjugaison.py | 2 + modules/ddg/__init__.py | 2 + modules/events.py | 6 +- modules/github.py | 2 + modules/imdb.py | 2 + modules/man.py | 2 + modules/mapquest.py | 2 + modules/mediawiki.py | 2 + modules/more.py | 175 ++++++++++++++++++++++++++++- modules/networking.py | 2 + modules/reddit.py | 1 + modules/rnd.py | 2 + modules/sap.py | 2 + modules/sleepytime.py | 2 + modules/sms.py | 2 + modules/spell/__init__.py | 2 + modules/syno.py | 2 + modules/translate.py | 2 + modules/velib.py | 2 + modules/watchWebsite/__init__.py | 5 +- modules/weather.py | 2 + modules/worldcup.py | 2 + modules/ycc.py | 2 + networkbot.py | 1 - response.py | 185 ------------------------------- server/IRC.py | 15 +-- server/__init__.py | 4 - 37 files changed, 257 insertions(+), 233 deletions(-) delete mode 100644 response.py diff --git a/bot.py b/bot.py index b54dc29..bf95380 100644 --- a/bot.py +++ b/bot.py @@ -36,7 +36,6 @@ from hooks.manager import HooksManager from networkbot import NetworkBot from server.IRC import IRCServer from server.DCC import DCC -import response logger = logging.getLogger("nemubot.bot") @@ -77,13 +76,14 @@ class Bot(threading.Thread): self.hooks = HooksManager() def in_ping(msg): if re.match("^ *(m[' ]?entends?[ -]+tu|h?ear me|do you copy|ping)", msg.text, re.I) is not None: - return response.Response(message="pong", channel=msg.receivers, nick=msg.nick) + return "PRIVMSG %s :%s: pong" % (",".join(msg.receivers), msg.nick) self.hooks.add_hook(MessageHook(in_ping), "in", "PRIVMSG", "ask") def _help_msg(msg): """Parse and response to help messages""" cmd = msg.cmds - res = response.Response() + from more import Response + res = Response() if len(cmd) > 1: if cmd[1] in self.modules: if len(cmd) > 2: @@ -470,9 +470,6 @@ def reload(): import prompt.builtins imp.reload(prompt.builtins) - import response - imp.reload(response) - import server rl,wl,xl = server._rlist,server._wlist,server._xlist imp.reload(server) diff --git a/consumer.py b/consumer.py index f5274ce..17283aa 100644 --- a/consumer.py +++ b/consumer.py @@ -26,7 +26,6 @@ import sys import bot from server.DCC import DCC from message import Message -from response import Response import server logger = logging.getLogger("nemubot.consumer") @@ -89,7 +88,7 @@ class MessageConsumer: if h.match(message=msg, server=self.srv): res = h.run(msg) if isinstance(res, list): - for i in xrange(len(res)): + for i in range(len(res)): if res[i] == msg: res.pop(i) break @@ -119,11 +118,11 @@ class MessageConsumer: res = h.run(msg) if isinstance(res, list): for r in res: - if isinstance(r, Response): + if hasattr(r, "set_sender"): r.set_sender(msg.sender) self.responses += res elif res is not None: - if isinstance(res, Response): + if hasattr(res, "set_sender"): res.set_sender(msg.sender) self.responses.append(res) @@ -140,15 +139,20 @@ class MessageConsumer: self.responses = list() while len(new_msg) > 0: - msg = self.first_treat(new_msg.pop(0)) + ff = new_msg.pop(0) + if isinstance(ff, str): + self.responses.append(ff) + continue + msg = self.first_treat(ff) for h in hm.get_hooks("post"): if h.match(message=msg, server=self.srv): res = h.run(msg) if isinstance(res, list): - for i in xrange(len(res)): - if res[i] == msg: - res.pop(i) + for i in range(len(res)): + if isinstance(res[i], str): + self.responses.append(res.pop(i)) break + msg = None new_msg += res elif res is not None and res != msg: new_msg.append(res) @@ -182,7 +186,9 @@ class MessageConsumer: for res in self.responses: to_server = None - if res.server is None: + if isinstance(res, str): + to_server = self.srv + 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: @@ -194,7 +200,7 @@ class MessageConsumer: continue # Sent the message only if treat_post authorize it - to_server.send_response(res) + to_server.write(res) class EventConsumer: """Store a event before treating""" diff --git a/exception.py b/exception.py index d73ec99..cfc721b 100644 --- a/exception.py +++ b/exception.py @@ -16,8 +16,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/>. -from response import Response - class IRCException(Exception): def __init__(self, message, personnal=True): @@ -26,4 +24,6 @@ class IRCException(Exception): self.personnal = personnal def fill_response(self, msg): + # TODO: no more Response usable here + from more import Response return Response(self.message, channel=msg.receivers, nick=(msg.nick if self.personnal else None)) diff --git a/hooks/messagehook.py b/hooks/messagehook.py index 2ae3572..0055d04 100644 --- a/hooks/messagehook.py +++ b/hooks/messagehook.py @@ -20,7 +20,8 @@ import re from exception import IRCException import hooks -from response import Response + +from message import Message class MessageHook(hooks.AbstractHook): @@ -39,8 +40,8 @@ class MessageHook(hooks.AbstractHook): def match(self, message, server=None): - if isinstance(message, Response): - return self.is_matching(None, message.channel, server) + if not isinstance(message, Message): + return True elif message.qual == "cmd": return self.is_matching(message.cmds[0], message.channel, server) diff --git a/importer.py b/importer.py index a3554af..ddee457 100644 --- a/importer.py +++ b/importer.py @@ -28,7 +28,6 @@ from bot import __version__ import event import exception import hooks -import response import xmlparser logger = logging.getLogger("nemubot.importer") @@ -159,7 +158,7 @@ class ModuleLoader(SourceLoader): def send_response(server, res): if server in self.context.servers: - return self.context.servers[server].send_response(res) + return self.context.servers[server].write("PRIVMSG %s :%s" % (",".join(res.receivers), res.get_message())) else: module.logger.error("Try to send a message to the unknown server: %s", server) return False @@ -205,7 +204,6 @@ class ModuleLoader(SourceLoader): module.ModuleEvent = event.ModuleEvent module.ModuleState = xmlparser.module_state.ModuleState - module.Response = response.Response module.IRCException = exception.IRCException # Load dependancies diff --git a/message.py b/message.py index 8e32d6d..3f6673d 100644 --- a/message.py +++ b/message.py @@ -19,8 +19,6 @@ from datetime import datetime import shlex -from response import Response - class Message: def __init__ (self, orig, private=False): self.cmd = orig.cmd diff --git a/modules/alias.py b/modules/alias.py index d54c25c..e790a86 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -10,6 +10,8 @@ from hooks import hook nemubotversion = 3.4 +from more import Response + def load(context): """Load this module""" global DATAS diff --git a/modules/birthday.py b/modules/birthday.py index 5c90019..88fef2d 100644 --- a/modules/birthday.py +++ b/modules/birthday.py @@ -14,6 +14,8 @@ from xmlparser.node import ModuleState nemubotversion = 3.4 +from more import Response + def load(context): global DATAS DATAS.setIndex("name", "birthday") diff --git a/modules/bonneannee.py b/modules/bonneannee.py index c5bff46..f50c9b2 100644 --- a/modules/bonneannee.py +++ b/modules/bonneannee.py @@ -9,6 +9,8 @@ from tools.countdown import countdown_format nemubotversion = 3.4 +from more import Response + yr = datetime.today().year yrn = datetime.today().year + 1 diff --git a/modules/books.py b/modules/books.py index 4544098..456ea0e 100644 --- a/modules/books.py +++ b/modules/books.py @@ -9,6 +9,8 @@ from tools import web nemubotversion = 3.4 +from more import Response + def load(context): if not CONF or not CONF.hasNode("goodreadsapi") or not CONF.getNode("goodreadsapi").hasAttribute("key"): print ("You need a Goodreads API key in order to use this " diff --git a/modules/conjugaison.py b/modules/conjugaison.py index 85e481d..7d49c34 100644 --- a/modules/conjugaison.py +++ b/modules/conjugaison.py @@ -14,6 +14,8 @@ from collections import defaultdict nemubotversion = 3.4 +from more import Response + s = [('present', '0'), ('présent', '0'), ('pr', '0'), ('passé simple', '12'), ('passe simple', '12'), ('ps', '12'), ('passé antérieur', '112'), ('passe anterieur', '112'), ('pa', '112'), diff --git a/modules/ddg/__init__.py b/modules/ddg/__init__.py index 7117c04..cc61eb5 100644 --- a/modules/ddg/__init__.py +++ b/modules/ddg/__init__.py @@ -8,6 +8,8 @@ from hooks import hook nemubotversion = 3.4 +from more import Response + from . import DDGSearch from . import UrbanDictionnary from . import WFASearch diff --git a/modules/events.py b/modules/events.py index 78a1236..a17227c 100644 --- a/modules/events.py +++ b/modules/events.py @@ -11,13 +11,15 @@ import time import threading import traceback -nemubotversion = 3.4 - from event import ModuleEvent from hooks import hook from tools.date import extractDate from tools.countdown import countdown_format, countdown +nemubotversion = 3.4 + +from more import Response + def help_full (): return "This module store a lot of events: ny, we, " + (", ".join(DATAS.index.keys())) + "\n!eventslist: gets list of timer\n!start /something/: launch a timer" diff --git a/modules/github.py b/modules/github.py index a2b9dd6..dd71147 100644 --- a/modules/github.py +++ b/modules/github.py @@ -12,6 +12,8 @@ from hooks import hook 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/." diff --git a/modules/imdb.py b/modules/imdb.py index 106d553..1bb36bc 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -10,6 +10,8 @@ from hooks import hook 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>" diff --git a/modules/man.py b/modules/man.py index 9993ac7..f2f1f2d 100644 --- a/modules/man.py +++ b/modules/man.py @@ -8,6 +8,8 @@ from hooks import hook nemubotversion = 3.4 +from more import Response + def help_tiny(): """Line inserted in the response to the command !help""" return "Read manual pages on IRC" diff --git a/modules/mapquest.py b/modules/mapquest.py index 17d9b8b..824f578 100644 --- a/modules/mapquest.py +++ b/modules/mapquest.py @@ -7,6 +7,8 @@ from urllib.request import urlopen nemubotversion = 3.4 +from more import Response + def load(context): if not CONF or not CONF.hasNode("mapquestapi") or not CONF.getNode("mapquestapi").hasAttribute("key"): print ("You need a MapQuest API key in order to use this " diff --git a/modules/mediawiki.py b/modules/mediawiki.py index 47e282c..0642701 100644 --- a/modules/mediawiki.py +++ b/modules/mediawiki.py @@ -12,6 +12,8 @@ from tools.web import striphtml nemubotversion = 3.4 +from more import Response + def get_raw_page(site, term, ssl=False): # Built IRL url = "http%s://%s/w/api.php?format=json&redirects&action=query&prop=revisions&rvprop=content&titles=%s" % ( diff --git a/modules/more.py b/modules/more.py index 3956954..f2b0148 100644 --- a/modules/more.py +++ b/modules/more.py @@ -18,22 +18,195 @@ """Progressive display of very long messages""" +import logging + from hooks import hook nemubotversion = 3.4 +logger = logging.getLogger("nemubot.response") + +class Response: + def __init__(self, message=None, channel=None, nick=None, server=None, + nomore="No more message", title=None, more="(suite) ", + count=None, ctcp=False, shown_first_count=-1, + line_treat=None): + self.nomore = nomore + self.more = more + self.line_treat = line_treat + self.rawtitle = title + self.server = server + self.messages = list() + self.alone = True + self.is_ctcp = ctcp + if message is not None: + self.append_message(message, shown_first_count=shown_first_count) + self.elt = 0 # Next element to display + + self.sender = None + self.channel = channel + self.nick = nick + self.count = count + + @property + def receivers(self): + if self.channel is None: + if self.nick is not None: + return [ self.nick ] + return [ self.sender.split("!")[0] ] + elif isinstance(self.channel, list): + return self.channel + else: + return [ self.channel ] + + def set_sender(self, sender): + if sender is None or sender.find("!") < 0: + if sender is not None: + logger.warn("Bad sender provided in Response, it will be ignored.", stack_info=True) + self.sender = None + else: + self.sender = sender + + def append_message(self, message, title=None, shown_first_count=-1): + if type(message) is str: + message = message.split('\n') + if len(message) > 1: + for m in message: + self.append_message(m) + return + else: + message = message[0] + if message is not None and len(message) > 0: + if shown_first_count >= 0: + self.messages.append(message[:shown_first_count]) + message = message[shown_first_count:] + self.messages.append(message) + self.alone = self.alone and len(self.messages) <= 1 + if isinstance(self.rawtitle, list): + self.rawtitle.append(title) + elif title is not None: + rawtitle = self.rawtitle + self.rawtitle = list() + for osef in self.messages: + self.rawtitle.append(rawtitle) + self.rawtitle.pop() + self.rawtitle.append(title) + + 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.alone = True + else: + self.messages[len(self.messages)-1] += message + self.alone = self.alone and len(self.messages) <= 1 + + @property + def empty(self): + return len(self.messages) <= 0 + + @property + def title(self): + if isinstance(self.rawtitle, list): + return self.rawtitle[0] + else: + return self.rawtitle + + def pop(self): + self.messages.pop(0) + self.elt = 0 + if isinstance(self.rawtitle, list): + self.rawtitle.pop(0) + if len(self.rawtitle) <= 0: + self.rawtitle = None + + def treat_ctcp(self, content): + if self.is_ctcp: + return "\x01" + content + "\x01" + else: + return content + + def get_message(self): + if self.alone and len(self.messages) > 1: + self.alone = False + + if self.empty: + return self.treat_ctcp(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() + + msg = "" + if self.channel is not None and self.nick is not None: + msg += self.nick + ": " + + if self.title is not None: + if self.elt > 0: + msg += self.title + " " + self.more + ": " + else: + msg += self.title + ": " + + if self.elt > 0: + msg += "[…] " + + elts = self.messages[0][self.elt:] + if isinstance(elts, list): + for e in elts: + if len(msg) + len(e) > 430: + msg += "[…]" + self.alone = False + return self.treat_ctcp(msg) + else: + msg += e + ", " + self.elt += 1 + self.pop() + return self.treat_ctcp(msg[:len(msg)-2]) + + else: + if len(elts.encode()) <= 432: + self.pop() + if self.count is not None: + return self.treat_ctcp(msg + elts + (self.count % len(self.messages))) + else: + return self.treat_ctcp(msg + elts) + + else: + words = elts.split(' ') + + if len(words[0].encode()) > 432 - len(msg.encode()): + self.elt += 432 - len(msg.encode()) + return self.treat_ctcp(msg + elts[:self.elt] + "[…]") + + for w in words: + if len(msg.encode()) + len(w.encode()) > 431: + msg += "[…]" + self.alone = False + return self.treat_ctcp(msg) + else: + msg += w + " " + self.elt += len(w) + 1 + self.pop() + return self.treat_ctcp(msg) + + SERVERS = dict() @hook("all_post") def parseresponse(res): # TODO: handle inter-bot communication NOMORE # TODO: check that the response is not the one already saved + rstr = res.get_message() + if not res.alone: if res.server not in SERVERS: SERVERS[res.server] = dict() for receiver in res.receivers: SERVERS[res.server][receiver] = res - return res + + ret = list() + for channel in res.receivers: + ret.append("%s %s :%s" % ("NOTICE" if res.is_ctcp else "PRIVMSG", channel, rstr)) + return ret @hook("cmd_hook", "more") diff --git a/modules/networking.py b/modules/networking.py index a5674aa..199fd6c 100644 --- a/modules/networking.py +++ b/modules/networking.py @@ -14,6 +14,8 @@ from tools import web nemubotversion = 3.4 +from more import Response + def load(context): if not CONF or not CONF.hasNode("whoisxmlapi") or not CONF.getNode("whoisxmlapi").hasAttribute("username") or not CONF.getNode("whoisxmlapi").hasAttribute("password"): print ("You need a WhoisXML API account in order to use the " diff --git a/modules/reddit.py b/modules/reddit.py index f33d25a..4f57197 100644 --- a/modules/reddit.py +++ b/modules/reddit.py @@ -9,6 +9,7 @@ import urllib nemubotversion = 3.4 from hooks import hook +from more import Response def help_full(): return "!subreddit /subreddit/: Display information on the subreddit." diff --git a/modules/rnd.py b/modules/rnd.py index 62ead64..873416f 100644 --- a/modules/rnd.py +++ b/modules/rnd.py @@ -8,6 +8,8 @@ from hooks import hook nemubotversion = 3.4 +from more import Response + @hook("cmd_hook", "choice") def cmd_choice(msg): if len(msg.cmds) > 1: diff --git a/modules/sap.py b/modules/sap.py index 3065724..3bc2cf3 100644 --- a/modules/sap.py +++ b/modules/sap.py @@ -12,6 +12,8 @@ from tools.web import striphtml nemubotversion = 3.4 +from more import Response + def help_full (): return "!tcode <transaction code|keywords>" diff --git a/modules/sleepytime.py b/modules/sleepytime.py index 35ceece..9cb1169 100644 --- a/modules/sleepytime.py +++ b/modules/sleepytime.py @@ -11,6 +11,8 @@ from hooks import hook nemubotversion = 3.4 +from more import Response + def help_full(): return "If you would like to sleep soon, use !sleepytime to know the best time to wake up; use !sleepytime hh:mm if you want to wake up at hh:mm" diff --git a/modules/sms.py b/modules/sms.py index b41aa55..ec1ac10 100644 --- a/modules/sms.py +++ b/modules/sms.py @@ -13,6 +13,8 @@ from hooks import hook nemubotversion = 3.4 +from more import Response + def load(context): global DATAS DATAS.setIndex("name", "phone") diff --git a/modules/spell/__init__.py b/modules/spell/__init__.py index b56ee2b..f63cab5 100644 --- a/modules/spell/__init__.py +++ b/modules/spell/__init__.py @@ -12,6 +12,8 @@ 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>." diff --git a/modules/syno.py b/modules/syno.py index 596f340..9016231 100644 --- a/modules/syno.py +++ b/modules/syno.py @@ -12,6 +12,8 @@ from tools import web nemubotversion = 3.4 +from more import Response + def help_full(): return "!syno <word>: give a list of synonyms for <word>." diff --git a/modules/translate.py b/modules/translate.py index 1bcb0f3..eeffdb3 100644 --- a/modules/translate.py +++ b/modules/translate.py @@ -11,6 +11,8 @@ from urllib.request import urlopen nemubotversion = 3.4 +from more import Response + import xmlparser LANG = ["ar", "zh", "cz", "en", "fr", "gr", "it", diff --git a/modules/velib.py b/modules/velib.py index ae8854a..d4c5ca7 100644 --- a/modules/velib.py +++ b/modules/velib.py @@ -9,6 +9,8 @@ from tools import web nemubotversion = 3.4 +from more import Response + def load(context): global DATAS DATAS.setIndex("name", "station") diff --git a/modules/watchWebsite/__init__.py b/modules/watchWebsite/__init__.py index efbcf52..3970ea7 100644 --- a/modules/watchWebsite/__init__.py +++ b/modules/watchWebsite/__init__.py @@ -15,10 +15,11 @@ from urllib.parse import urlparse from hooks import hook +nemubotversion = 3.4 + from networking import w3m from .atom import Atom - -nemubotversion = 3.4 +from more import Response def help_full (): return "This module is autonomous you can't interract with it." diff --git a/modules/weather.py b/modules/weather.py index 91c8a5c..ac35e66 100644 --- a/modules/weather.py +++ b/modules/weather.py @@ -14,6 +14,8 @@ import mapquest nemubotversion = 3.4 +from more import Response + def load(context): global DATAS DATAS.setIndex("name", "city") diff --git a/modules/worldcup.py b/modules/worldcup.py index b4f42a8..c829bc3 100644 --- a/modules/worldcup.py +++ b/modules/worldcup.py @@ -10,6 +10,8 @@ from urllib.request import urlopen nemubotversion = 3.4 +from more import Response + from hooks import hook API_URL="http://worldcup.sfg.io/%s" diff --git a/modules/ycc.py b/modules/ycc.py index e38608b..ca70432 100644 --- a/modules/ycc.py +++ b/modules/ycc.py @@ -11,6 +11,8 @@ from hooks import hook nemubotversion = 3.4 +from more import Response + def help_full(): return "!ycc [<url>]: with an argument, reduce the given <url> thanks to ycc.fr; without argument, reduce the last URL said on the current channel." diff --git a/networkbot.py b/networkbot.py index 37814e8..c4ba4ff 100644 --- a/networkbot.py +++ b/networkbot.py @@ -24,7 +24,6 @@ import zlib from server.DCC import DCC import hooks -from response import Response class NetworkBot: def __init__(self, context, srv, dest, dcc=None): diff --git a/response.py b/response.py deleted file mode 100644 index 6365fc1..0000000 --- a/response.py +++ /dev/null @@ -1,185 +0,0 @@ -# -*- coding: utf-8 -*- - -# Nemubot is a modulable IRC bot, built around XML configuration files. -# Copyright (C) 2012 Mercier Pierre-Olivier -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import logging -import traceback -import sys - -logger = logging.getLogger("nemubot.response") - -class Response: - def __init__(self, message=None, channel=None, nick=None, server=None, - nomore="No more message", title=None, more="(suite) ", - count=None, ctcp=False, shown_first_count=-1, - line_treat=None): - self.nomore = nomore - self.more = more - self.line_treat = line_treat - self.rawtitle = title - self.server = server - self.messages = list() - self.alone = True - self.is_ctcp = ctcp - if message is not None: - self.append_message(message, shown_first_count=shown_first_count) - self.elt = 0 # Next element to display - - self.sender = None - self.channel = channel - self.nick = nick - self.count = count - - @property - def receivers(self): - if self.channel is None: - if self.nick is not None: - return [ self.nick ] - return [ self.sender.split("!")[0] ] - elif isinstance(self.channel, list): - return self.channel - else: - return [ self.channel ] - - def set_sender(self, sender): - if sender is None or sender.find("!") < 0: - if sender is not None: - logger.warn("Bad sender provided in Response, it will be ignored.", stack_info=True) - self.sender = None - else: - self.sender = sender - - def append_message(self, message, title=None, shown_first_count=-1): - if type(message) is str: - message = message.split('\n') - if len(message) > 1: - for m in message: - self.append_message(m) - return - else: - message = message[0] - if message is not None and len(message) > 0: - if shown_first_count >= 0: - self.messages.append(message[:shown_first_count]) - message = message[shown_first_count:] - self.messages.append(message) - self.alone = self.alone and len(self.messages) <= 1 - if isinstance(self.rawtitle, list): - self.rawtitle.append(title) - elif title is not None: - rawtitle = self.rawtitle - self.rawtitle = list() - for osef in self.messages: - self.rawtitle.append(rawtitle) - self.rawtitle.pop() - self.rawtitle.append(title) - - 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.alone = True - else: - self.messages[len(self.messages)-1] += message - self.alone = self.alone and len(self.messages) <= 1 - - @property - def empty(self): - return len(self.messages) <= 0 - - @property - def title(self): - if isinstance(self.rawtitle, list): - return self.rawtitle[0] - else: - return self.rawtitle - - def pop(self): - self.messages.pop(0) - self.elt = 0 - if isinstance(self.rawtitle, list): - self.rawtitle.pop(0) - if len(self.rawtitle) <= 0: - self.rawtitle = None - - def treat_ctcp(self, content): - if self.is_ctcp: - return "\x01" + content + "\x01" - else: - return content - - def get_message(self): - if self.alone and len(self.messages) > 1: - self.alone = False - - if self.empty: - return self.treat_ctcp(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() - - msg = "" - if self.channel is not None and self.nick is not None: - msg += self.nick + ": " - - if self.title is not None: - if self.elt > 0: - msg += self.title + " " + self.more + ": " - else: - msg += self.title + ": " - - if self.elt > 0: - msg += "[…] " - - elts = self.messages[0][self.elt:] - if isinstance(elts, list): - for e in elts: - if len(msg) + len(e) > 430: - msg += "[…]" - self.alone = False - return self.treat_ctcp(msg) - else: - msg += e + ", " - self.elt += 1 - self.pop() - return self.treat_ctcp(msg[:len(msg)-2]) - - else: - if len(elts.encode()) <= 432: - self.pop() - if self.count is not None: - return self.treat_ctcp(msg + elts + (self.count % len(self.messages))) - else: - return self.treat_ctcp(msg + elts) - - else: - words = elts.split(' ') - - if len(words[0].encode()) > 432 - len(msg.encode()): - self.elt += 432 - len(msg.encode()) - return self.treat_ctcp(msg + elts[:self.elt] + "[…]") - - for w in words: - if len(msg.encode()) + len(w.encode()) > 431: - msg += "[…]" - self.alone = False - return self.treat_ctcp(msg) - else: - msg += w + " " - self.elt += len(w) + 1 - self.pop() - return self.treat_ctcp(msg) diff --git a/server/IRC.py b/server/IRC.py index c913fdb..085b72a 100644 --- a/server/IRC.py +++ b/server/IRC.py @@ -185,13 +185,6 @@ class IRCServer(SocketServer): return True return False - def send_response(self, res): - for channel in res.receivers: - if channel is not None and channel != self.nick: - self.write("%s %s :%s" % ("NOTICE" if res.is_ctcp else "PRIVMSG", channel, res.get_message())) - else: - raise Exception("Trying to send a message to an undefined channel: %s" % channel) - def _close(self): if self.socket is not None: self.write("QUIT") return SocketServer._close(self) @@ -219,17 +212,15 @@ class IRCServer(SocketServer): else: res = _ctcp_response("ERRMSG Unknown or unimplemented CTCP request") if res is not None: - res.set_sender(mes.sender) - self.send_response(res) + res = res % mes.nick + self.write(res) else: yield mes -from response import Response - def _ctcp_response(msg): - return Response(msg, ctcp=True) + return "NOTICE %%s :\x01%s\x01" % msg mgx = re.compile(b'''^(?:@(?P<tags>[^ ]+)\ )? diff --git a/server/__init__.py b/server/__init__.py index c10c90f..28cf765 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -64,10 +64,6 @@ class AbstractServer(io.IOBase): _xlist.remove(self) - def send_response(self, res): - return NotImplemented - - def write(self, message): """Send a message to the server using send_callback""" self._send_callback(message) From 99a5e8e5ad3fe66010f20d7979ef146e0807fb2d Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 27 Sep 2014 23:57:19 +0200 Subject: [PATCH 157/674] Can send command on connection, defined in configuration file --- server/IRC.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/IRC.py b/server/IRC.py index 085b72a..2bfe3be 100644 --- a/server/IRC.py +++ b/server/IRC.py @@ -106,7 +106,10 @@ class IRCServer(SocketServer): # Respond to 001 def _on_connect(msg): - # First, JOIN some channels + # First, send user defined command + if node.hasAttribute("on_connect"): + self.write(node["on_connect"]) + # Then, JOIN some channels for chn in node.getNodes("channel"): if chn["password"] is not None: self.write("JOIN %s %s" % (chn["name"], chn["password"])) From 74bb0caa1b0e5f3fb55fca6a5056c2af720f08e9 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 28 Sep 2014 20:34:49 +0200 Subject: [PATCH 158/674] Fix prompt commands: join, leave and part --- modules/cmd_server.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/cmd_server.py b/modules/cmd_server.py index b781088..957138a 100644 --- a/modules/cmd_server.py +++ b/modules/cmd_server.py @@ -114,6 +114,8 @@ def hotswap(data, toks, context, prompt): print (" Please SELECT a server or give its name in argument.") @hook("prompt_cmd", "join") +@hook("prompt_cmd", "leave") +@hook("prompt_cmd", "part") def join(data, toks, context, prompt): """Join or leave a channel""" rd = 1 @@ -136,11 +138,11 @@ def join(data, toks, context, prompt): if toks[0] == "join": if len(toks) > rd + 1: - srv.join(toks[rd], toks[rd + 1]) + srv.write("JOIN %s %s" % (toks[rd], toks[rd + 1])) else: - srv.join(toks[rd]) + srv.write("JOIN %s" % toks[rd]) elif toks[0] == "leave" or toks[0] == "part": - srv.leave(toks[rd]) + srv.write("PART %s" % toks[rd]) return @hook("prompt_cmd", "save") From 32cb79344bd39620d16c2bf40732a1dfaca5b758 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 29 Sep 2014 22:45:44 +0200 Subject: [PATCH 159/674] Mediawiki module: fix links --- modules/mediawiki.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/mediawiki.py b/modules/mediawiki.py index 0642701..af1ca91 100644 --- a/modules/mediawiki.py +++ b/modules/mediawiki.py @@ -60,7 +60,7 @@ def parse_wikitext(site, cnt, ssl=False): cnt = cnt.replace(i, get_unwikitextified(site, i, ssl), 1) # Strip [[...]] - cnt, _ = re.subn(r"\[\[([^]]*\|)?([^]]*?)\]\]", r"\2", cnt) + cnt, _ = re.subn(r"\[\[:?([^]]*\|)?([^]]+?)\]\]", r"\2", cnt) # Strip HTML tags cnt = striphtml(cnt) From c5a69f1bd0cfff99d40053c62eb23965a21b5776 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 30 Sep 2014 23:51:14 +0200 Subject: [PATCH 160/674] Internal timezone is now UTC --- bot.py | 9 ++++----- event.py | 8 ++++---- modules/alias.py | 6 ++---- modules/bonneannee.py | 14 +++++++------- modules/events.py | 21 ++++++++++----------- modules/sleepytime.py | 11 +++++------ modules/worldcup.py | 8 ++++---- server/IRC.py | 9 +++++---- tools/countdown.py | 19 ++++++++++--------- xmlparser/node.py | 7 +++---- 10 files changed, 54 insertions(+), 58 deletions(-) diff --git a/bot.py b/bot.py index bf95380..e04420e 100644 --- a/bot.py +++ b/bot.py @@ -16,8 +16,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 datetime import datetime -from datetime import timedelta +from datetime import datetime, timedelta, timezone import logging from queue import Queue import re @@ -277,8 +276,8 @@ class Bot(threading.Thread): if len(self.events) > 0: logger.debug("Update timer: next event in %d seconds", self.events[0].time_left.seconds) - if datetime.now() + timedelta(seconds=5) >= self.events[0].current: - while datetime.now() < self.events[0].current: + if datetime.now(timezone.utc) + timedelta(seconds=5) >= self.events[0].current: + while datetime.now(timezone.utc) < self.events[0].current: time.sleep(0.6) self._end_event_timer() else: @@ -292,7 +291,7 @@ class Bot(threading.Thread): def _end_event_timer(self): """Function called at the end of the event timer""" - while len(self.events) > 0 and datetime.now() >= self.events[0].current: + while len(self.events) > 0 and datetime.now(timezone.utc) >= self.events[0].current: evt = self.events.pop(0) self.cnsr_queue.put_nowait(EventConsumer(evt)) self._launch_consumers() diff --git a/event.py b/event.py index fc90785..857a51a 100644 --- a/event.py +++ b/event.py @@ -16,7 +16,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 datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone class ModuleEvent: @@ -77,7 +77,7 @@ class ModuleEvent: """Return the date of the near check""" if self.times != 0: if self._end is None: - self._end = datetime.now() + self.offset + self.interval + self._end = datetime.now(timezone.utc) + self.offset + self.interval return self._end return None @@ -88,7 +88,7 @@ class ModuleEvent: if self.times != 0: if self._end is None: return self.current - elif self._end < datetime.now(): + elif self._end < datetime.now(timezone.utc): self._end += self.interval return self._end return None @@ -98,7 +98,7 @@ class ModuleEvent: def time_left(self): """Return the time left before/after the near check""" if self.current is not None: - return self.current - datetime.now() + return self.current - datetime.now(timezone.utc) return 99999 #TODO: 99999 is not a valid time to return diff --git a/modules/alias.py b/modules/alias.py index e790a86..370ac1d 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -4,7 +4,7 @@ import re import sys -from datetime import datetime +from datetime import datetime, timezone from hooks import hook @@ -41,9 +41,7 @@ def get_variable(name, msg=None): elif name == "chan" or name == "channel": return msg.channel elif name == "date": - now = datetime.now() - return ("%d/%d/%d %d:%d:%d"%(now.day, now.month, now.year, now.hour, - now.minute, now.second)) + return datetime.now(timezone.utc).strftime("%c") elif name in DATAS.getNode("variables").index: return DATAS.getNode("variables").index[name]["value"] else: diff --git a/modules/bonneannee.py b/modules/bonneannee.py index f50c9b2..5597b96 100644 --- a/modules/bonneannee.py +++ b/modules/bonneannee.py @@ -2,7 +2,7 @@ """Wishes Happy New Year when the time comes""" -from datetime import datetime +from datetime import datetime, timezone from hooks import hook from tools.countdown import countdown_format @@ -11,15 +11,15 @@ nemubotversion = 3.4 from more import Response -yr = datetime.today().year -yrn = datetime.today().year + 1 +yr = datetime.now(timezone.utc).year +yrn = datetime.now(timezone.utc).year + 1 def load(context): - d = datetime(yrn, 1, 1, 0, 0, 0) - datetime.now() + d = datetime(yrn, 1, 1, 0, 0, 0) - datetime.now(timezone.utc) add_event(ModuleEvent(interval=0, offset=d.total_seconds(), call=bonneannee)) def bonneannee(): - txt = "Bonne année %d !" % datetime.today().year + txt = "Bonne année %d !" % yrn print (txt) send_response("localhost:2771", Response(txt, "#epitagueule")) send_response("localhost:2771", Response(txt, "#yaka")) @@ -31,7 +31,7 @@ def bonneannee(): @hook("cmd_hook", "newyear") @hook("cmd_hook", str(yrn), yrn) def cmd_newyear(msg, yr): - return Response(countdown_format(datetime(yr, 1, 1, 0, 0, 1), + return Response(countdown_format(datetime(yr, 1, 1, 0, 0, 1, 0, timezone.utc), "Il reste %s avant la nouvelle année.", "Nous faisons déjà la fête depuis %s !"), channel=msg.channel) @@ -43,7 +43,7 @@ def cmd_timetoyear(msg, cur): if yr == cur: return None - return Response(countdown_format(datetime(yr, 1, 1, 0, 0, 1), + return Response(countdown_format(datetime(yr, 1, 1, 0, 0, 1, 0, timezone.utc), "Il reste %s avant %d." % ("%s", yr), "Le premier janvier %d est passé depuis %s !" % (yr, "%s")), channel=msg.channel) diff --git a/modules/events.py b/modules/events.py index a17227c..af20867 100644 --- a/modules/events.py +++ b/modules/events.py @@ -5,8 +5,7 @@ import imp import re import sys -from datetime import timedelta -from datetime import datetime +from datetime import datetime, timedelta, timezone import time import threading import traceback @@ -32,7 +31,7 @@ def load(context): if DATAS.index[evt].hasAttribute("end"): event = ModuleEvent(call=fini, call_data=dict(strend=DATAS.index[evt])) event._end = DATAS.index[evt].getDate("end") - idt = context.add_event(event) + idt = add_event(event) if idt is not None: DATAS.index[evt]["id"] = idt @@ -44,8 +43,8 @@ def fini(d, strend): @hook("cmd_hook", "goûter") def cmd_gouter(msg): - ndate = datetime.today() - ndate = datetime(ndate.year, ndate.month, ndate.day, 16, 42) + ndate = datetime.now(timezone.utc) + ndate = datetime(ndate.year, ndate.month, ndate.day, 16, 42, 0, 0, timezone.utc) return Response(countdown_format(ndate, "Le goûter aura lieu dans %s, préparez vos biscuits !", "Nous avons %s de retard pour le goûter :("), @@ -53,8 +52,8 @@ def cmd_gouter(msg): @hook("cmd_hook", "week-end") def cmd_we(msg): - ndate = datetime.today() + timedelta(5 - datetime.today().weekday()) - ndate = datetime(ndate.year, ndate.month, ndate.day, 0, 0, 1) + ndate = datetime.now(timezone.utc) + timedelta(5 - datetime.today().weekday()) + ndate = datetime(ndate.year, ndate.month, ndate.day, 0, 0, 1, 0, timezone.utc) return Response(countdown_format(ndate, "Il reste %s avant le week-end, courage ;)", "Youhou, on est en week-end depuis %s."), @@ -95,14 +94,14 @@ 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) + strnd["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))) + strnd["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) + strnd["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) + strnd["end"] = datetime(now.year, now.month, now.day + 1, hou, minu, sec, timezone.utc) evt._end = strnd.getDate("end") strnd["id"] = add_event(evt) except: diff --git a/modules/sleepytime.py b/modules/sleepytime.py index 9cb1169..b662878 100644 --- a/modules/sleepytime.py +++ b/modules/sleepytime.py @@ -4,8 +4,7 @@ import re import imp -from datetime import datetime -from datetime import timedelta +from datetime import datetime, timedelta, timezone from hooks import hook @@ -22,9 +21,9 @@ def cmd_sleep(msg): msg.cmds[1]) is not None: # First, parse the hour p = re.match("([0-9]{1,2})[h':.,-]([0-9]{1,2})?[m':.,-]?", msg.cmds[1]) - f = [datetime(datetime.today().year, - datetime.today().month, - datetime.today().day, + f = [datetime(datetime.now(timezone.utc).year, + datetime.now(timezone.utc).month, + datetime.now(timezone.utc).day, hour=int(p.group(1)))] if p.group(2) is not None: f[0] += timedelta(minutes=int(p.group(2))) @@ -37,7 +36,7 @@ def cmd_sleep(msg): # Just get awake times else: - f = [datetime.now() + timedelta(minutes=15)] + f = [datetime.now(timezone.utc) + timedelta(minutes=15)] g = list() for i in range(0,6): f.append(f[i] + timedelta(hours=1,minutes=30)) diff --git a/modules/worldcup.py b/modules/worldcup.py index c829bc3..cd093ab 100644 --- a/modules/worldcup.py +++ b/modules/worldcup.py @@ -2,7 +2,7 @@ """The 2014 football worldcup module""" -import datetime +from datetime import datetime, timezone import json import re from urllib.parse import quote @@ -32,7 +32,7 @@ def start_watch(msg): w["channel"] = msg.channel w["proprio"] = msg.nick w["sender"] = msg.sender - w["start"] = datetime.datetime.now() + w["start"] = datetime.now(timezone.utc) DATAS.addChild(w) save() raise IRCException("This channel is now watching world cup events!") @@ -125,7 +125,7 @@ def txt_event(e): return "%se minutes : %s %s (%s)" % (e["time"], detail_event(e["type_of_event"]), e["player"], e["team"]["code"]) def prettify(match): - matchdate_local = datetime.datetime.strptime(match["datetime"].replace(':', ''), "%Y-%m-%dT%H%M%S.%f%z") + 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)) 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"])] @@ -135,7 +135,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.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).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/server/IRC.py b/server/IRC.py index 2bfe3be..d676dd9 100644 --- a/server/IRC.py +++ b/server/IRC.py @@ -16,8 +16,10 @@ # 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 calendar +from datetime import datetime, timezone import re +import time import shlex import bot @@ -194,7 +196,7 @@ class IRCServer(SocketServer): def read(self): for line in SocketServer.read(self): - msg = IRCMessage(line, datetime.now()) + msg = IRCMessage(line, datetime.now(timezone.utc)) if msg.cmd in self.hookscmd: self.hookscmd[msg.cmd](msg) @@ -279,8 +281,7 @@ class IRCMessage: """Add an IRCv3.2 Message Tags""" # Treat special tags if key == "time": - # TODO: this is UTC timezone, nemubot works with local timezone - value = datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%fZ") + value = datetime.fromtimestamp(calendar.timegm(time.strptime(value, "%Y-%m-%dT%H:%M:%S.%fZ")), timezone.utc) # Store tag self.tags[key] = value diff --git a/tools/countdown.py b/tools/countdown.py index ed73e49..5707085 100644 --- a/tools/countdown.py +++ b/tools/countdown.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timezone import time def countdown(delta, resolution=5): @@ -59,21 +59,22 @@ def countdown(delta, resolution=5): return sentence[1:] -def countdown_format(date, msg_before, msg_after, timezone=None): +def countdown_format(date, msg_before, msg_after, tz=None): """Replace in a text %s by a sentence incidated the remaining time before/after an event""" - if timezone != None: - os.environ['TZ'] = timezone + if tz != None: + oldtz = os.environ['TZ'] + os.environ['TZ'] = tz time.tzset() #Calculate time before the date - if datetime.now() > date: + if datetime.now(timezone.utc) > date: sentence_c = msg_after - delta = datetime.now() - date + delta = datetime.now(timezone.utc) - date else: sentence_c = msg_before - delta = date - datetime.now() + delta = date - datetime.now(timezone.utc) - if timezone != None: - os.environ['TZ'] = "Europe/Paris" + if tz != None: + os.environ['TZ'] = oldtz return sentence_c % countdown(delta) diff --git a/xmlparser/node.py b/xmlparser/node.py index 4a674e4..10608b5 100644 --- a/xmlparser/node.py +++ b/xmlparser/node.py @@ -1,8 +1,7 @@ # coding=utf-8 import xml.sax -from datetime import datetime -from datetime import date +from datetime import datetime, timezone import logging import sys import time @@ -67,12 +66,12 @@ class ModuleState: return source else: try: - return datetime.fromtimestamp(float(source)) + return datetime.fromtimestamp(float(source), timezone.utc) except ValueError: while True: try: return datetime.fromtimestamp(time.mktime( - time.strptime(source[:19], "%Y-%m-%d %H:%M:%S"))) + time.strptime(source[:19], "%Y-%m-%d %H:%M:%S")), timezone.utc) except ImportError: pass From 23c660ab5749ba7c32d9208a7c16392738a60180 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 1 Oct 2014 00:16:19 +0200 Subject: [PATCH 161/674] Mediawiki module: can search through opensearch or classic search --- modules/mediawiki.py | 54 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/modules/mediawiki.py b/modules/mediawiki.py index af1ca91..edb54e6 100644 --- a/modules/mediawiki.py +++ b/modules/mediawiki.py @@ -8,14 +8,14 @@ import urllib.parse import urllib.request from hooks import hook -from tools.web import striphtml +from tools import web nemubotversion = 3.4 from more import Response def get_raw_page(site, term, ssl=False): - # Built IRL + # 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)) print_debug(url) @@ -31,7 +31,7 @@ def get_raw_page(site, term, ssl=False): raise IRCException("article not found") def get_unwikitextified(site, wikitext, ssl=False): - # Built IRL + # Built URL url = "http%s://%s/w/api.php?format=json&action=expandtemplates&text=%s" % ( "s" if ssl else "", site, urllib.parse.quote(wikitext)) print_debug(url) @@ -63,7 +63,7 @@ def parse_wikitext(site, cnt, ssl=False): cnt, _ = re.subn(r"\[\[:?([^]]*\|)?([^]]+?)\]\]", r"\2", cnt) # Strip HTML tags - cnt = striphtml(cnt) + cnt = web.striphtml(cnt) return cnt @@ -75,6 +75,38 @@ def get_page(site, term, ssl=False): return strip_model(get_raw_page(site, term, ssl)) +def opensearch(site, term, ssl=False): + # Built URL + url = "http%s://%s/w/api.php?format=xml&action=opensearch&search=%s" % ( + "s" if ssl else "", site, urllib.parse.quote(term)) + print_debug(url) + + # Make the request + response = web.getXML(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(), + itm.getNode("Url").getContent()) + + +def search(site, term, ssl=False): + # 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)) + print_debug(url) + + # Make the request + raw = urllib.request.urlopen(url) + data = json.loads(raw.read().decode()) + + if data is not None and "query" in data and "search" in data["query"]: + for itm in data["query"]["search"]: + yield (web.striphtml(itm["titlesnippet"].replace("<span class='searchmatch'>", "\x03\x02").replace("</span>", "\x03\x02")), + web.striphtml(itm["snippet"].replace("<span class='searchmatch'>", "\x03\x02").replace("</span>", "\x03\x02"))) + + @hook("in_PRIVMSG_cmd", "mediawiki") def cmd_mediawiki(msg): """Read an article on a MediaWiki""" @@ -86,6 +118,20 @@ def cmd_mediawiki(msg): channel=msg.receivers) +@hook("in_PRIVMSG_cmd", "search_mediawiki") +def cmd_srchmediawiki(msg): + """Search an article on a MediaWiki""" + if len(msg.cmds) < 3: + raise IRCException("indicate a domain and a term to search") + + res = Response(channel=msg.receivers, nomore="No more results", count=" (%d more results)") + + for r in search(msg.cmds[1], " ".join(msg.cmds[2:])): + res.append_message("%s: %s" % r) + + return res + + @hook("in_PRIVMSG_cmd", "wikipedia") def cmd_wikipedia(msg): if len(msg.cmds) < 3: From 1c1139df9f6b5014b7e38097caac56018a653684 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 1 Oct 2014 00:17:36 +0200 Subject: [PATCH 162/674] Autojoin on invitations --- server/IRC.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/IRC.py b/server/IRC.py index d676dd9..476918a 100644 --- a/server/IRC.py +++ b/server/IRC.py @@ -178,6 +178,12 @@ class IRCServer(SocketServer): 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 + def _open(self): if SocketServer._open(self): From 302add96caecd12f95a7aaca92a599899bfe925b Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 1 Oct 2014 00:33:52 +0200 Subject: [PATCH 163/674] Add a connected state for socket --- server/IRC.py | 2 +- server/socket.py | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/server/IRC.py b/server/IRC.py index 476918a..2d7fa5e 100644 --- a/server/IRC.py +++ b/server/IRC.py @@ -197,7 +197,7 @@ class IRCServer(SocketServer): return False def _close(self): - if self.socket is not None: self.write("QUIT") + if self.connected: self.write("QUIT") return SocketServer._close(self) def read(self): diff --git a/server/socket.py b/server/socket.py index 88ab044..302422f 100644 --- a/server/socket.py +++ b/server/socket.py @@ -36,6 +36,10 @@ class SocketServer(AbstractServer): def fileno(self): return self.socket.fileno() if self.socket else None + @property + def connected(self): + return self.socket is not None + def _open(self): # Create the socket self.socket = socket.socket() @@ -58,7 +62,7 @@ class SocketServer(AbstractServer): def _close(self): self._sending_queue.join() - if self.socket is not None: + if self.connected: try: self.socket.shutdown(socket.SHUT_RDWR) self.socket.close() @@ -68,7 +72,7 @@ class SocketServer(AbstractServer): return True def _write(self, cnt): - if self.socket is None: return + if not self.connected: return self.socket.send(cnt) def format(self, txt): @@ -78,7 +82,7 @@ class SocketServer(AbstractServer): return txt.encode() + b'\r\n' def read(self): - if self.socket is None: return + if not self.connected: return raw = self.socket.recv(1024) temp = (self.readbuffer + raw).split(b'\r\n') self.readbuffer = temp.pop() From ada19a221cd2f59258532114b11e936ca45c5c7b Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 2 Oct 2014 06:59:54 +0200 Subject: [PATCH 164/674] In Response, nomore can now be a function --- modules/more.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/modules/more.py b/modules/more.py index f2b0148..61dcdd4 100644 --- a/modules/more.py +++ b/modules/more.py @@ -131,7 +131,21 @@ class Response: self.alone = False if self.empty: - return self.treat_ctcp(self.nomore) + if hasattr(self.nomore, '__call__'): + res = self.nomore(self) + if res is None: + return self.treat_ctcp("No more message") + elif isinstance(res, Response): + self.__dict__ = res.__dict__ + elif isinstance(res, list): + self.messages = res + elif isinstance(res, str): + self.messages.append(res) + else: + raise Exception("Type returned by nomore (%s) is not handled here." % type(res)) + return self.get_message() + else: + return self.treat_ctcp(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() From ce9dec7ed47a208e14357dfc795240f90780ea43 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 2 Oct 2014 07:03:27 +0200 Subject: [PATCH 165/674] Xmlparser module: getNodes is now a generator --- modules/books.py | 3 +-- xmlparser/node.py | 5 +---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/modules/books.py b/modules/books.py index 456ea0e..59a2f7f 100644 --- a/modules/books.py +++ b/modules/books.py @@ -66,8 +66,7 @@ def cmd_books(msg): res = Response(channel=msg.channel, title="%s" % (title), count=" (%d more books)") - books = search_books(title) - for book in 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())) return res diff --git a/xmlparser/node.py b/xmlparser/node.py index 10608b5..34e7d0e 100644 --- a/xmlparser/node.py +++ b/xmlparser/node.py @@ -152,15 +152,12 @@ class ModuleState: def getNodes(self, tagname): """Get all direct childs that have the given tagname""" - ret = list() for child in self.childs: if tagname is None or tagname == child.name: - ret.append(child) - return ret + yield child def hasNode(self, tagname): """Return True if at least one node with the given tagname exists""" - ret = list() for child in self.childs: if tagname is None or tagname == child.name: return True From 5d5030efe1722f6e6fe4c49614d7968aa50d12b9 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 4 Oct 2014 07:33:34 +0200 Subject: [PATCH 166/674] Syno module: add english language support; closing #64 --- modules/syno.py | 121 ++++++++++++++++++++++++++++++------------------ 1 file changed, 75 insertions(+), 46 deletions(-) diff --git a/modules/syno.py b/modules/syno.py index 9016231..2389021 100644 --- a/modules/syno.py +++ b/modules/syno.py @@ -1,11 +1,11 @@ # coding=utf-8 -"""Find french synonyms""" +"""Find synonyms""" +import json import re -import traceback -import sys from urllib.parse import quote +from urllib.request import urlopen from hooks import hook from tools import web @@ -15,56 +15,28 @@ nemubotversion = 3.4 from more import Response def help_full(): - return "!syno <word>: give a list of synonyms for <word>." + return "!syno [LANG] <word>: give a list of synonyms for <word>." -@hook("cmd_hook", "synonymes") -def cmd_syno(msg): - return go("synonymes", msg) - -@hook("cmd_hook", "antonymes") -def cmd_anto(msg): - return go("antonymes", msg) - -def go(what, msg): - if len(msg.cmds) < 2: - raise IRCException("de quel mot veux-tu connaître la liste des synonymes ?") - - word = ' '.join(msg.cmds[1:]) - try: - best, synos, anton = get_synos(word) - except: - best, synos, anton = (list(), list(), list()) - - if what == "synonymes": - if len(synos) > 0: - res = Response(best, channel=msg.channel, - title="Synonymes de %s" % word) - res.append_message(synos) - return res - else: - raise IRCException("Aucun synonyme de %s n'a été trouvé" % word) - - elif what == "antonymes": - if len(anton) > 0: - res = Response(anton, channel=msg.channel, - title="Antonymes de %s" % word) - return res - else: - raise IRCException("Aucun antonyme de %s n'a été trouvé" % word) +def load(context): + global lang_binding + if not CONF or not CONF.hasNode("bighugelabs") or not CONF.getNode("bighugelabs").hasAttribute("key"): + print ("You need a NigHugeLabs API key in order to have english " + "theasorus. Add it to the module configuration file:\n<bighugelabs" + " key=\"XXXXXXXXXXXXXXXX\" />\nRegister at " + "https://words.bighugelabs.com/getkey.php") else: - raise IRCException("WHAT?!") + lang_binding["en"] = lambda word: get_english_synos(CONF.getNode("bighugelabs")["key"], word) -def get_synos(word): +def get_french_synos(word): url = "http://www.crisco.unicaen.fr/des/synonymes/" + quote(word.encode("ISO-8859-1")) - print_debug (url) + print_debug(url) page = web.getURLContent(url) + best = list(); synos = list(); anton = list() + if page is not None: - best = list() - synos = list() - anton = list() for line in page.split("\n"): if line.find("!-- Fin liste des antonymes --") > 0: @@ -79,7 +51,64 @@ def get_synos(word): for elt in re.finditer(">&[^;]+;([^&]*)&[^;]+;<", line): best.append(elt.group(1)) - return (best, synos, anton) + return (best, synos, anton) + + +def get_english_synos(key, word): + raw = urlopen("http://words.bighugelabs.com/api/2/%s/%s/json" % (quote(key), quote(word.encode("ISO-8859-1")))) + cnt = json.loads(raw.read().decode()) + + best = list(); synos = list(); anton = list() + + if cnt is not None: + for k, c in cnt.items(): + if "syn" in c: best += c["syn"] + if "rel" in c: synos += c["rel"] + if "ant" in c: anton += c["ant"] + + return (best, synos, anton) + + +lang_binding = { 'fr': get_french_synos } + + +@hook("cmd_hook", "synonymes", data="synonymes") +@hook("cmd_hook", "antonymes", data="antonymes") +def go(msg, what): + if len(msg.cmds) < 2: + raise IRCException("de quel mot veux-tu connaître la liste des synonymes ?") + + # Detect lang + if msg.cmds[1] in lang_binding: + func = lang_binding[msg.cmds[1]] + word = ' '.join(msg.cmds[2:]) + else: + func = lang_binding["fr"] + word = ' '.join(msg.cmds[1:]) + # TODO: depreciate usage without lang + #raise IRCException("language %s is not handled yet." % msg.cmds[1]) + + try: + best, synos, anton = func(word) + except: + best, synos, anton = (list(), list(), list()) + + if what == "synonymes": + if len(synos) > 0 or len(best) > 0: + res = Response(channel=msg.channel, title="Synonymes de %s" % word) + if len(best) > 0: res.append_message(best) + if len(synos) > 0: res.append_message(synos) + return res + else: + raise IRCException("Aucun synonyme de %s n'a été trouvé" % word) + + elif what == "antonymes": + if len(anton) > 0: + res = Response(anton, channel=msg.channel, + title="Antonymes de %s" % word) + return res + else: + raise IRCException("Aucun antonyme de %s n'a été trouvé" % word) else: - return (list(), list(), list()) + raise IRCException("WHAT?!") From 49bfcdcae5b6f55bbeff04ea6724c5296d7000a0 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 4 Oct 2014 07:34:30 +0200 Subject: [PATCH 167/674] Bonneannee module: fix timezone --- modules/bonneannee.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/bonneannee.py b/modules/bonneannee.py index 5597b96..3a1167f 100644 --- a/modules/bonneannee.py +++ b/modules/bonneannee.py @@ -15,7 +15,7 @@ yr = datetime.now(timezone.utc).year yrn = datetime.now(timezone.utc).year + 1 def load(context): - d = datetime(yrn, 1, 1, 0, 0, 0) - datetime.now(timezone.utc) + d = datetime(yrn, 1, 1, 0, 0, 0, 0, timezone.utc) - datetime.now(timezone.utc) add_event(ModuleEvent(interval=0, offset=d.total_seconds(), call=bonneannee)) def bonneannee(): From 020759fdabc4beb0014f19b6d6aafd136f874cf1 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 4 Oct 2014 07:39:08 +0200 Subject: [PATCH 168/674] Decoding IRC message: use encoding from configuration file --- server/IRC.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/server/IRC.py b/server/IRC.py index 2d7fa5e..8d9f848 100644 --- a/server/IRC.py +++ b/server/IRC.py @@ -45,6 +45,11 @@ class IRCServer(SocketServer): #Keep a list of connected channels self.channels = dict() + if node.hasAttribute("encoding"): + self.encoding = node["encoding"] + else: + self.encoding = "utf-8" + if node.hasAttribute("caps"): if node["caps"].lower() == "no": self.capabilities = None @@ -196,13 +201,15 @@ class IRCServer(SocketServer): return True return False + def _close(self): if self.connected: self.write("QUIT") return SocketServer._close(self) + def read(self): for line in SocketServer.read(self): - msg = IRCMessage(line, datetime.now(timezone.utc)) + msg = IRCMessage(line, self.encoding) if msg.cmd in self.hookscmd: self.hookscmd[msg.cmd](msg) @@ -246,9 +253,11 @@ mgx = re.compile(b'''^(?:@(?P<tags>[^ ]+)\ )? class IRCMessage: - def __init__(self, raw, timestamp): - self.raw = raw - self.tags = { 'time': timestamp } + """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()) @@ -299,8 +308,7 @@ class IRCMessage: try: s = s.decode() except UnicodeDecodeError: - #TODO: use encoding from config file - s = s.decode('utf-8', 'replace') + s = s.decode(self.encoding, 'replace') return s From 981025610eccb71bfc2f79e7f32b5ad6a275dc80 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 4 Oct 2014 07:41:54 +0200 Subject: [PATCH 169/674] Fix IRC message pretty-printer --- server/IRC.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/server/IRC.py b/server/IRC.py index 8d9f848..d040493 100644 --- a/server/IRC.py +++ b/server/IRC.py @@ -22,7 +22,6 @@ import re import time import shlex -import bot from channel import Channel from message import Message import server @@ -83,6 +82,8 @@ class IRCServer(SocketServer): self.logger.error("DCC: unable to connect to %s:%d", ip, port) return _ctcp_response("ERRMSG unable to connect to %s:%d" % (ip, port)) + import bot + self.ctcp_capabilities["ACTION"] = lambda msg: print ("ACTION receive: %s" % msg.text) self.ctcp_capabilities["CLIENTINFO"] = _ctcp_clientinfo #self.ctcp_capabilities["DCC"] = _ctcp_dcc @@ -316,7 +317,13 @@ class IRCMessage: return Message(self) def to_irc_string(self, client=True): - res = ";".join(["@%s=%s" % (k,v) for k, v in self.tags.items()]) + """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) @@ -325,7 +332,7 @@ class IRCMessage: if len(self.params) > 0: if len(self.params) > 1: - res += " " + " ".join(self.params[:-1]) - res += " :" + self.params[-1] + res += " " + self.decode(b" ".join(self.params[:-1])) + res += " :" + self.decode(self.params[-1]) return res From dfde4c5f49d27f496382b59ac0f1260cfdbf69e4 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 5 Oct 2014 18:19:20 +0200 Subject: [PATCH 170/674] New message processing --- bot.py | 43 ++++----- consumer.py | 45 ++------- exception.py | 9 +- hooks/messagehook.py | 37 +++---- importer.py | 39 ++++---- message.py | 71 -------------- message/__init__.py | 160 +++++++++++++++++++++++++++++++ message/printer/IRC.py | 69 +++++++++++++ message/printer/__init__.py | 0 message/visitor.py | 25 +++++ modules/alias.py | 48 ++++------ modules/events.py | 26 ++--- modules/mediawiki.py | 6 +- modules/more.py | 100 +++++++++---------- modules/watchWebsite/__init__.py | 4 +- modules/worldcup.py | 1 - modules/ycc.py | 13 ++- prompt/builtins.py | 4 +- server/IRC.py | 131 +++++++++++++++---------- server/__init__.py | 15 +++ 20 files changed, 520 insertions(+), 326 deletions(-) delete mode 100644 message.py create mode 100644 message/__init__.py create mode 100644 message/printer/IRC.py create mode 100644 message/printer/__init__.py create mode 100644 message/visitor.py diff --git a/bot.py b/bot.py index e04420e..b136b26 100644 --- a/bot.py +++ b/bot.py @@ -25,7 +25,7 @@ import threading import time import uuid -__version__ = '3.4.dev0' +__version__ = '3.4.dev1' __author__ = 'nemunaire' from consumer import Consumer, EventConsumer, MessageConsumer @@ -33,7 +33,7 @@ from event import ModuleEvent from hooks.messagehook import MessageHook from hooks.manager import HooksManager from networkbot import NetworkBot -from server.IRC import IRCServer +from server.IRC import IRC as IRCServer from server.DCC import DCC logger = logging.getLogger("nemubot.bot") @@ -74,28 +74,27 @@ class Bot(threading.Thread): # Own hooks self.hooks = HooksManager() def in_ping(msg): - if re.match("^ *(m[' ]?entends?[ -]+tu|h?ear me|do you copy|ping)", msg.text, re.I) is not None: - return "PRIVMSG %s :%s: pong" % (",".join(msg.receivers), msg.nick) - self.hooks.add_hook(MessageHook(in_ping), "in", "PRIVMSG", "ask") + 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.hooks.add_hook(MessageHook(in_ping), "in", "DirectAsk") def _help_msg(msg): """Parse and response to help messages""" - cmd = msg.cmds from more import Response - res = Response() - if len(cmd) > 1: - if cmd[1] in self.modules: - if len(cmd) > 2: - if hasattr(self.modules[cmd[1]], "HELP_cmd"): - res.append_message(self.modules[cmd[1]].HELP_cmd(cmd[2])) + res = Response(channel=msg.frm) + if len(msg.args) > 1: + if msg.args[0] in self.modules: + if len(msg.args) > 2: + if hasattr(self.modules[msg.args[0]], "HELP_cmd"): + res.append_message(self.modules[msg.args[0]].HELP_cmd(msg.args[1])) else: - res.append_message("No help for command %s in module %s" % (cmd[2], cmd[1])) - elif hasattr(self.modules[cmd[1]], "help_full"): - res.append_message(self.modules[cmd[1]].help_full()) + res.append_message("No help for command %s in module %s" % (msg.args[1], msg.args[0])) + elif hasattr(self.modules[msg.args[0]], "help_full"): + res.append_message(self.modules[msg.args[0]].help_full()) else: - res.append_message("No help for module %s" % cmd[1]) + res.append_message("No help for module %s" % msg.args[0]) else: - res.append_message("No module named %s" % cmd[1]) + res.append_message("No module named %s" % msg.args[0]) else: res.append_message("Pour me demander quelque chose, commencez " "votre message par mon nom ; je réagis " @@ -113,7 +112,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.hooks.add_hook(MessageHook(_help_msg, "help"), "in", "PRIVMSG", "cmd") + self.hooks.add_hook(MessageHook(_help_msg, "help"), "in", "Command") # Other known bots, making a bots network self.network = dict() @@ -343,12 +342,10 @@ class Bot(threading.Thread): """Add a module to the context, if already exists, unload the old one before""" # Check if the module already exists - for mod in self.modules.keys(): - if self.modules[mod].name == module.name: - self.unload_module(self.modules[mod].name) - break + if module.__name__ in self.modules: + self.unload_module(module.__name__) - self.modules[module.name] = module + self.modules[module.__name__] = module return True diff --git a/consumer.py b/consumer.py index 17283aa..72e3635 100644 --- a/consumer.py +++ b/consumer.py @@ -20,13 +20,6 @@ import logging import queue import re import threading -import traceback -import sys - -import bot -from server.DCC import DCC -from message import Message -import server logger = logging.getLogger("nemubot.consumer") @@ -48,26 +41,10 @@ class MessageConsumer: msg -- The Message or Response to qualify """ - if not hasattr(msg, "qual") or msg.qual is None: - # Assume this is a message with no particulariry - msg.qual = "def" - # Define the source server if not already done if not hasattr(msg, "server") or msg.server is None: msg.server = self.srv.id - if isinstance(msg, Message): - if msg.cmd == "PRIVMSG" or msg.cmd == "NOTICE": - msg.is_owner = (msg.nick == self.srv.owner) - msg.private = msg.private or (len(msg.receivers) == 1 and msg.receivers[0] == self.srv.nick) - if msg.private: - msg.qual = "ask" - - # Remove nemubot: - if msg.qual != "cmd" and msg.text.find(self.srv.nick) == 0 and len(msg.text) > len(self.srv.nick) + 2 and msg.text[len(self.srv.nick)] == ":": - msg.text = msg.text[len(self.srv.nick) + 1:].strip() - msg.qual = "ask" - return msg @@ -84,8 +61,8 @@ class MessageConsumer: while len(new_msg) > 0: msg = new_msg.pop(0) - for h in hm.get_hooks("pre", msg.cmd, msg.qual): - if h.match(message=msg, server=self.srv): + for h in hm.get_hooks("pre", type(msg).__name__): + if h.match(msg, server=self.srv): res = h.run(msg) if isinstance(res, list): for i in range(len(res)): @@ -113,17 +90,12 @@ class MessageConsumer: self.responses = list() for msg in self.msgs: - for h in hm.get_hooks("in", msg.cmd, msg.qual): - if h.match(message=msg, server=self.srv): + for h in hm.get_hooks("in", type(msg).__name__): + if h.match(msg, server=self.srv): res = h.run(msg) if isinstance(res, list): - for r in res: - if hasattr(r, "set_sender"): - r.set_sender(msg.sender) self.responses += res elif res is not None: - if hasattr(res, "set_sender"): - res.set_sender(msg.sender) self.responses.append(res) @@ -145,7 +117,7 @@ class MessageConsumer: continue msg = self.first_treat(ff) for h in hm.get_hooks("post"): - if h.match(message=msg, server=self.srv): + if h.match(msg, server=self.srv): res = h.run(msg) if isinstance(res, list): for i in range(len(res)): @@ -154,6 +126,7 @@ class MessageConsumer: break msg = None new_msg += res + break elif res is not None and res != msg: new_msg.append(res) msg = None @@ -161,6 +134,8 @@ class MessageConsumer: elif res is None or res == False: msg = None break + else: + msg = res if msg is not None: self.responses.append(msg) @@ -182,7 +157,7 @@ class MessageConsumer: if self.responses is not None and len(self.responses) > 0: self.post_treat(context.hooks) except: - logger.exception("Error occurred during the processing of the message: %s", self.msgs[0].raw) + logger.exception("Error occurred during the processing of the %s: %s", type(self.msgs[0]).__name__, self.msgs[0]) for res in self.responses: to_server = None @@ -200,7 +175,7 @@ class MessageConsumer: continue # Sent the message only if treat_post authorize it - to_server.write(res) + to_server.send_response(res) class EventConsumer: """Store a event before treating""" diff --git a/exception.py b/exception.py index cfc721b..92229bf 100644 --- a/exception.py +++ b/exception.py @@ -16,6 +16,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/>. +from message import TextMessage, DirectAsk + class IRCException(Exception): def __init__(self, message, personnal=True): @@ -24,6 +26,7 @@ class IRCException(Exception): self.personnal = personnal def fill_response(self, msg): - # TODO: no more Response usable here - from more import Response - return Response(self.message, channel=msg.receivers, nick=(msg.nick if self.personnal else None)) + if self.personnal: + return DirectAsk(msg.frm, self.message, server=msg.server, to=msg.to_response) + else: + return TextMessage(self.message, server=msg.server, to=msg.to_response) diff --git a/hooks/messagehook.py b/hooks/messagehook.py index 0055d04..1ab09c6 100644 --- a/hooks/messagehook.py +++ b/hooks/messagehook.py @@ -20,8 +20,7 @@ import re from exception import IRCException import hooks - -from message import Message +import message class MessageHook(hooks.AbstractHook): @@ -39,24 +38,28 @@ class MessageHook(hooks.AbstractHook): self.channels = channels - def match(self, message, server=None): - if not isinstance(message, Message): + def match(self, msg, server=None): + if not isinstance(msg, message.AbstractMessage): return True - elif message.qual == "cmd": - return self.is_matching(message.cmds[0], message.channel, server) - elif hasattr(message, "text"): - return self.is_matching(message.text, message.channel, server) - elif len(message.params) > 0: - return self.is_matching(message.params[0], message.channel, server) + elif isinstance(msg, message.Command): + return self.is_matching(msg.cmd, msg.to, server) + elif isinstance(msg, message.TextMessage): + return self.is_matching(msg.message, msg.to, server) else: - return self.is_matching(message.cmd, message.channel, server) + return False - def is_matching(self, strcmp, channel=None, server=None): + def is_matching(self, strcmp, receivers=list(), server=None): """Test if the current hook correspond to the message""" - return (channel is None or len(self.channels) <= 0 or - channel in self.channels) and (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 (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 diff --git a/importer.py b/importer.py index ddee457..b01848c 100644 --- a/importer.py +++ b/importer.py @@ -28,6 +28,7 @@ from bot import __version__ import event import exception import hooks +from message import TextMessage import xmlparser logger = logging.getLogger("nemubot.importer") @@ -124,13 +125,6 @@ class ModuleLoader(SourceLoader): def load_module(self, fullname): module = self._load_module(fullname, sourceless=True) - # Remove the module from sys list - del sys.modules[fullname] - - # If the module was already loaded, then reload it - if hasattr(module, '__LOADED__'): - reload(module) - # Check that is a valid nemubot module if not hasattr(module, "nemubotversion"): raise ImportError("Module `%s' is not a nemubot module."%self.name) @@ -144,21 +138,25 @@ class ModuleLoader(SourceLoader): module.logger = logging.getLogger("nemubot.module." + fullname) def prnt(*args): - print("[%s]" % module.name, *args) + print("[%s]" % module.__name__, *args) module.logger.info(*args) def prnt_dbg(*args): if module.DEBUG: - print("{%s}" % module.name, *args) + print("{%s}" % module.__name__, *args) module.logger.debug(*args) def mod_save(): - fpath = self.context.data_path + "/" + module.name + ".xml" + fpath = self.context.data_path + "/" + module.__name__ + ".xml" module.print_debug("Saving DATAS to " + fpath) module.DATAS.save(fpath) def send_response(server, res): if server in self.context.servers: - return self.context.servers[server].write("PRIVMSG %s :%s" % (",".join(res.receivers), res.get_message())) + r = res.next_response() + if r.server is not None: + return self.context.servers[r.server].send_response(r) + else: + return self.context.servers[server].send_response(r) else: module.logger.error("Try to send a message to the unknown server: %s", server) return False @@ -183,7 +181,6 @@ class ModuleLoader(SourceLoader): module.REGISTERED_EVENTS = list() module.DEBUG = False module.DIR = self.mpath - module.name = fullname module.print = prnt module.print_debug = prnt_dbg module.send_response = send_response @@ -195,7 +192,7 @@ class ModuleLoader(SourceLoader): if not hasattr(module, "NODATA"): module.DATAS = xmlparser.parse_file(self.context.data_path - + module.name + ".xml") + + module.__name__ + ".xml") module.save = mod_save else: module.DATAS = None @@ -216,7 +213,7 @@ class ModuleLoader(SourceLoader): break if depend["name"] not in module.MODS: logger.error("In module `%s', module `%s' require by this " - "module but is not loaded.", module.name, + "module but is not loaded.", module.__name__, depend["name"]) return @@ -230,21 +227,21 @@ class ModuleLoader(SourceLoader): # Register hooks register_hooks(module, self.context, self.prompt) - logger.info("Module '%s' successfully loaded.", module.name) + logger.info("Module '%s' successfully loaded.", module.__name__) else: - logger.error("An error occurs while importing `%s'.", module.name) + logger.error("An error occurs while importing `%s'.", module.__name__) raise ImportError("An error occurs while importing `%s'." - % module.name) + % module.__name__) return module def convert_legacy_store(old): if old == "cmd_hook" or old == "cmd_rgxp" or old == "cmd_default": - return "in_PRIVMSG_cmd" + return "in_Command" elif old == "ask_hook" or old == "ask_rgxp" or old == "ask_default": - return "in_PRIVMSG_ask" + return "in_DirectAsk" elif old == "msg_hook" or old == "msg_rgxp" or old == "msg_default": - return "in_PRIVMSG_def" + return "in_TextMessage" elif old == "all_post": return "post" elif old == "all_pre": @@ -258,7 +255,7 @@ def add_cap_hook(prompt, module, cmd): prompt.add_cap_hook(cmd["name"], getattr(module, cmd["call"])) else: logger.warn("In module `%s', no function `%s' defined for `%s' " - "command hook.", module.name, cmd["call"], cmd["name"]) + "command hook.", module.__name__, cmd["call"], cmd["name"]) def register_hooks(module, context, prompt): """Register all available hooks""" diff --git a/message.py b/message.py deleted file mode 100644 index 3f6673d..0000000 --- a/message.py +++ /dev/null @@ -1,71 +0,0 @@ -# -*- coding: utf-8 -*- - -# Nemubot is a modulable IRC bot, built around XML configuration files. -# Copyright (C) 2012 Mercier Pierre-Olivier -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -from datetime import datetime -import shlex - -class Message: - def __init__ (self, orig, private=False): - self.cmd = orig.cmd - self.tags = orig.tags - self.params = orig.params - self.private = private - self.prefix = orig.prefix - self.nick = orig.nick - - # Special commands - if self.cmd == 'PRIVMSG' or self.cmd == 'NOTICE': - self.receivers = orig.decode(self.params[0]).split(',') - - # If CTCP, remove 0x01 - if len(self.params[1]) > 1 and (self.params[1][0] == 0x01 or self.params[1][1] == 0x01): - self.is_ctcp = True - self.text = orig.decode(self.params[1][1:len(self.params[1])-1]) - else: - self.is_ctcp = False - self.text = orig.decode(self.params[1]) - - # Split content by words - self.parse_content() - - else: - for i in range(0, len(self.params)): - self.params[i] = orig.decode(self.params[i]) - - - # TODO: here for legacy content - @property - def sender(self): - return self.prefix - @property - def channel(self): - return self.receivers[0] - - - def parse_content(self): - """Parse or reparse the message content""" - # Remove ! - if len(self.text) > 1 and self.text[0] == '!': - self.qual = "cmd" - self.text = self.text[1:].strip() - - # Split content by words - try: - self.cmds = shlex.split(self.text) - except ValueError: - self.cmds = self.text.split(' ') diff --git a/message/__init__.py b/message/__init__.py new file mode 100644 index 0000000..2b69b01 --- /dev/null +++ b/message/__init__.py @@ -0,0 +1,160 @@ +# -*- coding: utf-8 -*- + +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2014 nemunaire +# +# 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 + +class AbstractMessage: + + """This class represents an abstract message""" + + def __init__(self, server, date=None, to=None, to_response=None, frm=None): + """Initialize an abstract message + + Arguments: + server -- the servir identifier + date -- time of the message reception, default: now + to -- list of recipients + to_response -- if channel(s) where send the response differ + frm -- the sender + """ + + self.server = server + self.date = datetime.now(timezone.utc) if date is None else date + self.to = to if to is not None else list() + self._to_response = to_response if to_response is None or isinstance(to_response, list) else [ to_response ] + self.frm = frm # None allowed when it designate this bot + + + @property + def to_response(self): + if self._to_response is not None: + return self._to_response + else: + 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 + return self.to_response[0] + + @property + def nick(self): + # TODO: this is for legacy modules + return self.frm + + + def accept(self, visitor): + visitor.visit(self) + + + def export_args(self, without=list()): + if not isinstance(without, list): + without = [ without ] + + ret = { + "server": self.server, + "date": self.date, + "to": self.to, + "to_response": self._to_response, + "frm": self.frm + } + + for w in without: + if w in ret: + del ret[w] + + return ret + + +class TextMessage(AbstractMessage): + + """This class represent a simple message send to someone""" + + def __init__(self, message, *args, **kargs): + """Initialize a message with no particular specificity + + Argument: + message -- the parsed message + """ + + AbstractMessage.__init__(self, *args, **kargs) + + self.message = message + + def __str__(self): + return self.message + + @property + def text(self): + # TODO: this is for legacy modules + return self.message + + +class DirectAsk(TextMessage): + + """This class represents a message to this bot""" + + def __init__(self, designated, *args, **kargs): + """Initialize a message to a specific person + + Argument: + designated -- the user designated by the message + """ + + TextMessage.__init__(self, *args, **kargs) + + self.designated = designated + + + def respond(self, message): + return DirectAsk(self.frm, + message, + server=self.server, + to=self.to_response) + + +class Command(AbstractMessage): + + """This class represents a specialized TextMessage""" + + def __init__(self, cmd, args=None, *nargs, **kargs): + AbstractMessage.__init__(self, *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 + + +class OwnerCommand(Command): + + """This class represents a special command incomming from the owner""" + + pass diff --git a/message/printer/IRC.py b/message/printer/IRC.py new file mode 100644 index 0000000..83155ac --- /dev/null +++ b/message/printer/IRC.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- + +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2014 nemunaire +# +# 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 message import TextMessage +from message.visitor import AbstractVisitor + +class IRC(AbstractVisitor): + + def __init__(self): + self.pp = "" + + + def visit_TextMessage(self, msg): + self.pp += "PRIVMSG %s :" % ",".join(msg.to) + if isinstance(msg.message, str): + self.pp += msg.message + else: + msg.message.accept(self) + self.pp += "\r\n" + + + def visit_DirectAsk(self, msg): + 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): + res = TextMessage(msg.message, + server=msg.server, date=msg.date, + to=msg.to, frm=msg.frm) + res.accept(self) + + if len(others): + res = TextMessage("%s: %s" % (msg.designated, msg.message), + server=msg.server, date=msg.date, + to=others, frm=msg.frm) + res.accept(self) + + + def visit_Command(self, msg): + res = TextMessage("!%s%s%s" % (msg.cmd, + " " if len(msg.args) else "", + " ".join(msg.args)), + server=msg.server, date=msg.date, + to=msg.to, frm=msg.frm) + res.accept(self) + + + def visit_OwnerCommand(self, msg): + res = TextMessage("`%s%s%s" % (msg.cmd, + " " if len(msg.args) else "", + " ".join(msg.args)), + server=msg.server, date=msg.date, + to=msg.to, frm=msg.frm) + res.accept(self) diff --git a/message/printer/__init__.py b/message/printer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/message/visitor.py b/message/visitor.py new file mode 100644 index 0000000..7328254 --- /dev/null +++ b/message/visitor.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2014 nemunaire +# +# 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 AbstractVisitor: + + def visit(self, obj): + """Visit a node""" + method_name = "visit_%s" % obj.__class__.__name__ + method = getattr(self, method_name) + return method(obj) diff --git a/modules/alias.py b/modules/alias.py index 370ac1d..314f545 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -5,8 +5,10 @@ import re import sys from datetime import datetime, timezone +import shlex from hooks import hook +from message import TextMessage, Command nemubotversion = 3.4 @@ -34,10 +36,8 @@ def set_variable(name, value, creator): DATAS.getNode("variables").addChild(var) def get_variable(name, msg=None): - if name == "sender": - return msg.sender - elif name == "nick": - return msg.nick + if name == "sender" or name == "from" or name == "nick": + return msg.frm elif name == "chan" or name == "channel": return msg.channel elif name == "date": @@ -141,35 +141,25 @@ def replace_variables(cnt, msg=None): return " ".join(cnt) -@hook("all_post") -def treat_variables(res): - for i in range(0, len(res.messages)): - if isinstance(res.messages[i], list): - res.messages[i] = replace_variables(", ".join(res.messages[i]), res) - else: - res.messages[i] = replace_variables(res.messages[i], res) - return res - -@hook("pre_PRIVMSG_cmd") +@hook("pre_Command") def treat_alias(msg): - if msg.cmds[0] in DATAS.getNode("aliases").index: - oldcmd = msg.cmds[0] - msg.text = msg.text.replace(msg.cmds[0], - DATAS.getNode("aliases").index[msg.cmds[0]]["origin"], 1) - - msg.qual = "def" - msg.parse_content() + if msg.cmd in DATAS.getNode("aliases").index: + txt = DATAS.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[1:] + msg.args, **msg.export_args()) # Avoid infinite recursion - if oldcmd == msg.cmds[0]: - return msg - else: - return treat_alias(msg) + if msg.cmd != nmsg.cmd: + return nmsg + + return msg - else: - msg.text = replace_variables(msg.text, msg) - msg.parse_content() - return msg @hook("ask_default") def parseask(msg): diff --git a/modules/events.py b/modules/events.py index af20867..d0d9e6e 100644 --- a/modules/events.py +++ b/modules/events.py @@ -71,8 +71,7 @@ def start_countdown(msg): strnd["server"] = msg.server strnd["channel"] = msg.channel strnd["proprio"] = msg.nick - strnd["sender"] = msg.sender - strnd["start"] = msg.tags["time"] + strnd["start"] = msg.date strnd["name"] = msg.cmds[1] DATAS.addChild(strnd) @@ -84,7 +83,7 @@ def start_countdown(msg): result3 = re.match("(.*[^0-9])?([0-2]?[0-9]):([0-5]?[0-9])(:([0-5]?[0-9]))?", msg.cmds[2]) if result2 is not None or result3 is not None: try: - now = msg.tags["time"] + now = msg.date if result3 is None or result3.group(5) is None: sec = 0 else: sec = int(result3.group(5)) if result3 is None or result3.group(3) is None: minu = 0 @@ -109,7 +108,7 @@ def start_countdown(msg): raise IRCException("Mauvais format de date pour l'événement %s. Il n'a pas été créé." % msg.cmds[1]) elif result1 is not None and len(result1) > 0: - strnd["end"] = msg.tags["time"] + strnd["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)) @@ -131,11 +130,13 @@ def start_countdown(msg): save() if "end" in strnd: return Response("%s commencé le %s et se terminera le %s." % - (msg.cmds[1], msg.tags["time"].strftime("%A %d %B %Y à %H:%M:%S"), - strnd.getDate("end").strftime("%A %d %B %Y à %H:%M:%S"))) + (msg.cmds[1], msg.date.strftime("%A %d %B %Y à %H:%M:%S"), + strnd.getDate("end").strftime("%A %d %B %Y à %H:%M:%S")), + nick=msg.frm) else: return Response("%s commencé le %s"% (msg.cmds[1], - msg.tags["time"].strftime("%A %d %B %Y à %H:%M:%S"))) + msg.date.strftime("%A %d %B %Y à %H:%M:%S")), + nick=msg.frm) @hook("cmd_hook", "end") @hook("cmd_hook", "forceend") @@ -145,7 +146,7 @@ def end_countdown(msg): if msg.cmds[1] in DATAS.index: if DATAS.index[msg.cmds[1]]["proprio"] == msg.nick or (msg.cmds[0] == "forceend" and msg.is_owner): - duration = countdown(msg.tags["time"] - DATAS.index[msg.cmds[1]].getDate("start")) + duration = countdown(msg.date - DATAS.index[msg.cmds[1]].getDate("start")) del_event(DATAS.index[msg.cmds[1]]["id"]) DATAS.delChild(DATAS.index[msg.cmds[1]]) save() @@ -182,9 +183,9 @@ def parseanswer(msg): if DATAS.index[msg.cmds[0]].name == "strend": if DATAS.index[msg.cmds[0]].hasAttribute("end"): - res.append_message("%s commencé il y a %s et se terminera dans %s." % (msg.cmds[0], countdown(msg.tags["time"] - DATAS.index[msg.cmds[0]].getDate("start")), countdown(DATAS.index[msg.cmds[0]].getDate("end") - msg.tags["time"]))) + res.append_message("%s commencé il y a %s et se terminera dans %s." % (msg.cmds[0], countdown(msg.date - DATAS.index[msg.cmds[0]].getDate("start")), countdown(DATAS.index[msg.cmds[0]].getDate("end") - msg.date))) else: - res.append_message("%s commencé il y a %s." % (msg.cmds[0], countdown(msg.tags["time"] - DATAS.index[msg.cmds[0]].getDate("start")))) + res.append_message("%s commencé il y a %s." % (msg.cmds[0], countdown(msg.date - DATAS.index[msg.cmds[0]].getDate("start")))) else: res.append_message(countdown_format(DATAS.index[msg.cmds[0]].getDate("start"), DATAS.index[msg.cmds[0]]["msg_before"], DATAS.index[msg.cmds[0]]["msg_after"])) return res @@ -222,7 +223,6 @@ def parseask(msg): evt["server"] = msg.server evt["channel"] = msg.channel evt["proprio"] = msg.nick - evt["sender"] = msg.sender evt["name"] = name.group(1) evt["start"] = extDate evt["msg_after"] = msg_after @@ -237,12 +237,12 @@ def parseask(msg): evt["server"] = msg.server evt["channel"] = msg.channel evt["proprio"] = msg.nick - evt["sender"] = msg.sender evt["name"] = name.group(1) evt["msg_before"] = texts.group (2) DATAS.addChild(evt) save() - return Response("Nouvelle commande !%s ajoutée avec succès." % name.group(1)) + return Response("Nouvelle commande !%s ajoutée avec succès." % name.group(1), + channel=msg.channel) else: raise IRCException("Veuillez indiquez les messages d'attente et d'après événement entre guillemets.") diff --git a/modules/mediawiki.py b/modules/mediawiki.py index edb54e6..855238b 100644 --- a/modules/mediawiki.py +++ b/modules/mediawiki.py @@ -107,7 +107,7 @@ def search(site, term, ssl=False): web.striphtml(itm["snippet"].replace("<span class='searchmatch'>", "\x03\x02").replace("</span>", "\x03\x02"))) -@hook("in_PRIVMSG_cmd", "mediawiki") +@hook("cmd_hook", "mediawiki") def cmd_mediawiki(msg): """Read an article on a MediaWiki""" if len(msg.cmds) < 3: @@ -118,7 +118,7 @@ def cmd_mediawiki(msg): channel=msg.receivers) -@hook("in_PRIVMSG_cmd", "search_mediawiki") +@hook("cmd_hook", "search_mediawiki") def cmd_srchmediawiki(msg): """Search an article on a MediaWiki""" if len(msg.cmds) < 3: @@ -132,7 +132,7 @@ def cmd_srchmediawiki(msg): return res -@hook("in_PRIVMSG_cmd", "wikipedia") +@hook("cmd_hook", "wikipedia") def cmd_wikipedia(msg): if len(msg.cmds) < 3: raise IRCException("indicate a lang and a term to search") diff --git a/modules/more.py b/modules/more.py index 61dcdd4..4e09a5a 100644 --- a/modules/more.py +++ b/modules/more.py @@ -20,6 +20,7 @@ import logging +from message import TextMessage, DirectAsk from hooks import hook nemubotversion = 3.4 @@ -29,8 +30,7 @@ logger = logging.getLogger("nemubot.response") class Response: def __init__(self, message=None, channel=None, nick=None, server=None, nomore="No more message", title=None, more="(suite) ", - count=None, ctcp=False, shown_first_count=-1, - line_treat=None): + count=None, shown_first_count=-1, line_treat=None): self.nomore = nomore self.more = more self.line_treat = line_treat @@ -38,12 +38,10 @@ class Response: self.server = server self.messages = list() self.alone = True - self.is_ctcp = ctcp if message is not None: self.append_message(message, shown_first_count=shown_first_count) self.elt = 0 # Next element to display - self.sender = None self.channel = channel self.nick = nick self.count = count @@ -59,14 +57,6 @@ class Response: else: return [ self.channel ] - def set_sender(self, sender): - if sender is None or sender.find("!") < 0: - if sender is not None: - logger.warn("Bad sender provided in Response, it will be ignored.", stack_info=True) - self.sender = None - else: - self.sender = sender - def append_message(self, message, title=None, shown_first_count=-1): if type(message) is str: message = message.split('\n') @@ -120,13 +110,19 @@ class Response: if len(self.rawtitle) <= 0: self.rawtitle = None - def treat_ctcp(self, content): - if self.is_ctcp: - return "\x01" + content + "\x01" - else: - return content - def get_message(self): + def accept(self, visitor): + visitor.visit(self.next_response()) + + + def next_response(self, maxlen=440): + if self.nick: + return DirectAsk(self.nick, self.get_message(maxlen - len(self.nick) - 2), server=None, to=self.receivers) + else: + return TextMessage(self.get_message(maxlen), server=None, to=self.receivers) + + + def get_message(self, maxlen): if self.alone and len(self.messages) > 1: self.alone = False @@ -134,7 +130,7 @@ class Response: if hasattr(self.nomore, '__call__'): res = self.nomore(self) if res is None: - return self.treat_ctcp("No more message") + return "No more message" elif isinstance(res, Response): self.__dict__ = res.__dict__ elif isinstance(res, list): @@ -145,62 +141,59 @@ class Response: raise Exception("Type returned by nomore (%s) is not handled here." % type(res)) return self.get_message() else: - return self.treat_ctcp(self.nomore) + 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() msg = "" - if self.channel is not None and self.nick is not None: - msg += self.nick + ": " - if self.title is not None: if self.elt > 0: msg += self.title + " " + self.more + ": " else: msg += self.title + ": " - if self.elt > 0: + elif self.elt > 0: msg += "[…] " elts = self.messages[0][self.elt:] if isinstance(elts, list): for e in elts: - if len(msg) + len(e) > 430: + if len(msg) + len(e) > maxlen - 3: msg += "[…]" self.alone = False - return self.treat_ctcp(msg) + return msg else: msg += e + ", " self.elt += 1 self.pop() - return self.treat_ctcp(msg[:len(msg)-2]) + return msg[:len(msg)-2] else: - if len(elts.encode()) <= 432: + if len(elts.encode()) <= maxlen: self.pop() if self.count is not None: - return self.treat_ctcp(msg + elts + (self.count % len(self.messages))) + return msg + elts + (self.count % len(self.messages)) else: - return self.treat_ctcp(msg + elts) + return msg + elts else: words = elts.split(' ') - if len(words[0].encode()) > 432 - len(msg.encode()): - self.elt += 432 - len(msg.encode()) - return self.treat_ctcp(msg + elts[:self.elt] + "[…]") + if len(words[0].encode()) > maxlen - len(msg.encode()): + self.elt += maxlen - len(msg.encode()) + return msg + elts[:self.elt] + "[…]" for w in words: - if len(msg.encode()) + len(w.encode()) > 431: + if len(msg.encode()) + len(w.encode()) >= maxlen: msg += "[…]" self.alone = False - return self.treat_ctcp(msg) + return msg else: msg += w + " " self.elt += len(w) + 1 self.pop() - return self.treat_ctcp(msg) + return msg SERVERS = dict() @@ -209,18 +202,17 @@ SERVERS = dict() def parseresponse(res): # TODO: handle inter-bot communication NOMORE # TODO: check that the response is not the one already saved - rstr = res.get_message() - - if not res.alone: + if isinstance(res, Response): if res.server not in SERVERS: SERVERS[res.server] = dict() for receiver in res.receivers: - SERVERS[res.server][receiver] = res - - ret = list() - for channel in res.receivers: - ret.append("%s %s :%s" % ("NOTICE" if res.is_ctcp else "PRIVMSG", channel, rstr)) - return ret + if receiver in SERVERS[res.server]: + nw, bk = SERVERS[res.server][receiver] + else: + nw, bk = None, None + if nw != res: + SERVERS[res.server][receiver] = (res, bk) + return res @hook("cmd_hook", "more") @@ -228,9 +220,13 @@ def cmd_more(msg): """Display next chunck of the message""" res = list() if msg.server in SERVERS: - for receiver in msg.receivers: + for receiver in msg.to_response: if receiver in SERVERS[msg.server]: - res.append(SERVERS[msg.server][receiver]) + nw, bk = SERVERS[msg.server][receiver] + if nw is not None and not nw.alone: + bk = nw + SERVERS[msg.server][receiver] = None, bk + res.append(bk) return res @@ -239,8 +235,12 @@ def cmd_next(msg): """Display the next information include in the message""" res = list() if msg.server in SERVERS: - for receiver in msg.receivers: + for receiver in msg.to_response: if receiver in SERVERS[msg.server]: - SERVERS[msg.server][receiver].pop() - res.append(SERVERS[msg.server][receiver]) + nw, bk = SERVERS[msg.server][receiver] + if nw is not None and not nw.alone: + bk = nw + SERVERS[msg.server][receiver] = None, bk + bk.pop() + res.append(bk) return res diff --git a/modules/watchWebsite/__init__.py b/modules/watchWebsite/__init__.py index 3970ea7..2bd0d4c 100644 --- a/modules/watchWebsite/__init__.py +++ b/modules/watchWebsite/__init__.py @@ -56,7 +56,7 @@ def del_site(msg): site = DATAS.index[url] for a in site.getNodes("alert"): if a["channel"] == msg.channel: - if not (msg.sender == a["sender"] or msg.is_owner): + if not (msg.frm == a["nick"] or msg.is_owner): raise IRCException("vous ne pouvez pas supprimer cette URL.") site.delChild(a) if not site.hasNode("alert"): @@ -82,7 +82,7 @@ def add_site(msg, diffType="diff"): raise IRCException("je ne peux pas surveiller cette URL") alert = ModuleState("alert") - alert["sender"] = msg.sender + alert["nick"] = msg.nick alert["server"] = msg.server alert["channel"] = msg.channel alert["message"] = "{url} a changé !" diff --git a/modules/worldcup.py b/modules/worldcup.py index cd093ab..2426c7c 100644 --- a/modules/worldcup.py +++ b/modules/worldcup.py @@ -31,7 +31,6 @@ def start_watch(msg): w["server"] = msg.server w["channel"] = msg.channel w["proprio"] = msg.nick - w["sender"] = msg.sender w["start"] = datetime.now(timezone.utc) DATAS.addChild(w) save() diff --git a/modules/ycc.py b/modules/ycc.py index ca70432..6bf5ba8 100644 --- a/modules/ycc.py +++ b/modules/ycc.py @@ -8,11 +8,10 @@ from urllib.parse import quote from urllib.request import urlopen from hooks import hook +from message import TextMessage nemubotversion = 3.4 -from more import Response - def help_full(): return "!ycc [<url>]: with an argument, reduce the given <url> thanks to ycc.fr; without argument, reduce the last URL said on the current channel." @@ -22,24 +21,28 @@ def gen_response(res, msg, srv): if res is None: raise IRCException("la situation est embarassante, il semblerait que YCC soit down :(") elif isinstance(res, str): - return Response("URL pour %s : %s" % (srv, res), msg.channel) + return TextMessage("URL pour %s : %s" % (srv, res), server=None, to=msg.to_response) else: raise IRCException("mauvaise URL : %s" % srv) @hook("cmd_hook", "ycc") def cmd_ycc(msg): + minify = list() + if len(msg.cmds) == 1: global LAST_URLS if msg.channel in LAST_URLS and len(LAST_URLS[msg.channel]) > 0: - msg.cmds.append(LAST_URLS[msg.channel].pop()) + minify.append(LAST_URLS[msg.channel].pop()) else: raise IRCException("je n'ai pas d'autre URL à réduire.") if len(msg.cmds) > 5: raise IRCException("je ne peux pas réduire autant d'URL d'un seul coup.") + else: + minify += msg.cmds[1:] res = list() - for url in msg.cmds[1:]: + for url in minify: o = urlparse(url, "http") if o.scheme != "": snd_url = "http://ycc.fr/redirection/create/" + quote(url, "/:%@&=?") diff --git a/prompt/builtins.py b/prompt/builtins.py index ded17ea..6887852 100644 --- a/prompt/builtins.py +++ b/prompt/builtins.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 <http://www.gnu.org/licenses/>. +import imp import logging import os import xmlparser @@ -94,7 +95,8 @@ def load_file(filename, context): # Unexisting file, assume a name was passed, import the module! else: - __import__(filename) + tt = __import__(filename) + imp.reload(tt) def load(toks, context, prompt): diff --git a/server/IRC.py b/server/IRC.py index d040493..2410008 100644 --- a/server/IRC.py +++ b/server/IRC.py @@ -23,14 +23,15 @@ import time import shlex from channel import Channel -from message import Message -import server +import message +from message.printer.IRC import IRC as IRCPrinter from server.socket import SocketServer -class IRCServer(SocketServer): +class IRC(SocketServer): def __init__(self, node, nick, owner, realname): self.id = nick + "@" + node["host"] + ":" + node["port"] + self.printer = IRCPrinter SocketServer.__init__(self, node["host"], node["port"], @@ -60,18 +61,18 @@ class IRCServer(SocketServer): # Register CTCP capabilities self.ctcp_capabilities = dict() - def _ctcp_clientinfo(msg): + def _ctcp_clientinfo(msg, cmds): """Response to CLIENTINFO CTCP message""" - return _ctcp_response(" ".join(self.ctcp_capabilities.keys())) + return " ".join(self.ctcp_capabilities.keys()) - def _ctcp_dcc(msg): + def _ctcp_dcc(msg, cmds): """Response to DCC CTCP message""" try: - ip = srv.toIP(int(msg.cmds[3])) - port = int(msg.cmds[4]) + ip = srv.toIP(int(cmds[3])) + port = int(cmds[4]) conn = DCC(srv, msg.sender) except: - return _ctcp_response("ERRMSG invalid parameters provided as DCC CTCP request") + 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) @@ -80,27 +81,20 @@ class IRCServer(SocketServer): conn.send_dcc("Hello %s!" % conn.nick) else: self.logger.error("DCC: unable to connect to %s:%d", ip, port) - return _ctcp_response("ERRMSG unable to connect to %s:%d" % (ip, port)) + return "ERRMSG unable to connect to %s:%d" % (ip, port) import bot - self.ctcp_capabilities["ACTION"] = lambda msg: print ("ACTION receive: %s" % msg.text) + self.ctcp_capabilities["ACTION"] = lambda msg, cmds: print ("ACTION receive: %s" % cmds) self.ctcp_capabilities["CLIENTINFO"] = _ctcp_clientinfo #self.ctcp_capabilities["DCC"] = _ctcp_dcc - self.ctcp_capabilities["FINGER"] = lambda msg: _ctcp_response( - "VERSION nemubot v%s" % bot.__version__) - self.ctcp_capabilities["NEMUBOT"] = lambda msg: _ctcp_response( - "NEMUBOT %s" % bot.__version__) - self.ctcp_capabilities["PING"] = lambda msg: _ctcp_response( - "PING %s" % " ".join(msg.cmds[1:])) - self.ctcp_capabilities["SOURCE"] = lambda msg: _ctcp_response( - "SOURCE https://github.com/nemunaire/nemubot") - self.ctcp_capabilities["TIME"] = lambda msg: _ctcp_response( - "TIME %s" % (datetime.now())) - self.ctcp_capabilities["USERINFO"] = lambda msg: _ctcp_response( - "USERINFO %s" % self.realname) - self.ctcp_capabilities["VERSION"] = lambda msg: _ctcp_response( - "VERSION nemubot v%s" % bot.__version__) + self.ctcp_capabilities["FINGER"] = lambda msg, cmds: "VERSION nemubot v%s" % bot.__version__ + self.ctcp_capabilities["NEMUBOT"] = lambda msg, cmds: "NEMUBOT %s" % bot.__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" % bot.__version__ self.logger.debug("CTCP capabilities setup: %s", ", ".join(self.ctcp_capabilities)) @@ -190,6 +184,18 @@ class IRCServer(SocketServer): self.write("JOIN " + msg.decode(msg.params[1])) self.hookscmd["INVITE"] = _on_invite + # 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.nick, res)) + self.hookscmd["PRIVMSG"] = _on_ctcp + def _open(self): if SocketServer._open(self): @@ -215,31 +221,9 @@ class IRCServer(SocketServer): if msg.cmd in self.hookscmd: self.hookscmd[msg.cmd](msg) - else: - mes = msg.to_message() - mes.raw = msg.raw - - if hasattr(mes, "receivers"): - # Private message: prepare response - for i in range(len(mes.receivers)): - if mes.receivers[i] == self.nick: - mes.receivers[i] = mes.nick - - if (mes.cmd == "PRIVMSG" or mes.cmd == "NOTICE") and mes.is_ctcp: - if mes.cmds[0] in self.ctcp_capabilities: - res = self.ctcp_capabilities[mes.cmds[0]](mes) - else: - res = _ctcp_response("ERRMSG Unknown or unimplemented CTCP request") - if res is not None: - res = res % mes.nick - self.write(res) - - else: - yield mes - - -def _ctcp_response(msg): - return "NOTICE %%s :\x01%s\x01" % msg + mes = msg.to_message(self) + if mes is not None: + yield mes mgx = re.compile(b'''^(?:@(?P<tags>[^ ]+)\ )? @@ -303,6 +287,11 @@ class IRCMessage: self.tags[key] = value + @property + def is_ctcp(self): + 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""" if isinstance(s, bytes): @@ -313,8 +302,6 @@ class IRCMessage: return s - def to_message(self): - return Message(self) def to_irc_string(self, client=True): """Pretty print the message to close to original input string @@ -336,3 +323,43 @@ class IRCMessage: res += " :" + self.decode(self.params[-1]) return res + + + def to_message(self, srv): + if self.cmd == "PRIVMSG" or self.cmd == "NOTICE": + + receivers = self.decode(self.params[0]).split(',') + + common_args = { + "server": srv.id, + "date": self.tags["time"], + "to": receivers, + "to_response": [r if r != srv.nick else self.nick for r in receivers], + "frm": self.nick + } + + # 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 len(text) > 1 and text[0] == '!': + text = text[1:].strip() + + # Split content by words + try: + args = shlex.split(text) + except ValueError: + args = text.split(' ') + + return message.Command(cmd=args[0], args=args[1:], **common_args) + + elif text.find(srv.nick) == 0 and len(text) > len(srv.nick) + 2 and text[len(srv.nick)] == ":": + text = text[len(srv.nick) + 1:].strip() + return message.DirectAsk(designated=srv.nick, message=text, **common_args) + + else: + return message.TextMessage(message=text, **common_args) + + return None diff --git a/server/__init__.py b/server/__init__.py index 28cf765..9c7ee55 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -89,3 +89,18 @@ class AbstractServer(io.IOBase): def exception(self): """Exception occurs in fd""" print("Unhandle file descriptor exception on server " + self.id) + + + def send_response(self, response): + """Send a formated Message class""" + if response is None: + return + + elif isinstance(response, list): + for r in response: + self.send_response(r) + + else: + vprnt = self.printer() + response.accept(vprnt) + self.write(vprnt.pp) From f9ee1fe898a1f58bb08ef38872d452b186d7daf0 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 9 Oct 2014 07:30:04 +0200 Subject: [PATCH 171/674] Can use countdown without timezone --- modules/birthday.py | 3 +-- tools/countdown.py | 21 +++++++++++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/modules/birthday.py b/modules/birthday.py index 88fef2d..1ec8999 100644 --- a/modules/birthday.py +++ b/modules/birthday.py @@ -4,8 +4,7 @@ import re import sys -from datetime import datetime -from datetime import date +from datetime import date, datetime from hooks import hook from tools.countdown import countdown_format diff --git a/tools/countdown.py b/tools/countdown.py index 5707085..ad77865 100644 --- a/tools/countdown.py +++ b/tools/countdown.py @@ -67,12 +67,21 @@ def countdown_format(date, msg_before, msg_after, tz=None): time.tzset() #Calculate time before the date - if datetime.now(timezone.utc) > date: - sentence_c = msg_after - delta = datetime.now(timezone.utc) - date - else: - sentence_c = msg_before - delta = date - datetime.now(timezone.utc) + try: + if datetime.now(timezone.utc) > date: + sentence_c = msg_after + delta = datetime.now(timezone.utc) - date + else: + sentence_c = msg_before + delta = date - datetime.now(timezone.utc) + except TypeError: + if datetime.now() > date: + sentence_c = msg_after + delta = datetime.now() - date + else: + sentence_c = msg_before + delta = date - datetime.now() + if tz != None: os.environ['TZ'] = oldtz From 4dd837cf4b0f23d20d8961852a653155b2bbfbb8 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 9 Oct 2014 07:37:52 +0200 Subject: [PATCH 172/674] Change add_server behaviour, fix IRC parameters parsing, can use with Python statement for managing server scope --- bot.py | 19 ++++--- modules/cmd_server.py | 5 +- nemubot.py | 7 ++- prompt/builtins.py | 64 +++++++++++++++++++---- server/IRC.py | 117 +++++++++++++++++++++++++++++------------- server/__init__.py | 53 ++++++++++++++++--- server/socket.py | 19 ++++++- tools/__init__.py | 33 ++++++++++++ 8 files changed, 249 insertions(+), 68 deletions(-) diff --git a/bot.py b/bot.py index b136b26..48f9c5e 100644 --- a/bot.py +++ b/bot.py @@ -25,7 +25,7 @@ import threading import time import uuid -__version__ = '3.4.dev1' +__version__ = '3.4.dev2' __author__ = 'nemunaire' from consumer import Consumer, EventConsumer, MessageConsumer @@ -33,8 +33,6 @@ from event import ModuleEvent from hooks.messagehook import MessageHook from hooks.manager import HooksManager from networkbot import NetworkBot -from server.IRC import IRC as IRCServer -from server.DCC import DCC logger = logging.getLogger("nemubot.bot") @@ -312,13 +310,20 @@ class Bot(threading.Thread): c.start() - def add_server(self, node, nick, owner, realname): - """Add a new server to the context""" - srv = IRCServer(node, nick, owner, realname) + def add_server(self, srv, autoconnect=False): + """Add a new server to the context + + Arguments: + srv -- a concrete AbstractServer instance + autoconnect -- connect after add? + """ + if srv.id not in self.servers: self.servers[srv.id] = srv - srv.open() + if autoconnect: + srv.open() return True + else: return False diff --git a/modules/cmd_server.py b/modules/cmd_server.py index 957138a..9388e6f 100644 --- a/modules/cmd_server.py +++ b/modules/cmd_server.py @@ -19,8 +19,9 @@ import traceback import sys -from networkbot import NetworkBot from hooks import hook +from message import TextMessage +from networkbot import NetworkBot nemubotversion = 3.4 NODATA = True @@ -198,7 +199,7 @@ def send(data, toks, context, prompt): print ("send: not enough arguments.") return - srv.send_msg_final(chan, toks[rd]) + srv.send_response(TextMessage(" ".join(toks[rd:]), server=None, to=[chan])) return "done" @hook("prompt_cmd", "zap") diff --git a/nemubot.py b/nemubot.py index e56297e..c4a6682 100755 --- a/nemubot.py +++ b/nemubot.py @@ -1,8 +1,8 @@ -#!/usr/bin/python3 +#!/usr/bin/env python3.2 # -*- coding: utf-8 -*- -# Nemubot is a modulable IRC bot, built around XML configuration files. -# Copyright (C) 2012 Mercier Pierre-Olivier +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2014 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 @@ -21,7 +21,6 @@ import imp import logging import os import sys -import traceback import bot import prompt diff --git a/prompt/builtins.py b/prompt/builtins.py index 6887852..b653694 100644 --- a/prompt/builtins.py +++ b/prompt/builtins.py @@ -19,10 +19,12 @@ import imp import logging import os -import xmlparser logger = logging.getLogger("nemubot.prompt.builtins") +from server.IRC import IRC as IRCServer +import xmlparser + def end(toks, context, prompt): """Quit the prompt for reload or exit""" if toks[0] == "refresh": @@ -67,16 +69,58 @@ def load_file(filename, context): or config.getName() == "nemubotconfig"): # Preset each server in this file for server in config.getNodes("server"): - ip = server["ip"] if server.hasAttribute("ip") else config["ip"] - nick = server["nick"] if server.hasAttribute("nick") else config["nick"] - owner = server["owner"] if server.hasAttribute("owner") else config["owner"] - realname = server["realname"] if server.hasAttribute("realname") else config["realname"] - if context.add_server(server, nick, owner, realname): - print("Server `%s:%s' successfully added." % - (server["host"], server["port"])) + opts = { + "host": server["host"], + "ssl": server.hasAttribute("ssl") and server["ssl"].lower() == "true", + + "nick": server["nick"] if server.hasAttribute("nick") else config["nick"], + "owner": server["owner"] if server.hasAttribute("owner") else config["owner"], + } + + # Optional keyword arguments + for optional_opt in [ "port", "realname", "password", "encoding", "caps" ]: + if server.hasAttribute(optional_opt): + opts[optional_opt] = server[optional_opt] + elif optional_opt in config: + opts[optional_opt] = config[optional_opt] + + # Command to send on connection + if "on_connect" in server: + def on_connect(): + yield server["on_connect"] + opts["on_connect"] = on_connect + + # Channels to autojoin on connection + if server.hasNode("channel"): + opts["channels"] = list() + for chn in server.getNodes("channel"): + opts["channels"].append((chn["name"], chn["password"]) if chn["password"] is not None else chn["name"]) + + # Server/client capabilities + if "caps" in server or "caps" in config: + capsl = (server["caps"] if server.hasAttribute("caps") else config["caps"]).lower() + if capsl == "no" or capsl == "off" or capsl == "false": + opts["caps"] = None + else: + opts["caps"] = capsl.split(',') else: - print("Server `%s:%s' already added, skiped." % - (server["host"], server["port"])) + opts["caps"] = list() + + # Bind the protocol asked to the corresponding implementation + if "protocol" not in server or server["protocol"] == "irc": + srvcls = IRCServer + else: + raise Exception("Unhandled protocol '%s'" % server["protocol"]) + + # Initialize the server + srv = srvcls(**opts) + + # Add the server in the context + if context.add_server(srv, + "autoconnect" in server and server["autoconnect"].lower() != "false"): + print("Server '%s' successfully added." % srv.id) + else: + print("Can't add server '%s'." % srv.id) # Load module and their configuration for mod in config.getNodes("module"): diff --git a/server/IRC.py b/server/IRC.py index 2410008..fdb4379 100644 --- a/server/IRC.py +++ b/server/IRC.py @@ -26,37 +26,46 @@ from channel import Channel import message from message.printer.IRC import IRC as IRCPrinter from server.socket import SocketServer +import tools class IRC(SocketServer): - def __init__(self, node, nick, owner, realname): - self.id = nick + "@" + node["host"] + ":" + node["port"] - self.printer = IRCPrinter - SocketServer.__init__(self, - node["host"], - node["port"], - node["password"], - node.hasAttribute("ssl") and node["ssl"].lower() == "true") + def __init__(self, owner, nick="nemubot", host="localhost", port=6667, + ssl=False, password=None, realname="Nemubot", + encoding="utf-8", caps=None, channels=list(), + on_connect=None): + """Prepare a connection with an IRC server + Keyword arguments: + owner -- bot's owner + nick -- bot's nick + host -- host to join + port -- port on the host to reach + ssl -- is this server using a TLS socket + 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 (if a channel is password protected, give a tuple: (channel_name, password)) + on_connect -- generator to call when connection is done + """ + + self.id = nick + "@" + host + ":" + port + self.printer = IRCPrinter + SocketServer.__init__(self, host=host, port=port, ssl=ssl) + + self.password = password self.nick = nick self.owner = owner self.realname = realname - #Keep a list of connected channels + self.encoding = encoding + + # Keep a list of joined channels self.channels = dict() - if node.hasAttribute("encoding"): - self.encoding = node["encoding"] - else: - self.encoding = "utf-8" - - if node.hasAttribute("caps"): - if node["caps"].lower() == "no": - self.capabilities = None - else: - self.capabilities = node["caps"].split(",") - else: - self.capabilities = list() + # Server/client capabilities + self.capabilities = caps # Register CTCP capabilities self.ctcp_capabilities = dict() @@ -68,7 +77,7 @@ class IRC(SocketServer): def _ctcp_dcc(msg, cmds): """Response to DCC CTCP message""" try: - ip = srv.toIP(int(cmds[3])) + ip = tools.toIP(int(cmds[3])) port = int(cmds[4]) conn = DCC(srv, msg.sender) except: @@ -98,6 +107,7 @@ class IRC(SocketServer): self.logger.debug("CTCP capabilities setup: %s", ", ".join(self.ctcp_capabilities)) + # Register hooks on some IRC CMD self.hookscmd = dict() @@ -109,14 +119,15 @@ class IRC(SocketServer): # Respond to 001 def _on_connect(msg): # First, send user defined command - if node.hasAttribute("on_connect"): - self.write(node["on_connect"]) + if on_connect is not None: + for oc in on_connect(): + self.write(oc) # Then, JOIN some channels - for chn in node.getNodes("channel"): - if chn["password"] is not None: - self.write("JOIN %s %s" % (chn["name"], chn["password"])) + for chn in channels: + if isinstance(chn, tuple): + self.write("JOIN %s %s" % chn) else: - self.write("JOIN %s" % chn["name"]) + self.write("JOIN %s" % chn) self.hookscmd["001"] = _on_connect # Respond to ERROR @@ -141,9 +152,9 @@ class IRC(SocketServer): def _on_join(msg): if len(msg.params) == 0: return - for chname in msg.params[0].split(b","): + for chname in msg.decode(msg.params[0]).split(","): # Register the channel - chan = Channel(msg.decode(chname)) + chan = Channel(chname) self.channels[chname] = chan self.hookscmd["JOIN"] = _on_join # Respond to PART @@ -197,6 +208,8 @@ class IRC(SocketServer): self.hookscmd["PRIVMSG"] = _on_ctcp + # Open/close + def _open(self): if SocketServer._open(self): if self.password is not None: @@ -214,6 +227,10 @@ class IRC(SocketServer): return SocketServer._close(self) + # Writes: as inherited + + # Read + def read(self): for line in SocketServer.read(self): msg = IRCMessage(line, self.encoding) @@ -226,6 +243,8 @@ class IRC(SocketServer): yield mes +# Parsing stuff + mgx = re.compile(b'''^(?:@(?P<tags>[^ ]+)\ )? (?::(?P<prefix> (?P<nick>[^!@ ]+) @@ -269,7 +288,7 @@ class IRCMessage: self.cmd = self.decode(p.group("command")) # Parse params - if p.group("params") is not None: + 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) @@ -278,7 +297,13 @@ class IRCMessage: def add_tag(self, key, value=None): - """Add an IRCv3.2 Message Tags""" + """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": value = datetime.fromtimestamp(calendar.timegm(time.strptime(value, "%Y-%m-%dT%H:%M:%S.%fZ")), timezone.utc) @@ -289,11 +314,17 @@ class IRCMessage: @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""" + """Decode the content string usign a specific encoding + + Argument: + s -- string to decode + """ + if isinstance(s, bytes): try: s = s.decode() @@ -326,6 +357,12 @@ class IRCMessage: def to_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(',') @@ -344,6 +381,13 @@ class IRCMessage: 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() @@ -355,10 +399,11 @@ class IRCMessage: return message.Command(cmd=args[0], args=args[1:], **common_args) - elif text.find(srv.nick) == 0 and len(text) > len(srv.nick) + 2 and text[len(srv.nick)] == ":": - text = text[len(srv.nick) + 1:].strip() - return message.DirectAsk(designated=srv.nick, message=text, **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.TextMessage(message=text, **common_args) diff --git a/server/__init__.py b/server/__init__.py index 9c7ee55..c3f7509 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -38,6 +38,9 @@ class AbstractServer(io.IOBase): send_callback -- Callback when developper want to send a message """ + 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._sending_queue = queue.Queue() if send_callback is not None: @@ -46,8 +49,20 @@ class AbstractServer(io.IOBase): self._send_callback = self._write_select + # Open/close + + def __enter__(self): + self.open() + return self + + + def __exit__(self, type, value, traceback): + self.close() + + 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 self._open(): _rlist.append(self) _xlist.append(self) @@ -55,6 +70,7 @@ class AbstractServer(io.IOBase): 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) if self._close(): if self in _rlist: _rlist.remove(self) @@ -64,10 +80,18 @@ class AbstractServer(io.IOBase): _xlist.remove(self) + # Writes + def write(self, message): - """Send a message to the server using send_callback""" + """Asynchronymously send a message to the server using send_callback + + Argument: + message -- message to send + """ + self._send_callback(message) + def write_select(self): """Internal function used by the select function""" try: @@ -79,20 +103,27 @@ class AbstractServer(io.IOBase): except queue.Empty: pass + def _write_select(self, message): - """Send a message to the server safely through select""" + """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 Queue", message) if self not in _wlist: _wlist.append(self) - def exception(self): - """Exception occurs in fd""" - print("Unhandle file descriptor exception on server " + self.id) - def send_response(self, response): - """Send a formated Message class""" + """Send a formated Message class + + Argument: + response -- message to send + """ + if response is None: return @@ -104,3 +135,11 @@ class AbstractServer(io.IOBase): vprnt = self.printer() response.accept(vprnt) self.write(vprnt.pp) + + + # Exceptions + + def exception(self): + """Exception occurs in fd""" + self.logger.warning("Unhandle file descriptor exception on server %s", + self.id) diff --git a/server/socket.py b/server/socket.py index 302422f..33f7944 100644 --- a/server/socket.py +++ b/server/socket.py @@ -23,23 +23,28 @@ from server import AbstractServer class SocketServer(AbstractServer): - def __init__(self, host, port=6667, password=None, ssl=False): + def __init__(self, host, port, ssl=False): AbstractServer.__init__(self) self.host = host self.port = int(port) - self.password = password self.ssl = ssl self.socket = None self.readbuffer = b'' + def fileno(self): return self.socket.fileno() if self.socket else None + @property def connected(self): + """Indicator of the connection aliveness""" return self.socket is not None + + # Open/close + def _open(self): # Create the socket self.socket = socket.socket() @@ -60,6 +65,7 @@ class SocketServer(AbstractServer): return True + def _close(self): self._sending_queue.join() if self.connected: @@ -71,18 +77,27 @@ class SocketServer(AbstractServer): self.socket = None return True + + # Write + def _write(self, cnt): if not self.connected: return + self.socket.send(cnt) + def format(self, txt): if isinstance(txt, bytes): return txt + b'\r\n' else: return txt.encode() + b'\r\n' + + # Read + def read(self): if not self.connected: return + raw = self.socket.recv(1024) temp = (self.readbuffer + raw).split(b'\r\n') self.readbuffer = temp.pop() diff --git a/tools/__init__.py b/tools/__init__.py index e69de29..bce3ef0 100644 --- a/tools/__init__.py +++ b/tools/__init__.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- + +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2014 nemunaire +# +# 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 + +def intToIP(n): + ip = "" + for i in range(0,4): + mod = n % 256 + ip = "%d.%s" % (mod, ip) + n = (n - mod) / 256 + return ip[:len(ip) - 1] + +def ipToInt(ip): + sum = 0 + for b in ip.split("."): + sum = 256 * sum + int(b) + return sum From 4776fbe93176aeef2f56f934ff99297219428652 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 9 Oct 2014 07:39:38 +0200 Subject: [PATCH 173/674] Modify context reload for better maintainability --- bot.py | 31 +++++++++++++++---------------- hooks/__init__.py | 10 ++++++++++ message/__init__.py | 11 +++++++++++ message/printer/__init__.py | 23 +++++++++++++++++++++++ prompt/__init__.py | 4 ++++ server/__init__.py | 9 +++++++++ tools/__init__.py | 11 +++++++++++ 7 files changed, 83 insertions(+), 16 deletions(-) diff --git a/bot.py b/bot.py index 48f9c5e..b041e6c 100644 --- a/bot.py +++ b/bot.py @@ -429,6 +429,9 @@ class Bot(threading.Thread): def hotswap(bak): bak.stop = True + if bak.event_timer is not None: + bak.event_timer.cancel() + new = Bot(str(bak.ip), bak.modules_paths, bak.data_path) new.servers = bak.servers new.modules = bak.modules @@ -436,6 +439,8 @@ def hotswap(bak): new.events = bak.events new.hooks = bak.hooks new.network = bak.network + + new._update_event_timer() return new def reload(): @@ -455,10 +460,8 @@ def reload(): import hooks imp.reload(hooks) - import hooks.manager - imp.reload(hooks.manager) - import hooks.messagehook - imp.reload(hooks.messagehook) + + hooks.reload() import importer imp.reload(importer) @@ -466,28 +469,24 @@ def reload(): import message imp.reload(message) + message.reload() + import prompt imp.reload(prompt) - import prompt.builtins - imp.reload(prompt.builtins) + + prompt.reload() import server rl,wl,xl = server._rlist,server._wlist,server._xlist imp.reload(server) server._rlist,server._wlist,server._xlist = rl,wl,xl - import server.socket - imp.reload(server.socket) - import server.IRC - imp.reload(server.IRC) + + server.reload() import tools imp.reload(tools) - import tools.countdown - imp.reload(tools.countdown) - import tools.date - imp.reload(tools.date) - import tools.web - imp.reload(tools.web) + + tools.reload() import xmlparser imp.reload(xmlparser) diff --git a/hooks/__init__.py b/hooks/__init__.py index e1fa0cf..7c1e841 100644 --- a/hooks/__init__.py +++ b/hooks/__init__.py @@ -16,6 +16,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 imp + from exception import IRCException def call_game(call, *args, **kargs): @@ -74,3 +76,11 @@ def hook(store, *args, **kargs): last_registered.append((store, MessageHook(call, *args, **kargs))) return call return sec + + +def reload(): + import hooks.manager + imp.reload(hooks.manager) + + import hooks.messagehook + imp.reload(hooks.messagehook) diff --git a/message/__init__.py b/message/__init__.py index 2b69b01..3595f3a 100644 --- a/message/__init__.py +++ b/message/__init__.py @@ -17,6 +17,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. from datetime import datetime, timezone +import imp class AbstractMessage: @@ -158,3 +159,13 @@ class OwnerCommand(Command): """This class represents a special command incomming from the owner""" pass + + +def reload(): + import message.visitor + imp.reload(message.visitor) + + import message.printer + imp.reload(message.printer) + + message.printer.reload() diff --git a/message/printer/__init__.py b/message/printer/__init__.py index e69de29..e9bf5a5 100644 --- a/message/printer/__init__.py +++ b/message/printer/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2014 nemunaire +# +# 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 + +def reload(): + import message.printer.IRC + imp.reload(message.printer.IRC) diff --git a/prompt/__init__.py b/prompt/__init__.py index 9095991..0233e9c 100644 --- a/prompt/__init__.py +++ b/prompt/__init__.py @@ -101,3 +101,7 @@ class Prompt: def hotswap(prompt): return Prompt(prompt.HOOKS_CAPS, prompt.HOOKS_LIST) + +def reload(): + import prompt.builtins + imp.reload(prompt.builtins) diff --git a/server/__init__.py b/server/__init__.py index c3f7509..ace443c 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -17,6 +17,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import io +import imp import logging import socket import queue @@ -143,3 +144,11 @@ class AbstractServer(io.IOBase): """Exception occurs in fd""" self.logger.warning("Unhandle file descriptor exception on server %s", self.id) + + +def reload(): + import server.socket + imp.reload(server.socket) + + import server.IRC + imp.reload(server.IRC) diff --git a/tools/__init__.py b/tools/__init__.py index bce3ef0..fe39e49 100644 --- a/tools/__init__.py +++ b/tools/__init__.py @@ -31,3 +31,14 @@ def ipToInt(ip): for b in ip.split("."): sum = 256 * sum + int(b) return sum + + +def reload(): + import tools.countdown + imp.reload(tools.countdown) + + import tools.date + imp.reload(tools.date) + + import tools.web + imp.reload(tools.web) From 41c33354c3b85396ef88117b27fadd8ded63955d Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 10 Oct 2014 23:14:05 +0200 Subject: [PATCH 174/674] Choose another nick on nick collision --- server/IRC.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/server/IRC.py b/server/IRC.py index fdb4379..0ddb7f9 100644 --- a/server/IRC.py +++ b/server/IRC.py @@ -195,6 +195,13 @@ class IRC(SocketServer): 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 From 6c89f80bcf379b891ca01b11301e402d150b36cd Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 10 Oct 2014 23:14:32 +0200 Subject: [PATCH 175/674] Fix sample configuration --- bot_sample.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot_sample.xml b/bot_sample.xml index f02a20a..0867bb6 100644 --- a/bot_sample.xml +++ b/bot_sample.xml @@ -1,11 +1,11 @@ <nemubotconfig nick="nemubot" realname="nemubot clone" owner="someone"> - <server server="irc.rezosup.org" port="6667" autoconnect="true" caps="znc.in/server-time-iso"> + <server host="irc.rezosup.org" port="6667" autoconnect="true" caps="znc.in/server-time-iso"> <channel name="#nemutest" /> </server> <!-- - <server server="my_host.local" port="6667" password="secret" autoconnect="true" ip="10.69.42.23" ssl="on"> + <server host="my_host.local" port="6667" password="secret" autoconnect="true" ip="10.69.42.23" ssl="on"> <channel name="#nemutest" /> </server> --> From bd92f64449e36c5bb7cdb0aa3cd7032a025cb422 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 10 Oct 2014 23:15:11 +0200 Subject: [PATCH 176/674] More module: ensure that never return None --- modules/more.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/more.py b/modules/more.py index 4e09a5a..f30bc54 100644 --- a/modules/more.py +++ b/modules/more.py @@ -226,7 +226,8 @@ def cmd_more(msg): if nw is not None and not nw.alone: bk = nw SERVERS[msg.server][receiver] = None, bk - res.append(bk) + if bk is not None: + res.append(bk) return res @@ -242,5 +243,6 @@ def cmd_next(msg): bk = nw SERVERS[msg.server][receiver] = None, bk bk.pop() - res.append(bk) + if bk is not None: + res.append(bk) return res From fe709e630fbe69c34907713a28da1d240b5d4310 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 20 Oct 2014 21:57:04 +0200 Subject: [PATCH 177/674] SMS module: new nemubot version fix --- modules/sms.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/sms.py b/modules/sms.py index ec1ac10..f96bfad 100644 --- a/modules/sms.py +++ b/modules/sms.py @@ -62,10 +62,10 @@ def cmd_sms(msg): fails = list() for u in msg.cmds[1].split(","): DATAS.index[u]["lastuse"] = cur_epoch - if msg.private: - frm = msg.nick + if msg.to_response[0] == msg.frm: + frm = msg.frm else: - frm = msg.nick + "@" + msg.channel + frm = msg.frm + "@" + msg.to[0] test = send_sms(frm, DATAS.index[u]["user"], DATAS.index[u]["key"], " ".join(msg.cmds[2:])) if test is not None: fails.append( "%s: %s" % (u, test) ) From 67f6d49fb8ff32d75391497ad46d0c862a17a062 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 21 Oct 2014 20:55:39 +0200 Subject: [PATCH 178/674] Weather module: new nemubot version fix --- modules/weather.py | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/modules/weather.py b/modules/weather.py index ac35e66..49c9044 100644 --- a/modules/weather.py +++ b/modules/weather.py @@ -84,21 +84,20 @@ def format_timestamp(timestamp, tzname, tzoffset, format="%c"): def treat_coord(msg): - if len(msg.cmds) > 1: + if len(msg.args) > 0: # catch dans X[jh]$ - if len(msg.cmds) > 3 and msg.cmds[len(msg.cmds) - 2] == "dans" or msg.cmds[len(msg.cmds) - 2] == "in" or msg.cmds[len(msg.cmds) - 2] == "next": - specific = msg.cmds[len(msg.cmds) - 1] - msg.cmds = msg.cmds[:len(msg.cmds) - 2] + if len(msg.args) > 2 and (msg.args[-2] == "dans" or msg.args[-2] == "in" or msg.args[-2] == "next"): + specific = msg.args[-1] + city = " ".join(msg.args[:-2]).lower() else: specific = None + city = " ".join(msg.args).lower() - j = " ".join(msg.cmds[1:]).lower() - - if len(msg.cmds) == 3: - coords = msg.cmds[1:3] + if len(msg.args) == 2: + coords = msg.args else: - coords = msg.cmds[1].split(",") + coords = msg.args[0].split(",") try: if len(coords) == 2 and str(float(coords[0])) == coords[0] and str(float(coords[1])) == coords[1]: @@ -106,21 +105,21 @@ def treat_coord(msg): except ValueError: pass - if j in DATAS.index: + if city in DATAS.index: coords = list() - coords.append(DATAS.index[j]["lat"]) - coords.append(DATAS.index[j]["long"]) - return j, coords, specific + coords.append(DATAS.index[city]["lat"]) + coords.append(DATAS.index[city]["long"]) + return city, coords, specific else: - geocode = [x for x in mapquest.geocode(j)] + geocode = [x for x in mapquest.geocode(city)] if len(geocode): coords = list() coords.append(geocode[0]["latLng"]["lat"]) coords.append(geocode[0]["latLng"]["lng"]) return mapquest.where(geocode[0]), coords, specific - raise IRCException("Je ne sais pas où se trouve %s." % " ".join(msg.cmds[1:])) + raise IRCException("Je ne sais pas où se trouve %s." % city) else: raise IRCException("indique-moi un nom de ville ou des coordonnées.") @@ -138,15 +137,15 @@ def get_json_weather(coords): def cmd_coordinates(msg): - if len(msg.cmds) <= 1: + if len(msg.args) < 1: raise IRCException("indique-moi un nom de ville.") - j = msg.cmds[1].lower() + j = msg.args[0].lower() if j not in DATAS.index: - raise IRCException("%s n'est pas une ville connue" % msg.cmds[1]) + raise IRCException("%s n'est pas une ville connue" % msg.args[0]) coords = DATAS.index[j] - return Response("Les coordonnées de %s sont %s,%s" % (msg.cmds[1], coords["lat"], coords["long"]), channel=msg.channel) + return Response("Les coordonnées de %s sont %s,%s" % (msg.args[0], coords["lat"], coords["long"]), channel=msg.channel) def cmd_alert(msg): loc, coords, specific = treat_coord(msg) From 5e87843ddaed192639b2d58de8d40b4954c58461 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 22 Oct 2014 07:38:53 +0200 Subject: [PATCH 179/674] Mediawiki module: fetch namespaces list to hide categories --- modules/mediawiki.py | 42 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/modules/mediawiki.py b/modules/mediawiki.py index 855238b..3711c28 100644 --- a/modules/mediawiki.py +++ b/modules/mediawiki.py @@ -14,6 +14,21 @@ nemubotversion = 3.4 from more import Response +def get_namespaces(site, ssl=False): + # Built URL + url = "http%s://%s/w/api.php?format=json&action=query&meta=siteinfo&siprop=namespaces" % ( + "s" if ssl else "", site) + print_debug(url) + + # Make the request + raw = urllib.request.urlopen(url) + data = json.loads(raw.read().decode()) + + namespaces = dict() + for ns in data["query"]["namespaces"]: + namespaces[data["query"]["namespaces"][ns]["*"]] = data["query"]["namespaces"][ns] + return namespaces + def get_raw_page(site, term, ssl=False): # Built URL url = "http%s://%s/w/api.php?format=json&redirects&action=query&prop=revisions&rvprop=content&titles=%s" % ( @@ -55,12 +70,23 @@ def strip_model(cnt): cnt = re.sub(r"<ref.*?/ref>", "", cnt) return cnt -def parse_wikitext(site, cnt, ssl=False): +def parse_wikitext(site, cnt, namespaces=dict(), ssl=False): for i,_,_,_ in re.findall(r"({{([^{]|\s|({{(.|\s|{{.*?}})*?}})*?)*?}})", cnt): cnt = cnt.replace(i, get_unwikitextified(site, i, ssl), 1) # Strip [[...]] - cnt, _ = re.subn(r"\[\[:?([^]]*\|)?([^]]+?)\]\]", r"\2", cnt) + for full,args,lnk in re.findall(r"(\[\[(.*?|)?([^|]*?)\]\])", cnt): + ns = lnk.find(":") + if lnk == "": + cnt = cnt.replace(full, args[:-1], 1) + elif ns > 0: + namespace = lnk[:ns] + if namespace in namespaces and namespaces[namespace]["canonical"] == "Category": + cnt = cnt.replace(full, "", 1) + continue + cnt = cnt.replace(full, lnk, 1) + else: + cnt = cnt.replace(full, lnk, 1) # Strip HTML tags cnt = web.striphtml(cnt) @@ -113,8 +139,12 @@ def cmd_mediawiki(msg): if len(msg.cmds) < 3: raise IRCException("indicate a domain and a term to search") - return Response(get_page(msg.cmds[1], " ".join(msg.cmds[2:])), - line_treat=lambda line: irc_format(parse_wikitext(msg.cmds[1], line)), + site = msg.cmds[1] + + ns = get_namespaces(site) + + return Response(get_page(site, " ".join(msg.cmds[2:])), + line_treat=lambda line: irc_format(parse_wikitext(msg.cmds[1], line, ns)), channel=msg.receivers) @@ -139,6 +169,8 @@ def cmd_wikipedia(msg): site = msg.cmds[1] + ".wikipedia.org" + ns = get_namespaces(site) + return Response(get_page(site, " ".join(msg.cmds[2:])), - line_treat=lambda line: irc_format(parse_wikitext(site, line)), + line_treat=lambda line: irc_format(parse_wikitext(site, line, ns)), channel=msg.receivers) From f927d5ab0a8742aed94557135a23cdfddb6bbab5 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 27 Oct 2014 18:40:04 +0100 Subject: [PATCH 180/674] Convert nemuspeak as a module to nemubot --- modules/speak.py | 132 +++++++++++++++++++++++++++++++++ nemuspeak.py | 186 ----------------------------------------------- server/IRC.py | 3 + speak_sample.xml | 48 ++++++------ 4 files changed, 160 insertions(+), 209 deletions(-) create mode 100644 modules/speak.py delete mode 100755 nemuspeak.py diff --git a/modules/speak.py b/modules/speak.py new file mode 100644 index 0000000..b7b9230 --- /dev/null +++ b/modules/speak.py @@ -0,0 +1,132 @@ +# coding=utf-8 + +from datetime import timedelta +from queue import Queue +import re +import subprocess +from threading import Thread + +from hooks import hook +from message import TextMessage +from message.visitor import AbstractVisitor + +nemubotversion = 3.4 + +queue = Queue() +spk_th = None +last = None + +SMILEY = list() +CORRECTIONS = list() + +def load(context): + for smiley in CONF.getNodes("smiley"): + if smiley.hasAttribute("txt") and smiley.hasAttribute("mood"): + SMILEY.append((smiley.getAttribute("txt"), smiley.getAttribute("mood"))) + print ("%d smileys loaded" % len(SMILEY)) + + for correct in CONF.getNodes("correction"): + if correct.hasAttribute("bad") and correct.hasAttribute("good"): + CORRECTIONS.append((" " + (correct.getAttribute("bad") + " "), (" " + correct.getAttribute("good") + " "))) + print ("%d corrections loaded" % len(CORRECTIONS)) + + +class Speaker(Thread): + + def run(self): + global queue, spk_th + while not queue.empty(): + sentence = queue.get_nowait() + lang = "fr" + print_debug(sentence) + subprocess.call(["espeak", "-v", lang, "--", sentence]) + queue.task_done() + + spk_th = None + + +class SpeakerVisitor(AbstractVisitor): + + def __init__(self, last): + self.pp = "" + self.last = last + + + def visit_TextMessage(self, msg): + force = (self.last is None) + + if force or msg.date - self.last.date > timedelta(0, 500): + self.pp += "A %d heure %d : " % (msg.date.hour, msg.date.minute) + force = True + + if force or msg.channel != self.last.channel: + if msg.to_response == msg.to: + self.pp += "sur %s. " % (", ".join(msg.to)) + else: + self.pp += "en message priver. " + + action = False + if msg.message.find("ACTION ") == 0: + self.pp += "%s " % msg.frm + msg.message = msg.message.replace("ACTION ", "") + action = True + for (txt, mood) in SMILEY: + if msg.message.find(txt) >= 0: + self.pp += "%s %s : " % (msg.frm, mood) + msg.message = msg.message.replace(txt, "") + action = True + break + + if not action and (force or msg.frm != self.last.frm): + self.pp += "%s dit : " % msg.frm + + if re.match(".*https?://.*", msg.message) is not None: + msg.message = re.sub(r'https?://([^/]+)[^ ]*', " U.R.L \\1", msg.message) + + self.pp += msg.message + + + def visit_DirectAsk(self, msg): + res = TextMessage("%s: %s" % (msg.designated, msg.message), + server=msg.server, date=msg.date, + to=msg.to, frm=msg.frm) + res.accept(self) + + + def visit_Command(self, msg): + res = TextMessage("Bang %s%s%s" % (msg.cmd, + " " if len(msg.args) else "", + " ".join(msg.args)), + server=msg.server, date=msg.date, + to=msg.to, frm=msg.frm) + res.accept(self) + + + def visit_OwnerCommand(self, msg): + res = TextMessage("Owner Bang %s%s%s" % (msg.cmd, + " " if len(msg.args) else "", + " ".join(msg.args)), + server=msg.server, date=msg.date, + to=msg.to, frm=msg.frm) + res.accept(self) + + +@hook("in") +def treat_for_speak(msg): + if not msg.frm_owner: + append_message(msg) + +def append_message(msg): + global last, spk_th + + if msg.message.find("TYPING ") == 0: + return + + vprnt = SpeakerVisitor(last) + msg.accept(vprnt) + queue.put_nowait(vprnt.pp) + last = msg + + if spk_th is None: + spk_th = Speaker() + spk_th.start() diff --git a/nemuspeak.py b/nemuspeak.py deleted file mode 100755 index 48d338f..0000000 --- a/nemuspeak.py +++ /dev/null @@ -1,186 +0,0 @@ -#!/usr/bin/python3 -# coding=utf-8 - -import sys -import signal -import os -import re -import subprocess -import traceback -from datetime import datetime -from datetime import timedelta -import _thread - -if len(sys.argv) <= 1: - print ("This script takes exactly 1 arg: a XML config file") - sys.exit(1) - -def onSignal(signum, frame): - print ("\nSIGINT receive, saving states and close") - sys.exit (0) -signal.signal(signal.SIGINT, onSignal) - -if len(sys.argv) == 3: - basedir = sys.argv[2] -else: - basedir = "./" - -import xmlparser as msf -import message -from server.IRC import IRCServer - -SMILEY = list() -CORRECTIONS = list() -g_queue = list() -talkEC = 0 -stopSpk = 0 -lastmsg = None - -def speak(endstate): - global lastmsg, g_queue, talkEC, stopSpk - talkEC = 1 - stopSpk = 0 - - if lastmsg is None: - lastmsg = message.Message(b":Quelqun!someone@p0m.fr PRIVMSG channel nothing", datetime.now()) - - while not stopSpk and len(g_queue) > 0: - srv, msg = g_queue.pop(0) - lang = "fr" - sentence = "" - force = 0 - - #Skip identic body - if msg.content == lastmsg.content: - continue - - if force or msg.time - lastmsg.time > timedelta(0, 500): - sentence += "A {0} heure {1} : ".format(msg.time.hour, msg.time.minute) - force = 1 - - if force or msg.channel != lastmsg.channel: - if msg.channel == srv.owner: - sentence += "En message priver. " #Just to avoid é :p - else: - sentence += "Sur " + msg.channel + ". " - force = 1 - - action = 0 - if msg.content.find("ACTION ") == 1: - sentence += msg.nick + " " - msg.content = msg.content.replace("ACTION ", "") - action = 1 - for (txt, mood) in SMILEY: - if msg.content.find(txt) >= 0: - sentence += msg.nick + (" %s : "%mood) - msg.content = msg.content.replace(txt, "") - action = 1 - break - - for (bad, good) in CORRECTIONS: - if msg.content.find(bad) >= 0: - msg.content = (" " + msg.content + " ").replace(bad, good) - - if action == 0 and (force or msg.sender != lastmsg.sender): - sentence += msg.nick + " dit : " - - if re.match(".*(https?://)?(www\\.)?ycc.fr/[a-z0-9A-Z]+.*", msg.content) is not None: - msg.content = re.sub("(https?://)?(www\\.)?ycc.fr/[a-z0-9A-Z]+", " U.R.L Y.C.C ", msg.content) - - if re.match(".*https?://.*", msg.content) is not None: - msg.content = re.sub(r'https?://[^ ]+', " U.R.L ", msg.content) - - if re.match("^ *[^a-zA-Z0-9 ][a-zA-Z]{2}[^a-zA-Z0-9 ]", msg.content) is not None: - if sentence != "": - intro = subprocess.call(["espeak", "-v", "fr", "--", sentence]) - #intro.wait() - - lang = msg.content[1:3].lower() - sentence = msg.content[4:] - else: - sentence += msg.content - - spk = subprocess.call(["espeak", "-v", lang, "--", sentence]) - #spk.wait() - - lastmsg = msg - - if not stopSpk: - talkEC = endstate - else: - talkEC = 1 - - -class Server(IRCServer): - def treat_msg(self, line, private = False): - global stopSpk, talkEC, g_queue - try: - msg = message.Message (line, datetime.now(), private) - if msg.cmd == 'PING': - self.send_pong(msg.content) - elif msg.cmd == 'PRIVMSG' and self.accepted_channel(msg.channel): - if msg.nick != self.owner: - g_queue.append((self, msg)) - if talkEC == 0: - _thread.start_new_thread(speak, (0,)) - elif msg.content[0] == "`" and len(msg.content) > 1: - msg.cmds = msg.cmds[1:] - if msg.cmds[0] == "speak": - _thread.start_new_thread(speak, (0,)) - elif msg.cmds[0] == "reset": - while len(g_queue) > 0: - g_queue.pop() - elif msg.cmds[0] == "save": - if talkEC == 0: - talkEC = 1 - stopSpk = 1 - elif msg.cmds[0] == "add": - self.channels.append(msg.cmds[1]) - print (cmd[1] + " added to listened channels") - elif msg.cmds[0] == "del": - if self.channels.count(msg.cmds[1]) > 0: - self.channels.remove(msg.cmds[1]) - print (msg.cmds[1] + " removed from listened channels") - else: - print (cmd[1] + " not in listened channels") - except: - print ("\033[1;31mERROR:\033[0m occurred during the processing of the message: %s" % line) - exc_type, exc_value, exc_traceback = sys.exc_info() - traceback.print_exception(exc_type, exc_value, exc_traceback) - - -config = msf.parse_file(sys.argv[1]) - -for smiley in config.getNodes("smiley"): - if smiley.hasAttribute("txt") and smiley.hasAttribute("mood"): - SMILEY.append((smiley.getAttribute("txt"), smiley.getAttribute("mood"))) -print ("%d smileys loaded"%len(SMILEY)) - -for correct in config.getNodes("correction"): - if correct.hasAttribute("bad") and correct.hasAttribute("good"): - CORRECTIONS.append((" " + (correct.getAttribute("bad") + " "), (" " + correct.getAttribute("good") + " "))) -print ("%d corrections loaded"%len(CORRECTIONS)) - -for serveur in config.getNodes("server"): - srv = Server(serveur, config["nick"], config["owner"], config["realname"], serveur.hasAttribute("ssl")) - srv.launch(None) - -def sighup_h(signum, frame): - global talkEC, stopSpk - sys.stdout.write ("Signal reçu ... ") - if os.path.exists("/tmp/isPresent"): - _thread.start_new_thread(speak, (0,)) - print ("Morning!") - else: - print ("Sleeping!") - if talkEC == 0: - talkEC = 1 - stopSpk = 1 -signal.signal(signal.SIGHUP, sighup_h) - -print ("Nemuspeak ready, waiting for new messages...") -prompt="" -while prompt != "quit": - prompt=sys.stdin.readlines () - -sys.exit(0) diff --git a/server/IRC.py b/server/IRC.py index 0ddb7f9..2693a48 100644 --- a/server/IRC.py +++ b/server/IRC.py @@ -105,6 +105,9 @@ class IRC(SocketServer): self.ctcp_capabilities["USERINFO"] = lambda msg, cmds: "USERINFO %s" % self.realname self.ctcp_capabilities["VERSION"] = lambda msg, cmds: "VERSION nemubot v%s" % bot.__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)) diff --git a/speak_sample.xml b/speak_sample.xml index b7c3ff5..ee403ac 100644 --- a/speak_sample.xml +++ b/speak_sample.xml @@ -1,33 +1,35 @@ -<config nick="nemubot" realname="nemubot speaker" owner="someone"> +<nemubotconfig nick="nemubot" realname="nemubot speaker" owner="someone"> <!-- Bitlbee/Minbif --> - <server server="nemunai.re" port="6666" password="bitlbee" allowall="true" /> + <server host="nemunai.re" port="6666" password="bitlbee" autoconnect="true" caps="znc.in/server-time-iso" /> <!-- Rezosup --> <server server="nemunai.re" port="6667"> <channel name="#epita" /> </server> - <correction bad="ca" good="ça" /> + <module name="speak"> + <correction bad="ca" good="ça" /> - <smiley txt=":p" mood="tire la langue" /> - <smiley txt=":P" mood="tire la langue" /> - <smiley txt=":)" mood="sourit" /> - <smiley txt=":(" mood="est triste" /> - <smiley txt=";)" mood="fait un clin d'oeil" /> - <smiley txt="<3" mood="aime" /> - <smiley txt=":'(" mood="pleure" /> - <smiley txt=";(" mood="pleure" /> - <smiley txt=":D" mood="rit" /> - <smiley txt=":d" mood="rit" /> - <smiley txt="xd" mood="se marre" /> - <smiley txt="xD" mood="se marre" /> - <smiley txt="XD" mood="se marre" /> - <smiley txt="Xd" mood="se marre" /> - <smiley txt="x)" mood="se marre" /> - <smiley txt="X)" mood="se marre" /> - <smiley txt="/o/" mood="danse" /> - <smiley txt="\\o\\" mood="danse" /> - <smiley txt="\\o/" mood="fait une accolade" /> + <smiley txt=":p" mood="tire la langue" /> + <smiley txt=":P" mood="tire la langue" /> + <smiley txt=":)" mood="sourit" /> + <smiley txt=":(" mood="est triste" /> + <smiley txt=";)" mood="fait un clin d'oeil" /> + <smiley txt="<3" mood="aime" /> + <smiley txt=":'(" mood="pleure" /> + <smiley txt=";(" mood="pleure" /> + <smiley txt=":D" mood="rit" /> + <smiley txt=":d" mood="rit" /> + <smiley txt="xd" mood="se marre" /> + <smiley txt="xD" mood="se marre" /> + <smiley txt="XD" mood="se marre" /> + <smiley txt="Xd" mood="se marre" /> + <smiley txt="x)" mood="se marre" /> + <smiley txt="X)" mood="se marre" /> + <smiley txt="/o/" mood="danse" /> + <smiley txt="\\o\\" mood="danse" /> + <smiley txt="\\o/" mood="fait une accolade" /> + </module> -</config> +</nemubotconfig> From f8884a53ec5cc6181efad391bfeba1859807e155 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 23 Oct 2014 15:31:38 +0200 Subject: [PATCH 181/674] On exit, stop main loop --- nemubot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nemubot.py b/nemubot.py index c4a6682..f3a29cf 100755 --- a/nemubot.py +++ b/nemubot.py @@ -85,6 +85,7 @@ if __name__ == "__main__": logger.exception("\033[1;31mUnable to reload the prompt due to errors.\033[0" "m Fix them before trying to reload the prompt.") - print ("\nWaiting for other threads shuts down...") + context.quit() + print("\nWaiting for other threads shuts down...") sys.exit(0) From 5e097b54155e1cd2944f5a4f283d7b89dc872a7d Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 23 Oct 2014 15:47:00 +0200 Subject: [PATCH 182/674] New attribute on Messages: frm_owner, indicating a message coming from the bot owner --- consumer.py | 3 +++ message/__init__.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/consumer.py b/consumer.py index 72e3635..9cf2638 100644 --- a/consumer.py +++ b/consumer.py @@ -45,6 +45,9 @@ class MessageConsumer: if not hasattr(msg, "server") or msg.server is None: msg.server = self.srv.id + if hasattr(msg, "frm_owner"): + msg.frm_owner = (self.srv.owner == msg.frm) + return msg diff --git a/message/__init__.py b/message/__init__.py index 3595f3a..f3a107a 100644 --- a/message/__init__.py +++ b/message/__init__.py @@ -40,6 +40,8 @@ class AbstractMessage: self._to_response = to_response if to_response is None or isinstance(to_response, list) else [ to_response ] self.frm = frm # None allowed when it designate this bot + self.frm_owner = False # Filled later, in consumer + @property def to_response(self): From 0731803550367c37e493a6851692d9782b031e49 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 1 Nov 2014 18:37:14 +0100 Subject: [PATCH 183/674] Don't call _open if it is not defined --- server/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/__init__.py b/server/__init__.py index ace443c..11b9854 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -64,7 +64,7 @@ class AbstractServer(io.IOBase): 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 self._open(): + if not hasattr(self, "_open") or self._open(): _rlist.append(self) _xlist.append(self) @@ -72,7 +72,7 @@ class AbstractServer(io.IOBase): 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) - if self._close(): + if not hasattr(self, "_close") or self._close(): if self in _rlist: _rlist.remove(self) if self in _wlist: From fafa261811439e945a8827c7eaf1a375e2786974 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 2 Nov 2014 03:02:44 +0100 Subject: [PATCH 184/674] Update copyrights --- tools/countdown.py | 18 ++++++++++++++++++ tools/date.py | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/tools/countdown.py b/tools/countdown.py index ad77865..b0c7c71 100644 --- a/tools/countdown.py +++ b/tools/countdown.py @@ -1,3 +1,21 @@ +# -*- coding: utf-8 -*- + +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2014 nemunaire +# +# 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 time diff --git a/tools/date.py b/tools/date.py index 58182c9..15b694e 100644 --- a/tools/date.py +++ b/tools/date.py @@ -1,3 +1,21 @@ +# -*- coding: utf-8 -*- + +# Nemubot is a smart and modulable IM bot. +# Copyright (C) 2012-2014 nemunaire +# +# 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/>. + # Extraction/Format text from datetime import datetime From 7c12f31d2c1875db166fe270829e9bf38802542e Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 2 Nov 2014 03:25:24 +0100 Subject: [PATCH 185/674] Log XML parsing errors --- xmlparser/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/xmlparser/__init__.py b/xmlparser/__init__.py index adfb85b..8dd33d6 100644 --- a/xmlparser/__init__.py +++ b/xmlparser/__init__.py @@ -18,10 +18,13 @@ import os import imp +import logging import xml.sax from . import node as module_state +logger = logging.getLogger("nemubot.xmlparser") + class ModuleStatesFile(xml.sax.ContentHandler): def startDocument(self): self.root = None @@ -55,8 +58,10 @@ def parse_file(filename): parser.parse(open(filename, "r")) return mod.root except IOError: + logger.exception("error occurs during XML parsing of %s", filename) return module_state.ModuleState("nemubotstate") except: + logger.exception("error occurs during XML parsing of %s", filename) if mod.root is None: return module_state.ModuleState("nemubotstate") else: @@ -68,6 +73,7 @@ def parse_string(string): xml.sax.parseString(string, mod) return mod.root except: + logger.exception("error occurs during XML parsing") if mod.root is None: return module_state.ModuleState("nemubotstate") else: From 77b897a1e358d56206f2b8acf995f1b7b95178bc Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 2 Nov 2014 17:48:11 +0100 Subject: [PATCH 186/674] Fix in modules: is_owner is now frm_owner --- modules/alias.py | 2 +- modules/events.py | 2 +- modules/watchWebsite/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/alias.py b/modules/alias.py index 314f545..0f8c876 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -111,7 +111,7 @@ def cmd_unalias(msg): if alias[0] == "!" and len(alias) > 1: alias = alias[1:] if alias in DATAS.getNode("aliases").index: - if DATAS.getNode("aliases").index[alias]["creator"] == msg.nick or msg.is_owner: + if DATAS.getNode("aliases").index[alias]["creator"] == msg.nick or msg.frm_owner: DATAS.getNode("aliases").delChild(DATAS.getNode("aliases").index[alias]) res.append(Response("%s a bien été supprimé" % alias, channel=msg.channel)) else: diff --git a/modules/events.py b/modules/events.py index d0d9e6e..1de67fa 100644 --- a/modules/events.py +++ b/modules/events.py @@ -145,7 +145,7 @@ def end_countdown(msg): raise IRCException("quel événement terminer ?") if msg.cmds[1] in DATAS.index: - if DATAS.index[msg.cmds[1]]["proprio"] == msg.nick or (msg.cmds[0] == "forceend" and msg.is_owner): + if DATAS.index[msg.cmds[1]]["proprio"] == msg.nick or (msg.cmds[0] == "forceend" and msg.frm_owner): duration = countdown(msg.date - DATAS.index[msg.cmds[1]].getDate("start")) del_event(DATAS.index[msg.cmds[1]]["id"]) DATAS.delChild(DATAS.index[msg.cmds[1]]) diff --git a/modules/watchWebsite/__init__.py b/modules/watchWebsite/__init__.py index 2bd0d4c..0cc236c 100644 --- a/modules/watchWebsite/__init__.py +++ b/modules/watchWebsite/__init__.py @@ -56,7 +56,7 @@ def del_site(msg): site = DATAS.index[url] for a in site.getNodes("alert"): if a["channel"] == msg.channel: - if not (msg.frm == a["nick"] or msg.is_owner): + if not (msg.frm == a["nick"] or msg.frm_owner): raise IRCException("vous ne pouvez pas supprimer cette URL.") site.delChild(a) if not site.hasNode("alert"): From 745d2b04876b5cb170dab6496a3c2f2f4a7094ae Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 2 Nov 2014 18:16:47 +0100 Subject: [PATCH 187/674] Events module: display an error if server doesn't exist when registering the event --- modules/events.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/events.py b/modules/events.py index 1de67fa..5679fd0 100644 --- a/modules/events.py +++ b/modules/events.py @@ -30,6 +30,8 @@ def load(context): for evt in DATAS.index.keys(): if DATAS.index[evt].hasAttribute("end"): event = ModuleEvent(call=fini, call_data=dict(strend=DATAS.index[evt])) + if DATAS.index[evt]["server"] not in context.servers: + print("WARNING: registering event for a unexistant server: %s, please connect to it." % DATAS.index[evt]["server"]) event._end = DATAS.index[evt].getDate("end") idt = add_event(event) if idt is not None: From 8dfd0f07cc7c65672832a32e5ac32f71c0405ede Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 2 Nov 2014 18:31:53 +0100 Subject: [PATCH 188/674] IRC server: differentiate nick and username --- prompt/builtins.py | 2 +- server/IRC.py | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/prompt/builtins.py b/prompt/builtins.py index b653694..2af2e18 100644 --- a/prompt/builtins.py +++ b/prompt/builtins.py @@ -78,7 +78,7 @@ def load_file(filename, context): } # Optional keyword arguments - for optional_opt in [ "port", "realname", "password", "encoding", "caps" ]: + for optional_opt in [ "port", "username", "realname", "password", "encoding", "caps" ]: if server.hasAttribute(optional_opt): opts[optional_opt] = server[optional_opt] elif optional_opt in config: diff --git a/server/IRC.py b/server/IRC.py index 2693a48..9700a23 100644 --- a/server/IRC.py +++ b/server/IRC.py @@ -31,7 +31,7 @@ import tools class IRC(SocketServer): def __init__(self, owner, nick="nemubot", host="localhost", port=6667, - ssl=False, password=None, realname="Nemubot", + ssl=False, username=None, password=None, realname="Nemubot", encoding="utf-8", caps=None, channels=list(), on_connect=None): """Prepare a connection with an IRC server @@ -42,6 +42,7 @@ class IRC(SocketServer): host -- host to join port -- port on the host to reach ssl -- is this server using a TLS socket + 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 @@ -50,15 +51,16 @@ class IRC(SocketServer): on_connect -- generator to call when connection is done """ - self.id = nick + "@" + host + ":" + port - self.printer = IRCPrinter - SocketServer.__init__(self, host=host, port=port, ssl=ssl) - + self.username = username if username is not None else nick self.password = password self.nick = nick self.owner = owner self.realname = realname + self.id = self.username + "@" + host + ":" + port + self.printer = IRCPrinter + SocketServer.__init__(self, host=host, port=port, ssl=ssl) + self.encoding = encoding # Keep a list of joined channels @@ -227,7 +229,7 @@ class IRC(SocketServer): if self.capabilities is not None: self.write("CAP LS") self.write("NICK :" + self.nick) - self.write("USER %s %s bla :%s" % (self.nick, self.host, self.realname)) + self.write("USER %s %s bla :%s" % (self.username, self.host, self.realname)) return True return False From 95deafe7af5bf7bc20917fc044aad77f6fb0e01b Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 8 Nov 2014 11:01:54 +0100 Subject: [PATCH 189/674] [speak] Fix error on non-TextMessage arrival --- modules/speak.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/speak.py b/modules/speak.py index b7b9230..2b85035 100644 --- a/modules/speak.py +++ b/modules/speak.py @@ -119,7 +119,7 @@ def treat_for_speak(msg): def append_message(msg): global last, spk_th - if msg.message.find("TYPING ") == 0: + if hasattr(msg, "message") and msg.message.find("TYPING ") == 0: return vprnt = SpeakerVisitor(last) From e17996d85800bdbdb57bb2af5567d04f95293bfa Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 9 Nov 2014 14:11:54 +0100 Subject: [PATCH 190/674] PEP8 clean --- bot.py | 9 +- channel.py | 67 ++++++- consumer.py | 12 +- exception.py | 7 +- hooks/__init__.py | 2 + hooks/manager.py | 1 + hooks/messagehook.py | 7 +- message/__init__.py | 10 +- message/printer/IRC.py | 1 + message/printer/__init__.py | 1 + message/visitor.py | 1 + nemubot.py | 10 +- prompt/__init__.py | 12 +- prompt/builtins.py | 54 +++--- server/IRC.py | 29 ++- server/__init__.py | 7 +- server/socket.py | 14 +- tools/__init__.py | 4 +- tools/countdown.py | 21 ++- tools/date.py | 49 +++-- tools/web.py | 33 +++- xmlparser/__init__.py | 89 ++++----- xmlparser/node.py | 357 ++++++++++++++++++------------------ 23 files changed, 481 insertions(+), 316 deletions(-) diff --git a/bot.py b/bot.py index b041e6c..1c16278 100644 --- a/bot.py +++ b/bot.py @@ -36,11 +36,13 @@ from networkbot import NetworkBot logger = logging.getLogger("nemubot.bot") + 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_path="./datas/"): + def __init__(self, ip="127.0.0.1", modules_paths=list(), + data_path="./datas/"): """Initialize the bot context Keyword arguments: @@ -71,6 +73,7 @@ class Bot(threading.Thread): # Own hooks self.hooks = HooksManager() + 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") @@ -477,9 +480,9 @@ def reload(): prompt.reload() import server - rl,wl,xl = server._rlist,server._wlist,server._xlist + rl, wl, xl = server._rlist, server._wlist, server._xlist imp.reload(server) - server._rlist,server._wlist,server._xlist = rl,wl,xl + server._rlist, server._wlist, server._xlist = rl, wl, xl server.reload() diff --git a/channel.py b/channel.py index fb570a2..4223370 100644 --- a/channel.py +++ b/channel.py @@ -18,8 +18,19 @@ import logging + class Channel: + + """A chat room""" + def __init__(self, name, password=None): + """Initialize the channel + + Arguments: + name -- the channel name + password -- the optional password use to join it + """ + self.name = name self.password = password self.people = dict() @@ -27,6 +38,13 @@ class Channel: self.logger = logging.getLogger("nemubot.channel." + name) def treat(self, cmd, msg): + """Treat a incoming IRC command + + Arguments: + cmd -- the command + msg -- the whole message + """ + if cmd == "353": self.parse353(msg) elif cmd == "332": @@ -42,18 +60,35 @@ class Channel: elif cmd == "TOPIC": self.topic = self.text - def join(self, nick, level = 0): - """Someone join the channel""" + def join(self, nick, level=0): + """Someone join the channel + + Argument: + nick -- nickname of the user joining the channel + level -- authorization level of the user + """ + self.logger.debug("%s join", nick) self.people[nick] = level def chtopic(self, newtopic): - """Send command to change the topic""" + """Send command to change the topic + + Arguments: + newtopic -- the new topic of the channel + """ + self.srv.send_msg(self.name, newtopic, "TOPIC") self.topic = newtopic def nick(self, oldnick, newnick): - """Someone change his nick""" + """Someone change his nick + + Arguments: + oldnick -- the previous nick of the user + newnick -- the new nick of the user + """ + if oldnick in self.people: self.logger.debug("%s switch nick to %s on", oldnick, newnick) lvl = self.people[oldnick] @@ -61,12 +96,22 @@ class Channel: self.people[newnick] = lvl def part(self, nick): - """Someone leave the channel""" + """Someone leave the channel + + Argument: + nick -- name of the user that leave + """ + if nick in self.people: self.logger.debug("%s has left", nick) del self.people[nick] def mode(self, msg): + """Channel or user mode change + + Argument: + msg -- the whole message + """ if msg.text[0] == "-k": self.password = "" elif msg.text[0] == "+k": @@ -88,9 +133,21 @@ class Channel: self.people[msg.nick] &= ~1 def parse332(self, msg): + """Parse RPL_TOPIC message + + Argument: + msg -- the whole message + """ + self.topic = msg.text def parse353(self, msg): + """Parse RPL_ENDOFWHO message + + Argument: + msg -- the whole message + """ + for p in msg.text: p = p.decode() if p[0] == "@": diff --git a/consumer.py b/consumer.py index 9cf2638..1aeb01c 100644 --- a/consumer.py +++ b/consumer.py @@ -23,6 +23,7 @@ import threading logger = logging.getLogger("nemubot.consumer") + class MessageConsumer: """Store a message before treating""" @@ -77,7 +78,7 @@ class MessageConsumer: new_msg.append(res) msg = None break - elif res is None or res == False: + elif res is None or res is False: msg = None break if msg is not None: @@ -134,7 +135,7 @@ class MessageConsumer: new_msg.append(res) msg = None break - elif res is None or res == False: + elif res is None or res is False: msg = None break else: @@ -160,7 +161,8 @@ class MessageConsumer: if self.responses is not None and len(self.responses) > 0: self.post_treat(context.hooks) except: - logger.exception("Error occurred during the processing of the %s: %s", type(self.msgs[0]).__name__, self.msgs[0]) + logger.exception("Error occurred during the processing of the %s: " + "%s", type(self.msgs[0]).__name__, self.msgs[0]) for res in self.responses: to_server = None @@ -180,6 +182,7 @@ class MessageConsumer: # Sent the message only if treat_post authorize it to_server.send_response(res) + class EventConsumer: """Store a event before treating""" def __init__(self, evt, timeout=20): @@ -198,7 +201,8 @@ class EventConsumer: context.add_event(self.evt, eid=self.evt.id) # Or remove reference of this event - elif hasattr(self.evt, "module_src") and self.evt.module_src is not None: + elif (hasattr(self.evt, "module_src") and + self.evt.module_src is not None): self.evt.module_src.REGISTERED_EVENTS.remove(self.evt.id) diff --git a/exception.py b/exception.py index 92229bf..ccbe362 100644 --- a/exception.py +++ b/exception.py @@ -18,6 +18,7 @@ from message import TextMessage, DirectAsk + class IRCException(Exception): def __init__(self, message, personnal=True): @@ -27,6 +28,8 @@ class IRCException(Exception): def fill_response(self, msg): if self.personnal: - return DirectAsk(msg.frm, self.message, server=msg.server, to=msg.to_response) + return DirectAsk(msg.frm, self.message, + server=msg.server, to=msg.to_response) else: - return TextMessage(self.message, server=msg.server, to=msg.to_response) + return TextMessage(self.message, + server=msg.server, to=msg.to_response) diff --git a/hooks/__init__.py b/hooks/__init__.py index 7c1e841..288ea3a 100644 --- a/hooks/__init__.py +++ b/hooks/__init__.py @@ -20,6 +20,7 @@ import imp from exception import IRCException + def call_game(call, *args, **kargs): """TODO""" l = list() @@ -70,6 +71,7 @@ from hooks.messagehook import MessageHook last_registered = [] + def hook(store, *args, **kargs): """Function used as a decorator for module loading""" def sec(call): diff --git a/hooks/manager.py b/hooks/manager.py index 6db0989..2331d68 100644 --- a/hooks/manager.py +++ b/hooks/manager.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 <http://www.gnu.org/licenses/>. + class HooksManager: """Class to manage hooks""" diff --git a/hooks/messagehook.py b/hooks/messagehook.py index 1ab09c6..e0600c1 100644 --- a/hooks/messagehook.py +++ b/hooks/messagehook.py @@ -22,6 +22,7 @@ from exception import IRCException import hooks import message + class MessageHook(hooks.AbstractHook): """Class storing hook information, specialized for a generic Message""" @@ -52,9 +53,9 @@ class MessageHook(hooks.AbstractHook): 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 ((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: diff --git a/message/__init__.py b/message/__init__.py index f3a107a..67ce5f7 100644 --- a/message/__init__.py +++ b/message/__init__.py @@ -19,6 +19,7 @@ from datetime import datetime, timezone import imp + class AbstractMessage: """This class represents an abstract message""" @@ -37,10 +38,12 @@ class AbstractMessage: self.server = server self.date = datetime.now(timezone.utc) if date is None else date self.to = to if to is not None else list() - self._to_response = to_response if to_response is None or isinstance(to_response, list) else [ to_response ] - self.frm = frm # None allowed when it designate this bot + self._to_response = (to_response if (to_response is None or + isinstance(to_response, list)) + else [ to_response ]) + self.frm = frm # None allowed when it designate this bot - self.frm_owner = False # Filled later, in consumer + self.frm_owner = False # Filled later, in consumer @property @@ -129,7 +132,6 @@ class DirectAsk(TextMessage): self.designated = designated - def respond(self, message): return DirectAsk(self.frm, message, diff --git a/message/printer/IRC.py b/message/printer/IRC.py index 83155ac..1b4670f 100644 --- a/message/printer/IRC.py +++ b/message/printer/IRC.py @@ -19,6 +19,7 @@ from message import TextMessage from message.visitor import AbstractVisitor + class IRC(AbstractVisitor): def __init__(self): diff --git a/message/printer/__init__.py b/message/printer/__init__.py index e9bf5a5..215b464 100644 --- a/message/printer/__init__.py +++ b/message/printer/__init__.py @@ -18,6 +18,7 @@ import imp + def reload(): import message.printer.IRC imp.reload(message.printer.IRC) diff --git a/message/visitor.py b/message/visitor.py index 7328254..1041a45 100644 --- a/message/visitor.py +++ b/message/visitor.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 <http://www.gnu.org/licenses/>. + class AbstractVisitor: def visit(self, obj): diff --git a/nemubot.py b/nemubot.py index f3a29cf..1b2121f 100755 --- a/nemubot.py +++ b/nemubot.py @@ -31,7 +31,8 @@ if __name__ == "__main__": # Setup loggin interface logger = logging.getLogger("nemubot") - formatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s %(message)s') + formatter = logging.Formatter( + '%(asctime)s %(name)s %(levelname)s %(message)s') ch = logging.StreamHandler() ch.setFormatter(formatter) @@ -52,7 +53,7 @@ if __name__ == "__main__": # Register the hook for futur import sys.meta_path.append(importer.ModuleFinder(context, prmpt)) - #Add modules dir path + # Add modules dir path if os.path.isdir("./modules/"): context.add_modules_path( os.path.realpath(os.path.abspath("./modules/"))) @@ -82,8 +83,9 @@ if __name__ == "__main__": bot.__version__) context.start() except: - logger.exception("\033[1;31mUnable to reload the prompt due to errors.\033[0" - "m Fix them before trying to reload the prompt.") + logger.exception("\033[1;31mUnable to reload the prompt due to " + "errors.\033[0m Fix them before trying to reload " + "the prompt.") context.quit() print("\nWaiting for other threads shuts down...") diff --git a/prompt/__init__.py b/prompt/__init__.py index 0233e9c..d0ad4b8 100644 --- a/prompt/__init__.py +++ b/prompt/__init__.py @@ -25,7 +25,9 @@ import traceback from . import builtins + class Prompt: + def __init__(self, hc=dict(), hl=dict()): self.selectedServer = None @@ -56,8 +58,8 @@ class Prompt: return ret except: exc_type, exc_value, exc_traceback = sys.exc_info() - sys.stderr.write (traceback.format_exception_only( - exc_type, exc_value)[0]) + sys.stderr.write(traceback.format_exception_only(exc_type, + exc_value)[0]) return ret def exec_cmd(self, toks, context): @@ -65,7 +67,7 @@ class Prompt: if toks[0] in builtins.CAPS: return builtins.CAPS[toks[0]](toks, context, self) elif toks[0] in self.HOOKS_CAPS: - (f,d) = self.HOOKS_CAPS[toks[0]] + f, d = self.HOOKS_CAPS[toks[0]] return f(d, toks, context, self) else: print("Unknown command: `%s'" % toks[0]) @@ -90,7 +92,8 @@ class Prompt: ret = self.exec_cmd(toks, context) except: exc_type, exc_value, exc_traceback = sys.exc_info() - traceback.print_exception(exc_type, exc_value, exc_traceback) + traceback.print_exception(exc_type, exc_value, + exc_traceback) except KeyboardInterrupt: print("") except EOFError: @@ -102,6 +105,7 @@ class Prompt: def hotswap(prompt): return Prompt(prompt.HOOKS_CAPS, prompt.HOOKS_LIST) + def reload(): import prompt.builtins imp.reload(prompt.builtins) diff --git a/prompt/builtins.py b/prompt/builtins.py index 2af2e18..f0e5c68 100644 --- a/prompt/builtins.py +++ b/prompt/builtins.py @@ -25,6 +25,13 @@ logger = logging.getLogger("nemubot.prompt.builtins") from server.IRC import IRC as IRCServer import xmlparser + +def get_boolean(d, k): + return (k in d and + mod["autoload"].lower() != "false" and + mod["autoload"].lower() != "off") + + def end(toks, context, prompt): """Quit the prompt for reload or exit""" if toks[0] == "refresh": @@ -52,7 +59,7 @@ def liste(toks, context, prompt): if len(context.modules) == 0: print (" > No module loaded") elif l in prompt.HOOKS_LIST: - (f,d) = prompt.HOOKS_LIST[l] + f, d = prompt.HOOKS_LIST[l] f(d, context, prompt) else: print (" Unknown list `%s'" % l) @@ -65,8 +72,7 @@ def load_file(filename, context): config = xmlparser.parse_file(filename) # This is a true nemubot configuration file, load it! - if (config.getName() == "botconfig" - or config.getName() == "nemubotconfig"): + if config.getName() == "nemubotconfig": # Preset each server in this file for server in config.getNodes("server"): opts = { @@ -78,7 +84,8 @@ def load_file(filename, context): } # Optional keyword arguments - for optional_opt in [ "port", "username", "realname", "password", "encoding", "caps" ]: + for optional_opt in [ "port", "username", "realname", + "password", "encoding", "caps" ]: if server.hasAttribute(optional_opt): opts[optional_opt] = server[optional_opt] elif optional_opt in config: @@ -94,11 +101,14 @@ def load_file(filename, context): if server.hasNode("channel"): opts["channels"] = list() for chn in server.getNodes("channel"): - opts["channels"].append((chn["name"], chn["password"]) if chn["password"] is not None else chn["name"]) + opts["channels"].append((chn["name"], chn["password"]) + if chn["password"] is not None + else chn["name"]) # Server/client capabilities if "caps" in server or "caps" in config: - capsl = (server["caps"] if server.hasAttribute("caps") else config["caps"]).lower() + capsl = (server["caps"] if server.hasAttribute("caps") + else config["caps"]).lower() if capsl == "no" or capsl == "off" or capsl == "false": opts["caps"] = None else: @@ -110,14 +120,14 @@ def load_file(filename, context): if "protocol" not in server or server["protocol"] == "irc": srvcls = IRCServer else: - raise Exception("Unhandled protocol '%s'" % server["protocol"]) + raise Exception("Unhandled protocol '%s'" % + server["protocol"]) # Initialize the server srv = srvcls(**opts) # Add the server in the context - if context.add_server(srv, - "autoconnect" in server and server["autoconnect"].lower() != "false"): + if context.add_server(srv, get_boolean(server, "autoconnect")): print("Server '%s' successfully added." % srv.id) else: print("Can't add server '%s'." % srv.id) @@ -125,7 +135,7 @@ def load_file(filename, context): # Load module and their configuration for mod in config.getNodes("module"): context.modules_configuration[mod["name"]] = mod - if not mod.hasAttribute("autoload") or (mod["autoload"].lower() != "false" and mod["autoload"].lower() != "off"): + if get_boolean(mod, "autoload"): __import__(mod["name"]) # Load files asked by the configuration file @@ -155,8 +165,8 @@ def load(toks, context, prompt): 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 (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: @@ -197,15 +207,15 @@ def debug(toks, context, prompt): print ("Not enough arguments. `debug' takes a module name.") -#Register build-ins +# 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 + '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/server/IRC.py b/server/IRC.py index 9700a23..bbe6334 100644 --- a/server/IRC.py +++ b/server/IRC.py @@ -28,8 +28,11 @@ from message.printer.IRC import IRC as IRCPrinter from server.socket import SocketServer import tools + class IRC(SocketServer): + """Concrete implementation of a connexion to an IRC server""" + def __init__(self, owner, nick="nemubot", host="localhost", port=6667, ssl=False, username=None, password=None, realname="Nemubot", encoding="utf-8", caps=None, channels=list(), @@ -155,7 +158,8 @@ class IRC(SocketServer): # Respond to JOIN def _on_join(msg): - if len(msg.params) == 0: return + if len(msg.params) == 0: + return for chname in msg.decode(msg.params[0]).split(","): # Register the channel @@ -164,7 +168,8 @@ class IRC(SocketServer): self.hookscmd["JOIN"] = _on_join # Respond to PART def _on_part(msg): - if len(msg.params) != 1 and len(msg.params) != 2: return + if len(msg.params) != 1 and len(msg.params) != 2: + return for chname in msg.params[0].split(b","): if chname in self.channels: @@ -175,7 +180,8 @@ class IRC(SocketServer): 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 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 @@ -186,8 +192,10 @@ class IRC(SocketServer): 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 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[\]\\`_^{|}-]*)$") @@ -196,7 +204,8 @@ class IRC(SocketServer): # Respond to INVITE def _on_invite(msg): - if len(msg.params) != 2: return + if len(msg.params) != 2: + return self.write("JOIN " + msg.decode(msg.params[1])) self.hookscmd["INVITE"] = _on_invite @@ -209,7 +218,8 @@ class IRC(SocketServer): # Handle CTCP requests def _on_ctcp(msg): - if len(msg.params) != 2 or not msg.is_ctcp: return + 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) @@ -353,9 +363,10 @@ class IRCMessage: 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()]) + 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) + if not client: + res += " :%s!%s@%s" % (self.nick, self.user, self.host) res += " " + self.cmd diff --git a/server/__init__.py b/server/__init__.py index 11b9854..3ae04f2 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -27,6 +27,7 @@ _rlist = [] _wlist = [] _xlist = [] + # Extends from IOBase in order to be compatible with select function class AbstractServer(io.IOBase): @@ -62,7 +63,8 @@ class AbstractServer(io.IOBase): def open(self): - """Generic open function that register the server un _rlist in case of successful _open""" + """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) @@ -70,7 +72,8 @@ class AbstractServer(io.IOBase): def close(self): - """Generic close function that register the server un _{r,w,x}list in case of successful _close""" + """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) if not hasattr(self, "_close") or self._close(): if self in _rlist: diff --git a/server/socket.py b/server/socket.py index 33f7944..bacfdac 100644 --- a/server/socket.py +++ b/server/socket.py @@ -21,8 +21,11 @@ import socket from server import AbstractServer + class SocketServer(AbstractServer): + """Concrete implementation of a socket connexion (can be wrapped with TLS)""" + def __init__(self, host, port, ssl=False): AbstractServer.__init__(self) self.host = host @@ -55,12 +58,13 @@ class SocketServer(AbstractServer): self.socket = ctx.wrap_socket(self.socket) try: - self.socket.connect((self.host, self.port)) #Connect to server + self.socket.connect((self.host, self.port)) # Connect to server self.logger.info("Connected to %s:%d", self.host, self.port) except socket.error as e: self.socket = None self.logger.critical("Unable to connect to %s:%d: %s", - self.host, self.port, os.strerror(e.errno)) + self.host, self.port, + os.strerror(e.errno)) return False return True @@ -81,7 +85,8 @@ class SocketServer(AbstractServer): # Write def _write(self, cnt): - if not self.connected: return + if not self.connected: + return self.socket.send(cnt) @@ -96,7 +101,8 @@ class SocketServer(AbstractServer): # Read def read(self): - if not self.connected: return + if not self.connected: + return raw = self.socket.recv(1024) temp = (self.readbuffer + raw).split(b'\r\n') diff --git a/tools/__init__.py b/tools/__init__.py index fe39e49..78b9d0c 100644 --- a/tools/__init__.py +++ b/tools/__init__.py @@ -18,14 +18,16 @@ import imp + def intToIP(n): ip = "" - for i in range(0,4): + for i in range(0, 4): mod = n % 256 ip = "%d.%s" % (mod, ip) n = (n - mod) / 256 return ip[:len(ip) - 1] + def ipToInt(ip): sum = 0 for b in ip.split("."): diff --git a/tools/countdown.py b/tools/countdown.py index b0c7c71..4f66b21 100644 --- a/tools/countdown.py +++ b/tools/countdown.py @@ -19,6 +19,7 @@ from datetime import datetime, timezone import time + def countdown(delta, resolution=5): sec = delta.seconds hours, remainder = divmod(sec, 3600) @@ -31,7 +32,7 @@ def countdown(delta, resolution=5): if resolution > 0 and (force or an > 0): force = True - sentence += " %i an"%(an) + sentence += " %i an" % an if an > 1: sentence += "s" @@ -42,7 +43,7 @@ def countdown(delta, resolution=5): if resolution > 1 and (force or days > 0): force = True - sentence += " %i jour"%(days) + sentence += " %i jour" % days if days > 1: sentence += "s" @@ -53,7 +54,7 @@ def countdown(delta, resolution=5): if resolution > 2 and (force or hours > 0): force = True - sentence += " %i heure"%(hours) + sentence += " %i heure" % hours if hours > 1: sentence += "s" if resolution > 4: @@ -63,7 +64,7 @@ def countdown(delta, resolution=5): if resolution > 3 and (force or minutes > 0): force = True - sentence += " %i minute"%(minutes) + sentence += " %i minute" % minutes if minutes > 1: sentence += "s" if resolution > 4: @@ -71,20 +72,21 @@ def countdown(delta, resolution=5): if resolution > 4 and (force or seconds > 0): force = True - sentence += " %i seconde"%(seconds) + sentence += " %i seconde" % seconds if seconds > 1: sentence += "s" return sentence[1:] def countdown_format(date, msg_before, msg_after, tz=None): - """Replace in a text %s by a sentence incidated the remaining time before/after an event""" - if tz != None: + """Replace in a text %s by a sentence incidated the remaining time + before/after an event""" + if tz is not None: oldtz = os.environ['TZ'] os.environ['TZ'] = tz time.tzset() - #Calculate time before the date + # Calculate time before the date try: if datetime.now(timezone.utc) > date: sentence_c = msg_after @@ -100,8 +102,7 @@ def countdown_format(date, msg_before, msg_after, tz=None): sentence_c = msg_before delta = date - datetime.now() - - if tz != None: + if tz is not None: os.environ['TZ'] = oldtz return sentence_c % countdown(delta) diff --git a/tools/date.py b/tools/date.py index 15b694e..f15ed95 100644 --- a/tools/date.py +++ b/tools/date.py @@ -29,24 +29,37 @@ xtrdt = re.compile(r'''^.*? (?P<day>[0-9]{1,4}) .+? (?:[^0-9]*[m\":][^0-9]*(?P<second>[0-9]{1,2}))?)?)?.*? $''', re.X) + def extractDate(msg): """Parse a message to extract a time and date""" result = xtrdt.match(msg.lower()) 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 == "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 year = result.group("year") @@ -57,9 +70,12 @@ def extractDate(msg): minute = result.group("minute") second = result.group("second") - if year is None: year = date.today().year - if hour is None: hour = 0 - if minute is None: minute = 0 + if year is None: + year = date.today().year + if hour is None: + hour = 0 + if minute is None: + minute = 0 if second is None: second = 1 else: @@ -68,6 +84,7 @@ def extractDate(msg): minute = int(minute) + 1 second = 0 - return datetime(int(year), int(month), int(day), int(hour), int(minute), int(second)) + return datetime(int(year), int(month), int(day), + int(hour), int(minute), int(second)) else: return None diff --git a/tools/web.py b/tools/web.py index 020a616..a9e59fa 100644 --- a/tools/web.py +++ b/tools/web.py @@ -28,31 +28,39 @@ from urllib.request import urlopen from exception import IRCException import xmlparser + def isURL(url): """Return True if the URL can be parsed""" o = urlparse(url) return o.scheme == "" and o.netloc == "" and o.path == "" + def getScheme(url): """Return the protocol of a given URL""" o = urlparse(url) return o.scheme + def getHost(url): """Return the domain of a given URL""" return urlparse(url).netloc + def getPort(url): """Return the port of a given URL""" return urlparse(url).port + def getPath(url): """Return the page request of a given URL""" return urlparse(url).path + def getUser(url): """Return the page request of a given URL""" return urlparse(url).username + + def getPassword(url): """Return the page request of a given URL""" return urlparse(url).password @@ -67,16 +75,19 @@ def getURLContent(url, timeout=15): o = urlparse("http://" + url) if o.scheme == "http": - conn = http.client.HTTPConnection(o.netloc, port=o.port, timeout=timeout) + conn = http.client.HTTPConnection(o.netloc, port=o.port, + timeout=timeout) elif o.scheme == "https": - conn = http.client.HTTPSConnection(o.netloc, port=o.port, timeout=timeout) + conn = http.client.HTTPSConnection(o.netloc, port=o.port, + timeout=timeout) elif o.scheme is None or o.scheme == "": conn = http.client.HTTPConnection(o.netloc, port=80, timeout=timeout) else: return None try: if o.query != '': - conn.request("GET", o.path + "?" + o.query, None, {"User-agent": "Nemubot v3"}) + conn.request("GET", o.path + "?" + o.query, + None, {"User-agent": "Nemubot v3"}) else: conn.request("GET", o.path, None, {"User-agent": "Nemubot v3"}) except socket.timeout: @@ -115,10 +126,14 @@ def getURLContent(url, timeout=15): if res.status == http.client.OK or res.status == http.client.SEE_OTHER: return data.decode(charset) - elif (res.status == http.client.FOUND or res.status == http.client.MOVED_PERMANENTLY) and res.getheader("Location") != url: + elif ((res.status == http.client.FOUND or + res.status == http.client.MOVED_PERMANENTLY) and + res.getheader("Location") != url): return getURLContent(res.getheader("Location"), timeout) else: - raise IRCException("A HTTP error occurs: %d - %s" % (res.status, http.client.responses[res.status])) + raise IRCException("A HTTP error occurs: %d - %s" % + (res.status, http.client.responses[res.status])) + def getXML(url, timeout=15): """Get content page and return XML parsed content""" @@ -128,6 +143,7 @@ def getXML(url, timeout=15): else: return xmlparser.parse_string(cnt.encode()) + def getJSON(url, timeout=15): """Get content page and return JSON content""" cnt = getURLContent(url, timeout) @@ -136,6 +152,7 @@ def getJSON(url, timeout=15): else: return json.loads(cnt.decode()) + # Other utils def htmlentitydecode(s): @@ -143,7 +160,11 @@ def htmlentitydecode(s): return re.sub('&(%s);' % '|'.join(name2codepoint), lambda m: chr(name2codepoint[m.group(1)]), s) + def striphtml(data): """Remove HTML tags from text""" p = re.compile(r'<.*?>') - return htmlentitydecode(p.sub('', data).replace("(", "/(").replace(")", ")/").replace(""", "\"")) + return htmlentitydecode(p.sub('', data) + .replace("(", "/(") + .replace(")", ")/") + .replace(""", "\"")) diff --git a/xmlparser/__init__.py b/xmlparser/__init__.py index 8dd33d6..722c6bf 100644 --- a/xmlparser/__init__.py +++ b/xmlparser/__init__.py @@ -25,56 +25,59 @@ from . import node as module_state logger = logging.getLogger("nemubot.xmlparser") + class ModuleStatesFile(xml.sax.ContentHandler): - def startDocument(self): - self.root = None - self.stack = list() + def startDocument(self): + self.root = None + self.stack = list() - def startElement(self, name, attrs): - cur = module_state.ModuleState(name) + def startElement(self, name, attrs): + cur = module_state.ModuleState(name) - for name in attrs.keys(): - cur.setAttribute(name, attrs.getValue(name)) + for name in attrs.keys(): + cur.setAttribute(name, attrs.getValue(name)) - self.stack.append(cur) + self.stack.append(cur) - def characters(self, content): - self.stack[len(self.stack)-1].content += content + def characters(self, content): + self.stack[len(self.stack)-1].content += content + + def endElement(self, name): + child = self.stack.pop() + size = len(self.stack) + if size > 0: + self.stack[size - 1].content = self.stack[size - 1].content.strip() + self.stack[size - 1].addChild(child) + else: + self.root = child - def endElement(self, name): - child = self.stack.pop() - size = len(self.stack) - if size > 0: - self.stack[size - 1].content = self.stack[size - 1].content.strip() - self.stack[size - 1].addChild(child) - else: - self.root = child def parse_file(filename): - parser = xml.sax.make_parser() - mod = ModuleStatesFile() - parser.setContentHandler(mod) - try: - parser.parse(open(filename, "r")) - return mod.root - except IOError: - logger.exception("error occurs during XML parsing of %s", filename) - return module_state.ModuleState("nemubotstate") - except: - logger.exception("error occurs during XML parsing of %s", filename) - if mod.root is None: - return module_state.ModuleState("nemubotstate") - else: - return mod.root + parser = xml.sax.make_parser() + mod = ModuleStatesFile() + parser.setContentHandler(mod) + try: + parser.parse(open(filename, "r")) + return mod.root + except IOError: + logger.exception("error occurs during XML parsing of %s", filename) + return module_state.ModuleState("nemubotstate") + except: + logger.exception("error occurs during XML parsing of %s", filename) + if mod.root is None: + return module_state.ModuleState("nemubotstate") + else: + return mod.root + def parse_string(string): - mod = ModuleStatesFile() - try: - xml.sax.parseString(string, mod) - return mod.root - except: - logger.exception("error occurs during XML parsing") - if mod.root is None: - return module_state.ModuleState("nemubotstate") - else: - return mod.root + mod = ModuleStatesFile() + try: + xml.sax.parseString(string, mod) + return mod.root + except: + logger.exception("error occurs during XML parsing") + if mod.root is None: + return module_state.ModuleState("nemubotstate") + else: + return mod.root diff --git a/xmlparser/node.py b/xmlparser/node.py index 34e7d0e..6f7f91c 100644 --- a/xmlparser/node.py +++ b/xmlparser/node.py @@ -9,197 +9,206 @@ import traceback logger = logging.getLogger("nemubot.xmlparser.node") + class ModuleState: - """Tiny tree representation of an XML file""" + """Tiny tree representation of an XML file""" - def __init__(self, name): - self.name = name - self.content = "" - self.attributes = dict() - self.childs = list() - self.index = dict() - self.index_fieldname = None - self.index_tagname = None + def __init__(self, name): + self.name = name + self.content = "" + self.attributes = dict() + self.childs = list() + self.index = dict() + self.index_fieldname = None + self.index_tagname = None - def getName(self): - """Get the name of the current node""" - return self.name + def getName(self): + """Get the name of the current node""" + return self.name - def display(self, level = 0): - ret = "" - out = list() - for k in self.attributes: - out.append("%s : %s" % (k, self.attributes[k])) - ret += "%s%s { %s } = '%s'\n" % (' ' * level, self.name, ' ; '.join(out), self.content) - for c in self.childs: - ret += c.display(level + 2) - return ret + def display(self, level = 0): + ret = "" + out = list() + for k in self.attributes: + out.append("%s : %s" % (k, self.attributes[k])) + ret += "%s%s { %s } = '%s'\n" % (' ' * level, self.name, + ' ; '.join(out), self.content) + for c in self.childs: + ret += c.display(level + 2) + return ret - def __str__(self): - return self.display() + def __str__(self): + return self.display() - def __getitem__(self, i): - """Return the attribute asked""" - return self.getAttribute(i) + def __getitem__(self, i): + """Return the attribute asked""" + return self.getAttribute(i) - def __setitem__(self, i, c): - """Set the attribute""" - return self.setAttribute(i, c) + def __setitem__(self, i, c): + """Set the attribute""" + return self.setAttribute(i, c) - def getAttribute(self, name): - """Get the asked argument or return None if doesn't exist""" - if name in self.attributes: - return self.attributes[name] - else: - return None + def getAttribute(self, name): + """Get the asked argument or return None if doesn't exist""" + if name in self.attributes: + return self.attributes[name] + else: + return None - def getDate(self, name=None): - """Get the asked argument and return it as a date""" - if name is None: - source = self.content - elif name in self.attributes.keys(): - source = self.attributes[name] - else: + def getDate(self, name=None): + """Get the asked argument and return it as a date""" + if name is None: + source = self.content + elif name in self.attributes.keys(): + source = self.attributes[name] + else: + return None + + if isinstance(source, datetime): + return source + else: + try: + return datetime.fromtimestamp(float(source), timezone.utc) + except ValueError: + while True: + try: + return datetime.fromtimestamp(time.mktime( + time.strptime(source[:19], "%Y-%m-%d %H:%M:%S")), + timezone.utc) + except ImportError: + pass + + def getInt(self, name=None): + """Get the asked argument and return it as an integer""" + if name is None: + source = self.content + elif name in self.attributes.keys(): + source = self.attributes[name] + else: + return None + + return int(float(source)) + + def getBool(self, name=None): + """Get the asked argument and return it as an integer""" + if name is None: + source = self.content + elif name in self.attributes.keys(): + source = self.attributes[name] + else: + return False + + return (isinstance(source, bool) and source) or source == "True" + + def tmpIndex(self, fieldname="name", tagname=None): + index = dict() + for child in self.childs: + if ((tagname is None or tagname == child.name) and + child.hasAttribute(fieldname)): + index[child[fieldname]] = child + return index + + def setIndex(self, fieldname="name", tagname=None): + """Defines an hash table to accelerate childs search. + You have just to define a common attribute""" + self.index = self.tmpIndex(fieldname, tagname) + self.index_fieldname = fieldname + self.index_tagname = tagname + + def __contains__(self, i): + """Return true if i is found in the index""" + if self.index: + return i in self.index + else: + return self.hasAttribute(i) + + def hasAttribute(self, name): + """DOM like method""" + return (name in self.attributes) + + def setAttribute(self, name, value): + """DOM like method""" + if (isinstance(value, datetime) or isinstance(value, str) or + isinstance(value, int) or isinstance(value, float)): + self.attributes[name] = value + else: + raise TypeError("attributes must be primary type " + "or datetime (here %s)" % type(value)) + + def getContent(self): + return self.content + + def getChilds(self): + """Return a full list of direct child of this node""" + return self.childs + + def getNode(self, tagname): + """Get a unique node (or the last one) with the given tagname""" + ret = None + for child in self.childs: + if tagname is None or tagname == child.name: + ret = child + return ret + + def getFirstNode(self, tagname): + """Get a unique node (or the last one) with the given tagname""" + for child in self.childs: + if tagname is None or tagname == child.name: + return child return None - if isinstance(source, datetime): - return source - else: - try: - return datetime.fromtimestamp(float(source), timezone.utc) - except ValueError: - while True: - try: - return datetime.fromtimestamp(time.mktime( - time.strptime(source[:19], "%Y-%m-%d %H:%M:%S")), timezone.utc) - except ImportError: - pass + def getNodes(self, tagname): + """Get all direct childs that have the given tagname""" + for child in self.childs: + if tagname is None or tagname == child.name: + yield child - def getInt(self, name=None): - """Get the asked argument and return it as an integer""" - if name is None: - source = self.content - elif name in self.attributes.keys(): - source = self.attributes[name] - else: - return None - - return int(float(source)) - - def getBool(self, name=None): - """Get the asked argument and return it as an integer""" - if name is None: - source = self.content - elif name in self.attributes.keys(): - source = self.attributes[name] - else: + def hasNode(self, tagname): + """Return True if at least one node with the given tagname exists""" + for child in self.childs: + if tagname is None or tagname == child.name: + return True return False - return (isinstance(source, bool) and source) or source == "True" + def addChild(self, child): + """Add a child to this node""" + self.childs.append(child) + if self.index_fieldname is not None: + self.setIndex(self.index_fieldname, self.index_tagname) - def tmpIndex(self, fieldname="name", tagname=None): - index = dict() - for child in self.childs: - if (tagname is None or tagname == child.name) and child.hasAttribute(fieldname): - index[child[fieldname]] = child - return index + def delChild(self, child): + """Remove the given child from this node""" + self.childs.remove(child) + if self.index_fieldname is not None: + self.setIndex(self.index_fieldname, self.index_tagname) - def setIndex(self, fieldname="name", tagname=None): - """Defines an hash table to accelerate childs search. You have just to define a common attribute""" - self.index = self.tmpIndex(fieldname, tagname) - self.index_fieldname = fieldname - self.index_tagname = tagname + def save_node(self, gen): + """Serialize this node as a XML node""" + attribs = {} + for att in self.attributes.keys(): + if att[0] != "_": # Don't save attribute starting by _ + if isinstance(self.attributes[att], datetime): + attribs[att] = str(time.mktime( + self.attributes[att].timetuple())) + else: + attribs[att] = str(self.attributes[att]) + attrs = xml.sax.xmlreader.AttributesImpl(attribs) - def __contains__(self, i): - """Return true if i is found in the index""" - if self.index: - return i in self.index - else: - return self.hasAttribute(i) + try: + gen.startElement(self.name, attrs) - def hasAttribute(self, name): - """DOM like method""" - return (name in self.attributes) + for child in self.childs: + child.save_node(gen) - def setAttribute(self, name, value): - """DOM like method""" - if isinstance(value, datetime) or isinstance(value, str) or isinstance(value, int) or isinstance(value, float): - self.attributes[name] = value - else: - raise TypeError("attributes must be primary type or datetime (here %s)" % type(value)) + gen.endElement(self.name) + except: + logger.exception("Error occured when saving the following " + "XML node: %s with %s", self.name, attrs) - def getContent(self): - return self.content - - def getChilds(self): - """Return a full list of direct child of this node""" - return self.childs - - def getNode(self, tagname): - """Get a unique node (or the last one) with the given tagname""" - ret = None - for child in self.childs: - if tagname is None or tagname == child.name: - ret = child - return ret - - def getFirstNode(self, tagname): - """Get a unique node (or the last one) with the given tagname""" - for child in self.childs: - if tagname is None or tagname == child.name: - return child - return None - - def getNodes(self, tagname): - """Get all direct childs that have the given tagname""" - for child in self.childs: - if tagname is None or tagname == child.name: - yield child - - def hasNode(self, tagname): - """Return True if at least one node with the given tagname exists""" - for child in self.childs: - if tagname is None or tagname == child.name: - return True - return False - - def addChild(self, child): - """Add a child to this node""" - self.childs.append(child) - if self.index_fieldname is not None: - self.setIndex(self.index_fieldname, self.index_tagname) - - def delChild(self, child): - """Remove the given child from this node""" - self.childs.remove(child) - if self.index_fieldname is not None: - self.setIndex(self.index_fieldname, self.index_tagname) - - def save_node(self, gen): - """Serialize this node as a XML node""" - attribs = {} - for att in self.attributes.keys(): - if att[0] != "_": # Don't save attribute starting by _ - if isinstance(self.attributes[att], datetime): - attribs[att] = str(time.mktime(self.attributes[att].timetuple())) - else: - attribs[att] = str(self.attributes[att]) - attrs = xml.sax.xmlreader.AttributesImpl(attribs) - - try: - gen.startElement(self.name, attrs) - - for child in self.childs: - child.save_node(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""" - with open(filename,"w") as f: - gen = xml.sax.saxutils.XMLGenerator(f, "utf-8") - gen.startDocument() - self.save_node(gen) - gen.endDocument() + def save(self, filename): + """Save the current node as root node in a XML file""" + with open(filename, "w") as f: + gen = xml.sax.saxutils.XMLGenerator(f, "utf-8") + gen.startDocument() + self.save_node(gen) + gen.endDocument() From b6c5bf4f109080534c5c86bdaf2c71e492e131f2 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 10 Nov 2014 21:01:22 +0100 Subject: [PATCH 191/674] Move configuration file loading from prompt to tools --- prompt/builtins.py | 97 +-------------------------------- tools/config.py | 133 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 95 deletions(-) create mode 100644 tools/config.py diff --git a/prompt/builtins.py b/prompt/builtins.py index f0e5c68..a666586 100644 --- a/prompt/builtins.py +++ b/prompt/builtins.py @@ -20,17 +20,10 @@ import imp import logging import os +from tools.config import load_file + logger = logging.getLogger("nemubot.prompt.builtins") -from server.IRC import IRC as IRCServer -import xmlparser - - -def get_boolean(d, k): - return (k in d and - mod["autoload"].lower() != "false" and - mod["autoload"].lower() != "off") - def end(toks, context, prompt): """Quit the prompt for reload or exit""" @@ -67,92 +60,6 @@ def liste(toks, context, prompt): print (" Please give a list to show: servers, ...") -def load_file(filename, context): - if os.path.isfile(filename): - config = xmlparser.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"): - opts = { - "host": server["host"], - "ssl": server.hasAttribute("ssl") and server["ssl"].lower() == "true", - - "nick": server["nick"] if server.hasAttribute("nick") else config["nick"], - "owner": server["owner"] if server.hasAttribute("owner") else config["owner"], - } - - # Optional keyword arguments - for optional_opt in [ "port", "username", "realname", - "password", "encoding", "caps" ]: - if server.hasAttribute(optional_opt): - opts[optional_opt] = server[optional_opt] - elif optional_opt in config: - opts[optional_opt] = config[optional_opt] - - # Command to send on connection - if "on_connect" in server: - def on_connect(): - yield server["on_connect"] - opts["on_connect"] = on_connect - - # Channels to autojoin on connection - if server.hasNode("channel"): - opts["channels"] = list() - for chn in server.getNodes("channel"): - opts["channels"].append((chn["name"], chn["password"]) - if chn["password"] is not None - else chn["name"]) - - # Server/client capabilities - if "caps" in server or "caps" in config: - capsl = (server["caps"] if server.hasAttribute("caps") - else config["caps"]).lower() - if capsl == "no" or capsl == "off" or capsl == "false": - opts["caps"] = None - else: - opts["caps"] = capsl.split(',') - else: - opts["caps"] = list() - - # Bind the protocol asked to the corresponding implementation - if "protocol" not in server or server["protocol"] == "irc": - srvcls = IRCServer - else: - raise Exception("Unhandled protocol '%s'" % - server["protocol"]) - - # Initialize the server - srv = srvcls(**opts) - - # Add the server in the context - if context.add_server(srv, get_boolean(server, "autoconnect")): - print("Server '%s' successfully added." % srv.id) - else: - print("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"): - __import__(mod["name"]) - - # Load files asked by the configuration file - for load in config.getNodes("include"): - load_file(load["path"], context) - - # Other formats - else: - print (" Can't load `%s'; this is not a valid nemubot " - "configuration file." % filename) - - # Unexisting file, assume a name was passed, import the module! - else: - tt = __import__(filename) - imp.reload(tt) - - def load(toks, context, prompt): """Load an XML configuration file""" if len(toks) > 1: diff --git a/tools/config.py b/tools/config.py new file mode 100644 index 0000000..f4f9a5f --- /dev/null +++ b/tools/config.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- + +# Nemubot is a modulable IRC bot, built around XML configuration files. +# Copyright (C) 2012 Mercier Pierre-Olivier +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import logging +import os + +import xmlparser + +logger = logging.getLogger("nemubot.tools.config") + + +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)) + + +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 + 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 server.IRC import IRC as IRCServer + srvcls = IRCServer + else: + raise Exception("Unhandled protocol '%s'" % + xmlnode["protocol"]) + + # Initialize the server + return srvcls(**opts) + + +def load_file(filename, context): + """Load the configuration file + + Arguments: + filename -- the path to the file to load + """ + + if os.path.isfile(filename): + config = xmlparser.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")): + print("Server '%s' successfully added." % srv.id) + else: + print("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): + __import__(mod["name"]) + + # Load files asked by the configuration file + for load in config.getNodes("include"): + load_file(load["path"], context) + + # Other formats + else: + print (" Can't load `%s'; this is not a valid nemubot " + "configuration file." % filename) + + # Unexisting file, assume a name was passed, import the module! + else: + tt = __import__(filename) + imp.reload(tt) From 4f27232fd4b2b2c76c21d8b0a1e572a498e4bb6a Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 10 Nov 2014 21:10:09 +0100 Subject: [PATCH 192/674] [bonneannee] Unharcode channel to send message on 1 January --- modules/bonneannee.py | 52 ++++++++++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/modules/bonneannee.py b/modules/bonneannee.py index 3a1167f..f29b6c4 100644 --- a/modules/bonneannee.py +++ b/modules/bonneannee.py @@ -14,28 +14,42 @@ from more import Response yr = datetime.now(timezone.utc).year yrn = datetime.now(timezone.utc).year + 1 -def load(context): - d = datetime(yrn, 1, 1, 0, 0, 0, 0, timezone.utc) - datetime.now(timezone.utc) - add_event(ModuleEvent(interval=0, offset=d.total_seconds(), call=bonneannee)) -def bonneannee(): - txt = "Bonne année %d !" % yrn - print (txt) - send_response("localhost:2771", Response(txt, "#epitagueule")) - send_response("localhost:2771", Response(txt, "#yaka")) - send_response("localhost:2771", Response(txt, "#epita2014")) - send_response("localhost:2771", Response(txt, "#ykar")) - send_response("localhost:2771", Response(txt, "#42sh")) - send_response("localhost:2771", Response(txt, "#nemubot")) +def load(context): + if not CONF or not CONF.hasNode("sayon"): + print("You can append in your configuration some balise to " + "automaticaly wish an happy new year on some channels like:\n" + "<sayon hostid=\"nemubot@irc.freenode.net:6667\" " + "channel=\"#nemutest\" />") + + def bonneannee(): + txt = "Bonne année %d !" % yrn + print(txt) + if CONF and CONF.hasNode("sayon"): + for sayon in CONF.getNodes("sayon"): + if "hostid" not in sayon or "channel" not in sayon: + print("Error: missing hostif or channel") + continue + srv = sayon["hostid"] + chan = sayon["channel"] + send_response(srv, Response(txt, chan)) + + d = datetime(yrn, 1, 1, 0, 0, 0, 0, + timezone.utc) - datetime.now(timezone.utc) + add_event(ModuleEvent(interval=0, offset=d.total_seconds(), + call=bonneannee)) + @hook("cmd_hook", "newyear") @hook("cmd_hook", str(yrn), yrn) def cmd_newyear(msg, yr): - return Response(countdown_format(datetime(yr, 1, 1, 0, 0, 1, 0, timezone.utc), - "Il reste %s avant la nouvelle année.", - "Nous faisons déjà la fête depuis %s !"), + return Response(countdown_format(datetime(yr, 1, 1, 0, 0, 1, 0, + timezone.utc), + "Il reste %s avant la nouvelle année.", + "Nous faisons déjà la fête depuis %s !"), channel=msg.channel) + @hook("cmd_rgxp", data=yrn, regexp="^[0-9]{4}$") def cmd_timetoyear(msg, cur): yr = int(msg.cmds[0]) @@ -43,7 +57,9 @@ def cmd_timetoyear(msg, cur): if yr == cur: return None - return Response(countdown_format(datetime(yr, 1, 1, 0, 0, 1, 0, timezone.utc), - "Il reste %s avant %d." % ("%s", yr), - "Le premier janvier %d est passé depuis %s !" % (yr, "%s")), + return Response(countdown_format(datetime(yr, 1, 1, 0, 0, 1, 0, + timezone.utc), + "Il reste %s avant %d." % ("%s", yr), + "Le premier janvier %d est passé " + "depuis %s !" % (yr, "%s")), channel=msg.channel) From e1aff6c4cf01f43ab4519afff15704fad7de2513 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 12 Nov 2014 23:50:48 +0100 Subject: [PATCH 193/674] channel or nick required when creating a Response --- modules/alias.py | 6 +++--- modules/more.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/alias.py b/modules/alias.py index 0f8c876..3b0fc2b 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -51,10 +51,10 @@ def get_variable(name, msg=None): def cmd_set(msg): if len (msg.cmds) > 2: set_variable(msg.cmds[1], " ".join(msg.cmds[2:]), msg.nick) - res = Response("Variable \$%s définie." % msg.cmds[1]) + res = Response("Variable \$%s définie." % msg.cmds[1], channel=msg.channel) save() return res - return Response("!set prend au minimum deux arguments : le nom de la variable et sa valeur.") + return Response("!set prend au minimum deux arguments : le nom de la variable et sa valeur.", channel=msg.channel) @hook("cmd_hook", "listalias") def cmd_listalias(msg): @@ -174,7 +174,7 @@ def parseask(msg): alias["origin"] = result.group(3) alias["creator"] = msg.nick DATAS.getNode("aliases").addChild(alias) - res = Response("Nouvel alias %s défini avec succès." % result.group(1)) + res = Response("Nouvel alias %s défini avec succès." % result.group(1), channel=msg.channel) save() return res return None diff --git a/modules/more.py b/modules/more.py index f30bc54..d067487 100644 --- a/modules/more.py +++ b/modules/more.py @@ -51,7 +51,7 @@ class Response: if self.channel is None: if self.nick is not None: return [ self.nick ] - return [ self.sender.split("!")[0] ] + return list() elif isinstance(self.channel, list): return self.channel else: From 2dfe1f0e9abb7481da9f4b16dcbfc731d79bc365 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 13 Nov 2014 02:51:49 +0100 Subject: [PATCH 194/674] PEP8 clean --- modules/alias.py | 77 +++++++++++++++++++++++++++--------------- modules/birthday.py | 23 ++++++++----- modules/books.py | 34 ++++++++++++++----- modules/cmd_server.py | 41 +++++++++++++++++----- modules/conjugaison.py | 44 +++++++++++++----------- modules/github.py | 70 +++++++++++++++++++++++++++----------- modules/man.py | 25 +++++++++----- modules/mediawiki.py | 11 ++++-- modules/more.py | 24 +++++++------ modules/reddit.py | 34 ++++++++++++++----- modules/rnd.py | 5 ++- modules/sap.py | 37 ++++++++++++-------- modules/sleepytime.py | 18 ++++++---- modules/velib.py | 13 +++++-- modules/ycc.py | 34 ++++++++++++------- 15 files changed, 330 insertions(+), 160 deletions(-) diff --git a/modules/alias.py b/modules/alias.py index 3b0fc2b..61234c1 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -14,6 +14,7 @@ nemubotversion = 3.4 from more import Response + def load(context): """Load this module""" global DATAS @@ -25,9 +26,10 @@ def load(context): DATAS.getNode("variables").setIndex("name") -def help_full (): +def help_full(): return "TODO" + def set_variable(name, value, creator): var = ModuleState("variable") var["name"] = name @@ -35,6 +37,7 @@ def set_variable(name, value, creator): var["creator"] = creator DATAS.getNode("variables").addChild(var) + def get_variable(name, msg=None): if name == "sender" or name == "from" or name == "nick": return msg.frm @@ -47,14 +50,19 @@ def get_variable(name, msg=None): else: return "" + @hook("cmd_hook", "set") def cmd_set(msg): - if len (msg.cmds) > 2: + if len(msg.cmds) > 2: set_variable(msg.cmds[1], " ".join(msg.cmds[2:]), msg.nick) - res = Response("Variable \$%s définie." % msg.cmds[1], channel=msg.channel) + res = Response("Variable \$%s définie." % msg.cmds[1], + channel=msg.channel) save() return res - return Response("!set prend au minimum deux arguments : le nom de la variable et sa valeur.", channel=msg.channel) + return Response("!set prend au minimum deux arguments : " + "le nom de la variable et sa valeur.", + channel=msg.channel) + @hook("cmd_hook", "listalias") def cmd_listalias(msg): @@ -68,7 +76,10 @@ def cmd_listalias(msg): res.append("%s n'a pas encore créé d'alias" % user) return Response(" ; ".join(res), channel=msg.channel) else: - return Response("Alias connus : %s." % ", ".join(DATAS.getNode("aliases").index.keys()), channel=msg.channel) + return Response("Alias connus : %s." % + ", ".join(DATAS.getNode("aliases").index.keys()), + channel=msg.channel) + @hook("cmd_hook", "listvars") def cmd_listvars(msg): @@ -82,18 +93,21 @@ def cmd_listvars(msg): res.append("%s n'a pas encore créé de variable" % user) return Response(" ; ".join(res), channel=msg.channel) else: - return Response("Variables connues : %s." % ", ".join(DATAS.getNode("variables").index.keys()), channel=msg.channel) + return Response("Variables connues : %s." % + ", ".join(DATAS.getNode("variables").index.keys()), + channel=msg.channel) + @hook("cmd_hook", "alias") def cmd_alias(msg): - if len (msg.cmds) > 1: + if len(msg.cmds) > 1: res = list() for alias in msg.cmds[1:]: if alias[0] == "!": alias = alias[1:] if alias in DATAS.getNode("aliases").index: - res.append(Response("!%s correspond à %s" % (alias, - DATAS.getNode("aliases").index[alias]["origin"]), + res.append(Response("!%s correspond à %s" % + (alias, DATAS.getNode("aliases").index[alias]["origin"]), channel=msg.channel)) else: res.append(Response("!%s n'est pas un alias" % alias, @@ -103,9 +117,10 @@ def cmd_alias(msg): return Response("!alias prend en argument l'alias à étendre.", channel=msg.channel) + @hook("cmd_hook", "unalias") def cmd_unalias(msg): - if len (msg.cmds) > 1: + if len(msg.cmds) > 1: res = list() for alias in msg.cmds[1:]: if alias[0] == "!" and len(alias) > 1: @@ -113,31 +128,38 @@ def cmd_unalias(msg): if alias in DATAS.getNode("aliases").index: if DATAS.getNode("aliases").index[alias]["creator"] == msg.nick or msg.frm_owner: DATAS.getNode("aliases").delChild(DATAS.getNode("aliases").index[alias]) - res.append(Response("%s a bien été supprimé" % alias, channel=msg.channel)) + res.append(Response("%s a bien été supprimé" % alias, + channel=msg.channel)) else: - res.append(Response("Vous n'êtes pas le createur de l'alias %s." % alias, channel=msg.channel)) + res.append(Response("Vous n'êtes pas le createur de " + "l'alias %s." % alias, + channel=msg.channel)) else: - res.append(Response("%s n'est pas un alias" % alias, channel=msg.channel)) + res.append(Response("%s n'est pas un alias" % alias, + channel=msg.channel)) return res else: - return Response("!unalias prend en argument l'alias à supprimer.", channel=msg.channel) + return Response("!unalias prend en argument l'alias à supprimer.", + channel=msg.channel) + def replace_variables(cnt, msg=None): cnt = cnt.split(' ') unsetCnt = list() for i in range(0, len(cnt)): - if i not in unsetCnt: - res = re.match("^([^$]*)(\\\\)?\\$([a-zA-Z0-9]+)(.*)$", cnt[i]) - if res is not None: - try: - varI = int(res.group(3)) - unsetCnt.append(varI) - cnt[i] = res.group(1) + msg.cmds[varI] + res.group(4) - except: - if res.group(2) != "": - cnt[i] = res.group(1) + "$" + res.group(3) + res.group(4) - else: - cnt[i] = res.group(1) + get_variable(res.group(3), msg) + res.group(4) + if i not in unsetCnt: + res = re.match("^([^$]*)(\\\\)?\\$([a-zA-Z0-9]+)(.*)$", cnt[i]) + if res is not None: + try: + varI = int(res.group(3)) + unsetCnt.append(varI) + cnt[i] = res.group(1) + msg.cmds[varI] + res.group(4) + except: + if res.group(2) != "": + cnt[i] = res.group(1) + "$" + res.group(3) + res.group(4) + else: + cnt[i] = (res.group(1) + get_variable(res.group(3), msg) + + res.group(4)) return " ".join(cnt) @@ -174,7 +196,8 @@ def parseask(msg): alias["origin"] = result.group(3) alias["creator"] = msg.nick DATAS.getNode("aliases").addChild(alias) - res = Response("Nouvel alias %s défini avec succès." % result.group(1), channel=msg.channel) + res = Response("Nouvel alias %s défini avec succès." % + result.group(1), channel=msg.channel) save() return res return None diff --git a/modules/birthday.py b/modules/birthday.py index 1ec8999..f4765aa 100644 --- a/modules/birthday.py +++ b/modules/birthday.py @@ -15,17 +15,22 @@ nemubotversion = 3.4 from more import Response + def load(context): global DATAS DATAS.setIndex("name", "birthday") def help_full(): - return "!anniv /who/: gives the remaining time before the anniversary of /who/\n!age /who/: gives the age of /who/\nIf /who/ is not given, gives the remaining time before your anniversary.\n\n To set yout birthday, say it to nemubot :)" + return ("!anniv /who/: gives the remaining time before the anniversary of " + "/who/\n!age /who/: gives the age of /who/\nIf /who/ is not given," + " gives the remaining time before your anniversary.\n\n To set you" + "r birthday, say it to nemubot :)") def findName(msg): - if len(msg.cmds) < 2 or msg.cmds[1].lower() == "moi" or msg.cmds[1].lower() == "me": + if (len(msg.cmds) < 2 or msg.cmds[1].lower() == "moi" or + msg.cmds[1].lower() == "me"): name = msg.nick.lower() else: name = msg.cmds[1].lower() @@ -35,9 +40,9 @@ def findName(msg): if name in DATAS.index: matches.append(name) else: - for k in DATAS.index.keys (): - if k.find (name) == 0: - matches.append (k) + for k in DATAS.index.keys(): + if k.find(name) == 0: + matches.append(k) return (matches, name) @@ -52,9 +57,9 @@ def cmd_anniv(msg): if (tyd.day == datetime.today().day and tyd.month == datetime.today().month): return Response(countdown_format( - DATAS.index[name].getDate("born"), "", - "C'est aujourd'hui l'anniversaire de %s !" - " Il a %s. Joyeux anniversaire :)" % (name, "%s")), + DATAS.index[name].getDate("born"), "", + "C'est aujourd'hui l'anniversaire de %s !" + " Il a %s. Joyeux anniversaire :)" % (name, "%s")), msg.channel) else: if tyd < datetime.today(): @@ -69,6 +74,7 @@ def cmd_anniv(msg): " de %s. Quand est-il né ?" % name, msg.channel, msg.nick) + @hook("cmd_hook", "age") def cmd_age(msg): (matches, name) = findName(msg) @@ -85,6 +91,7 @@ def cmd_age(msg): " Quand est-il né ?" % name, msg.channel, msg.nick) return True + @hook("ask_default") 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) diff --git a/modules/books.py b/modules/books.py index 59a2f7f..4d316c7 100644 --- a/modules/books.py +++ b/modules/books.py @@ -11,6 +11,7 @@ nemubotversion = 3.4 from more import Response + def load(context): if not CONF or not CONF.hasNode("goodreadsapi") or not CONF.getNode("goodreadsapi").hasAttribute("key"): print ("You need a Goodreads API key in order to use this " @@ -19,24 +20,34 @@ def load(context): "https://www.goodreads.com/api/keys") return None + def get_book(title): - response = web.getXML("https://www.goodreads.com/book/title.xml?key=%s&title=%s" % (CONF.getNode("goodreadsapi")["key"], urllib.parse.quote(title))) + """Retrieve a book from its title""" + response = web.getXML("https://www.goodreads.com/book/title.xml?key=%s&title=%s" % + (CONF.getNode("goodreadsapi")["key"], urllib.parse.quote(title))) if response is not None and response.hasNode("book"): return response.getNode("book") else: return None + def search_books(title): - response = web.getXML("https://www.goodreads.com/search.xml?key=%s&q=%s" % (CONF.getNode("goodreadsapi")["key"], urllib.parse.quote(title))) + """Get a list of book matching given title""" + response = web.getXML("https://www.goodreads.com/search.xml?key=%s&q=%s" % + (CONF.getNode("goodreadsapi")["key"], urllib.parse.quote(title))) if response is not None and response.hasNode("search"): return response.getNode("search").getNode("results").getNodes("work") else: return [] + def search_author(name): - response = web.getXML("https://www.goodreads.com/api/author_url/%s?key=%s" % (urllib.parse.quote(name), CONF.getNode("goodreadsapi")["key"])) + """Looking for an author""" + response = web.getXML("https://www.goodreads.com/api/author_url/%s?key=%s" % + (urllib.parse.quote(name), CONF.getNode("goodreadsapi")["key"])) if response is not None and response.hasNode("author") and response.getNode("author").hasAttribute("id"): - response = web.getXML("https://www.goodreads.com/author/show/%s.xml?key=%s" % (urllib.parse.quote(response.getNode("author")["id"]), CONF.getNode("goodreadsapi")["key"])) + response = web.getXML("https://www.goodreads.com/author/show/%s.xml?key=%s" % + (urllib.parse.quote(response.getNode("author")["id"]), CONF.getNode("goodreadsapi")["key"])) if response is not None and response.hasNode("author"): return response.getNode("author") return None @@ -53,10 +64,10 @@ def cmd_book(msg): 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()) - )) + web.striphtml(book.getNode("description").getContent()))) return res + @hook("cmd_hook", "search_books") def cmd_books(msg): if len(msg.cmds) < 2: @@ -64,16 +75,21 @@ def cmd_books(msg): title = " ".join(msg.cmds[1:]) res = Response(channel=msg.channel, - title="%s" % (title), count=" (%d more books)") + title="%s" % (title), + 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.getNode("best_book").getNode("title").getContent(), + book.getNode("best_book").getNode("author").getNode("name").getContent())) return res + @hook("cmd_hook", "author_books") def cmd_author(msg): if len(msg.cmds) < 2: raise IRCException("please give me an author to search") ath = search_author(" ".join(msg.cmds[1:])) - return Response([b.getNode("title").getContent() for b in ath.getNode("books").getNodes("book")], channel=msg.channel, title=ath.getNode("name").getContent()) + return Response([b.getNode("title").getContent() for b in ath.getNode("books").getNodes("book")], + channel=msg.channel, + title=ath.getNode("name").getContent()) diff --git a/modules/cmd_server.py b/modules/cmd_server.py index 9388e6f..670d553 100644 --- a/modules/cmd_server.py +++ b/modules/cmd_server.py @@ -26,6 +26,7 @@ from networkbot import NetworkBot nemubotversion = 3.4 NODATA = True + def getserver(toks, context, prompt): """Choose the server in toks or prompt""" if len(toks) > 1 and toks[0] in context.servers: @@ -35,6 +36,7 @@ def getserver(toks, context, prompt): else: return (None, toks) + @hook("prompt_cmd", "close") def close(data, toks, context, prompt): """Disconnect and forget (remove from the servers list) the server""" @@ -51,6 +53,7 @@ def close(data, toks, context, prompt): prompt.selectedServer = None return + @hook("prompt_cmd", "connect") def connect(data, toks, context, prompt): """Make the connexion to a server""" @@ -66,6 +69,7 @@ def connect(data, toks, context, prompt): else: print (" Please SELECT a server or give its name in argument.") + @hook("prompt_cmd", "disconnect") def disconnect(data, toks, context, prompt): """Close the connection to a server""" @@ -83,6 +87,7 @@ def disconnect(data, toks, context, prompt): else: print (" Please SELECT a server or give its name in argument.") + @hook("prompt_cmd", "discover") def discover(data, toks, context, prompt): """Discover a new bot on a server""" @@ -93,10 +98,12 @@ def discover(data, toks, context, prompt): bot = context.add_networkbot(srv, name) bot.connect() else: - print (" %s is not a valid fullname, for example: nemubot!nemubotV3@bot.nemunai.re") + print (" %s is not a valid fullname, for example: " + "nemubot!nemubotV3@bot.nemunai.re") else: print (" Please SELECT a server or give its name in first argument.") + @hook("prompt_cmd", "hotswap") def hotswap(data, toks, context, prompt): """Reload a server class""" @@ -114,6 +121,7 @@ def hotswap(data, toks, context, prompt): else: print (" Please SELECT a server or give its name in argument.") + @hook("prompt_cmd", "join") @hook("prompt_cmd", "leave") @hook("prompt_cmd", "part") @@ -134,7 +142,7 @@ def join(data, toks, context, prompt): return if len(toks) <= rd: - print ("%s: not enough arguments." % toks[0]) + print("%s: not enough arguments." % toks[0]) return if toks[0] == "join": @@ -146,6 +154,7 @@ def join(data, toks, context, prompt): srv.write("PART %s" % toks[rd]) return + @hook("prompt_cmd", "save") def save_mod(data, toks, context, prompt): """Force save module data""" @@ -161,6 +170,7 @@ def save_mod(data, toks, context, prompt): print ("save: no module named `%s´" % mod) return + @hook("prompt_cmd", "send") def send(data, toks, context, prompt): """Send a message on a channel""" @@ -182,7 +192,7 @@ def send(data, toks, context, prompt): print ("send: not enough arguments.") return - #Check the server is connected + # Check the server is connected if not srv.connected: print ("send: server `%s' not connected." % srv.id) return @@ -202,6 +212,7 @@ def send(data, toks, context, prompt): srv.send_response(TextMessage(" ".join(toks[rd:]), server=None, to=[chan])) return "done" + @hook("prompt_cmd", "zap") def zap(data, toks, context, prompt): """Hard change connexion state""" @@ -216,20 +227,28 @@ def zap(data, toks, context, prompt): else: print (" Please SELECT a server or give its name in argument.") + @hook("prompt_cmd", "top") def top(data, toks, context, prompt): """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)) + 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)) + 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("################ Stack trace for thread %u ################" % th.ident) + print(("#" * 15 + " Stack trace for thread %u " + "#" * 15) % + th.ident) traceback.print_stack(sys._current_frames()[th.ident]) + @hook("prompt_cmd", "netstat") def netstat(data, toks, context, prompt): """Display sockets in use and many other things""" @@ -241,10 +260,14 @@ def netstat(data, toks, context, prompt): 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: + 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()))) + 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(): diff --git a/modules/conjugaison.py b/modules/conjugaison.py index 7d49c34..4347603 100644 --- a/modules/conjugaison.py +++ b/modules/conjugaison.py @@ -30,16 +30,19 @@ s = [('present', '0'), ('présent', '0'), ('pr', '0'), d = defaultdict(list) for k, v in s: - d[k].append(v) + d[k].append(v) + def help_full(): - return "!conjugaison <tens> <verb>: give the conjugaison for <verb> in <tens>." + return ("!conjugaison <tens> <verb>: give the conjugaison for <verb> in " + "<tens>.") @hook("cmd_hook", "conjugaison") def cmd_conjug(msg): if len(msg.cmds) < 3: - raise IRCException("donne moi un temps et un verbe, et je te donnerai sa conjugaison!") + raise IRCException("donne moi un temps et un verbe, et je te donnerai " + "sa conjugaison!") tens = ' '.join(msg.cmds[1:-1]) print_debug(tens) @@ -58,31 +61,34 @@ def cmd_conjug(msg): def get_conjug(verb, stringTens): url = ("http://leconjugueur.lefigaro.fr/conjugaison/verbe/%s.html" % quote(verb.encode("ISO-8859-1"))) - print_debug (url) + print_debug(url) page = web.getURLContent(url) if page is not None: for line in page.split("\n"): if re.search('<div class="modeBloc">', line) is not None: - return compute_line(line, stringTens) + return compute_line(line, stringTens) return list() + def compute_line(line, stringTens): - try: - idTemps = d[stringTens] - except: - raise IRCException("le temps demandé n'existe pas") + try: + idTemps = d[stringTens] + except: + raise IRCException("le temps demandé n'existe pas") - if len(idTemps) == 0: - raise IRCException("le temps demandé n'existe pas") + if len(idTemps) == 0: + raise IRCException("le temps demandé n'existe pas") - index = line.index('<div id="temps' + idTemps[0] + '\"') - endIndex = line[index:].index('<div class=\"conjugBloc\"') + index = line.index('<div id="temps' + idTemps[0] + '\"') + endIndex = line[index:].index('<div class=\"conjugBloc\"') - endIndex += index - newLine = line[index:endIndex] + endIndex += index + newLine = line[index:endIndex] - res = list() - for elt in re.finditer("[p|/]>([^/]*/b>)", newLine): - res.append(striphtml(elt.group(1).replace("<b>", "\x02").replace("</b>", "\x0F"))) - return res + res = list() + for elt in re.finditer("[p|/]>([^/]*/b>)", newLine): + res.append(striphtml(elt.group(1) + .replace("<b>", "\x02") + .replace("</b>", "\x0F"))) + return res diff --git a/modules/github.py b/modules/github.py index dd71147..41ee743 100644 --- a/modules/github.py +++ b/modules/github.py @@ -14,24 +14,30 @@ 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/." +def help_full(): + return ("!github /repo/: Display information about /repo/.\n" + "!github_user /user/: Display information about /user/.") def info_repos(repo): - raw = urlopen("https://api.github.com/search/repositories?q=%s" % quote(repo), timeout=10) + raw = urlopen("https://api.github.com/search/repositories?q=%s" % + quote(repo), timeout=10) return json.loads(raw.read().decode()) + def info_user(username): - raw = urlopen("https://api.github.com/users/%s" % quote(username), timeout=10) + raw = urlopen("https://api.github.com/users/%s" % quote(username), + timeout=10) user = json.loads(raw.read().decode()) - raw = urlopen("https://api.github.com/users/%s/repos?sort=updated" % quote(username), timeout=10) + raw = urlopen("https://api.github.com/users/%s/repos?sort=updated" % + quote(username), timeout=10) user["repos"] = json.loads(raw.read().decode()) return user + def info_issue(repo, issue=None): rp = info_repos(repo) if rp["items"]: @@ -41,14 +47,17 @@ def info_issue(repo, issue=None): try: if issue is not None: - raw = urlopen("https://api.github.com/repos/%s/issues/%s" % (quote(fullname), quote(issue)), timeout=10) - return [ json.loads(raw.read().decode()) ] + raw = urlopen("https://api.github.com/repos/%s/issues/%s" % + (quote(fullname), quote(issue)), timeout=10) + return [json.loads(raw.read().decode())] else: - raw = urlopen("https://api.github.com/repos/%s/issues?sort=updated" % quote(fullname), timeout=10) + raw = urlopen("https://api.github.com/repos/%s/issues?sort=updated" + % quote(fullname), timeout=10) return json.loads(raw.read().decode()) except urllib.error.HTTPError: raise IRCException("Repository not found") + def info_commit(repo, commit=None): rp = info_repos(repo) if rp["items"]: @@ -58,10 +67,12 @@ def info_commit(repo, commit=None): try: if commit is not None: - raw = urlopen("https://api.github.com/repos/%s/commits/%s" % (quote(fullname), quote(commit)), timeout=10) - return [ json.loads(raw.read().decode()) ] + raw = urlopen("https://api.github.com/repos/%s/commits/%s" % + (quote(fullname), quote(commit)), timeout=10) + return [json.loads(raw.read().decode())] else: - raw = urlopen("https://api.github.com/repos/%s/commits" % quote(fullname), timeout=10) + raw = urlopen("https://api.github.com/repos/%s/commits" % + quote(fullname), timeout=10) return json.loads(raw.read().decode()) except urllib.error.HTTPError: raise IRCException("Repository not found") @@ -74,16 +85,27 @@ def cmd_github(msg): repos = info_repos(" ".join(msg.cmds[1:])) - res = Response(channel=msg.channel, nomore="No more repository", count=" (%d more repo)") + res = Response(channel=msg.channel, + nomore="No more repository", + count=" (%d more repo)") for repo in repos["items"]: homepage = "" if repo["homepage"] is not None: homepage = repo["homepage"] + " - " - res.append_message("Repository %s: %s%s Main language: %s; %d forks; %d stars; %d watchers; %d opened_issues; view it at %s" % (repo["full_name"], homepage, repo["description"], repo["language"], repo["forks"], repo["stargazers_count"], repo["watchers_count"], repo["open_issues_count"], repo["html_url"])) + res.append_message("Repository %s: %s%s Main language: %s; %d forks; %d stars; %d watchers; %d opened_issues; view it at %s" % + (repo["full_name"], + homepage, + repo["description"], + repo["language"], repo["forks"], + repo["stargazers_count"], + repo["watchers_count"], + repo["open_issues_count"], + repo["html_url"])) return res + @hook("cmd_hook", "github_user") def cmd_github(msg): if len(msg.cmds) < 2: @@ -95,14 +117,22 @@ def cmd_github(msg): if "login" in user: if user["repos"]: - kf = " Known for: " + ", ".join([repo["name"] for repo in user["repos"]]) + kf = (" Known for: " + + ", ".join([repo["name"] for repo in user["repos"]])) else: kf = "" if "name" in user: name = user["name"] else: name = user["login"] - res.append_message("User %s: %d public repositories; %d public gists; %d followers; %d following; view it at %s.%s" % (name, user["public_repos"], user["public_gists"], user["followers"], user["following"], user["html_url"], kf)) + res.append_message("User %s: %d public repositories; %d public gists; %d followers; %d following; view it at %s.%s" % + (name, + user["public_repos"], + user["public_gists"], + user["followers"], + user["following"], + user["html_url"], + kf)) else: raise IRCException("User not found") @@ -166,9 +196,9 @@ def cmd_github(msg): commits = info_commit(repo, commit) for commit in commits: - res.append_message("Commit %s by %s on %s: %s" % (commit["sha"][:10], - commit["commit"]["author"]["name"], - commit["commit"]["author"]["date"], - commit["commit"]["message"].replace("\n", " "))) - + res.append_message("Commit %s by %s on %s: %s" % + (commit["sha"][:10], + commit["commit"]["author"]["name"], + commit["commit"]["author"]["date"], + commit["commit"]["message"].replace("\n", " "))) return res diff --git a/modules/man.py b/modules/man.py index f2f1f2d..2e897a3 100644 --- a/modules/man.py +++ b/modules/man.py @@ -1,5 +1,7 @@ # coding=utf-8 +"Read manual pages on IRC" + import subprocess import re import os @@ -10,15 +12,13 @@ nemubotversion = 3.4 from more import Response -def help_tiny(): - """Line inserted in the response to the command !help""" - return "Read manual pages on IRC" def help_full(): return "!man [0-9] /what/: gives informations about /what/." RGXP_s = re.compile(b'\x1b\\[[0-9]+m') + @hook("cmd_hook", "MAN") def cmd_man(msg): args = ["man"] @@ -35,33 +35,40 @@ def cmd_man(msg): os.unsetenv("LANG") res = Response(channel=msg.channel) - with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc: + with subprocess.Popen(args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) as proc: for line in proc.stdout.read().split(b"\n"): (line, n) = RGXP_s.subn(b'', line) res.append_message(line.decode()) if len(res.messages) <= 0: if num is not None: - res.append_message("Il n'y a pas d'entrée %s dans la section %d du manuel." % (msg.cmds[1], num)) + res.append_message("There is no entry %s in section %d." % + (msg.cmds[1], num)) else: - res.append_message("Il n'y a pas de page de manuel pour %s." % msg.cmds[1]) + res.append_message("There is no man page for %s." % msg.cmds[1]) return res + @hook("cmd_hook", "man") def cmd_whatis(msg): args = ["whatis", " ".join(msg.cmds[1:])] res = Response(channel=msg.channel) - with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc: + with subprocess.Popen(args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) as proc: for line in proc.stdout.read().split(b"\n"): (line, n) = RGXP_s.subn(b'', line) res.append_message(" ".join(line.decode().split())) if len(res.messages) <= 0: if num is not None: - res.append_message("Il n'y a pas d'entrée %s dans la section %d du manuel." % (msg.cmds[1], num)) + res.append_message("There is no entry %s in section %d." % + (msg.cmds[1], num)) else: - res.append_message("Il n'y a pas de page de manuel pour %s." % msg.cmds[1]) + res.append_message("There is no man page for %s." % msg.cmds[1]) return res diff --git a/modules/mediawiki.py b/modules/mediawiki.py index 3711c28..4815f4f 100644 --- a/modules/mediawiki.py +++ b/modules/mediawiki.py @@ -14,6 +14,7 @@ nemubotversion = 3.4 from more import Response + def get_namespaces(site, ssl=False): # Built URL url = "http%s://%s/w/api.php?format=json&action=query&meta=siteinfo&siprop=namespaces" % ( @@ -29,6 +30,7 @@ def get_namespaces(site, ssl=False): namespaces[data["query"]["namespaces"][ns]["*"]] = data["query"]["namespaces"][ns] return namespaces + def get_raw_page(site, term, ssl=False): # Built URL url = "http%s://%s/w/api.php?format=json&redirects&action=query&prop=revisions&rvprop=content&titles=%s" % ( @@ -45,6 +47,7 @@ def get_raw_page(site, term, ssl=False): except: raise IRCException("article not found") + def get_unwikitextified(site, wikitext, ssl=False): # Built URL url = "http%s://%s/w/api.php?format=json&action=expandtemplates&text=%s" % ( @@ -61,7 +64,6 @@ def get_unwikitextified(site, wikitext, ssl=False): def strip_model(cnt): # Strip models at begin and end: mostly useless cnt = re.sub(r"^(({{([^{]|\s|({{(.|\s|{{.*?}})*?}})*?)*?}}|\[\[(.|\s|\[\[.*?\]\])*?\]\])\s*)+", "", cnt) - #cnt = re.sub(r"({{([^{]|\s|({{(.|\s|{{.*?}})*?}})*?)*?}}\s?)+$", "", cnt) # Strip HTML comments cnt = re.sub(r"<!--.*?-->", "", cnt) @@ -70,12 +72,13 @@ def strip_model(cnt): cnt = re.sub(r"<ref.*?/ref>", "", cnt) return cnt + def parse_wikitext(site, cnt, namespaces=dict(), ssl=False): - for i,_,_,_ in re.findall(r"({{([^{]|\s|({{(.|\s|{{.*?}})*?}})*?)*?}})", cnt): + for i, _, _, _ in re.findall(r"({{([^{]|\s|({{(.|\s|{{.*?}})*?}})*?)*?}})", cnt): cnt = cnt.replace(i, get_unwikitextified(site, i, ssl), 1) # Strip [[...]] - for full,args,lnk in re.findall(r"(\[\[(.*?|)?([^|]*?)\]\])", cnt): + for full, args, lnk in re.findall(r"(\[\[(.*?|)?([^|]*?)\]\])", cnt): ns = lnk.find(":") if lnk == "": cnt = cnt.replace(full, args[:-1], 1) @@ -93,10 +96,12 @@ def parse_wikitext(site, cnt, namespaces=dict(), ssl=False): return cnt + def irc_format(cnt): cnt, _ = re.subn(r"(?P<title>==+)\s*(.*?)\s*(?P=title)\n*", "\x03\x16" + r"\2" + " :\x03\x16 ", cnt) return cnt.replace("'''", "\x03\x02").replace("''", "\x03\x1f") + def get_page(site, term, ssl=False): return strip_model(get_raw_page(site, term, ssl)) diff --git a/modules/more.py b/modules/more.py index d067487..a8dbd4f 100644 --- a/modules/more.py +++ b/modules/more.py @@ -27,6 +27,7 @@ nemubotversion = 3.4 logger = logging.getLogger("nemubot.response") + class Response: def __init__(self, message=None, channel=None, nick=None, server=None, nomore="No more message", title=None, more="(suite) ", @@ -40,7 +41,7 @@ class Response: self.alone = True if message is not None: self.append_message(message, shown_first_count=shown_first_count) - self.elt = 0 # Next element to display + self.elt = 0 # Next element to display self.channel = channel self.nick = nick @@ -50,12 +51,12 @@ class Response: def receivers(self): if self.channel is None: if self.nick is not None: - return [ self.nick ] + return [self.nick] return list() elif isinstance(self.channel, list): return self.channel else: - return [ self.channel ] + return [self.channel] def append_message(self, message, title=None, shown_first_count=-1): if type(message) is str: @@ -110,17 +111,17 @@ class Response: if len(self.rawtitle) <= 0: self.rawtitle = None - def accept(self, visitor): visitor.visit(self.next_response()) - def next_response(self, maxlen=440): if self.nick: - return DirectAsk(self.nick, self.get_message(maxlen - len(self.nick) - 2), server=None, to=self.receivers) + return DirectAsk(self.nick, + self.get_message(maxlen - len(self.nick) - 2), + server=None, to=self.receivers) else: - return TextMessage(self.get_message(maxlen), server=None, to=self.receivers) - + return TextMessage(self.get_message(maxlen), + server=None, to=self.receivers) def get_message(self, maxlen): if self.alone and len(self.messages) > 1: @@ -138,13 +139,15 @@ class Response: elif isinstance(res, str): self.messages.append(res) else: - raise Exception("Type returned by nomore (%s) is not handled here." % type(res)) + raise Exception("Type returned by nomore (%s) is not " + "handled here." % type(res)) return self.get_message() else: 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() + self.messages[0] = (self.line_treat(self.messages[0]) + .replace("\n", " ").strip()) msg = "" if self.title is not None: @@ -198,6 +201,7 @@ class Response: SERVERS = dict() + @hook("all_post") def parseresponse(res): # TODO: handle inter-bot communication NOMORE diff --git a/modules/reddit.py b/modules/reddit.py index 4f57197..ae0e3a2 100644 --- a/modules/reddit.py +++ b/modules/reddit.py @@ -11,11 +11,13 @@ nemubotversion = 3.4 from hooks import hook from more import Response + def help_full(): return "!subreddit /subreddit/: Display information on the subreddit." LAST_SUBS = dict() + @hook("cmd_hook", "subreddit") def cmd_subreddit(msg): global LAST_SUBS @@ -23,7 +25,8 @@ 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? type !horny or !bored") + raise IRCException("Which subreddit? Need inspiration? " + "type !horny or !bored") else: subs = msg.cmds[1:] @@ -36,30 +39,45 @@ def cmd_subreddit(msg): else: where = "r" try: - req = urllib.request.Request("http://www.reddit.com/%s/%s/about.json" % (where, sub.group(2)), headers={ 'User-Agent' : "nemubot v3" }) + req = urllib.request.Request( + "http://www.reddit.com/%s/%s/about.json" % + (where, sub.group(2)), + headers={'User-Agent': "nemubot v3"}) 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 IRCException("HTTP error occurs: %s %s" % + (e.code, e.reason)) sbr = json.loads(raw.read().decode()) if "title" in sbr["data"]: - res = Response(channel=msg.channel, nomore="No more information") - res.append_message(("[NSFW] " if sbr["data"]["over18"] else "") + sbr["data"]["url"] + " " + sbr["data"]["title"] + ": " + sbr["data"]["public_description" if sbr["data"]["public_description"] != "" else "description"].replace("\n", " ") + " %s subscriber(s)" % sbr["data"]["subscribers"]) + res = Response(channel=msg.channel, + nomore="No more information") + res.append_message( + ("[NSFW] " if sbr["data"]["over18"] else "") + + sbr["data"]["url"] + " " + sbr["data"]["title"] + ": " + + sbr["data"]["public_description" if sbr["data"]["public_description"] != "" else "description"].replace("\n", " ") + + " %s subscriber(s)" % sbr["data"]["subscribers"]) if sbr["data"]["public_description"] != "": - res.append_message(sbr["data"]["description"].replace("\n", " ")) + res.append_message( + sbr["data"]["description"].replace("\n", " ")) all_res.append(res) else: - all_res.append(Response("/%s/%s doesn't exist" % (where, sub.group(2)), channel=msg.channel)) + all_res.append(Response("/%s/%s doesn't exist" % + (where, sub.group(2)), + channel=msg.channel)) else: - all_res.append(Response("%s is not a valid subreddit" % osub, channel=msg.channel, nick=msg.nick)) + all_res.append(Response("%s is not a valid subreddit" % osub, + channel=msg.channel, nick=msg.nick)) return all_res + @hook("msg_default") def parselisten(msg): parseresponse(msg) return None + @hook("all_post") def parseresponse(msg): global LAST_SUBS diff --git a/modules/rnd.py b/modules/rnd.py index 873416f..9cae89c 100644 --- a/modules/rnd.py +++ b/modules/rnd.py @@ -10,9 +10,12 @@ nemubotversion = 3.4 from more import Response + @hook("cmd_hook", "choice") def cmd_choice(msg): if len(msg.cmds) > 1: - return Response(random.choice(msg.cmds[1:]), channel=msg.channel, nick=msg.nick) + return Response(random.choice(msg.cmds[1:]), + channel=msg.channel, + nick=msg.nick) else: raise IRCException("indicate some terms to pick!") diff --git a/modules/sap.py b/modules/sap.py index 3bc2cf3..e0a4775 100644 --- a/modules/sap.py +++ b/modules/sap.py @@ -14,25 +14,32 @@ nemubotversion = 3.4 from more import Response -def help_full (): - return "!tcode <transaction code|keywords>" + +def help_full(): + return "!tcode <transaction code|keywords>" + @hook("cmd_hook", "tcode") def cmd_tcode(msg): - if len(msg.cmds) != 2: - raise IRCException("indicate a transaction code or a keyword to search!") + if len(msg.cmds) != 2: + raise IRCException("indicate a transaction code or " + "a keyword to search!") - url = "http://www.tcodesearch.com/tcodes/search?q=%s" % urllib.parse.quote(msg.cmds[1]) - page = web.getURLContent(url) + url = ("http://www.tcodesearch.com/tcodes/search?q=%s" % + urllib.parse.quote(msg.cmds[1])) + page = web.getURLContent(url) - res = Response(channel=msg.channel, - nomore="No more transaction code", count=" (%d more tcodes)") + res = Response(channel=msg.channel, + nomore="No more transaction code", + count=" (%d more tcodes)") - if page is not None: - index = page.index('<div id="searchresults">') + len('<div id="searchresults">') - end = page[index:].index('</div>')+index - strscope = page[index:end] - for tcode in re.finditer('<strong> ([a-zA-Z0-9_]*)</strong> - ([^\n]*)\n', strscope): - res.append_message("\x02%s\x0F - %s" % (tcode.group(1), striphtml(tcode.group(2)))) + if page is not None: + index = (page.index('<div id="searchresults">') + + len('<div id="searchresults">')) + end = page[index:].index('</div>')+index + strscope = page[index:end] + for tcode in re.finditer('<strong> ([a-zA-Z0-9_]*)</strong> - ([^\n]*)\n', strscope): + res.append_message("\x02%s\x0F - %s" % (tcode.group(1), + striphtml(tcode.group(2)))) - return res + return res diff --git a/modules/sleepytime.py b/modules/sleepytime.py index b662878..d082072 100644 --- a/modules/sleepytime.py +++ b/modules/sleepytime.py @@ -12,12 +12,16 @@ nemubotversion = 3.4 from more import Response + def help_full(): - return "If you would like to sleep soon, use !sleepytime to know the best time to wake up; use !sleepytime hh:mm if you want to wake up at hh:mm" + return ("If you would like to sleep soon, use !sleepytime to know the best" + " time to wake up; use !sleepytime hh:mm if you want to wake up at" + " hh:mm") + @hook("cmd_hook", "sleepytime") def cmd_sleep(msg): - if len (msg.cmds) > 1 and re.match("[0-9]{1,2}[h':.,-]([0-9]{1,2})?[m'\":.,-]?", + if len(msg.cmds) > 1 and re.match("[0-9]{1,2}[h':.,-]([0-9]{1,2})?[m'\":.,-]?", msg.cmds[1]) is not None: # First, parse the hour p = re.match("([0-9]{1,2})[h':.,-]([0-9]{1,2})?[m':.,-]?", msg.cmds[1]) @@ -26,10 +30,10 @@ def cmd_sleep(msg): datetime.now(timezone.utc).day, hour=int(p.group(1)))] if p.group(2) is not None: - f[0] += timedelta(minutes=int(p.group(2))) + f[0] += timedelta(minutes=int(p.group(2))) g = list() - for i in range(0,6): - f.append(f[i] - timedelta(hours=1,minutes=30)) + for i in range(6): + f.append(f[i] - timedelta(hours=1, minutes=30)) g.append(f[i+1].strftime("%H:%M")) return Response("You should try to fall asleep at one of the following" " times: %s" % ', '.join(g), channel=msg.channel) @@ -38,8 +42,8 @@ def cmd_sleep(msg): else: f = [datetime.now(timezone.utc) + timedelta(minutes=15)] g = list() - for i in range(0,6): - f.append(f[i] + timedelta(hours=1,minutes=30)) + for i in range(6): + f.append(f[i] + timedelta(hours=1, minutes=30)) g.append(f[i+1].strftime("%H:%M")) return Response("If you head to bed right now, you should try to wake" " up at one of the following times: %s" % diff --git a/modules/velib.py b/modules/velib.py index d4c5ca7..f45372d 100644 --- a/modules/velib.py +++ b/modules/velib.py @@ -11,6 +11,7 @@ nemubotversion = 3.4 from more import Response + def load(context): global DATAS DATAS.setIndex("name", "station") @@ -20,8 +21,10 @@ def load(context): # station_status) # context.add_event(evt) -def help_full (): - return "!velib /number/ ...: gives available bikes and slots at the station /number/." + +def help_full(): + return ("!velib /number/ ...: gives available bikes and slots at " + "the station /number/.") def station_status(station): @@ -42,11 +45,13 @@ def station_status(station): else: return (None, None) + def station_available(station): """Gets available velib at a given velib station""" (a, f) = station_status(station) return a + def station_free(station): """Gets free slots at a given velib station""" (a, f) = station_status(station) @@ -62,6 +67,7 @@ def print_station_status(msg, station): channel=msg.channel, nick=msg.nick) raise IRCException("station %s inconnue." % station) + @hook("cmd_hook", "velib") def ask_stations(msg): """Hook entry from !velib""" @@ -74,7 +80,8 @@ def ask_stations(msg): if re.match("^[0-9]{4,5}$", station): return print_station_status(msg, station) elif station in DATAS.index: - return print_station_status(msg, DATAS.index[station]["number"]) + return print_station_status(msg, + DATAS.index[station]["number"]) else: raise IRCException("numéro de station invalide.") diff --git a/modules/ycc.py b/modules/ycc.py index 6bf5ba8..ce14779 100644 --- a/modules/ycc.py +++ b/modules/ycc.py @@ -12,19 +12,25 @@ from message import TextMessage nemubotversion = 3.4 + def help_full(): - return "!ycc [<url>]: with an argument, reduce the given <url> thanks to ycc.fr; without argument, reduce the last URL said on the current channel." + return ("!ycc [<url>]: with an argument, reduce the given <url> thanks to " + "ycc.fr; without argument, reduce the last URL said on the current" + " channel.") LAST_URLS = dict() + def gen_response(res, msg, srv): if res is None: raise IRCException("la situation est embarassante, il semblerait que YCC soit down :(") elif isinstance(res, str): - return TextMessage("URL pour %s : %s" % (srv, res), server=None, to=msg.to_response) + return TextMessage("URL pour %s : %s" % (srv, res), server=None, + to=msg.to_response) else: raise IRCException("mauvaise URL : %s" % srv) + @hook("cmd_hook", "ycc") def cmd_ycc(msg): minify = list() @@ -45,7 +51,8 @@ def cmd_ycc(msg): for url in minify: o = urlparse(url, "http") if o.scheme != "": - snd_url = "http://ycc.fr/redirection/create/" + quote(url, "/:%@&=?") + snd_url = "http://ycc.fr/redirection/create/" + quote(url, + "/:%@&=?") print_debug(snd_url) raw = urlopen(snd_url, timeout=10) if o.netloc == "": @@ -56,24 +63,27 @@ def cmd_ycc(msg): res.append(gen_response(False, msg, url)) return res + @hook("msg_default") def parselisten(msg): parseresponse(msg) return None + @hook("all_post") def parseresponse(msg): global LAST_URLS try: - urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?[^ ]+)", msg.text) - for url in urls: - o = urlparse(url) - if o.scheme != "": - if o.netloc == "ycc.fr" or (o.netloc == "" and len(o.path) < 10): - continue - if msg.channel not in LAST_URLS: - LAST_URLS[msg.channel] = list() - LAST_URLS[msg.channel].append(url) + urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?[^ ]+)", msg.text) + for url in urls: + o = urlparse(url) + if o.scheme != "": + if o.netloc == "ycc.fr" or (o.netloc == "" and + len(o.path) < 10): + continue + if msg.channel not in LAST_URLS: + LAST_URLS[msg.channel] = list() + LAST_URLS[msg.channel].append(url) except: pass return msg From 093581f646be6560f9c80085a7f718bcf434c220 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 14 Nov 2014 14:01:00 +0100 Subject: [PATCH 195/674] Fix missing import --- prompt/builtins.py | 2 -- tools/config.py | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/prompt/builtins.py b/prompt/builtins.py index a666586..c20df22 100644 --- a/prompt/builtins.py +++ b/prompt/builtins.py @@ -16,9 +16,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 imp import logging -import os from tools.config import load_file diff --git a/tools/config.py b/tools/config.py index f4f9a5f..e16b153 100644 --- a/tools/config.py +++ b/tools/config.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 <http://www.gnu.org/licenses/>. +import imp import logging import os From 63cc770800ef6885f7b38e023aa1bd256846b694 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 14 Nov 2014 14:25:52 +0100 Subject: [PATCH 196/674] [cmd_server] launch and disconnect function doesn't exist anymore --- modules/cmd_server.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/cmd_server.py b/modules/cmd_server.py index 670d553..964dbf8 100644 --- a/modules/cmd_server.py +++ b/modules/cmd_server.py @@ -43,12 +43,12 @@ def close(data, toks, context, prompt): if len(toks) > 1: for s in toks[1:]: if s in servers: - context.servers[s].disconnect() + context.servers[s].close() del context.servers[s] else: print ("close: server `%s' not found." % s) elif prompt.selectedServer is not None: - prompt.selectedServer.disconnect() + prompt.selectedServer.close() del prompt.servers[selectedServer.id] prompt.selectedServer = None return @@ -60,12 +60,12 @@ def connect(data, toks, context, prompt): if len(toks) > 1: for s in toks[1:]: if s in context.servers: - context.servers[s].launch(context.receive_message) + context.servers[s].open() else: print ("connect: server `%s' not found." % s) elif prompt.selectedServer is not None: - prompt.selectedServer.launch(context.receive_message) + prompt.selectedServer.open() else: print (" Please SELECT a server or give its name in argument.") @@ -76,12 +76,12 @@ def disconnect(data, toks, context, prompt): if len(toks) > 1: for s in toks[1:]: if s in context.servers: - if not context.servers[s].disconnect(): + if not context.servers[s].close(): print ("disconnect: server `%s' already disconnected." % s) else: print ("disconnect: server `%s' not found." % s) elif prompt.selectedServer is not None: - if not prompt.selectedServer.disconnect(): + if not prompt.selectedServer.close(): print ("disconnect: server `%s' already disconnected." % prompt.selectedServer.id) else: From fd5fbf6c6c986710603ea6347d63cdc4eb846be0 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 13 Nov 2014 21:38:18 +0100 Subject: [PATCH 197/674] Fix module load and reload --- bot.py | 18 ++++++++++++++++-- hooks/manager.py | 4 ++-- tools/config.py | 4 +--- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/bot.py b/bot.py index 1c16278..2c38871 100644 --- a/bot.py +++ b/bot.py @@ -17,6 +17,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. from datetime import datetime, timedelta, timezone +import imp import logging from queue import Queue import re @@ -346,6 +347,21 @@ class Bot(threading.Thread): return False + def import_module(self, name): + """Load a module + + Argument: + name -- name of the module to load + """ + + if name in self.modules: + self.unload_module(name) + tt = __import__(name) + imp.reload(tt) + else: + __import__(name) + + def add_module(self, module): """Add a module to the context, if already exists, unload the old one before""" @@ -447,8 +463,6 @@ def hotswap(bak): return new def reload(): - import imp - import channel imp.reload(channel) diff --git a/hooks/manager.py b/hooks/manager.py index 2331d68..687fcf1 100644 --- a/hooks/manager.py +++ b/hooks/manager.py @@ -62,9 +62,9 @@ class HooksManager: if trigger in self.hooks: if hook is None: del self.hooks[trigger] - return True else: - return self.hooks[trigger].remove(hook) + self.hooks[trigger].remove(hook) + return True return False diff --git a/tools/config.py b/tools/config.py index e16b153..e1a2b37 100644 --- a/tools/config.py +++ b/tools/config.py @@ -16,7 +16,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/>. -import imp import logging import os @@ -130,5 +129,4 @@ def load_file(filename, context): # Unexisting file, assume a name was passed, import the module! else: - tt = __import__(filename) - imp.reload(tt) + context.import_module(filename) From 001ff35758fb397e18f12d56a5d5f6c0685c0f3c Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 13 Nov 2014 21:39:07 +0100 Subject: [PATCH 198/674] In servers list, display its state --- prompt/builtins.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/prompt/builtins.py b/prompt/builtins.py index c20df22..28aa4ab 100644 --- a/prompt/builtins.py +++ b/prompt/builtins.py @@ -41,17 +41,21 @@ def liste(toks, context, prompt): l = l.lower() if l == "server" or l == "servers": for srv in context.servers.keys(): - print (" - %s ;" % srv) + 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) else: From 23b60814b75bfb7e6e3be5ec25c688e4f8731d34 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 13 Nov 2014 21:46:35 +0100 Subject: [PATCH 199/674] Remove dead code in importer --- importer.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/importer.py b/importer.py index b01848c..fe246aa 100644 --- a/importer.py +++ b/importer.py @@ -250,15 +250,16 @@ def convert_legacy_store(old): print("UNKNOWN store:", old) return old -def add_cap_hook(prompt, module, cmd): - if hasattr(module, cmd["call"]): - prompt.add_cap_hook(cmd["name"], getattr(module, cmd["call"])) - else: - logger.warn("In module `%s', no function `%s' defined for `%s' " - "command hook.", module.__name__, cmd["call"], cmd["name"]) def register_hooks(module, context, prompt): - """Register all available hooks""" + """Register all available hooks + + Arguments: + module -- the loaded Python module + context -- bot context + prompt -- the current Prompt instance + """ + # Register decorated functions for s, h in hooks.last_registered: if s == "prompt_cmd": From 58d330c333a259df423cea24b3257cbc8555c947 Mon Sep 17 00:00:00 2001 From: Bob <bob@23.tf> Date: Mon, 17 Nov 2014 14:24:18 +0100 Subject: [PATCH 200/674] add cve module --- modules/cve.py | 37 +++++++++++++++++++++++++++++++++++++ modules/cve.xml | 4 ++++ 2 files changed, 41 insertions(+) create mode 100644 modules/cve.py create mode 100644 modules/cve.xml diff --git a/modules/cve.py b/modules/cve.py new file mode 100644 index 0000000..bfd6c98 --- /dev/null +++ b/modules/cve.py @@ -0,0 +1,37 @@ +import urllib.request +from bs4 import BeautifulSoup +import pprint +from hooks import hook +from more import Response + +nemubotversion = 3.4 + +def help_tiny(): + return "CVE description" + +def help_full(): + return "No help " + + +@hook("cmd_hook", "cve") +def get_cve_desc(msg): + DESC_INDEX = 17 + BASEURL_MITRE = 'http://cve.mitre.org/cgi-bin/cvename.cgi?name=' + + cve_id = '' + + if msg.cmds[1][:3] == 'cve' : + cve_id = msg.cmds[1] + + else: + cve_id = 'cve-' + msg.cmds[1] + + search_url = BASEURL_MITRE + cve_id.upper() + + url = urllib.request.urlopen(search_url) + soup = BeautifulSoup(url) + + desc = soup.body.findAll('td') + + return Response(desc[DESC_INDEX].text, msg.channel) + diff --git a/modules/cve.xml b/modules/cve.xml new file mode 100644 index 0000000..a465227 --- /dev/null +++ b/modules/cve.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" ?> +<nemubotmodule name="cve"> + <message type="cmd" name="cve" call="get_cve_desc" /> +</nemubotmodule> From 0dd6036808b24f7fe815fb591afd052747ca92e8 Mon Sep 17 00:00:00 2001 From: Bob <bob@23.tf> Date: Mon, 17 Nov 2014 14:56:08 +0100 Subject: [PATCH 201/674] del xml --- modules/cve.xml | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 modules/cve.xml diff --git a/modules/cve.xml b/modules/cve.xml deleted file mode 100644 index a465227..0000000 --- a/modules/cve.xml +++ /dev/null @@ -1,4 +0,0 @@ -<?xml version="1.0" ?> -<nemubotmodule name="cve"> - <message type="cmd" name="cve" call="get_cve_desc" /> -</nemubotmodule> From bbd928c6fa02f14b011b0c358215ca1db6b357f0 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 17 Nov 2014 22:48:17 +0100 Subject: [PATCH 202/674] Prompt: documentation, factoring --- importer.py | 2 ++ prompt/__init__.py | 68 ++++++++++++++++++++++++++++++---------------- 2 files changed, 46 insertions(+), 24 deletions(-) diff --git a/importer.py b/importer.py index fe246aa..48ccb6e 100644 --- a/importer.py +++ b/importer.py @@ -264,6 +264,8 @@ def register_hooks(module, context, prompt): for s, h in hooks.last_registered: if s == "prompt_cmd": prompt.add_cap_hook(h.name, h.call) + elif s == "prompt_list": + prompt.add_list_hook(h.name, h.call) else: s = convert_legacy_store(s) module.REGISTERED_HOOKS.append((s, h)) diff --git a/prompt/__init__.py b/prompt/__init__.py index d0ad4b8..7408088 100644 --- a/prompt/__init__.py +++ b/prompt/__init__.py @@ -28,42 +28,54 @@ from . import builtins class Prompt: - def __init__(self, hc=dict(), hl=dict()): + def __init__(self): self.selectedServer = None - self.HOOKS_CAPS = hc - self.HOOKS_LIST = hl + self.HOOKS_CAPS = dict() + self.HOOKS_LIST = dict() def add_cap_hook(self, name, call, data=None): self.HOOKS_CAPS[name] = (lambda d, t, c, p: call(d, t, c, p), data) + def add_list_hook(self, name, call): + self.HOOKS_LIST[name] = call def lex_cmd(self, line): - """Return an array of tokens""" - ret = list() + """Return an array of tokens + + Argument: + line -- the line to lex + """ + try: cmds = shlex.split(line) - bgn = 0 - for i in range(0, len(cmds)): - if cmds[i] == ';': - if i != bgn: - cmds[bgn] = cmds[bgn].lower() - ret.append(cmds[bgn:i]) - bgn = i + 1 - - if bgn != len(cmds): - cmds[bgn] = cmds[bgn].lower() - ret.append(cmds[bgn:len(cmds)]) - - return ret except: - exc_type, exc_value, exc_traceback = sys.exc_info() + exc_type, exc_value, _ = sys.exc_info() sys.stderr.write(traceback.format_exception_only(exc_type, exc_value)[0]) - return ret + 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""" + """Execute the command + + Arguments: + toks -- lexed tokens to executes + context -- current bot context + """ + if toks[0] in builtins.CAPS: return builtins.CAPS[toks[0]](toks, context, self) elif toks[0] in self.HOOKS_CAPS: @@ -81,7 +93,12 @@ class Prompt: return self.selectedServer.id def run(self, context): - """Launch the prompt""" + """Launch the prompt + + Argument: + context -- current bot context + """ + ret = "" while ret != "quit" and ret != "reset" and ret != "refresh": try: @@ -102,8 +119,11 @@ class Prompt: return ret != "quit" -def hotswap(prompt): - return Prompt(prompt.HOOKS_CAPS, prompt.HOOKS_LIST) +def hotswap(bak): + p = Prompt() + p.HOOKS_CAPS = bak.HOOKS_CAPS + p.HOOKS_LIST = bak.HOOKS_LIST + return p def reload(): From f181d644b4ed3ba401219b1242784d2d7e305142 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 1 Dec 2014 18:13:58 +0100 Subject: [PATCH 203/674] [networking] Refactor module --- modules/networking.py | 298 ------------------------------- modules/networking/__init__.py | 109 +++++++++++ modules/networking/isup.py | 22 +++ modules/networking/page.py | 119 ++++++++++++ modules/networking/w3c.py | 30 ++++ modules/networking/whois.py | 106 +++++++++++ modules/watchWebsite/__init__.py | 7 +- 7 files changed, 390 insertions(+), 301 deletions(-) delete mode 100644 modules/networking.py create mode 100644 modules/networking/__init__.py create mode 100644 modules/networking/isup.py create mode 100644 modules/networking/page.py create mode 100644 modules/networking/w3c.py create mode 100644 modules/networking/whois.py diff --git a/modules/networking.py b/modules/networking.py deleted file mode 100644 index 199fd6c..0000000 --- a/modules/networking.py +++ /dev/null @@ -1,298 +0,0 @@ -# coding=utf-8 - -"""Various network tools (w3m, w3c validator, curl, traceurl, ...)""" - -import datetime -import http.client -import json -import socket -import subprocess -import urllib - -from hooks import hook -from tools import web - -nemubotversion = 3.4 - -from more import Response - -def load(context): - if not CONF or not CONF.hasNode("whoisxmlapi") or not CONF.getNode("whoisxmlapi").hasAttribute("username") or not CONF.getNode("whoisxmlapi").hasAttribute("password"): - print ("You need a WhoisXML API account in order to use the " - "!netwhois feature. Add it to the module configuration file:\n" - "<whoisxmlapi username=\"XX\" password=\"XXX\" />\nRegister at " - "http://www.whoisxmlapi.com/newaccount.php") - else: - from hooks.messagehook import MessageHook - add_hook("cmd_hook", MessageHook(cmd_whois, "netwhois")) - -def help_full(): - return "!traceurl /url/: Follow redirections from /url/." - -def w3m(url): - args = ["w3m", "-T", "text/html", "-dump"] - args.append(url) - with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc: - return proc.stdout.read().decode() - -@hook("cmd_hook", "w3m") -def cmd_w3m(msg): - if len(msg.cmds) > 1: - res = Response(channel=msg.channel) - for line in w3m(" ".join(msg.cmds[1:])).split("\n"): - res.append_message(line) - return res - else: - raise IRCException("Indicate the URL to visit.") - -def curl(url): - try: - req = web.getURLContent(url) - if req is not None: - return req - else: - raise IRCException("An error occurs when trying to access the page") - except socket.timeout: - raise IRCException("The request timeout when trying to access the page") - except socket.error as e: - raise IRCException(e.strerror) - -@hook("cmd_hook", "curl") -def cmd_curl(msg): - if len(msg.cmds) < 2: - raise IRCException("Indicate the URL to visit.") - - res = Response(channel=msg.channel) - for m in curl(" ".join(msg.cmds[1:])).split("\n"): - res.append_message(m) - return res - -@hook("cmd_hook", "curly") -def cmd_curly(msg): - if len(msg.cmds) < 2: - raise IRCException("Indicate the URL to visit.") - - url = msg.cmds[1] - o = urllib.parse.urlparse(url, "http") - if o.netloc == "": - raise IRCException("URL invalide") - if o.scheme == "http": - conn = http.client.HTTPConnection(o.netloc, port=o.port, timeout=5) - else: - conn = http.client.HTTPSConnection(o.netloc, port=o.port, timeout=5) - try: - conn.request("HEAD", o.path, None, {"User-agent": "Nemubot v3"}) - except socket.timeout: - raise IRCException("Délais d'attente dépassé") - except socket.gaierror: - print ("<tools.web> Unable to receive page %s from %s on %d." - % (o.path, o.netloc, o.port)) - raise IRCException("Une erreur innatendue est survenue") - - try: - res = conn.getresponse() - except http.client.BadStatusLine: - raise IRCException("Une erreur est survenue") - finally: - conn.close() - - return Response("Entêtes de la page %s : HTTP/%s, statut : %d %s ; headers : %s" % (url, res.version, res.status, res.reason, ", ".join(["\x03\x02" + h + "\x03\x02: " + v for h, v in res.getheaders()])), channel=msg.channel) - -@hook("cmd_hook", "traceurl") -def cmd_traceurl(msg): - if 1 < len(msg.cmds) < 6: - res = list() - for url in msg.cmds[1:]: - trace = traceURL(url) - res.append(Response(trace, channel=msg.channel, title="TraceURL")) - return res - else: - raise IRCException("Indiquer a URL to trace!") - - -def extractdate(str): - tries = [ - "%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", - "%Y-%m-%d %H:%M:%S%Z", - "%Y-%m-%d %H:%M:%S%z", - "%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 "name" in entity: - ret += entity["name"] - - if "country" in entity or "city" in entity or "telephone" in entity or "email" in entity: - ret += " (from " - if "street1" in entity: - ret += entity["street1"] + " " - if "city" in entity: - ret += entity["city"] + " " - if "state" in entity: - ret += entity["state"] + " " - if "country" in entity: - ret += entity["country"] + " " - if "telephone" in entity: - ret += entity["telephone"] + " " - if "email" in entity: - ret += entity["email"] + " " - ret = ret.rstrip() + ")" - - return ret.lstrip() - -def cmd_whois(msg): - if len(msg.cmds) < 2: - raise IRCException("Indiquer un domaine ou une IP à whois !") - - dom = msg.cmds[1] - - try: - req = urllib.request.Request("http://www.whoisxmlapi.com/whoisserver/WhoisService?rid=1&domainName=%s&outputFormat=json&userName=%s&password=%s" % (urllib.parse.quote(dom), urllib.parse.quote(CONF.getNode("whoisxmlapi")["username"]), urllib.parse.quote(CONF.getNode("whoisxmlapi")["password"])), headers={ 'User-Agent' : "nemubot v3" }) - raw = urllib.request.urlopen(req, timeout=10) - except socket.timeout: - raise IRCException("Sorry, the request has timed out.") - except urllib.error.HTTPError as e: - raise IRCException("HTTP error occurs: %s %s" % (e.code, e.reason)) - - js = json.loads(raw.read().decode()) - - if "ErrorMessage" in js: - err = js["ErrorMessage"] - raise IRCException(js["ErrorMessage"]["msg"]) - - whois = js["WhoisRecord"] - - res = Response(channel=msg.channel, nomore="No more whois information") - - 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"] + " " 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 - -@hook("cmd_hook", "isup") -def cmd_isup(msg): - if 1 < len(msg.cmds) < 6: - res = list() - for url in msg.cmds[1:]: - o = urllib.parse.urlparse(url, "http") - if o.netloc == "": - o = urllib.parse.urlparse("http://" + url) - if o.netloc != "": - req = urllib.request.Request("http://isitup.org/%s.json" % (o.netloc), headers={ 'User-Agent' : "nemubot v3" }) - raw = urllib.request.urlopen(req, timeout=10) - isup = json.loads(raw.read().decode()) - if "status_code" in isup and isup["status_code"] == 1: - res.append(Response("%s est accessible (temps de reponse : %ss)" % (isup["domain"], isup["response_time"]), channel=msg.channel)) - else: - res.append(Response("%s n'est pas accessible :(" % (isup["domain"]), channel=msg.channel)) - else: - res.append(Response("%s n'est pas une URL valide" % url, channel=msg.channel)) - return res - else: - return Response("Indiquer une URL à vérifier !", channel=msg.channel) - -def traceURL(url, timeout=5, stack=None): - """Follow redirections and return the redirections stack""" - if stack is None: - stack = list() - stack.append(url) - - if len(stack) > 15: - stack.append('stack overflow :(') - return stack - - o = urllib.parse.urlparse(url, "http") - if o.netloc == "": - return stack - if o.scheme == "http": - conn = http.client.HTTPConnection(o.netloc, port=o.port, timeout=timeout) - else: - conn = http.client.HTTPSConnection(o.netloc, port=o.port, timeout=timeout) - try: - conn.request("HEAD", o.path, None, {"User-agent": "Nemubot v3"}) - except socket.timeout: - stack.append("Timeout") - return stack - except socket.gaierror: - print ("<tools.web> Unable to receive page %s from %s on %d." - % (o.path, o.netloc, o.port)) - return stack - - try: - res = conn.getresponse() - except http.client.BadStatusLine: - return stack - finally: - conn.close() - - if res.status == http.client.OK: - return stack - elif res.status == http.client.FOUND or res.status == http.client.MOVED_PERMANENTLY or res.status == http.client.SEE_OTHER: - url = res.getheader("Location") - if url in stack: - stack.append("loop on " + url) - return stack - else: - return traceURL(url, timeout, stack) - else: - return stack - -@hook("cmd_hook", "w3c") -def cmd_w3c(msg): - if len(msg.cmds) < 2: - raise IRCException("Indiquer une URL à valider !") - - o = urllib.parse.urlparse(msg.cmds[1], "http") - if o.netloc == "": - o = urllib.parse.urlparse("http://" + msg.cmds[1]) - if o.netloc == "": - raise IRCException("Indiquer une URL valide !") - - try: - req = urllib.request.Request("http://validator.w3.org/check?uri=%s&output=json" % (urllib.parse.quote(o.geturl())), headers={ 'User-Agent' : "nemubot v3" }) - raw = urllib.request.urlopen(req, timeout=10) - except urllib.error.HTTPError as e: - raise IRCException("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 "")) - - validator = json.loads(raw.read().decode()) - - res = Response(channel=msg.channel, nomore="No more error") - - res.append_message("%s: status: %s, %s warning(s), %s error(s)" % (validator["url"], headers["X-W3C-Validator-Status"], headers["X-W3C-Validator-Warnings"], headers["X-W3C-Validator-Errors"])) - - for m in validator["messages"]: - if "lastLine" not in m: - res.append_message("%s%s: %s" % (m["type"][0].upper(), m["type"][1:], m["message"])) - else: - res.append_message("%s%s on line %s, col %s: %s" % (m["type"][0].upper(), m["type"][1:], m["lastLine"], m["lastColumn"], m["message"])) - - return res diff --git a/modules/networking/__init__.py b/modules/networking/__init__.py new file mode 100644 index 0000000..3cffa66 --- /dev/null +++ b/modules/networking/__init__.py @@ -0,0 +1,109 @@ +# coding=utf-8 + +"""Various network tools (w3m, w3c validator, curl, traceurl, ...)""" + +from hooks import hook + +nemubotversion = 3.4 + +from more import Response + +from . import isup +from . import page +from . import w3c +from . import whois + +def load(context): + for mod in [isup, page, w3c, whois]: + mod.IRCException = IRCException + mod.ModuleEvent = ModuleEvent + mod.add_event = add_event + mod.save = save + mod.print = print + mod.print_debug = print_debug + page.load(CONF, add_hook) + whois.load(CONF, add_hook) + + +def help_full(): + return "!traceurl /url/: Follow redirections from /url/." + + +@hook("cmd_hook", "curly") +def cmd_curly(msg): + if len(msg.cmds) < 2: + raise IRCException("Indicate the URL to visit.") + + url = " ".join(msg.cmds[1:]) + version, status, reason, headers = page.headers(url) + + 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") +def cmd_curl(msg): + if len(msg.cmds) < 2: + raise IRCException("Indicate the URL to visit.") + + res = Response(channel=msg.channel) + for m in page.fetch(" ".join(msg.cmds[1:])).split("\n"): + res.append_message(m) + return res + + +@hook("cmd_hook", "w3m") +def cmd_w3m(msg): + if len(msg.cmds) > 1: + res = Response(channel=msg.channel) + for line in page.render(" ".join(msg.cmds[1:])).split("\n"): + res.append_message(line) + return res + else: + raise IRCException("Indicate the URL to visit.") + + +@hook("cmd_hook", "traceurl") +def cmd_traceurl(msg): + if 1 < len(msg.cmds) < 6: + res = list() + for url in msg.cmds[1:]: + trace = page.traceURL(url) + res.append(Response(trace, channel=msg.channel, title="TraceURL")) + return res + else: + raise IRCException("Indicate an URL to trace!") + + +@hook("cmd_hook", "isup") +def cmd_isup(msg): + if 1 < len(msg.cmds) < 6: + res = list() + for url in msg.cmds[1:]: + rep = isup.isup(url) + if rep: + res.append(Response("%s is up (response time: %ss)" % (url, rep), channel=msg.channel)) + else: + res.append(Response("%s is down" % (url), channel=msg.channel)) + return res + else: + return Response("Indicate an URL to check!", channel=msg.channel) + + +@hook("cmd_hook", "w3c") +def cmd_w3c(msg): + if len(msg.cmds) < 2: + raise IRCException("Indicate an URL to validate!") + + headers, validator = w3c.validator(msg.cmds[1]) + + res = Response(channel=msg.channel, nomore="No more error") + + res.append_message("%s: status: %s, %s warning(s), %s error(s)" % (validator["url"], headers["X-W3C-Validator-Status"], headers["X-W3C-Validator-Warnings"], headers["X-W3C-Validator-Errors"])) + + for m in validator["messages"]: + if "lastLine" not in m: + res.append_message("%s%s: %s" % (m["type"][0].upper(), m["type"][1:], m["message"])) + else: + res.append_message("%s%s on line %s, col %s: %s" % (m["type"][0].upper(), m["type"][1:], m["lastLine"], m["lastColumn"], m["message"])) + + return res diff --git a/modules/networking/isup.py b/modules/networking/isup.py new file mode 100644 index 0000000..6db94e1 --- /dev/null +++ b/modules/networking/isup.py @@ -0,0 +1,22 @@ +import json +import urllib + + +def isup(url): + """Determine if the given URL is up or not + + Argument: + url -- the URL to check + """ + + o = urllib.parse.urlparse(url, "http") + if o.netloc == "": + o = urllib.parse.urlparse("http://" + url) + if o.netloc != "": + req = urllib.request.Request("http://isitup.org/%s.json" % (o.netloc), headers={ 'User-Agent' : "nemubot v3" }) + raw = urllib.request.urlopen(req, timeout=10) + isup = json.loads(raw.read().decode()) + if "status_code" in isup and isup["status_code"] == 1: + return isup["response_time"] + + return None diff --git a/modules/networking/page.py b/modules/networking/page.py new file mode 100644 index 0000000..b1f3a56 --- /dev/null +++ b/modules/networking/page.py @@ -0,0 +1,119 @@ +import http.client +import socket +import subprocess +import tempfile +import urllib + +from tools import web + + +def load(CONF, add_hook): + # check w3m exists + pass + + +def headers(url): + """Retrieve HTTP header for the given URL + + Argument: + url -- the page URL to get header + """ + + o = urllib.parse.urlparse(url, "http") + if o.netloc == "": + raise IRCException("invalid URL") + if o.scheme == "http": + conn = http.client.HTTPConnection(o.netloc, port=o.port, timeout=5) + else: + conn = http.client.HTTPSConnection(o.netloc, port=o.port, timeout=5) + try: + conn.request("HEAD", o.path, None, {"User-agent": "Nemubot v3"}) + except socket.timeout: + raise IRCException("request timeout") + except socket.gaierror: + print ("<tools.web> Unable to receive page %s from %s on %d." + % (o.path, o.netloc, o.port)) + raise IRCException("an unexpected error occurs") + + try: + res = conn.getresponse() + except http.client.BadStatusLine: + raise IRCException("An error occurs") + finally: + conn.close() + + return (res.version, res.status, res.reason, res.getheaders()) + + +def _onNoneDefault(): + raise IRCException("An error occurs when trying to access the page") + + +def fetch(url, onNone=_onNoneDefault): + """Retrieve the content of the given URL + + Argument: + url -- the URL to fetch + """ + + try: + req = web.getURLContent(url) + if req is not None: + return req + else: + if onNone is not None: + return onNone() + else: + return None + except socket.timeout: + raise IRCException("The request timeout when trying to access the page") + except socket.error as e: + raise IRCException(e.strerror) + + +def render(url, onNone=_onNoneDefault): + """Use w3m to render the given url + + Argument: + url -- the URL to render + """ + + with tempfile.NamedTemporaryFile() as fp: + cnt = fetch(url, onNone) + if cnt is None: + return None + fp.write(cnt.encode()) + + args = ["w3m", "-T", "text/html", "-dump"] + args.append(fp.name) + with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc: + return proc.stdout.read().decode() + + +def traceURL(url, stack=None): + """Follow redirections and return the redirections stack + + Argument: + url -- the URL to trace + """ + + if stack is None: + stack = list() + stack.append(url) + + if len(stack) > 15: + stack.append('stack overflow :(') + return stack + + _, status, _, headers = headers(url) + + if status == http.client.FOUND or status == http.client.MOVED_PERMANENTLY or status == http.client.SEE_OTHER: + for h, c in headers: + if h == "Location": + url = c + if url in stack: + stack.append("loop on " + url) + return stack + else: + return traceURL(url, stack) + return stack diff --git a/modules/networking/w3c.py b/modules/networking/w3c.py new file mode 100644 index 0000000..db10753 --- /dev/null +++ b/modules/networking/w3c.py @@ -0,0 +1,30 @@ +import json +import urllib + +def validator(url): + """Run the w3c validator on the given URL + + Argument: + url -- the URL to validate + """ + + o = urllib.parse.urlparse(url, "http") + if o.netloc == "": + o = urllib.parse.urlparse("http://" + url) + if o.netloc == "": + raise IRCException("Indiquer une URL valide !") + + try: + req = urllib.request.Request("http://validator.w3.org/check?uri=%s&output=json" % (urllib.parse.quote(o.geturl())), headers={ 'User-Agent' : "nemubot v3" }) + raw = urllib.request.urlopen(req, timeout=10) + except urllib.error.HTTPError as e: + raise IRCException("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 "")) + + return headers, json.loads(raw.read().decode()) diff --git a/modules/networking/whois.py b/modules/networking/whois.py new file mode 100644 index 0000000..d79d918 --- /dev/null +++ b/modules/networking/whois.py @@ -0,0 +1,106 @@ +import datetime +import json +import socket +import urllib + +from more import Response + +URL_WHOIS = "http://www.whoisxmlapi.com/whoisserver/WhoisService?rid=1&domainName=%%s&outputFormat=json&userName=%s&password=%s" + +def load(CONF, add_hook): + global URL_WHOIS + + if not CONF or not CONF.hasNode("whoisxmlapi") or not CONF.getNode("whoisxmlapi").hasAttribute("username") or not CONF.getNode("whoisxmlapi").hasAttribute("password"): + print ("You need a WhoisXML API account in order to use the " + "!netwhois feature. Add it to the module configuration file:\n" + "<whoisxmlapi username=\"XX\" password=\"XXX\" />\nRegister at " + "http://www.whoisxmlapi.com/newaccount.php") + else: + URL_WHOIS = URL_WHOIS % (urllib.parse.quote(CONF.getNode("whoisxmlapi")["username"]), urllib.parse.quote(CONF.getNode("whoisxmlapi")["password"])) + + from hooks.messagehook import MessageHook + add_hook("cmd_hook", MessageHook(cmd_whois, "netwhois")) + + +def extractdate(str): + tries = [ + "%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", + "%Y-%m-%d %H:%M:%S%Z", + "%Y-%m-%d %H:%M:%S%z", + "%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 "name" in entity: + ret += entity["name"] + + if "country" in entity or "city" in entity or "telephone" in entity or "email" in entity: + ret += " (from " + if "street1" in entity: + ret += entity["street1"] + " " + if "city" in entity: + ret += entity["city"] + " " + if "state" in entity: + ret += entity["state"] + " " + if "country" in entity: + ret += entity["country"] + " " + if "telephone" in entity: + ret += entity["telephone"] + " " + if "email" in entity: + ret += entity["email"] + " " + ret = ret.rstrip() + ")" + + return ret.lstrip() + + +def cmd_whois(msg): + if len(msg.cmds) < 2: + raise IRCException("Indiquer un domaine ou une IP à whois !") + + dom = msg.cmds[1] + + try: + req = urllib.request.Request(URL_WHOIS % urllib.parse.quote(dom), headers={ 'User-Agent' : "nemubot v3" }) + raw = urllib.request.urlopen(req, timeout=10) + except socket.timeout: + raise IRCException("Sorry, the request has timed out.") + except urllib.error.HTTPError as e: + raise IRCException("HTTP error occurs: %s %s" % (e.code, e.reason)) + + js = json.loads(raw.read().decode()) + + if "ErrorMessage" in js: + err = js["ErrorMessage"] + raise IRCException(js["ErrorMessage"]["msg"]) + + whois = js["WhoisRecord"] + + res = Response(channel=msg.channel, nomore="No more whois information") + + 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"] + " " 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 diff --git a/modules/watchWebsite/__init__.py b/modules/watchWebsite/__init__.py index 0cc236c..d1826a3 100644 --- a/modules/watchWebsite/__init__.py +++ b/modules/watchWebsite/__init__.py @@ -17,11 +17,11 @@ from hooks import hook nemubotversion = 3.4 -from networking import w3m +from networking import page from .atom import Atom from more import Response -def help_full (): +def help_full(): return "This module is autonomous you can't interract with it." def load(context): @@ -37,7 +37,8 @@ def load(context): def start_watching(site, offset=0): o = urlparse(site["url"], "http") print_debug("Add event for site: %s" % o.netloc) - evt = ModuleEvent(func=lambda url: w3m(url), cmp_data=site["lastcontent"], + evt = ModuleEvent(func=lambda url: page.render(url, None), + cmp_data=site["lastcontent"], func_data=site["url"], offset=offset, interval=site.getInt("time"), call=alert_change, call_data=site) From 48149fadc106cd521eaf0d379760f39d7a13174f Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 1 Dec 2014 18:45:12 +0100 Subject: [PATCH 204/674] [networking] update netwhois --- modules/networking/whois.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/networking/whois.py b/modules/networking/whois.py index d79d918..f5f29f0 100644 --- a/modules/networking/whois.py +++ b/modules/networking/whois.py @@ -24,12 +24,16 @@ def load(CONF, add_hook): 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", @@ -95,7 +99,7 @@ def cmd_whois(msg): res = Response(channel=msg.channel, nomore="No more whois information") 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"] + " " if "status" in whois else "", + 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 "", From 52cf7b5ad7084932ae295c5e17635b19eac05d31 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 2 Dec 2014 07:12:34 +0100 Subject: [PATCH 205/674] Date tool: fix forgotten import when extracting a date without hours --- tools/date.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/date.py b/tools/date.py index f15ed95..c79cc05 100644 --- a/tools/date.py +++ b/tools/date.py @@ -18,7 +18,7 @@ # Extraction/Format text -from datetime import datetime +from datetime import datetime, date import re xtrdt = re.compile(r'''^.*? (?P<day>[0-9]{1,4}) .+? From c7baf6ecbe2316181a7b603c210f68368d5d58cf Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 2 Dec 2014 07:25:12 +0100 Subject: [PATCH 206/674] Date tool: can extract date with year and without hours --- tools/date.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/date.py b/tools/date.py index c79cc05..cc4481b 100644 --- a/tools/date.py +++ b/tools/date.py @@ -23,10 +23,10 @@ import re 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<year>[0-9]{1,4}))? [^0-9]+ + (?:.+?(?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}) - (?:[^0-9]*[m\":][^0-9]*(?P<second>[0-9]{1,2}))?)?)?.*? + (?:[^0-9]*[m\":][^0-9]*(?P<second>[0-9]{1,2}))?)?)?.*?)? $''', re.X) From c75d85b88b9bd882e1c375b7b70522405dd67bc8 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 2 Dec 2014 07:33:55 +0100 Subject: [PATCH 207/674] [birthday] fix date saving for other people --- modules/birthday.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/birthday.py b/modules/birthday.py index f4765aa..539c9c9 100644 --- a/modules/birthday.py +++ b/modules/birthday.py @@ -107,7 +107,7 @@ def parseask(msg): if nick == "my" or nick == "I" or nick == "i" or nick == "je" or nick == "mon" or nick == "ma": nick = msg.nick if nick.lower() in DATAS.index: - DATAS.index[msg.nick.lower()]["born"] = extDate + DATAS.index[nick.lower()]["born"] = extDate else: ms = ModuleState("birthday") ms.setAttribute("name", nick.lower()) From 5dcf0d69615bc5f5420e99bbd61937cf00724a63 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 4 Dec 2014 21:51:28 +0100 Subject: [PATCH 208/674] [networking] integrate watchwebsite module to networking + doc and reworking --- modules/networking/__init__.py | 23 +++- modules/{watchWebsite => networking}/atom.py | 48 +++---- .../watchWebsite.py} | 122 +++++++++++------- 3 files changed, 121 insertions(+), 72 deletions(-) rename modules/{watchWebsite => networking}/atom.py (75%) mode change 100755 => 100644 rename modules/{watchWebsite/__init__.py => networking/watchWebsite.py} (64%) diff --git a/modules/networking/__init__.py b/modules/networking/__init__.py index 3cffa66..10558f7 100644 --- a/modules/networking/__init__.py +++ b/modules/networking/__init__.py @@ -11,17 +11,20 @@ from more import Response from . import isup from . import page from . import w3c +from . import watchWebsite from . import whois def load(context): - for mod in [isup, page, w3c, whois]: + for mod in [isup, page, w3c, watchWebsite, whois]: mod.IRCException = IRCException mod.ModuleEvent = ModuleEvent mod.add_event = add_event mod.save = save mod.print = print mod.print_debug = print_debug + mod.send_response = send_response page.load(CONF, add_hook) + watchWebsite.load(DATAS) whois.load(CONF, add_hook) @@ -107,3 +110,21 @@ def cmd_w3c(msg): res.append_message("%s%s on line %s, col %s: %s" % (m["type"][0].upper(), m["type"][1:], m["lastLine"], m["lastColumn"], m["message"])) return res + + + +@hook("cmd_hook", "watch", data="diff") +@hook("cmd_hook", "updown", data="updown") +def cmd_watch(msg, diffType="diff"): + if len(msg.cmds) <= 1: + raise IRCException("indicate an URL to watch!") + + return watchWebsite.add_site(msg.cmds[1]) + + +@hook("cmd_hook", "unwatch") +def cmd_unwatch(msg): + if len(msg.cmds) <= 1: + raise IRCException("which URL should I stop watching?") + + return watchWebsite.add_site(msg.cmds[1]) diff --git a/modules/watchWebsite/atom.py b/modules/networking/atom.py old mode 100755 new mode 100644 similarity index 75% rename from modules/watchWebsite/atom.py rename to modules/networking/atom.py index 30272e0..88505e5 --- a/modules/watchWebsite/atom.py +++ b/modules/networking/atom.py @@ -6,8 +6,10 @@ from xml.dom.minidom import parse from xml.dom.minidom import parseString from xml.dom.minidom import getDOMImplementation + class AtomEntry: - def __init__ (self, node): + + def __init__(self, node): self.id = node.getElementsByTagName("id")[0].firstChild.nodeValue if node.getElementsByTagName("title")[0].firstChild is not None: self.title = node.getElementsByTagName("title")[0].firstChild.nodeValue @@ -19,36 +21,38 @@ class AtomEntry: try: self.updated = time.strptime(node.getElementsByTagName("updated")[0].firstChild.nodeValue[:10], "%Y-%m-%d") except: - print (node.getElementsByTagName("updated")[0].firstChild.nodeValue[:10]) - self.updated = time.localtime () + print(node.getElementsByTagName("updated")[0].firstChild.nodeValue[:10]) + self.updated = time.localtime() if len(node.getElementsByTagName("summary")) > 0 and node.getElementsByTagName("summary")[0].firstChild is not None: self.summary = node.getElementsByTagName("summary")[0].firstChild.nodeValue else: self.summary = None if len(node.getElementsByTagName("link")) > 0: - self.link = node.getElementsByTagName("link")[0].getAttribute ("href") + self.link = node.getElementsByTagName("link")[0].getAttribute("href") else: self.link = None - if len (node.getElementsByTagName("category")) >= 1: - self.category = node.getElementsByTagName("category")[0].getAttribute ("term") + if len(node.getElementsByTagName("category")) >= 1: + self.category = node.getElementsByTagName("category")[0].getAttribute("term") else: self.category = None - if len (node.getElementsByTagName("link")) > 1: - self.link2 = node.getElementsByTagName("link")[1].getAttribute ("href") + if len(node.getElementsByTagName("link")) > 1: + self.link2 = node.getElementsByTagName("link")[1].getAttribute("href") else: self.link2 = None + class Atom: - def __init__ (self, string): + + def __init__(self, string): self.raw = string - self.feed = parseString (string).documentElement + self.feed = parseString(string).documentElement self.id = self.feed.getElementsByTagName("id")[0].firstChild.nodeValue self.title = self.feed.getElementsByTagName("title")[0].firstChild.nodeValue self.updated = None - self.entries = dict () + self.entries = dict() for item in self.feed.getElementsByTagName("entry"): - entry = AtomEntry (item) + entry = AtomEntry(item) self.entries[entry.id] = entry if self.updated is None or self.updated < entry.updated: self.updated = entry.updated @@ -56,13 +60,13 @@ class Atom: def __str__(self): return self.raw - def diff (self, other): - differ = list () - for k in other.entries.keys (): + def diff(self, other): + differ = list() + for k in other.entries.keys(): if self.updated is None and k not in self.entries: self.updated = other.entries[k].updated if k not in self.entries and other.entries[k].updated >= self.updated: - differ.append (other.entries[k]) + differ.append(other.entries[k]) return differ @@ -75,10 +79,10 @@ if __name__ == "__main__": with open("rss.php", "r") as f: for line in f: content2 += line - a = Atom (content1) - print (a.updated) - b = Atom (content2) - print (b.updated) + a = Atom(content1) + print(a.updated) + b = Atom(content2) + print(b.updated) - diff = a.diff (b) - print (diff) + diff = a.diff(b) + print(diff) diff --git a/modules/watchWebsite/__init__.py b/modules/networking/watchWebsite.py similarity index 64% rename from modules/watchWebsite/__init__.py rename to modules/networking/watchWebsite.py index d1826a3..074522b 100644 --- a/modules/watchWebsite/__init__.py +++ b/modules/networking/watchWebsite.py @@ -1,31 +1,20 @@ -# coding=utf-8 - """Alert on changes on websites""" -from datetime import datetime -from datetime import timedelta -import http.client -import hashlib from random import randint -import re -import socket -import sys import urllib.parse from urllib.parse import urlparse from hooks import hook +from more import Response nemubotversion = 3.4 -from networking import page from .atom import Atom -from more import Response +from . import page -def help_full(): - return "This module is autonomous you can't interract with it." -def load(context): - """Register watched website""" +def load(DATAS): + """Register events on watched website""" DATAS.setIndex("url", "watch") for site in DATAS.getNodes("watch"): if site.hasNode("alert"): @@ -34,23 +23,13 @@ def load(context): print("No alert defined for this site: " + site["url"]) #DATAS.delChild(site) -def start_watching(site, offset=0): - o = urlparse(site["url"], "http") - print_debug("Add event for site: %s" % o.netloc) - evt = ModuleEvent(func=lambda url: page.render(url, None), - cmp_data=site["lastcontent"], - func_data=site["url"], offset=offset, - interval=site.getInt("time"), - call=alert_change, call_data=site) - site["_evt_id"] = add_event(evt) +def del_site(url): + """Remove a site from watching list -@hook("cmd_hook", "unwatch") -def del_site(msg): - if len(msg.cmds) <= 1: - raise IRCException("quel site dois-je arrêter de surveiller ?") - - url = msg.cmds[1] + Argument: + url -- URL to unwatch + """ o = urlparse(url, "http") if o.scheme != "" and url in DATAS.index: @@ -58,35 +37,33 @@ def del_site(msg): for a in site.getNodes("alert"): if a["channel"] == msg.channel: if not (msg.frm == a["nick"] or msg.frm_owner): - raise IRCException("vous ne pouvez pas supprimer cette URL.") + raise IRCException("you cannot unwatch this URL.") site.delChild(a) if not site.hasNode("alert"): del_event(site["_evt_id"]) DATAS.delChild(site) save() - return Response("je ne surveille désormais plus cette URL.", + return Response("I don't watch this URL anymore.", channel=msg.channel, nick=msg.nick) - raise IRCException("je ne surveillais pas cette URL !") + raise IRCException("I didn't watch this URL!") -@hook("cmd_hook", "watch", data="diff") -@hook("cmd_hook", "updown", data="updown") -def add_site(msg, diffType="diff"): - print (diffType) - if len(msg.cmds) <= 1: - raise IRCException("quel site dois-je surveiller ?") +def add_site(url): + """Add a site to watching list - url = msg.cmds[1] + Argument: + url -- URL to watch + """ o = urlparse(url, "http") if o.netloc == "": - raise IRCException("je ne peux pas surveiller cette URL") + raise IRCException("sorry, I can't watch this URL :(") alert = ModuleState("alert") alert["nick"] = msg.nick alert["server"] = msg.server alert["channel"] = msg.channel - alert["message"] = "{url} a changé !" + alert["message"] = "{url} just changed!" if url not in DATAS.index: watch = ModuleState("watch") @@ -101,15 +78,41 @@ def add_site(msg, diffType="diff"): save() return Response(channel=msg.channel, nick=msg.nick, - message="ce site est maintenant sous ma surveillance.") + message="this site is now under my supervision.") + def format_response(site, link='%s', title='%s', categ='%s', content='%s'): + """Format and send response for given site + + Argument: + site -- DATAS structure representing a site to watch + + Keyword arguments: + link -- link to the content + title -- for ATOM feed: title of the new article + categ -- for ATOM feed: category of the new article + content -- content of the page/new article + """ + for a in site.getNodes("alert"): - send_response(a["server"], Response(a["message"].format(url=site["url"], link=link, title=title, categ=categ, content=content), - channel=a["channel"], server=a["server"])) + send_response(a["server"], + Response(a["message"].format(url=site["url"], + link=link, + title=title, + categ=categ, + content=content), + channel=a["channel"], + server=a["server"])) + def alert_change(content, site): - """Alert when a change is detected""" + """Function called when a change is detected on a given site + + Arguments: + content -- The new content + site -- DATAS structure representing a site to watch + """ + if site["type"] == "updown": if site["lastcontent"] is None: site["lastcontent"] = content is not None @@ -132,7 +135,7 @@ def alert_change(content, site): try: page = Atom(content) except: - print ("An error occurs during Atom parsing. Restart event...") + print("An error occurs during Atom parsing. Restart event...") start_watching(site) return diff = site["_lastpage"].diff(page) @@ -152,10 +155,31 @@ def alert_change(content, site): format_response(site, link=d.link, title=urllib.parse.unquote(d.title)) else: start_watching(site) - return #Stop here, no changes, so don't save + return # Stop here, no changes, so don't save - else: # Just looking for any changes + else: # Just looking for any changes format_response(site, link=site["url"], content=content) site["lastcontent"] = content start_watching(site) save() + + +def start_watching(site, offset=0): + """Launch the event watching given site + + Argument: + site -- DATAS structure representing a site to watch + + Keyword argument: + offset -- offset time to delay the launch of the first check + """ + + o = urlparse(site["url"], "http") + print_debug("Add %s event for site: %s" % (site["type"], o.netloc)) + + evt = ModuleEvent(func=lambda url: page.render(url, None), + cmp_data=site["lastcontent"], + func_data=site["url"], offset=offset, + interval=site.getInt("time"), + call=alert_change, call_data=site) + site["_evt_id"] = add_event(evt) From d6ea5736a53cc65eb9cc4fdb3976197dd04c9e1a Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 6 Dec 2014 09:00:53 +0100 Subject: [PATCH 209/674] Move xmlparser to tools --- bot.py | 7 +------ importer.py | 6 +++--- modules/birthday.py | 2 +- modules/ddg/DDGSearch.py | 4 ++-- modules/ddg/WFASearch.py | 4 ++-- modules/translate.py | 1 - tools/__init__.py | 5 +++++ tools/config.py | 4 ++-- tools/web.py | 4 ++-- {xmlparser => tools/xmlparser}/__init__.py | 4 +--- {xmlparser => tools/xmlparser}/node.py | 4 +--- 11 files changed, 20 insertions(+), 25 deletions(-) rename {xmlparser => tools/xmlparser}/__init__.py (97%) rename {xmlparser => tools/xmlparser}/node.py (98%) diff --git a/bot.py b/bot.py index 2c38871..9a8c339 100644 --- a/bot.py +++ b/bot.py @@ -26,7 +26,7 @@ import threading import time import uuid -__version__ = '3.4.dev2' +__version__ = '3.4.dev3' __author__ = 'nemunaire' from consumer import Consumer, EventConsumer, MessageConsumer @@ -504,8 +504,3 @@ def reload(): imp.reload(tools) tools.reload() - - import xmlparser - imp.reload(xmlparser) - import xmlparser.node - imp.reload(xmlparser.node) diff --git a/importer.py b/importer.py index 48ccb6e..05d9f2d 100644 --- a/importer.py +++ b/importer.py @@ -29,7 +29,7 @@ import event import exception import hooks from message import TextMessage -import xmlparser +from tools.xmlparser import parse_file, module_state logger = logging.getLogger("nemubot.importer") @@ -191,7 +191,7 @@ class ModuleLoader(SourceLoader): module.del_event = del_event if not hasattr(module, "NODATA"): - module.DATAS = xmlparser.parse_file(self.context.data_path + module.DATAS = parse_file(self.context.data_path + module.__name__ + ".xml") module.save = mod_save else: @@ -200,7 +200,7 @@ class ModuleLoader(SourceLoader): module.CONF = self.config module.ModuleEvent = event.ModuleEvent - module.ModuleState = xmlparser.module_state.ModuleState + module.ModuleState = module_state.ModuleState module.IRCException = exception.IRCException # Load dependancies diff --git a/modules/birthday.py b/modules/birthday.py index 539c9c9..c48ace5 100644 --- a/modules/birthday.py +++ b/modules/birthday.py @@ -9,7 +9,7 @@ from datetime import date, datetime from hooks import hook from tools.countdown import countdown_format from tools.date import extractDate -from xmlparser.node import ModuleState +from tools.xmlparser.node import ModuleState nemubotversion = 3.4 diff --git a/modules/ddg/DDGSearch.py b/modules/ddg/DDGSearch.py index 67d0429..ed59138 100644 --- a/modules/ddg/DDGSearch.py +++ b/modules/ddg/DDGSearch.py @@ -3,15 +3,15 @@ from urllib.parse import quote from urllib.request import urlopen -import xmlparser from tools import web +from tools.xmlparser import parse_string class DDGSearch: def __init__(self, terms): self.terms = terms raw = urlopen("https://api.duckduckgo.com/?q=%s&format=xml&no_redirect=1" % quote(terms), timeout=10) - self.ddgres = xmlparser.parse_string(raw.read()) + self.ddgres = parse_string(raw.read()) @property def type(self): diff --git a/modules/ddg/WFASearch.py b/modules/ddg/WFASearch.py index b91fa2c..73755ca 100644 --- a/modules/ddg/WFASearch.py +++ b/modules/ddg/WFASearch.py @@ -3,7 +3,7 @@ from urllib.parse import quote from urllib.request import urlopen -import xmlparser +from tools.xmlparser import parse_string class WFASearch: def __init__(self, terms): @@ -13,7 +13,7 @@ class WFASearch: "input=%s&appid=%s" % (quote(terms), CONF.getNode("wfaapi")["key"]), timeout=15) - self.wfares = xmlparser.parse_string(raw.read()) + self.wfares = parse_string(raw.read()) except (TypeError, KeyError): print ("You need a Wolfram|Alpha API key in order to use this " "module. Add it to the module configuration file:\n<wfaapi" diff --git a/modules/translate.py b/modules/translate.py index eeffdb3..7576981 100644 --- a/modules/translate.py +++ b/modules/translate.py @@ -13,7 +13,6 @@ nemubotversion = 3.4 from more import Response -import xmlparser LANG = ["ar", "zh", "cz", "en", "fr", "gr", "it", "ja", "ko", "pl", "pt", "ro", "es", "tr"] diff --git a/tools/__init__.py b/tools/__init__.py index 78b9d0c..d2004b5 100644 --- a/tools/__init__.py +++ b/tools/__init__.py @@ -44,3 +44,8 @@ def reload(): import tools.web imp.reload(tools.web) + + import tools.xmlparser + imp.reload(tools.xmlparser) + import tools.xmlparser.node + imp.reload(tools.xmlparser.node) diff --git a/tools/config.py b/tools/config.py index e1a2b37..11d4e58 100644 --- a/tools/config.py +++ b/tools/config.py @@ -19,7 +19,7 @@ import logging import os -import xmlparser +from tools.xmlparser import parse_file logger = logging.getLogger("nemubot.tools.config") @@ -98,7 +98,7 @@ def load_file(filename, context): """ if os.path.isfile(filename): - config = xmlparser.parse_file(filename) + config = parse_file(filename) # This is a true nemubot configuration file, load it! if config.getName() == "nemubotconfig": diff --git a/tools/web.py b/tools/web.py index a9e59fa..095f457 100644 --- a/tools/web.py +++ b/tools/web.py @@ -26,7 +26,7 @@ from urllib.parse import urlparse from urllib.request import urlopen from exception import IRCException -import xmlparser +from tools.xmlparser import parse_string def isURL(url): @@ -141,7 +141,7 @@ def getXML(url, timeout=15): if cnt is None: return None else: - return xmlparser.parse_string(cnt.encode()) + return parse_string(cnt.encode()) def getJSON(url, timeout=15): diff --git a/xmlparser/__init__.py b/tools/xmlparser/__init__.py similarity index 97% rename from xmlparser/__init__.py rename to tools/xmlparser/__init__.py index 722c6bf..a11ea25 100644 --- a/xmlparser/__init__.py +++ b/tools/xmlparser/__init__.py @@ -16,14 +16,12 @@ # 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 imp import logging import xml.sax from . import node as module_state -logger = logging.getLogger("nemubot.xmlparser") +logger = logging.getLogger("nemubot.tools.xmlparser") class ModuleStatesFile(xml.sax.ContentHandler): diff --git a/xmlparser/node.py b/tools/xmlparser/node.py similarity index 98% rename from xmlparser/node.py rename to tools/xmlparser/node.py index 6f7f91c..5202b33 100644 --- a/xmlparser/node.py +++ b/tools/xmlparser/node.py @@ -3,11 +3,9 @@ import xml.sax from datetime import datetime, timezone import logging -import sys import time -import traceback -logger = logging.getLogger("nemubot.xmlparser.node") +logger = logging.getLogger("nemubot.tools.xmlparser.node") class ModuleState: From 65aa371fdcc072d3044f7e8cd06b2e3b8508fd67 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 15 Dec 2014 21:36:59 +0100 Subject: [PATCH 210/674] Use argparse to parse CLI argument --- bot.py | 13 ------------- importer.py | 26 +++++++++++++------------- nemubot.py | 46 +++++++++++++++++++++++++++++++++------------- 3 files changed, 46 insertions(+), 39 deletions(-) diff --git a/bot.py b/bot.py index 9a8c339..b9829cc 100644 --- a/bot.py +++ b/bot.py @@ -334,19 +334,6 @@ class Bot(threading.Thread): # Modules methods - def add_modules_path(self, path): - """Add a path to the modules_path array, used by module loader""" - # The path must end by / char - if path[-1] != "/": - path += "/" - - if path not in self.modules_paths: - self.modules_paths.append(path) - return True - - return False - - def import_module(self, name): """Load a module diff --git a/importer.py b/importer.py index 05d9f2d..8138cac 100644 --- a/importer.py +++ b/importer.py @@ -39,16 +39,16 @@ class ModuleFinder(Finder): self.prompt = prompt def find_module(self, fullname, path=None): - #print ("looking for", fullname, "in", path) + # print ("looking for", fullname, "in", path) # Search only for new nemubot modules (packages init) if path is None: for mpath in self.context.modules_paths: - #print ("looking for", fullname, "in", mpath) - if (os.path.isfile(mpath + fullname + ".py") or - os.path.isfile(mpath + fullname + "/__init__.py")): + # print ("looking for", fullname, "in", mpath) + if (os.path.isfile(os.path.join(mpath, fullname + ".py")) or + os.path.isfile(os.path.join(os.path.join(mpath, fullname), "__init__.py"))): return ModuleLoader(self.context, self.prompt, fullname, mpath) - #print ("not found") + # print ("not found") return None @@ -63,14 +63,14 @@ class ModuleLoader(SourceLoader): else: self.config = None - if os.path.isfile(path + fullname + ".py"): - self.source_path = path + self.name + ".py" + if os.path.isfile(os.path.join(path, fullname + ".py")): + self.source_path = os.path.join(path, self.name + ".py") self.package = False self.mpath = path - elif os.path.isfile(path + fullname + "/__init__.py"): - self.source_path = path + self.name + "/__init__.py" + elif os.path.isfile(os.path.join(os.path.join(path, fullname), "__init__.py")): + self.source_path = os.path.join(os.path.join(path, self.name), "__init__.py") self.package = True - self.mpath = path + self.name + "/" + self.mpath = path + self.name + os.sep else: raise ImportError @@ -146,7 +146,7 @@ class ModuleLoader(SourceLoader): module.logger.debug(*args) def mod_save(): - fpath = self.context.data_path + "/" + module.__name__ + ".xml" + fpath = os.path.join(self.context.data_path, module.__name__ + ".xml") module.print_debug("Saving DATAS to " + fpath) module.DATAS.save(fpath) @@ -191,8 +191,8 @@ class ModuleLoader(SourceLoader): module.del_event = del_event if not hasattr(module, "NODATA"): - module.DATAS = parse_file(self.context.data_path - + module.__name__ + ".xml") + module.DATAS = parse_file(os.path.join(self.context.data_path, + module.__name__ + ".xml")) module.save = mod_save else: module.DATAS = None diff --git a/nemubot.py b/nemubot.py index 1b2121f..3311593 100755 --- a/nemubot.py +++ b/nemubot.py @@ -17,6 +17,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 argparse import imp import logging import os @@ -44,8 +45,33 @@ if __name__ == "__main__": fh.setLevel(logging.DEBUG) logger.addHandler(fh) + # Parse command line arguments + parser = argparse.ArgumentParser() + + parser.add_argument("-M", "--modules-path", nargs='*', + default=["./modules/"], + help="Directory to use as modules store") + + parser.add_argument("-D", "--data-path", default="./datas/", + help="Path to use to save bot data") + + parser.add_argument('files', metavar='FILE', nargs='*', + help="Configuration files to load") + + + args = parser.parse_args() + + # Add modules dir paths + modules_paths = list() + for path in args.modules_path: + if os.path.isdir(path): + modules_paths.append( + os.path.realpath(os.path.abspath(path))) + else: + logger.error("%s is not a directory", path) + # Create bot context - context = bot.Bot() + context = bot.Bot(modules_paths=modules_paths, data_path=args.data_path) # Load the prompt prmpt = prompt.Prompt() @@ -53,18 +79,12 @@ if __name__ == "__main__": # Register the hook for futur import sys.meta_path.append(importer.ModuleFinder(context, prmpt)) - # Add modules dir path - if os.path.isdir("./modules/"): - context.add_modules_path( - os.path.realpath(os.path.abspath("./modules/"))) - - # Parse command line arguments - if len(sys.argv) >= 2: - for arg in sys.argv[1:]: - if os.path.isdir(arg): - context.add_modules_path(arg) - else: - load_file(arg, context) + # Load requested configuration files + for path in args.files: + if os.path.isfile(path): + load_file(path, context) + else: + logger.error("%s is not a readable file", path) print ("Nemubot v%s ready, my PID is %i!" % (bot.__version__, os.getpid())) From 3265006adbd1885cd787d44107d293ad867eaf5b Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 15 Dec 2014 21:43:40 +0100 Subject: [PATCH 211/674] argparse: add verbosity level --- bot.py | 4 +++- importer.py | 2 +- nemubot.py | 43 +++++++++++++++++++++++++------------------ 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/bot.py b/bot.py index b9829cc..61845d7 100644 --- a/bot.py +++ b/bot.py @@ -43,7 +43,7 @@ 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_path="./datas/"): + data_path="./datas/", verbosity=0): """Initialize the bot context Keyword arguments: @@ -56,6 +56,8 @@ class Bot(threading.Thread): logger.info("Initiate nemubot v%s", __version__) + self.verbosity = verbosity + # External IP for accessing this bot self.ip = ip diff --git a/importer.py b/importer.py index 8138cac..c1fd900 100644 --- a/importer.py +++ b/importer.py @@ -179,7 +179,7 @@ class ModuleLoader(SourceLoader): # Set module common functions and datas module.REGISTERED_HOOKS = list() module.REGISTERED_EVENTS = list() - module.DEBUG = False + module.DEBUG = self.context.verbosity > 0 module.DIR = self.mpath module.print = prnt module.print_debug = prnt_dbg diff --git a/nemubot.py b/nemubot.py index 3311593..2001898 100755 --- a/nemubot.py +++ b/nemubot.py @@ -29,25 +29,13 @@ from prompt.builtins import load_file import importer if __name__ == "__main__": - # Setup loggin interface - logger = logging.getLogger("nemubot") - - formatter = logging.Formatter( - '%(asctime)s %(name)s %(levelname)s %(message)s') - - ch = logging.StreamHandler() - ch.setFormatter(formatter) - ch.setLevel(logging.INFO) - logger.addHandler(ch) - - fh = logging.FileHandler('./nemubot.log') - fh.setFormatter(formatter) - fh.setLevel(logging.DEBUG) - logger.addHandler(fh) - # Parse command line arguments parser = argparse.ArgumentParser() + parser.add_argument("-v", "--verbose", action="count", + default=0, + help="Verbosity level") + parser.add_argument("-M", "--modules-path", nargs='*', default=["./modules/"], help="Directory to use as modules store") @@ -58,9 +46,27 @@ if __name__ == "__main__": parser.add_argument('files', metavar='FILE', nargs='*', help="Configuration files to load") - args = parser.parse_args() + # Setup loggin interface + logger = logging.getLogger("nemubot") + + formatter = logging.Formatter( + '%(asctime)s %(name)s %(levelname)s %(message)s') + + ch = logging.StreamHandler() + ch.setFormatter(formatter) + if args.verbose > 1: + ch.setLevel(logging.DEBUG) + else: + ch.setLevel(logging.INFO) + logger.addHandler(ch) + + fh = logging.FileHandler('./nemubot.log') + fh.setFormatter(formatter) + fh.setLevel(logging.DEBUG) + logger.addHandler(fh) + # Add modules dir paths modules_paths = list() for path in args.modules_path: @@ -71,7 +77,8 @@ if __name__ == "__main__": logger.error("%s is not a directory", path) # Create bot context - context = bot.Bot(modules_paths=modules_paths, data_path=args.data_path) + context = bot.Bot(modules_paths=modules_paths, data_path=args.data_path, + verbosity=args.verbose) # Load the prompt prmpt = prompt.Prompt() From f575674d47e569323d513e61b8fea5082407f0a5 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 15 Dec 2014 21:48:59 +0100 Subject: [PATCH 212/674] argparse: add version information option --- nemubot.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/nemubot.py b/nemubot.py index 2001898..8992545 100755 --- a/nemubot.py +++ b/nemubot.py @@ -34,20 +34,27 @@ if __name__ == "__main__": parser.add_argument("-v", "--verbose", action="count", default=0, - help="Verbosity level") + help="verbosity level") + + parser.add_argument("-V", "--version", action="store_true", + help="display nemubot version and exit") parser.add_argument("-M", "--modules-path", nargs='*', default=["./modules/"], - help="Directory to use as modules store") + help="directory to use as modules store") parser.add_argument("-D", "--data-path", default="./datas/", - help="Path to use to save bot data") + help="path to use to save bot data") parser.add_argument('files', metavar='FILE', nargs='*', - help="Configuration files to load") + help="configuration files to load") args = parser.parse_args() + if args.version: + print(bot.__version__) + sys.exit(0) + # Setup loggin interface logger = logging.getLogger("nemubot") From 22a2ba76c4a6eacde4c24d3058e4fa900aadfe9a Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 15 Dec 2014 21:53:40 +0100 Subject: [PATCH 213/674] argparse: add --no-connect option to disable autoconnect rules in configuration files --- bot.py | 2 +- nemubot.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 61845d7..57c0ac4 100644 --- a/bot.py +++ b/bot.py @@ -326,7 +326,7 @@ class Bot(threading.Thread): if srv.id not in self.servers: self.servers[srv.id] = srv - if autoconnect: + if autoconnect and not hasattr(self, "noautoconnect"): srv.open() return True diff --git a/nemubot.py b/nemubot.py index 8992545..1964269 100755 --- a/nemubot.py +++ b/nemubot.py @@ -32,6 +32,9 @@ if __name__ == "__main__": # Parse command line arguments parser = argparse.ArgumentParser() + parser.add_argument("-a", "--no-connect", action="store_true", + help="disable auto-connect to servers at startup") + parser.add_argument("-v", "--verbose", action="count", default=0, help="verbosity level") @@ -87,6 +90,9 @@ if __name__ == "__main__": context = bot.Bot(modules_paths=modules_paths, data_path=args.data_path, verbosity=args.verbose) + if args.no_connect: + context.noautoconnect = True + # Load the prompt prmpt = prompt.Prompt() From 02acad59681c19c78454d2d163814b4d90d9af9d Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 15 Dec 2014 22:08:21 +0100 Subject: [PATCH 214/674] argparse: -m to load modules --- nemubot.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/nemubot.py b/nemubot.py index 1964269..3718d73 100755 --- a/nemubot.py +++ b/nemubot.py @@ -46,6 +46,9 @@ if __name__ == "__main__": default=["./modules/"], help="directory to use as modules store") + parser.add_argument("-m", "--module", nargs='*', + help="load given modules") + parser.add_argument("-D", "--data-path", default="./datas/", help="path to use to save bot data") @@ -106,6 +109,10 @@ if __name__ == "__main__": else: logger.error("%s is not a readable file", path) + if args.module: + for module in args.module: + __import__(module) + print ("Nemubot v%s ready, my PID is %i!" % (bot.__version__, os.getpid())) context.start() From 86fdaa4dd9f2c9346246c0bad4945fb3cfb116da Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 15 Dec 2014 23:53:26 +0100 Subject: [PATCH 215/674] web: fix new usage of getURLContent in getJSON --- tools/web.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/web.py b/tools/web.py index 095f457..6cc3c73 100644 --- a/tools/web.py +++ b/tools/web.py @@ -150,7 +150,7 @@ def getJSON(url, timeout=15): if cnt is None: return None else: - return json.loads(cnt.decode()) + return json.loads(cnt) # Other utils From a1c086a8076b7417c5d62a1718c990458bc0e602 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 15 Dec 2014 23:57:37 +0100 Subject: [PATCH 216/674] [networking] use getJSON --- modules/networking/isup.py | 6 ++---- modules/networking/whois.py | 13 ++----------- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/modules/networking/isup.py b/modules/networking/isup.py index 6db94e1..f400381 100644 --- a/modules/networking/isup.py +++ b/modules/networking/isup.py @@ -1,6 +1,6 @@ -import json import urllib +from tools.web import getJSON def isup(url): """Determine if the given URL is up or not @@ -13,9 +13,7 @@ def isup(url): if o.netloc == "": o = urllib.parse.urlparse("http://" + url) if o.netloc != "": - req = urllib.request.Request("http://isitup.org/%s.json" % (o.netloc), headers={ 'User-Agent' : "nemubot v3" }) - raw = urllib.request.urlopen(req, timeout=10) - isup = json.loads(raw.read().decode()) + isup = getJSON("http://isitup.org/%s.json" % o.netloc) if "status_code" in isup and isup["status_code"] == 1: return isup["response_time"] diff --git a/modules/networking/whois.py b/modules/networking/whois.py index f5f29f0..2ec51ed 100644 --- a/modules/networking/whois.py +++ b/modules/networking/whois.py @@ -1,9 +1,8 @@ import datetime -import json -import socket import urllib from more import Response +from tools.web import getJSON URL_WHOIS = "http://www.whoisxmlapi.com/whoisserver/WhoisService?rid=1&domainName=%%s&outputFormat=json&userName=%s&password=%s" @@ -80,15 +79,7 @@ def cmd_whois(msg): dom = msg.cmds[1] - try: - req = urllib.request.Request(URL_WHOIS % urllib.parse.quote(dom), headers={ 'User-Agent' : "nemubot v3" }) - raw = urllib.request.urlopen(req, timeout=10) - except socket.timeout: - raise IRCException("Sorry, the request has timed out.") - except urllib.error.HTTPError as e: - raise IRCException("HTTP error occurs: %s %s" % (e.code, e.reason)) - - js = json.loads(raw.read().decode()) + js = getJSON(URL_WHOIS % urllib.parse.quote(dom)) if "ErrorMessage" in js: err = js["ErrorMessage"] From dd285b67d173ee8adfd0d1298dae9b643a354ebe Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 16 Dec 2014 00:45:01 +0100 Subject: [PATCH 217/674] fix netloc != hostname --- modules/networking/page.py | 6 +++--- tools/web.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/networking/page.py b/modules/networking/page.py index b1f3a56..0c0e24f 100644 --- a/modules/networking/page.py +++ b/modules/networking/page.py @@ -23,16 +23,16 @@ def headers(url): if o.netloc == "": raise IRCException("invalid URL") if o.scheme == "http": - conn = http.client.HTTPConnection(o.netloc, port=o.port, timeout=5) + conn = http.client.HTTPConnection(o.hostname, port=o.port, timeout=5) else: - conn = http.client.HTTPSConnection(o.netloc, port=o.port, timeout=5) + conn = http.client.HTTPSConnection(o.hostname, port=o.port, timeout=5) try: conn.request("HEAD", o.path, None, {"User-agent": "Nemubot v3"}) except socket.timeout: raise IRCException("request timeout") except socket.gaierror: print ("<tools.web> Unable to receive page %s from %s on %d." - % (o.path, o.netloc, o.port)) + % (o.path, o.hostname, o.port)) raise IRCException("an unexpected error occurs") try: diff --git a/tools/web.py b/tools/web.py index 6cc3c73..3265f34 100644 --- a/tools/web.py +++ b/tools/web.py @@ -43,7 +43,7 @@ def getScheme(url): def getHost(url): """Return the domain of a given URL""" - return urlparse(url).netloc + return urlparse(url).hostname def getPort(url): @@ -75,13 +75,13 @@ def getURLContent(url, timeout=15): o = urlparse("http://" + url) if o.scheme == "http": - conn = http.client.HTTPConnection(o.netloc, port=o.port, + conn = http.client.HTTPConnection(o.hostname, port=o.port, timeout=timeout) elif o.scheme == "https": - conn = http.client.HTTPSConnection(o.netloc, port=o.port, + conn = http.client.HTTPSConnection(o.hostname, port=o.port, timeout=timeout) elif o.scheme is None or o.scheme == "": - conn = http.client.HTTPConnection(o.netloc, port=80, timeout=timeout) + conn = http.client.HTTPConnection(o.hostname, port=80, timeout=timeout) else: return None try: From 0b06261d1847facbef276bd7db0cfd8fda93a67c Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 16 Dec 2014 00:46:07 +0100 Subject: [PATCH 218/674] tools/web: allow empty Content-Type --- tools/web.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/tools/web.py b/tools/web.py index 3265f34..8f522d6 100644 --- a/tools/web.py +++ b/tools/web.py @@ -102,23 +102,24 @@ def getURLContent(url, timeout=15): size = int(res.getheader("Content-Length", 200000)) cntype = res.getheader("Content-Type") - if size > 200000 or (cntype[:4] != "text" and cntype[:4] != "appl"): + if size > 200000 or (cntype is not None and cntype[:4] != "text" and cntype[:4] != "appl"): return None data = res.read(size) # Decode content charset = "utf-8" - lcharset = res.getheader("Content-Type").split(";") - if len(lcharset) > 1: - for c in charset: - ch = c.split("=") - if ch[0].strip().lower() == "charset" and len(ch) > 1: - cha = ch[1].split(".") - if len(cha) > 1: - charset = cha[1] - else: - charset = cha[0] + if cntype is not None: + lcharset = res.getheader("Content-Type").split(";") + if len(lcharset) > 1: + for c in charset: + ch = c.split("=") + if ch[0].strip().lower() == "charset" and len(ch) > 1: + cha = ch[1].split(".") + if len(cha) > 1: + charset = cha[1] + else: + charset = cha[0] except http.client.BadStatusLine: raise IRCException("Invalid HTTP response") finally: From a7b166498c33e034af034aee13d0eff983cde9b6 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 16 Dec 2014 00:56:47 +0100 Subject: [PATCH 219/674] xmlparser: don't manage errors at this level --- importer.py | 8 ++++++-- tools/xmlparser/__init__.py | 25 ++++--------------------- 2 files changed, 10 insertions(+), 23 deletions(-) diff --git a/importer.py b/importer.py index c1fd900..bfb6d34 100644 --- a/importer.py +++ b/importer.py @@ -191,8 +191,12 @@ class ModuleLoader(SourceLoader): module.del_event = del_event if not hasattr(module, "NODATA"): - module.DATAS = parse_file(os.path.join(self.context.data_path, - module.__name__ + ".xml")) + data_file = os.path.join(self.context.data_path, + module.__name__ + ".xml") + if os.path.isfile(data_file): + module.DATAS = parse_file(data_file) + else: + module.DATAS = module_state.ModuleState("nemubotstate") module.save = mod_save else: module.DATAS = None diff --git a/tools/xmlparser/__init__.py b/tools/xmlparser/__init__.py index a11ea25..502d198 100644 --- a/tools/xmlparser/__init__.py +++ b/tools/xmlparser/__init__.py @@ -54,28 +54,11 @@ def parse_file(filename): parser = xml.sax.make_parser() mod = ModuleStatesFile() parser.setContentHandler(mod) - try: - parser.parse(open(filename, "r")) - return mod.root - except IOError: - logger.exception("error occurs during XML parsing of %s", filename) - return module_state.ModuleState("nemubotstate") - except: - logger.exception("error occurs during XML parsing of %s", filename) - if mod.root is None: - return module_state.ModuleState("nemubotstate") - else: - return mod.root + parser.parse(open(filename, "r")) + return mod.root def parse_string(string): mod = ModuleStatesFile() - try: - xml.sax.parseString(string, mod) - return mod.root - except: - logger.exception("error occurs during XML parsing") - if mod.root is None: - return module_state.ModuleState("nemubotstate") - else: - return mod.root + xml.sax.parseString(string, mod) + return mod.root From 99106d26a9e7bb1d1e0f440f0597b39e8096f440 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 16 Dec 2014 07:09:15 +0100 Subject: [PATCH 220/674] Fix #70: new module tpb using an API with a TPB dump --- modules/tpb.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 modules/tpb.py diff --git a/modules/tpb.py b/modules/tpb.py new file mode 100644 index 0000000..ab4d23c --- /dev/null +++ b/modules/tpb.py @@ -0,0 +1,39 @@ +import urllib + +from tools.web import getJSON + +nemubotversion = 3.4 + +from hooks import hook +from more import Response + +URL_TPBAPI = None + +def load(context): + global URL_TPBAPI + + if not CONF or not CONF.hasNode("tpbapi") or not CONF.getNode("tpbapi").hasAttribute("url"): + print ("You need a TPB API in order to use the !tpb feature. Add it to " + "the module configuration file:\n" + "<tpbapi url=\"http://tpbapi.org/\" />\nSample API: " + "https://gist.github.com/colona/07a925f183cfb47d5f20") + else: + URL_TPBAPI = CONF.getNode("tpbapi")["url"] + + from hooks.messagehook import MessageHook + add_hook("cmd_hook", MessageHook(cmd_tpb, "tpb")) + + +def cmd_tpb(msg): + if len(msg.cmds) < 1: + raise IRCException("indicate an item to search!") + + torrents = getJSON(URL_TPBAPI + urllib.parse.quote(" ".join(msg.cmds[1:]))) + + res = Response(channel=msg.channel, nomore="No more torrents", count=" (%d more torrents)") + + if torrents: + for t in torrents: + res.append_message("\x03\x02{title}\x03\x02 in {category}, {size}B; id: {id}; magnet: {magnet}".format(**t)) + + return res From 6b6ff0cb56bf43ea2988521f52a104d23d55454e Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 17 Dec 2014 06:58:41 +0100 Subject: [PATCH 221/674] New tool to convert some content to human readable strings --- tools/human.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 tools/human.py diff --git a/tools/human.py b/tools/human.py new file mode 100644 index 0000000..620e167 --- /dev/null +++ b/tools/human.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- + +# Nemubot is a modulable IRC bot, built around XML configuration files. +# Copyright (C) 2012 Mercier Pierre-Olivier +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import math + +def size(size, unit=True): + """Convert a given byte size to an more human readable way + + Argument: + size -- the size to convert + unit -- append the unit at the end of the string + """ + + if size <= 0: + return "0 B" if unit else "0" + + units = ['B','KiB','MiB','GiB','TiB','PiB','EiB','ZiB','YiB'] + p = math.floor(math.log(size, 2) / 10) + + if unit: + return "%.3f %s" % (size / math.pow(1024,p), units[int(p)]) + else: + return "%.3f" % (size / math.pow(1024,p)) From 66ec7cb7cad2de49c513496f1c1b8b61a1e458bc Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 17 Dec 2014 07:07:54 +0100 Subject: [PATCH 222/674] [tpb] More usefull information --- modules/tpb.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/tpb.py b/modules/tpb.py index ab4d23c..f6e38a9 100644 --- a/modules/tpb.py +++ b/modules/tpb.py @@ -1,5 +1,7 @@ +from datetime import datetime import urllib +from tools import human from tools.web import getJSON nemubotversion = 3.4 @@ -34,6 +36,8 @@ def cmd_tpb(msg): if torrents: for t in torrents: - res.append_message("\x03\x02{title}\x03\x02 in {category}, {size}B; id: {id}; magnet: {magnet}".format(**t)) + t["sizeH"] = human.size(t["size"]) + t["dateH"] = datetime.fromtimestamp(t["date"]).strftime('%Y-%m-%d %H:%M:%S') + res.append_message("\x03\x02{title}\x03\x02 in {category}, {sizeH}; added at {dateH}; magnet: {magnet}; id: {id}".format(**t)) return res From d14fec4cec07a210dad5111b66150e1a6b38d4cb Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 17 Dec 2014 07:32:34 +0100 Subject: [PATCH 223/674] Modules: global dusting: call getJSON instead of making raw calls to urllib --- modules/books.py | 2 +- modules/ddg/DDGSearch.py | 9 +++-- modules/ddg/UrbanDictionnary.py | 11 +++--- modules/ddg/WFASearch.py | 18 +++++----- modules/github.py | 60 ++++++++++++++------------------- modules/imdb.py | 20 ++++++----- modules/mapquest.py | 34 ++++++++++++------- modules/mediawiki.py | 13 +++---- modules/reddit.py | 20 +++++------ modules/sap.py | 3 +- modules/syno.py | 8 ++--- modules/translate.py | 14 +++----- modules/weather.py | 14 +++++--- modules/ycc.py | 8 ++--- 14 files changed, 118 insertions(+), 116 deletions(-) diff --git a/modules/books.py b/modules/books.py index 4d316c7..de4a9bf 100644 --- a/modules/books.py +++ b/modules/books.py @@ -2,7 +2,7 @@ """Looking for books""" -import urllib.request +import urllib from hooks import hook from tools import web diff --git a/modules/ddg/DDGSearch.py b/modules/ddg/DDGSearch.py index ed59138..7a79c14 100644 --- a/modules/ddg/DDGSearch.py +++ b/modules/ddg/DDGSearch.py @@ -1,17 +1,20 @@ # coding=utf-8 from urllib.parse import quote -from urllib.request import urlopen from tools import web from tools.xmlparser import parse_string + class DDGSearch: + def __init__(self, terms): self.terms = terms - raw = urlopen("https://api.duckduckgo.com/?q=%s&format=xml&no_redirect=1" % quote(terms), timeout=10) - self.ddgres = parse_string(raw.read()) + self.ddgres = web.getXML( + "https://api.duckduckgo.com/?q=%s&format=xml&no_redirect=1" % + quote(terms), + timeout=10) @property def type(self): diff --git a/modules/ddg/UrbanDictionnary.py b/modules/ddg/UrbanDictionnary.py index 904669c..439b313 100644 --- a/modules/ddg/UrbanDictionnary.py +++ b/modules/ddg/UrbanDictionnary.py @@ -1,15 +1,18 @@ # coding=utf-8 -import json from urllib.parse import quote -from urllib.request import urlopen + +from tools import web + class UrbanDictionnary: + def __init__(self, terms): self.terms = terms - raw = urlopen("http://api.urbandictionary.com/v0/define?term=%s" % quote(terms), timeout=10) - self.udres = json.loads(raw.read().decode()) + self.udres = web.getJSON( + "http://api.urbandictionary.com/v0/define?term=%s" % quote(terms), + timeout=10) @property def result_type(self): diff --git a/modules/ddg/WFASearch.py b/modules/ddg/WFASearch.py index 73755ca..f465165 100644 --- a/modules/ddg/WFASearch.py +++ b/modules/ddg/WFASearch.py @@ -1,19 +1,17 @@ # coding=utf-8 from urllib.parse import quote -from urllib.request import urlopen -from tools.xmlparser import parse_string +from tools import web + class WFASearch: def __init__(self, terms): self.terms = terms try: - raw = urlopen("http://api.wolframalpha.com/v2/query?" - "input=%s&appid=%s" - % (quote(terms), - CONF.getNode("wfaapi")["key"]), timeout=15) - self.wfares = parse_string(raw.read()) + url = ("http://api.wolframalpha.com/v2/query?input=%s&appid=%s" % + (quote(terms), CONF.getNode("wfaapi")["key"])) + self.wfares = web.getXML(url) except (TypeError, KeyError): print ("You need a Wolfram|Alpha API key in order to use this " "module. Add it to the module configuration file:\n<wfaapi" @@ -33,7 +31,8 @@ class WFASearch: if self.wfares is None: return "An error occurs during computation." elif self.wfares["error"] == "true": - return "An error occurs during computation: " + self.wfares.getNode("error").getNode("msg").getContent() + return ("An error occurs during computation: " + + self.wfares.getNode("error").getNode("msg").getContent()) elif self.wfares.hasNode("didyoumeans"): start = "Did you mean: " tag = "didyoumean" @@ -66,6 +65,7 @@ class WFASearch: 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() + yield (node["title"] + " " + subnode["title"] + ": " + + subnode.getFirstNode("plaintext").getContent()) except IndexError: pass diff --git a/modules/github.py b/modules/github.py index 41ee743..d22b832 100644 --- a/modules/github.py +++ b/modules/github.py @@ -2,13 +2,11 @@ """Repositories, users or issues on GitHub""" -import json import re -import urllib.error from urllib.parse import quote -from urllib.request import urlopen from hooks import hook +from tools import web nemubotversion = 3.4 @@ -21,19 +19,17 @@ def help_full(): def info_repos(repo): - raw = urlopen("https://api.github.com/search/repositories?q=%s" % - quote(repo), timeout=10) - return json.loads(raw.read().decode()) + return web.getJSON("https://api.github.com/search/repositories?q=%s" % + quote(repo), timeout=10) def info_user(username): - raw = urlopen("https://api.github.com/users/%s" % quote(username), - timeout=10) - user = json.loads(raw.read().decode()) + user = web.getJSON("https://api.github.com/users/%s" % quote(username), + timeout=10) - raw = urlopen("https://api.github.com/users/%s/repos?sort=updated" % - quote(username), timeout=10) - user["repos"] = json.loads(raw.read().decode()) + user["repos"] = web.getJSON("https://api.github.com/users/%s/" + "repos?sort=updated" % quote(username), + timeout=10) return user @@ -45,17 +41,12 @@ def info_issue(repo, issue=None): else: fullname = repo - try: - if issue is not None: - raw = urlopen("https://api.github.com/repos/%s/issues/%s" % - (quote(fullname), quote(issue)), timeout=10) - return [json.loads(raw.read().decode())] - else: - raw = urlopen("https://api.github.com/repos/%s/issues?sort=updated" - % quote(fullname), timeout=10) - return json.loads(raw.read().decode()) - except urllib.error.HTTPError: - raise IRCException("Repository not found") + if issue is not None: + return [web.getJSON("https://api.github.com/repos/%s/issues/%s" % + (quote(fullname), quote(issue)))] + else: + return web.getJSON("https://api.github.com/repos/%s/issues?" + "sort=updated" % quote(fullname)) def info_commit(repo, commit=None): @@ -65,17 +56,12 @@ def info_commit(repo, commit=None): else: fullname = repo - try: - if commit is not None: - raw = urlopen("https://api.github.com/repos/%s/commits/%s" % - (quote(fullname), quote(commit)), timeout=10) - return [json.loads(raw.read().decode())] - else: - raw = urlopen("https://api.github.com/repos/%s/commits" % - quote(fullname), timeout=10) - return json.loads(raw.read().decode()) - except urllib.error.HTTPError: - raise IRCException("Repository not found") + if commit is not None: + return [web.getJSON("https://api.github.com/repos/%s/commits/%s" % + (quote(fullname), quote(commit)))] + else: + return web.getJSON("https://api.github.com/repos/%s/commits" % + quote(fullname)) @hook("cmd_hook", "github") @@ -162,6 +148,9 @@ def cmd_github(msg): issues = info_issue(repo, issue) + if issues is None: + raise IRCException("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" % (issue["state"][0].upper(), @@ -195,6 +184,9 @@ def cmd_github(msg): commits = info_commit(repo, commit) + if commits is None: + raise IRCException("Repository not found") + for commit in commits: res.append_message("Commit %s by %s on %s: %s" % (commit["sha"][:10], diff --git a/modules/imdb.py b/modules/imdb.py index 1bb36bc..7ae4163 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -2,19 +2,21 @@ """Show many information about a movie or serie""" -import json import re -import urllib.request +import urllib.parse from hooks import hook +from 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>" + def get_movie(title=None, year=None, imdbid=None, fullplot=True, tomatoes=False): """Returns the information about the matching movie""" @@ -34,8 +36,7 @@ def get_movie(title=None, year=None, imdbid=None, fullplot=True, tomatoes=False) print_debug(url) # Make the request - response = urllib.request.urlopen(url) - data = json.loads(response.read().decode()) + data = web.getJSON(url) # Return data if "Error" in data: @@ -47,6 +48,7 @@ def get_movie(title=None, year=None, imdbid=None, fullplot=True, tomatoes=False) else: raise IRCException("An error occurs during movie search") + def find_movies(title): """Find existing movies matching a approximate title""" @@ -55,8 +57,7 @@ def find_movies(title): print_debug(url) # Make the request - raw = urllib.request.urlopen(url) - data = json.loads(raw.read().decode()) + data = web.getJSON(url) # Return data if "Error" in data: @@ -86,9 +87,9 @@ def cmd_imdb(msg): else: data = get_movie(title=title) - 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 = 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'])) @@ -97,6 +98,7 @@ def cmd_imdb(msg): % (data['Type'], data['Country'], data['Released'], data['Genre'], data['Director'], data['Writer'], data['Actors'])) return res + @hook("cmd_hook", "imdbs") def cmd_search(msg): """!imdbs <approximative title> to search a movie title""" diff --git a/modules/mapquest.py b/modules/mapquest.py index 824f578..4f51d49 100644 --- a/modules/mapquest.py +++ b/modules/mapquest.py @@ -1,14 +1,17 @@ # coding=utf-8 -import json +"""The mapquest module""" + import re from urllib.parse import quote -from urllib.request import urlopen + +from tools import web nemubotversion = 3.4 from more import Response + def load(context): if not CONF or not CONF.hasNode("mapquestapi") or not CONF.getNode("mapquestapi").hasAttribute("key"): print ("You need a MapQuest API key in order to use this " @@ -21,31 +24,38 @@ def load(context): add_hook("cmd_hook", MessageHook(cmd_geocode, "geocode")) -def help_tiny (): - """Line inserted in the response to the command !help""" - return "The mapquest module" - -def help_full (): +def help_full(): return "!geocode /place/: get coordinate of /place/." def geocode(location): - raw = urlopen("http://open.mapquestapi.com/geocoding/v1/address?key=%s&location=%s" % (CONF.getNode("mapquestapi")["key"], quote(location))) - obj = json.loads(raw.read().decode()) + obj = web.getJSON("http://open.mapquestapi.com/geocoding/v1/address?key=%s&location=%s" % + (CONF.getNode("mapquestapi")["key"], quote(location))) if "results" in obj and "locations" in obj["results"][0]: for loc in obj["results"][0]["locations"]: yield loc + def where(loc): - return re.sub(" +", " ", "%s %s %s %s %s" % (loc["street"], loc["adminArea5"], loc["adminArea4"], loc["adminArea3"], loc["adminArea1"])).strip() + return re.sub(" +", " ", + "{street} {adminArea5} {adminArea4} {adminArea3} " + "{adminArea1}".format(**loc)).strip() + def cmd_geocode(msg): if len(msg.cmds) < 2: raise IRCException("indicate a name") locname = ' '.join(msg.cmds[1:]) - res = Response(channel=msg.channel, nick=msg.nick, nomore="No more geocode", count=" (%s more geocode)") + res = Response(channel=msg.channel, nick=msg.nick, + nomore="No more geocode", count=" (%s more geocode)") + for loc in geocode(locname): - res.append_message("%s is at %s,%s (%s precision)" % (where(loc), loc["latLng"]["lat"], loc["latLng"]["lng"], loc["geocodeQuality"].lower())) + res.append_message("%s is at %s,%s (%s precision)" % + (where(loc), + loc["latLng"]["lat"], + loc["latLng"]["lng"], + loc["geocodeQuality"].lower())) + return res diff --git a/modules/mediawiki.py b/modules/mediawiki.py index 4815f4f..e483a60 100644 --- a/modules/mediawiki.py +++ b/modules/mediawiki.py @@ -5,7 +5,6 @@ import json import re import urllib.parse -import urllib.request from hooks import hook from tools import web @@ -22,8 +21,7 @@ def get_namespaces(site, ssl=False): print_debug(url) # Make the request - raw = urllib.request.urlopen(url) - data = json.loads(raw.read().decode()) + data = web.getJSON(url) namespaces = dict() for ns in data["query"]["namespaces"]: @@ -38,8 +36,7 @@ def get_raw_page(site, term, ssl=False): print_debug(url) # Make the request - raw = urllib.request.urlopen(url) - data = json.loads(raw.read().decode()) + data = web.getJSON(url) for k in data["query"]["pages"]: try: @@ -55,8 +52,7 @@ def get_unwikitextified(site, wikitext, ssl=False): print_debug(url) # Make the request - raw = urllib.request.urlopen(url) - data = json.loads(raw.read().decode()) + data = web.getJSON(url) return data["expandtemplates"]["*"] @@ -129,8 +125,7 @@ def search(site, term, ssl=False): print_debug(url) # Make the request - raw = urllib.request.urlopen(url) - data = json.loads(raw.read().decode()) + data = web.getJSON(url) if data is not None and "query" in data and "search" in data["query"]: for itm in data["query"]["search"]: diff --git a/modules/reddit.py b/modules/reddit.py index ae0e3a2..3b47bf0 100644 --- a/modules/reddit.py +++ b/modules/reddit.py @@ -2,9 +2,9 @@ """Get information about subreddit""" -import json import re -import urllib + +from tools import web nemubotversion = 3.4 @@ -38,16 +38,12 @@ def cmd_subreddit(msg): where = sub.group(1) else: where = "r" - try: - req = urllib.request.Request( - "http://www.reddit.com/%s/%s/about.json" % - (where, sub.group(2)), - headers={'User-Agent': "nemubot v3"}) - raw = urllib.request.urlopen(req, timeout=10) - except urllib.error.HTTPError as e: - raise IRCException("HTTP error occurs: %s %s" % - (e.code, e.reason)) - sbr = json.loads(raw.read().decode()) + + sbr = web.getJSON("http://www.reddit.com/%s/%s/about.json" % + (where, sub.group(2))) + + if sbr is None: + raise IRCException("subreddit not found") if "title" in sbr["data"]: res = Response(channel=msg.channel, diff --git a/modules/sap.py b/modules/sap.py index e0a4775..81eccea 100644 --- a/modules/sap.py +++ b/modules/sap.py @@ -2,9 +2,8 @@ """Find information about an SAP transaction codes""" -import urllib.request -import json import re +import urllib.parse from hooks import hook from tools import web diff --git a/modules/syno.py b/modules/syno.py index 2389021..5747c29 100644 --- a/modules/syno.py +++ b/modules/syno.py @@ -2,10 +2,8 @@ """Find synonyms""" -import json import re from urllib.parse import quote -from urllib.request import urlopen from hooks import hook from tools import web @@ -14,9 +12,11 @@ nemubotversion = 3.4 from more import Response + def help_full(): return "!syno [LANG] <word>: give a list of synonyms for <word>." + def load(context): global lang_binding @@ -55,8 +55,8 @@ def get_french_synos(word): def get_english_synos(key, word): - raw = urlopen("http://words.bighugelabs.com/api/2/%s/%s/json" % (quote(key), quote(word.encode("ISO-8859-1")))) - cnt = json.loads(raw.read().decode()) + cnt = web.getJSON("http://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/translate.py b/modules/translate.py index 7576981..6e0b825 100644 --- a/modules/translate.py +++ b/modules/translate.py @@ -2,12 +2,10 @@ """Translation module""" -import http.client import re -import socket -import json from urllib.parse import quote -from urllib.request import urlopen + +from tools import web nemubotversion = 3.4 @@ -59,11 +57,7 @@ def cmd_translate(msg): langTo = "fr" term = ' '.join(msg.cmds[1:]) - try: - raw = urlopen(URL % (langFrom, langTo, quote(term))) - except: - raise IRCException("invalid request") - wres = json.loads(raw.read().decode()) + wres = web.getJSON(URL % (langFrom, langTo, quote(term))) if "Error" in wres: raise IRCException(wres["Note"]) @@ -87,6 +81,7 @@ def cmd_translate(msg): extract_traslation(ent[i]))) return res + def meaning(entry): ret = list() if "sense" in entry and len(entry["sense"]) > 0: @@ -98,6 +93,7 @@ def meaning(entry): else: return "" + def extract_traslation(entry): ret = list() for i in [ "FirstTranslation", "SecondTranslation", "ThirdTranslation", "FourthTranslation" ]: diff --git a/modules/weather.py b/modules/weather.py index 49c9044..6df1882 100644 --- a/modules/weather.py +++ b/modules/weather.py @@ -3,12 +3,11 @@ """The weather module""" import datetime -import json import re from urllib.parse import quote -from urllib.request import urlopen from hooks import hook +from tools import web import mapquest @@ -16,6 +15,7 @@ nemubotversion = 3.4 from more import Response + def load(context): global DATAS DATAS.setIndex("name", "city") @@ -40,9 +40,11 @@ def help_full (): 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 @@ -62,6 +64,7 @@ def format_wth(wth): int(wth["ozone"]) )) + 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" % ( @@ -77,6 +80,7 @@ def format_forecast_daily(wth): int(wth["ozone"]) )) + def format_timestamp(timestamp, tzname, tzoffset, format="%c"): tz = datetime.timezone(datetime.timedelta(hours=tzoffset), tzname) time = datetime.datetime.fromtimestamp(timestamp, tz=tz) @@ -126,8 +130,7 @@ def treat_coord(msg): def get_json_weather(coords): - raw = urlopen("https://api.forecast.io/forecast/%s/%s,%s" % (CONF.getNode("darkskyapi")["key"], float(coords[0]), float(coords[1])), timeout=10) - wth = json.loads(raw.read().decode()) + wth = web.getJSON("https://api.forecast.io/forecast/%s/%s,%s" % (CONF.getNode("darkskyapi")["key"], float(coords[0]), float(coords[1]))) # First read flags if "darksky-unavailable" in wth["flags"]: @@ -147,6 +150,7 @@ def cmd_coordinates(msg): coords = DATAS.index[j] return Response("Les coordonnées de %s sont %s,%s" % (msg.args[0], coords["lat"], coords["long"]), channel=msg.channel) + def cmd_alert(msg): loc, coords, specific = treat_coord(msg) wth = get_json_weather(coords) @@ -159,6 +163,7 @@ def cmd_alert(msg): return res + def cmd_weather(msg): loc, coords, specific = treat_coord(msg) wth = get_json_weather(coords) @@ -211,6 +216,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") def parseask(msg): res = gps_ask.match(msg.text) diff --git a/modules/ycc.py b/modules/ycc.py index ce14779..bc90cf2 100644 --- a/modules/ycc.py +++ b/modules/ycc.py @@ -5,10 +5,10 @@ import re from urllib.parse import urlparse from urllib.parse import quote -from urllib.request import urlopen from hooks import hook from message import TextMessage +from tools import web nemubotversion = 3.4 @@ -54,11 +54,11 @@ def cmd_ycc(msg): snd_url = "http://ycc.fr/redirection/create/" + quote(url, "/:%@&=?") print_debug(snd_url) - raw = urlopen(snd_url, timeout=10) + page = web.getURLContent(snd_url) if o.netloc == "": - res.append(gen_response(raw.read().decode(), msg, o.scheme)) + res.append(gen_response(page, msg, o.scheme)) else: - res.append(gen_response(raw.read().decode(), msg, o.netloc)) + res.append(gen_response(page, msg, o.netloc)) else: res.append(gen_response(False, msg, url)) return res From bf266dd21fe8a5d94d63d86a773de9b553edd1c7 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 18 Dec 2014 13:02:19 +0100 Subject: [PATCH 224/674] [ycc] Dusting --- modules/ycc.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/modules/ycc.py b/modules/ycc.py index bc90cf2..0a42fc3 100644 --- a/modules/ycc.py +++ b/modules/ycc.py @@ -21,14 +21,24 @@ def help_full(): LAST_URLS = dict() +def reduce(url): + """Ask YCC website to reduce given URL + + Argument: + url -- the URL to reduce + """ + + snd_url = "http://ycc.fr/redirection/create/" + quote(url, "/:%@&=?") + print_debug(snd_url) + return web.getURLContent(snd_url) + + def gen_response(res, msg, srv): if res is None: - raise IRCException("la situation est embarassante, il semblerait que YCC soit down :(") - elif isinstance(res, str): + raise IRCException("mauvaise URL : %s" % srv) + else: return TextMessage("URL pour %s : %s" % (srv, res), server=None, to=msg.to_response) - else: - raise IRCException("mauvaise URL : %s" % srv) @hook("cmd_hook", "ycc") @@ -51,16 +61,13 @@ def cmd_ycc(msg): for url in minify: o = urlparse(url, "http") if o.scheme != "": - snd_url = "http://ycc.fr/redirection/create/" + quote(url, - "/:%@&=?") - print_debug(snd_url) - page = web.getURLContent(snd_url) + minief_url = reduce(url) if o.netloc == "": - res.append(gen_response(page, msg, o.scheme)) + res.append(gen_response(minief_url, msg, o.scheme)) else: - res.append(gen_response(page, msg, o.netloc)) + res.append(gen_response(minief_url, msg, o.netloc)) else: - res.append(gen_response(False, msg, url)) + res.append(gen_response(None, msg, url)) return res From c691450111ed16e1be4dfa1aa64f68fb1760e448 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 19 Dec 2014 07:27:27 +0100 Subject: [PATCH 225/674] [tpb] Give working magnet link --- modules/tpb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/tpb.py b/modules/tpb.py index f6e38a9..4cc1187 100644 --- a/modules/tpb.py +++ b/modules/tpb.py @@ -38,6 +38,6 @@ def cmd_tpb(msg): for t in torrents: t["sizeH"] = human.size(t["size"]) t["dateH"] = datetime.fromtimestamp(t["date"]).strftime('%Y-%m-%d %H:%M:%S') - res.append_message("\x03\x02{title}\x03\x02 in {category}, {sizeH}; added at {dateH}; magnet: {magnet}; id: {id}".format(**t)) + res.append_message("\x03\x02{title}\x03\x02 in {category}, {sizeH}; added at {dateH}; id: {id}; magnet:?xt=urn:btih:{magnet}&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=udp%3A%2F%2Ftracker.publicbt.com%3A80&tr=udp%3A%2F%2Ftracker.istole.it%3A6969&tr=udp%3A%2F%2Fopen.demonii.com%3A1337".format(**t)) return res From 463faed6978e55d0e0a3ae1715c3740069742d52 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 20 Dec 2014 08:26:32 +0100 Subject: [PATCH 226/674] [web] new maximal downloaded size: 512k (old: 200k) --- tools/web.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/web.py b/tools/web.py index 8f522d6..576fb4d 100644 --- a/tools/web.py +++ b/tools/web.py @@ -99,10 +99,10 @@ def getURLContent(url, timeout=15): try: res = conn.getresponse() - size = int(res.getheader("Content-Length", 200000)) + size = int(res.getheader("Content-Length", 524288)) cntype = res.getheader("Content-Type") - if size > 200000 or (cntype is not None and cntype[:4] != "text" and cntype[:4] != "appl"): + if size > 524288 or (cntype is not None and cntype[:4] != "text" and cntype[:4] != "appl"): return None data = res.read(size) From 7805f27458e71660b893918062e72f40b7c37eef Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 29 Dec 2014 07:23:11 +0100 Subject: [PATCH 227/674] Modify importer to work with Python 3.3 and above --- bot.py | 2 +- importer.py | 88 +++++++++++------------------------------------------ 2 files changed, 19 insertions(+), 71 deletions(-) diff --git a/bot.py b/bot.py index 57c0ac4..f99344c 100644 --- a/bot.py +++ b/bot.py @@ -26,7 +26,7 @@ import threading import time import uuid -__version__ = '3.4.dev3' +__version__ = '3.4.dev4' __author__ = 'nemunaire' from consumer import Consumer, EventConsumer, MessageConsumer diff --git a/importer.py b/importer.py index bfb6d34..3122bd9 100644 --- a/importer.py +++ b/importer.py @@ -18,7 +18,7 @@ from distutils.version import LooseVersion from importlib.abc import Finder -from importlib.abc import SourceLoader +from importlib.machinery import SourceFileLoader import imp import logging import os @@ -44,94 +44,43 @@ class ModuleFinder(Finder): if path is None: for mpath in self.context.modules_paths: # print ("looking for", fullname, "in", mpath) - if (os.path.isfile(os.path.join(mpath, fullname + ".py")) or - os.path.isfile(os.path.join(os.path.join(mpath, fullname), "__init__.py"))): - return ModuleLoader(self.context, self.prompt, - fullname, mpath) + if os.path.isfile(os.path.join(mpath, fullname + ".py")): + return ModuleLoader(self.context, self.prompt, fullname, + os.path.join(mpath, fullname + ".py")) + elif os.path.isfile(os.path.join(os.path.join(mpath, fullname), "__init__.py")): + return ModuleLoader(self.context, self.prompt, fullname, + os.path.join( + os.path.join(mpath, fullname), + "__init__.py")) # print ("not found") return None -class ModuleLoader(SourceLoader): +class ModuleLoader(SourceFileLoader): + def __init__(self, context, prompt, fullname, path): self.context = context self.prompt = prompt - self.name = fullname - if self.name in self.context.modules_configuration: - self.config = self.context.modules_configuration[self.name] + if fullname in self.context.modules_configuration: + self.config = self.context.modules_configuration[fullname] else: self.config = None - if os.path.isfile(os.path.join(path, fullname + ".py")): - self.source_path = os.path.join(path, self.name + ".py") - self.package = False - self.mpath = path - elif os.path.isfile(os.path.join(os.path.join(path, fullname), "__init__.py")): - self.source_path = os.path.join(os.path.join(path, self.name), "__init__.py") - self.package = True - self.mpath = path + self.name + os.sep - else: - raise ImportError + SourceFileLoader.__init__(self, fullname, path) - def get_filename(self, fullname): - """Return the path to the source file as found by the finder.""" - return self.source_path - - def get_data(self, path): - """Return the data from path as raw bytes.""" - with open(path, 'rb') as file: - return file.read() - - def path_mtime(self, path): - st = os.stat(path) - return int(st.st_mtime) - - def set_data(self, path, data): - """Write bytes data to a file.""" - parent, filename = os.path.split(path) - path_parts = [] - # Figure out what directories are missing. - while parent and not os.path.isdir(parent): - parent, part = os.path.split(parent) - path_parts.append(part) - # Create needed directories. - for part in reversed(path_parts): - parent = os.path.join(parent, part) - try: - os.mkdir(parent) - except FileExistsError: - # Probably another Python process already created the dir. - continue - except PermissionError: - # If can't get proper access, then just forget about writing - # the data. - return - try: - with open(path, 'wb') as file: - file.write(data) - except (PermissionError, FileExistsError): - pass - - def get_code(self, fullname): - return SourceLoader.get_code(self, fullname) - - def get_source(self, fullname): - return SourceLoader.get_source(self, fullname) - - def is_package(self, fullname): - return self.package def load_module(self, fullname): - module = self._load_module(fullname, sourceless=True) + module = SourceFileLoader.load_module(self, fullname) # Check that is a valid nemubot module if not hasattr(module, "nemubotversion"): - raise ImportError("Module `%s' is not a nemubot module."%self.name) + raise ImportError("Module `%s' is not a nemubot module." % + fullname) # Check module version if LooseVersion(__version__) < LooseVersion(str(module.nemubotversion)): raise ImportError("Module `%s' is not compatible with this " - "version." % self.name) + "version." % fullname) # Set module common functions and data module.__LOADED__ = True @@ -180,7 +129,6 @@ class ModuleLoader(SourceLoader): module.REGISTERED_HOOKS = list() module.REGISTERED_EVENTS = list() module.DEBUG = self.context.verbosity > 0 - module.DIR = self.mpath module.print = prnt module.print_debug = prnt_dbg module.send_response = send_response From 192a26b5eab4843b08ef086c18354e825e3bd53b Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 29 Dec 2014 07:32:15 +0100 Subject: [PATCH 228/674] Update README: add requirements part --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index e021df2..c0d796e 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,16 @@ An extremely modulable IRC bot, built around XML configuration files! + +## Requirements + +*nemubot* requires at least Python 3.3 to work. + +Some modules (like `cve` or `nextstop`) require the +[BeautifulSoup module](http://www.crummy.com/software/BeautifulSoup/), +but the core and framework has no dependency. + + ## Documentation Have a look to the wiki at https://github.com/nemunaire/nemubot/wiki From 466ec31be7cd8ff19f2923c1cb6c8f6cfa052045 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 28 Dec 2014 17:24:56 +0100 Subject: [PATCH 229/674] [nextstop] Dusting --- modules/nextstop/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/nextstop/__init__.py b/modules/nextstop/__init__.py index 35227e7..f53aec4 100644 --- a/modules/nextstop/__init__.py +++ b/modules/nextstop/__init__.py @@ -3,6 +3,7 @@ """Informe les usagers des prochains passages des transports en communs de la RATP""" from hooks import hook +from more import Response nemubotversion = 3.4 From 17bbb000adf30ae46ecdb785750b786b84c58f79 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 29 Dec 2014 07:50:27 +0100 Subject: [PATCH 230/674] [networking] Oops, watchWebsite wasn't working; fixed --- modules/networking/__init__.py | 5 +++-- modules/networking/watchWebsite.py | 27 +++++++++++++++++---------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/modules/networking/__init__.py b/modules/networking/__init__.py index 10558f7..3371a09 100644 --- a/modules/networking/__init__.py +++ b/modules/networking/__init__.py @@ -19,6 +19,7 @@ def load(context): mod.IRCException = IRCException mod.ModuleEvent = ModuleEvent mod.add_event = add_event + mod.del_event = del_event mod.save = save mod.print = print mod.print_debug = print_debug @@ -119,7 +120,7 @@ def cmd_watch(msg, diffType="diff"): if len(msg.cmds) <= 1: raise IRCException("indicate an URL to watch!") - return watchWebsite.add_site(msg.cmds[1]) + return watchWebsite.add_site(msg.cmds[1], msg.frm, msg.channel, msg.server, diffType) @hook("cmd_hook", "unwatch") @@ -127,4 +128,4 @@ def cmd_unwatch(msg): if len(msg.cmds) <= 1: raise IRCException("which URL should I stop watching?") - return watchWebsite.add_site(msg.cmds[1]) + return watchWebsite.del_site(msg.cmds[1], msg.frm, msg.channel, msg.frm_owner) diff --git a/modules/networking/watchWebsite.py b/modules/networking/watchWebsite.py index 074522b..7415672 100644 --- a/modules/networking/watchWebsite.py +++ b/modules/networking/watchWebsite.py @@ -6,15 +6,22 @@ from urllib.parse import urlparse from hooks import hook from more import Response +from tools.xmlparser.node import ModuleState nemubotversion = 3.4 from .atom import Atom from . import page +DATAS = None -def load(DATAS): + +def load(datas): """Register events on watched website""" + + global DATAS + DATAS = datas + DATAS.setIndex("url", "watch") for site in DATAS.getNodes("watch"): if site.hasNode("alert"): @@ -24,7 +31,7 @@ def load(DATAS): #DATAS.delChild(site) -def del_site(url): +def del_site(url, nick, channel, frm_owner): """Remove a site from watching list Argument: @@ -35,8 +42,8 @@ def del_site(url): if o.scheme != "" and url in DATAS.index: site = DATAS.index[url] for a in site.getNodes("alert"): - if a["channel"] == msg.channel: - if not (msg.frm == a["nick"] or msg.frm_owner): + if a["channel"] == channel: + if not (nick == a["nick"] or frm_owner): raise IRCException("you cannot unwatch this URL.") site.delChild(a) if not site.hasNode("alert"): @@ -44,11 +51,11 @@ def del_site(url): DATAS.delChild(site) save() return Response("I don't watch this URL anymore.", - channel=msg.channel, nick=msg.nick) + channel=channel, nick=nick) raise IRCException("I didn't watch this URL!") -def add_site(url): +def add_site(url, nick, channel, server, diffType="diff"): """Add a site to watching list Argument: @@ -60,9 +67,9 @@ def add_site(url): raise IRCException("sorry, I can't watch this URL :(") alert = ModuleState("alert") - alert["nick"] = msg.nick - alert["server"] = msg.server - alert["channel"] = msg.channel + alert["nick"] = nick + alert["server"] = server + alert["channel"] = channel alert["message"] = "{url} just changed!" if url not in DATAS.index: @@ -77,7 +84,7 @@ def add_site(url): DATAS.index[url].addChild(alert) save() - return Response(channel=msg.channel, nick=msg.nick, + return Response(channel=channel, nick=nick, message="this site is now under my supervision.") From 0d4130b391667d60de7a2defbfa2e5c360076401 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 30 Dec 2014 21:22:56 +0100 Subject: [PATCH 231/674] [events] ids don't have to be saved --- modules/events.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/events.py b/modules/events.py index 5679fd0..ce2dae3 100644 --- a/modules/events.py +++ b/modules/events.py @@ -35,7 +35,7 @@ def load(context): event._end = DATAS.index[evt].getDate("end") idt = add_event(event) if idt is not None: - DATAS.index[evt]["id"] = idt + DATAS.index[evt]["_id"] = idt def fini(d, strend): @@ -104,7 +104,7 @@ def start_countdown(msg): else: strnd["end"] = datetime(now.year, now.month, now.day + 1, hou, minu, sec, timezone.utc) evt._end = strnd.getDate("end") - strnd["id"] = add_event(evt) + strnd["_id"] = add_event(evt) except: DATAS.delChild(strnd) raise IRCException("Mauvais format de date pour l'événement %s. Il n'a pas été créé." % msg.cmds[1]) @@ -127,7 +127,7 @@ def start_countdown(msg): evt._end = strnd.getDate("end") eid = add_event(evt) if eid is not None: - strnd["id"] = eid + strnd["_id"] = eid save() if "end" in strnd: @@ -149,7 +149,7 @@ def end_countdown(msg): if msg.cmds[1] in DATAS.index: if DATAS.index[msg.cmds[1]]["proprio"] == msg.nick or (msg.cmds[0] == "forceend" and msg.frm_owner): duration = countdown(msg.date - DATAS.index[msg.cmds[1]].getDate("start")) - del_event(DATAS.index[msg.cmds[1]]["id"]) + del_event(DATAS.index[msg.cmds[1]]["_id"]) DATAS.delChild(DATAS.index[msg.cmds[1]]) save() return Response("%s a duré %s." % (msg.cmds[1], duration), From 116c81f5b202360a410cd8841aacd00b7391d614 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 31 Dec 2014 07:54:05 +0100 Subject: [PATCH 232/674] Use ipaddress module to store IP --- bot.py | 3 ++- server/IRC.py | 4 ++-- tools/__init__.py | 16 ---------------- 3 files changed, 4 insertions(+), 19 deletions(-) diff --git a/bot.py b/bot.py index f99344c..1f74b8d 100644 --- a/bot.py +++ b/bot.py @@ -18,6 +18,7 @@ from datetime import datetime, timedelta, timezone import imp +import ipaddress import logging from queue import Queue import re @@ -59,7 +60,7 @@ class Bot(threading.Thread): self.verbosity = verbosity # External IP for accessing this bot - self.ip = ip + self.ip = ipaddress.ip_address(ip) # Context paths self.modules_paths = modules_paths diff --git a/server/IRC.py b/server/IRC.py index bbe6334..ae01a74 100644 --- a/server/IRC.py +++ b/server/IRC.py @@ -18,6 +18,7 @@ import calendar from datetime import datetime, timezone +import ipaddress import re import time import shlex @@ -26,7 +27,6 @@ from channel import Channel import message from message.printer.IRC import IRC as IRCPrinter from server.socket import SocketServer -import tools class IRC(SocketServer): @@ -82,7 +82,7 @@ class IRC(SocketServer): def _ctcp_dcc(msg, cmds): """Response to DCC CTCP message""" try: - ip = tools.toIP(int(cmds[3])) + ip = ipaddress.ip_address(int(cmds[3])) port = int(cmds[4]) conn = DCC(srv, msg.sender) except: diff --git a/tools/__init__.py b/tools/__init__.py index d2004b5..036b9ad 100644 --- a/tools/__init__.py +++ b/tools/__init__.py @@ -19,22 +19,6 @@ import imp -def intToIP(n): - ip = "" - for i in range(0, 4): - mod = n % 256 - ip = "%d.%s" % (mod, ip) - n = (n - mod) / 256 - return ip[:len(ip) - 1] - - -def ipToInt(ip): - sum = 0 - for b in ip.split("."): - sum = 256 * sum + int(b) - return sum - - def reload(): import tools.countdown imp.reload(tools.countdown) From 1a04a107acce2aefcb5584c8fbf9cd39b3072110 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 1 Jan 2015 19:57:44 +0100 Subject: [PATCH 233/674] web tools: handle no route to host error --- tools/web.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/web.py b/tools/web.py index 576fb4d..ed88010 100644 --- a/tools/web.py +++ b/tools/web.py @@ -92,6 +92,8 @@ def getURLContent(url, timeout=15): conn.request("GET", o.path, None, {"User-agent": "Nemubot v3"}) except socket.timeout: return None + except OSError: # [Errno 113] No route to host + return None except socket.gaierror: print ("<tools.web> Unable to receive page %s on %s from %s." % (o.path, o.netloc, url)) From 0d21b1fa2cb7c428e2904fd9068536988879cd0f Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 1 Jan 2015 21:46:57 +0100 Subject: [PATCH 234/674] Indicate full version in UserAgent HTTP header --- modules/networking/page.py | 4 +++- tools/web.py | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/modules/networking/page.py b/modules/networking/page.py index 0c0e24f..df561e0 100644 --- a/modules/networking/page.py +++ b/modules/networking/page.py @@ -4,6 +4,7 @@ import subprocess import tempfile import urllib +from bot import __version__ from tools import web @@ -27,7 +28,8 @@ def headers(url): else: conn = http.client.HTTPSConnection(o.hostname, port=o.port, timeout=5) try: - conn.request("HEAD", o.path, None, {"User-agent": "Nemubot v3"}) + conn.request("HEAD", o.path, None, {"User-agent": + "Nemubot v%s" % __version__}) except socket.timeout: raise IRCException("request timeout") except socket.gaierror: diff --git a/tools/web.py b/tools/web.py index ed88010..ef9f0cc 100644 --- a/tools/web.py +++ b/tools/web.py @@ -25,6 +25,7 @@ from urllib.parse import quote from urllib.parse import urlparse from urllib.request import urlopen +from bot import __version__ from exception import IRCException from tools.xmlparser import parse_string @@ -87,9 +88,10 @@ def getURLContent(url, timeout=15): try: if o.query != '': conn.request("GET", o.path + "?" + o.query, - None, {"User-agent": "Nemubot v3"}) + None, {"User-agent": "Nemubot v%s" % __version__}) else: - conn.request("GET", o.path, None, {"User-agent": "Nemubot v3"}) + conn.request("GET", o.path, None, {"User-agent": + "Nemubot v%s" % __version__}) except socket.timeout: return None except OSError: # [Errno 113] No route to host From fd6d9288f7e20858df191807e02e22c04bb9e651 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 1 Jan 2015 23:17:02 +0100 Subject: [PATCH 235/674] Rework prompt: add exception classes for errors and reload/quit --- nemubot.py | 9 ++++++++- prompt/__init__.py | 29 ++++++++++++++++++----------- prompt/builtins.py | 20 +++++++++++++------- prompt/error.py | 23 +++++++++++++++++++++++ prompt/reset.py | 23 +++++++++++++++++++++++ server/__init__.py | 4 ++++ 6 files changed, 89 insertions(+), 19 deletions(-) create mode 100644 prompt/error.py create mode 100644 prompt/reset.py diff --git a/nemubot.py b/nemubot.py index 3718d73..780832d 100755 --- a/nemubot.py +++ b/nemubot.py @@ -26,6 +26,7 @@ import sys import bot import prompt from prompt.builtins import load_file +from prompt.reset import PromptReset import importer if __name__ == "__main__": @@ -116,7 +117,13 @@ if __name__ == "__main__": print ("Nemubot v%s ready, my PID is %i!" % (bot.__version__, os.getpid())) context.start() - while prmpt.run(context): + while True: + try: + prmpt.run(context) + except PromptReset as e: + if e.type == "quit": + break + try: # Reload context imp.reload(bot) diff --git a/prompt/__init__.py b/prompt/__init__.py index 7408088..2cc90bc 100644 --- a/prompt/__init__.py +++ b/prompt/__init__.py @@ -23,6 +23,8 @@ import shlex import sys import traceback +from .error import PromptError +from .reset import PromptReset from . import builtins @@ -30,12 +32,14 @@ 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 d, t, c, p: call(d, t, c, p), data) + 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 @@ -77,13 +81,12 @@ class Prompt: """ if toks[0] in builtins.CAPS: - return builtins.CAPS[toks[0]](toks, context, self) + self.lastretcode = builtins.CAPS[toks[0]](toks, context, self) elif toks[0] in self.HOOKS_CAPS: - f, d = self.HOOKS_CAPS[toks[0]] - return f(d, toks, context, self) + self.lastretcode = self.HOOKS_CAPS[toks[0]](toks, context) else: print("Unknown command: `%s'" % toks[0]) - return "" + self.lastretcode = 127 def getPS1(self): """Get the PS1 associated to the selected server""" @@ -99,14 +102,19 @@ class Prompt: context -- current bot context """ - ret = "" - while ret != "quit" and ret != "reset" and ret != "refresh": + while True: # Stopped by exception try: - line = input("\033[0;33m%s§\033[0m " % self.getPS1()) + 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: - ret = self.exec_cmd(toks, context) + 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, @@ -114,9 +122,8 @@ class Prompt: except KeyboardInterrupt: print("") except EOFError: - ret = "quit" print("quit") - return ret != "quit" + raise PromptReset("quit") def hotswap(bak): diff --git a/prompt/builtins.py b/prompt/builtins.py index 28aa4ab..30e250d 100644 --- a/prompt/builtins.py +++ b/prompt/builtins.py @@ -18,6 +18,7 @@ import logging +from .reset import PromptReset from tools.config import load_file logger = logging.getLogger("nemubot.prompt.builtins") @@ -26,12 +27,10 @@ logger = logging.getLogger("nemubot.prompt.builtins") def end(toks, context, prompt): """Quit the prompt for reload or exit""" if toks[0] == "refresh": - return "refresh" + raise PromptReset("refresh") elif toks[0] == "reset": - return "reset" - else: - context.quit() - return "quit" + raise PromptReset("reset") + raise PromptReset("quit") def liste(toks, context, prompt): @@ -58,8 +57,11 @@ def liste(toks, 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): @@ -69,7 +71,7 @@ def load(toks, context, prompt): load_file(filename, context) else: print ("Not enough arguments. `load' takes a filename.") - return + return 1 def select(toks, context, prompt): @@ -80,9 +82,9 @@ def select(toks, context, prompt): prompt.selectedServer = context.servers[toks[1]] else: print ("select: server `%s' not found." % toks[1]) + return 1 else: prompt.selectedServer = None - return def unload(toks, context, prompt): @@ -96,8 +98,10 @@ def unload(toks, context, prompt): 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): @@ -112,8 +116,10 @@ def debug(toks, context, prompt): 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 diff --git a/prompt/error.py b/prompt/error.py new file mode 100644 index 0000000..4ef7ede --- /dev/null +++ b/prompt/error.py @@ -0,0 +1,23 @@ +# coding=utf-8 + +# Nemubot is a modulable IRC bot, built around XML configuration files. +# Copyright (C) 2012 Mercier Pierre-Olivier +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +class PromptError(Exception): + + def __init__(self, message): + super(PromptError, self).__init__(message) + self.message = message diff --git a/prompt/reset.py b/prompt/reset.py new file mode 100644 index 0000000..d6bec5f --- /dev/null +++ b/prompt/reset.py @@ -0,0 +1,23 @@ +# coding=utf-8 + +# Nemubot is a modulable IRC bot, built around XML configuration files. +# Copyright (C) 2012 Mercier Pierre-Olivier +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +class PromptReset(Exception): + + def __init__(self, type): + super(PromptReset, self).__init__("Prompt reset asked") + self.type = type diff --git a/server/__init__.py b/server/__init__.py index 3ae04f2..92f2989 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -69,6 +69,8 @@ class AbstractServer(io.IOBase): if not hasattr(self, "_open") or self._open(): _rlist.append(self) _xlist.append(self) + return True + return False def close(self): @@ -82,6 +84,8 @@ class AbstractServer(io.IOBase): _wlist.remove(self) if self in _xlist: _xlist.remove(self) + return True + return False # Writes From 8aebeb634657736af8f6bdd073b51a2d7e5ca817 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 2 Jan 2015 20:44:56 +0100 Subject: [PATCH 236/674] [cmd_server] rework due to previous prompt rework --- modules/cmd_server.py | 217 ++++++++++++++---------------------------- 1 file changed, 73 insertions(+), 144 deletions(-) diff --git a/modules/cmd_server.py b/modules/cmd_server.py index 964dbf8..449f4da 100644 --- a/modules/cmd_server.py +++ b/modules/cmd_server.py @@ -19,6 +19,7 @@ import traceback import sys +from prompt.error import PromptError from hooks import hook from message import TextMessage from networkbot import NetworkBot @@ -27,209 +28,137 @@ nemubotversion = 3.4 NODATA = True -def getserver(toks, context, prompt): - """Choose the server in toks or prompt""" - if len(toks) > 1 and toks[0] in context.servers: - return (context.servers[toks[0]], toks[1:]) - elif prompt.selectedServer is not None: - return (prompt.selectedServer, toks) +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: - return (None, toks) + raise PromptError("Please SELECT a server or give its name in argument.") @hook("prompt_cmd", "close") -def close(data, toks, context, prompt): +def close(toks, context, **kwargs): """Disconnect and forget (remove from the servers list) the server""" - if len(toks) > 1: - for s in toks[1:]: - if s in servers: - context.servers[s].close() - del context.servers[s] - else: - print ("close: server `%s' not found." % s) - elif prompt.selectedServer is not None: - prompt.selectedServer.close() - del prompt.servers[selectedServer.id] - prompt.selectedServer = None - return + 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(data, toks, context, prompt): +def connect(toks, **kwargs): """Make the connexion to a server""" - if len(toks) > 1: - for s in toks[1:]: - if s in context.servers: - context.servers[s].open() - else: - print ("connect: server `%s' not found." % s) + srv = getserver(toks, mandatory=True, **kwargs) - elif prompt.selectedServer is not None: - prompt.selectedServer.open() - else: - print (" Please SELECT a server or give its name in argument.") + return not srv.open() @hook("prompt_cmd", "disconnect") -def disconnect(data, toks, context, prompt): +def disconnect(toks, **kwargs): """Close the connection to a server""" - if len(toks) > 1: - for s in toks[1:]: - if s in context.servers: - if not context.servers[s].close(): - print ("disconnect: server `%s' already disconnected." % s) - else: - print ("disconnect: server `%s' not found." % s) - elif prompt.selectedServer is not None: - if not prompt.selectedServer.close(): - print ("disconnect: server `%s' already disconnected." - % prompt.selectedServer.id) - else: - print (" Please SELECT a server or give its name in argument.") + srv = getserver(toks, mandatory=True, **kwargs) + + return not srv.close() @hook("prompt_cmd", "discover") -def discover(data, toks, context, prompt): +def discover(toks, context, **kwargs): """Discover a new bot on a server""" - (srv, toks) = getserver(toks, context, prompt) - if srv is not None: - for name in toks[1:]: - if "!" in name: - bot = context.add_networkbot(srv, name) - bot.connect() - else: - print (" %s is not a valid fullname, for example: " - "nemubot!nemubotV3@bot.nemunai.re") - else: - print (" Please SELECT a server or give its name in first argument.") + srv = getserver(toks, context=context, mandatory=True, **kwargs) - -@hook("prompt_cmd", "hotswap") -def hotswap(data, toks, context, prompt): - """Reload a server class""" - if len(toks) > 1: - print ("hotswap: apply only on selected server") - elif prompt.selectedServer is not None: - del context.servers[prompt.selectedServer.id] - srv = server.Server(selectedServer.node, selectedServer.nick, - selectedServer.owner, selectedServer.realname, - selectedServer.s) - context.servers[srv.id] = srv - prompt.selectedServer.kill() - prompt.selectedServer = srv - prompt.selectedServer.start() + if len(toks) > 1 and "!" in toks[1]: + bot = context.add_networkbot(srv, name) + return not bot.connect() else: - print (" Please SELECT a server or give its name in argument.") + 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(data, toks, context, prompt): +def join(toks, **kwargs): """Join or leave a channel""" - rd = 1 - if len(toks) <= rd: - print ("%s: not enough arguments." % toks[0]) - return + srv = getserver(toks, mandatory=True, **kwargs) - if toks[rd] in context.servers: - srv = context.servers[toks[rd]] - rd += 1 - elif prompt.selectedServer is not None: - srv = prompt.selectedServer - else: - print (" Please SELECT a server or give its name in argument.") - return - - if len(toks) <= rd: + if len(toks) <= 2: print("%s: not enough arguments." % toks[0]) - return + return 1 if toks[0] == "join": - if len(toks) > rd + 1: - srv.write("JOIN %s %s" % (toks[rd], toks[rd + 1])) + if len(toks) > 2: + srv.write("JOIN %s %s" % (toks[1], toks[2])) else: - srv.write("JOIN %s" % toks[rd]) + srv.write("JOIN %s" % toks[1]) + elif toks[0] == "leave" or toks[0] == "part": - srv.write("PART %s" % toks[rd]) - return + 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(data, toks, context, prompt): +def save_mod(toks, context, **kwargs): """Force save module data""" if len(toks) < 2: - print ("save: not enough arguments.") - return + 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) + print("save: module `%s´ saved successfully" % mod) else: - print ("save: no module named `%s´" % mod) - return + wrn += 1 + print("save: no module named `%s´" % mod) + return wrn @hook("prompt_cmd", "send") -def send(data, toks, context, prompt): +def send(toks, **kwargs): """Send a message on a channel""" - rd = 1 - if len(toks) <= rd: - print ("send: not enough arguments.") - return - - if toks[rd] in context.servers: - srv = context.servers[toks[rd]] - rd += 1 - elif prompt.selectedServer is not None: - srv = prompt.selectedServer - else: - print (" Please SELECT a server or give its name in argument.") - return - - if len(toks) <= rd: - print ("send: not enough arguments.") - return + srv = getserver(toks, mandatory=True, **kwargs) # Check the server is connected if not srv.connected: print ("send: server `%s' not connected." % srv.id) - return + return 2 - if toks[rd] in srv.channels: - chan = toks[rd] - rd += 1 - else: - print ("send: channel `%s' not authorized in server `%s'." - % (toks[rd], srv.id)) - return - - if len(toks) <= rd: + if len(toks) <= 3: print ("send: not enough arguments.") - return + return 1 - srv.send_response(TextMessage(" ".join(toks[rd:]), server=None, to=[chan])) - return "done" + if toks[1] not in srv.channels: + print ("send: channel `%s' not authorized in server `%s'." + % (toks[1], srv.id)) + return 3 + + srv.send_response(TextMessage(" ".join(toks[2:]), server=None, + to=[toks[1]])) + return 0 @hook("prompt_cmd", "zap") -def zap(data, toks, context, prompt): +def zap(toks, **kwargs): """Hard change connexion state""" - if len(toks) > 1: - for s in toks[1:]: - if s in context.servers: - context.servers[s].connected = not context.servers[s].connected - else: - print ("zap: server `%s' not found." % s) - elif prompt.selectedServer is not None: - prompt.selectedServer.connected = not prompt.selectedServer.connected - else: - print (" Please SELECT a server or give its name in argument.") + srv = getserver(toks, mandatory=True, **kwargs) + + srv.connected = not srv.connected @hook("prompt_cmd", "top") -def top(data, toks, context, prompt): +def top(toks, context, **kwargs): """Display consumers load information""" print("Queue size: %d, %d thread(s) running (counter: %d)" % (context.cnsr_queue.qsize(), @@ -250,7 +179,7 @@ def top(data, toks, context, prompt): @hook("prompt_cmd", "netstat") -def netstat(data, toks, context, prompt): +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)) From 41f7dc24567d74a8ea0f677ee87b81a7e08ec0e7 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 3 Jan 2015 18:49:01 +0100 Subject: [PATCH 237/674] [wip] move files in order to have a clean directory structure --- nemubot.py => bin/nemubot | 0 bot.py => nemubot/__init__.py | 0 channel.py => nemubot/channel.py | 0 consumer.py => nemubot/consumer.py | 0 event.py => nemubot/event.py | 0 exception.py => nemubot/exception.py | 0 {hooks => nemubot/hooks}/__init__.py | 0 {hooks => nemubot/hooks}/manager.py | 0 {hooks => nemubot/hooks}/messagehook.py | 0 importer.py => nemubot/importer.py | 0 {message => nemubot/message}/__init__.py | 0 {message => nemubot/message}/printer/IRC.py | 0 {message => nemubot/message}/printer/__init__.py | 0 {message => nemubot/message}/visitor.py | 0 networkbot.py => nemubot/networkbot.py | 0 {prompt => nemubot/prompt}/__init__.py | 0 {prompt => nemubot/prompt}/builtins.py | 0 {prompt => nemubot/prompt}/error.py | 0 {prompt => nemubot/prompt}/reset.py | 0 {server => nemubot/server}/DCC.py | 0 {server => nemubot/server}/IRC.py | 0 {server => nemubot/server}/__init__.py | 0 {server => nemubot/server}/socket.py | 0 {tools => nemubot/tools}/__init__.py | 0 {tools => nemubot/tools}/config.py | 0 {tools => nemubot/tools}/countdown.py | 0 {tools => nemubot/tools}/date.py | 0 {tools => nemubot/tools}/human.py | 0 {tools => nemubot/tools}/web.py | 0 {tools => nemubot/tools}/xmlparser/__init__.py | 0 {tools => nemubot/tools}/xmlparser/node.py | 0 31 files changed, 0 insertions(+), 0 deletions(-) rename nemubot.py => bin/nemubot (100%) rename bot.py => nemubot/__init__.py (100%) rename channel.py => nemubot/channel.py (100%) rename consumer.py => nemubot/consumer.py (100%) rename event.py => nemubot/event.py (100%) rename exception.py => nemubot/exception.py (100%) rename {hooks => nemubot/hooks}/__init__.py (100%) rename {hooks => nemubot/hooks}/manager.py (100%) rename {hooks => nemubot/hooks}/messagehook.py (100%) rename importer.py => nemubot/importer.py (100%) rename {message => nemubot/message}/__init__.py (100%) rename {message => nemubot/message}/printer/IRC.py (100%) rename {message => nemubot/message}/printer/__init__.py (100%) rename {message => nemubot/message}/visitor.py (100%) rename networkbot.py => nemubot/networkbot.py (100%) rename {prompt => nemubot/prompt}/__init__.py (100%) rename {prompt => nemubot/prompt}/builtins.py (100%) rename {prompt => nemubot/prompt}/error.py (100%) rename {prompt => nemubot/prompt}/reset.py (100%) rename {server => nemubot/server}/DCC.py (100%) rename {server => nemubot/server}/IRC.py (100%) rename {server => nemubot/server}/__init__.py (100%) rename {server => nemubot/server}/socket.py (100%) rename {tools => nemubot/tools}/__init__.py (100%) rename {tools => nemubot/tools}/config.py (100%) rename {tools => nemubot/tools}/countdown.py (100%) rename {tools => nemubot/tools}/date.py (100%) rename {tools => nemubot/tools}/human.py (100%) rename {tools => nemubot/tools}/web.py (100%) rename {tools => nemubot/tools}/xmlparser/__init__.py (100%) rename {tools => nemubot/tools}/xmlparser/node.py (100%) diff --git a/nemubot.py b/bin/nemubot similarity index 100% rename from nemubot.py rename to bin/nemubot diff --git a/bot.py b/nemubot/__init__.py similarity index 100% rename from bot.py rename to nemubot/__init__.py diff --git a/channel.py b/nemubot/channel.py similarity index 100% rename from channel.py rename to nemubot/channel.py diff --git a/consumer.py b/nemubot/consumer.py similarity index 100% rename from consumer.py rename to nemubot/consumer.py diff --git a/event.py b/nemubot/event.py similarity index 100% rename from event.py rename to nemubot/event.py diff --git a/exception.py b/nemubot/exception.py similarity index 100% rename from exception.py rename to nemubot/exception.py diff --git a/hooks/__init__.py b/nemubot/hooks/__init__.py similarity index 100% rename from hooks/__init__.py rename to nemubot/hooks/__init__.py diff --git a/hooks/manager.py b/nemubot/hooks/manager.py similarity index 100% rename from hooks/manager.py rename to nemubot/hooks/manager.py diff --git a/hooks/messagehook.py b/nemubot/hooks/messagehook.py similarity index 100% rename from hooks/messagehook.py rename to nemubot/hooks/messagehook.py diff --git a/importer.py b/nemubot/importer.py similarity index 100% rename from importer.py rename to nemubot/importer.py diff --git a/message/__init__.py b/nemubot/message/__init__.py similarity index 100% rename from message/__init__.py rename to nemubot/message/__init__.py diff --git a/message/printer/IRC.py b/nemubot/message/printer/IRC.py similarity index 100% rename from message/printer/IRC.py rename to nemubot/message/printer/IRC.py diff --git a/message/printer/__init__.py b/nemubot/message/printer/__init__.py similarity index 100% rename from message/printer/__init__.py rename to nemubot/message/printer/__init__.py diff --git a/message/visitor.py b/nemubot/message/visitor.py similarity index 100% rename from message/visitor.py rename to nemubot/message/visitor.py diff --git a/networkbot.py b/nemubot/networkbot.py similarity index 100% rename from networkbot.py rename to nemubot/networkbot.py diff --git a/prompt/__init__.py b/nemubot/prompt/__init__.py similarity index 100% rename from prompt/__init__.py rename to nemubot/prompt/__init__.py diff --git a/prompt/builtins.py b/nemubot/prompt/builtins.py similarity index 100% rename from prompt/builtins.py rename to nemubot/prompt/builtins.py diff --git a/prompt/error.py b/nemubot/prompt/error.py similarity index 100% rename from prompt/error.py rename to nemubot/prompt/error.py diff --git a/prompt/reset.py b/nemubot/prompt/reset.py similarity index 100% rename from prompt/reset.py rename to nemubot/prompt/reset.py diff --git a/server/DCC.py b/nemubot/server/DCC.py similarity index 100% rename from server/DCC.py rename to nemubot/server/DCC.py diff --git a/server/IRC.py b/nemubot/server/IRC.py similarity index 100% rename from server/IRC.py rename to nemubot/server/IRC.py diff --git a/server/__init__.py b/nemubot/server/__init__.py similarity index 100% rename from server/__init__.py rename to nemubot/server/__init__.py diff --git a/server/socket.py b/nemubot/server/socket.py similarity index 100% rename from server/socket.py rename to nemubot/server/socket.py diff --git a/tools/__init__.py b/nemubot/tools/__init__.py similarity index 100% rename from tools/__init__.py rename to nemubot/tools/__init__.py diff --git a/tools/config.py b/nemubot/tools/config.py similarity index 100% rename from tools/config.py rename to nemubot/tools/config.py diff --git a/tools/countdown.py b/nemubot/tools/countdown.py similarity index 100% rename from tools/countdown.py rename to nemubot/tools/countdown.py diff --git a/tools/date.py b/nemubot/tools/date.py similarity index 100% rename from tools/date.py rename to nemubot/tools/date.py diff --git a/tools/human.py b/nemubot/tools/human.py similarity index 100% rename from tools/human.py rename to nemubot/tools/human.py diff --git a/tools/web.py b/nemubot/tools/web.py similarity index 100% rename from tools/web.py rename to nemubot/tools/web.py diff --git a/tools/xmlparser/__init__.py b/nemubot/tools/xmlparser/__init__.py similarity index 100% rename from tools/xmlparser/__init__.py rename to nemubot/tools/xmlparser/__init__.py diff --git a/tools/xmlparser/node.py b/nemubot/tools/xmlparser/node.py similarity index 100% rename from tools/xmlparser/node.py rename to nemubot/tools/xmlparser/node.py From 5a6230d844ef1b178c950a99eaf8958419f6d2a5 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 3 Jan 2015 20:17:46 +0100 Subject: [PATCH 238/674] [wip] changes import to reflect new directory structure --- bin/nemubot | 28 +++++------ nemubot/__init__.py | 74 ++++++++++++++--------------- nemubot/channel.py | 4 +- nemubot/consumer.py | 4 +- nemubot/event.py | 13 ++--- nemubot/exception.py | 6 +-- nemubot/hooks/__init__.py | 14 +++--- nemubot/hooks/manager.py | 2 +- nemubot/hooks/messagehook.py | 18 +++---- nemubot/importer.py | 26 +++++----- nemubot/message/__init__.py | 12 ++--- nemubot/message/printer/IRC.py | 6 +-- nemubot/message/printer/__init__.py | 6 +-- nemubot/message/visitor.py | 2 +- nemubot/networkbot.py | 8 ++-- nemubot/prompt/__init__.py | 20 +++++--- nemubot/prompt/builtins.py | 8 ++-- nemubot/prompt/error.py | 4 +- nemubot/prompt/reset.py | 4 +- nemubot/server/DCC.py | 8 ++-- nemubot/server/IRC.py | 18 +++---- nemubot/server/__init__.py | 10 ++-- nemubot/server/socket.py | 4 +- nemubot/tools/__init__.py | 28 ++++++----- nemubot/tools/config.py | 8 ++-- nemubot/tools/countdown.py | 2 +- nemubot/tools/date.py | 2 +- nemubot/tools/human.py | 4 +- nemubot/tools/web.py | 10 ++-- nemubot/tools/xmlparser/__init__.py | 7 +-- nemubot/tools/xmlparser/node.py | 16 +++++++ 31 files changed, 202 insertions(+), 174 deletions(-) diff --git a/bin/nemubot b/bin/nemubot index 780832d..c59fee9 100755 --- a/bin/nemubot +++ b/bin/nemubot @@ -1,8 +1,8 @@ -#!/usr/bin/env python3.2 +#!/usr/bin/env python3.3 # -*- coding: utf-8 -*- # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2014 Mercier Pierre-Olivier +# 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 @@ -23,11 +23,11 @@ import logging import os import sys -import bot -import prompt -from prompt.builtins import load_file -from prompt.reset import PromptReset -import importer +import nemubot +import nemubot.prompt as prompt +from nemubot.prompt.builtins import load_file +from nemubot.prompt.reset import PromptReset +from nemubot.importer import ModuleFinder if __name__ == "__main__": # Parse command line arguments @@ -59,7 +59,7 @@ if __name__ == "__main__": args = parser.parse_args() if args.version: - print(bot.__version__) + print(nemubot.__version__) sys.exit(0) # Setup loggin interface @@ -91,7 +91,7 @@ if __name__ == "__main__": logger.error("%s is not a directory", path) # Create bot context - context = bot.Bot(modules_paths=modules_paths, data_path=args.data_path, + context = nemubot.Bot(modules_paths=modules_paths, data_path=args.data_path, verbosity=args.verbose) if args.no_connect: @@ -101,7 +101,7 @@ if __name__ == "__main__": prmpt = prompt.Prompt() # Register the hook for futur import - sys.meta_path.append(importer.ModuleFinder(context, prmpt)) + sys.meta_path.append(ModuleFinder(context, prmpt)) # Load requested configuration files for path in args.files: @@ -114,7 +114,7 @@ if __name__ == "__main__": for module in args.module: __import__(module) - print ("Nemubot v%s ready, my PID is %i!" % (bot.__version__, + print ("Nemubot v%s ready, my PID is %i!" % (nemubot.__version__, os.getpid())) context.start() while True: @@ -127,14 +127,14 @@ if __name__ == "__main__": try: # Reload context imp.reload(bot) - context = bot.hotswap(context) + context = nemubot.hotswap(context) # Reload prompt imp.reload(prompt) prmpt = prompt.hotswap(prmpt) # Reload all other modules - bot.reload() + nemubot.reload() print("\033[1;32mContext reloaded\033[0m, now in Nemubot %s" % - bot.__version__) + nemubot.__version__) context.start() except: logger.exception("\033[1;31mUnable to reload the prompt due to " diff --git a/nemubot/__init__.py b/nemubot/__init__.py index 1f74b8d..a69ec0b 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -# Nemubot is a modulable IRC bot, built around XML configuration files. -# Copyright (C) 2012 Mercier Pierre-Olivier +# 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 @@ -27,16 +27,16 @@ import threading import time import uuid -__version__ = '3.4.dev4' +__version__ = '4.0.dev0' __author__ = 'nemunaire' -from consumer import Consumer, EventConsumer, MessageConsumer -from event import ModuleEvent -from hooks.messagehook import MessageHook -from hooks.manager import HooksManager -from networkbot import NetworkBot +from nemubot.consumer import Consumer, EventConsumer, MessageConsumer +from nemubot.event import ModuleEvent +from nemubot.hooks.messagehook import MessageHook +from nemubot.hooks.manager import HooksManager +from nemubot.networkbot import NetworkBot -logger = logging.getLogger("nemubot.bot") +logger = logging.getLogger("nemubot") class Bot(threading.Thread): @@ -129,7 +129,7 @@ class Bot(threading.Thread): def run(self): - from server import _rlist, _wlist, _xlist + from nemubot.server import _rlist, _wlist, _xlist self.stop = False while not self.stop: @@ -453,44 +453,44 @@ def hotswap(bak): return new def reload(): - import channel - imp.reload(channel) + import nemubot.channel + imp.reload(nemubot.channel) - import consumer - imp.reload(consumer) + import nemubot.consumer + imp.reload(nemubot.consumer) - import event - imp.reload(event) + import nemubot.event + imp.reload(nemubot.event) - import exception - imp.reload(exception) + import nemubot.exception + imp.reload(nemubot.exception) - import hooks - imp.reload(hooks) + import nemubot.hooks + imp.reload(nemubot.hooks) - hooks.reload() + nemubot.hooks.reload() - import importer - imp.reload(importer) + import nemubot.importer + imp.reload(nemubot.importer) - import message - imp.reload(message) + import nemubot.message + imp.reload(nemubot.message) - message.reload() + nemubot.message.reload() - import prompt - imp.reload(prompt) + import nemubot.prompt + imp.reload(nemubot.prompt) - prompt.reload() + nemubot.prompt.reload() - import server - rl, wl, xl = server._rlist, server._wlist, server._xlist - imp.reload(server) - server._rlist, server._wlist, server._xlist = rl, wl, xl + import nemubot.server + rl, wl, xl = nemubot.server._rlist, nemubot.server._wlist, nemubot.server._xlist + imp.reload(nemubot.server) + nemubot.server._rlist, nemubot.server._wlist, nemubot.server._xlist = rl, wl, xl - server.reload() + nemubot.server.reload() - import tools - imp.reload(tools) + import nemubot.tools + imp.reload(nemubot.tools) - tools.reload() + nemubot.tools.reload() diff --git a/nemubot/channel.py b/nemubot/channel.py index 4223370..45031eb 100644 --- a/nemubot/channel.py +++ b/nemubot/channel.py @@ -1,7 +1,7 @@ # coding=utf-8 -# Nemubot is a modulable IRC bot, built around XML configuration files. -# Copyright (C) 2012 Mercier Pierre-Olivier +# 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 diff --git a/nemubot/consumer.py b/nemubot/consumer.py index 1aeb01c..371f69b 100644 --- a/nemubot/consumer.py +++ b/nemubot/consumer.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -# Nemubot is a modulable IRC bot, built around XML configuration files. -# Copyright (C) 2012 Mercier Pierre-Olivier +# 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 diff --git a/nemubot/event.py b/nemubot/event.py index 857a51a..96f226a 100644 --- a/nemubot/event.py +++ b/nemubot/event.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2014 nemunaire +# 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 @@ -18,6 +18,7 @@ from datetime import datetime, timedelta, timezone + class ModuleEvent: """Representation of a event initiated by a bot module""" @@ -64,14 +65,13 @@ class ModuleEvent: self.call_data = func_data # Store times - self.offset = timedelta(seconds=offset) # Time to wait before the first check + self.offset = timedelta(seconds=offset) # Time to wait before the first check self.interval = timedelta(seconds=interval) - self._end = None # Cache + self._end = None # Cache # How many times do this event? self.times = times - @property def current(self): """Return the date of the near check""" @@ -81,7 +81,6 @@ class ModuleEvent: return self._end return None - @property def next(self): """Return the date of the next check""" @@ -93,14 +92,12 @@ class ModuleEvent: 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 99999 # TODO: 99999 is not a valid time to return def check(self): """Run a check and realized the event if this is time""" diff --git a/nemubot/exception.py b/nemubot/exception.py index ccbe362..88b17a9 100644 --- a/nemubot/exception.py +++ b/nemubot/exception.py @@ -1,7 +1,7 @@ # coding=utf-8 -# Nemubot is a modulable IRC bot, built around XML configuration files. -# Copyright (C) 2012 Mercier Pierre-Olivier +# 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 @@ -16,7 +16,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 message import TextMessage, DirectAsk +from nemubot.message import TextMessage, DirectAsk class IRCException(Exception): diff --git a/nemubot/hooks/__init__.py b/nemubot/hooks/__init__.py index 288ea3a..e067774 100644 --- a/nemubot/hooks/__init__.py +++ b/nemubot/hooks/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2014 nemunaire +# 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 @@ -18,7 +18,7 @@ import imp -from exception import IRCException +from nemubot.exception import IRCException def call_game(call, *args, **kargs): @@ -67,7 +67,7 @@ class AbstractHook: return ret -from hooks.messagehook import MessageHook +from nemubot.hooks.messagehook import MessageHook last_registered = [] @@ -81,8 +81,8 @@ def hook(store, *args, **kargs): def reload(): - import hooks.manager - imp.reload(hooks.manager) + import nemubot.hooks.manager + imp.reload(nemubot.hooks.manager) - import hooks.messagehook - imp.reload(hooks.messagehook) + import nemubot.hooks.messagehook + imp.reload(nemubot.hooks.messagehook) diff --git a/nemubot/hooks/manager.py b/nemubot/hooks/manager.py index 687fcf1..ddc99c5 100644 --- a/nemubot/hooks/manager.py +++ b/nemubot/hooks/manager.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2014 nemunaire +# 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 diff --git a/nemubot/hooks/messagehook.py b/nemubot/hooks/messagehook.py index e0600c1..cd78458 100644 --- a/nemubot/hooks/messagehook.py +++ b/nemubot/hooks/messagehook.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2014 nemunaire +# 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 @@ -18,19 +18,19 @@ import re -from exception import IRCException -import hooks -import message +from nemubot.exception import IRCException +from nemubot.hooks import AbstractHook +import nemubot.message -class MessageHook(hooks.AbstractHook): +class MessageHook(AbstractHook): """Class storing hook information, specialized for a generic Message""" def __init__(self, call, name=None, data=None, regexp=None, channels=list(), server=None, mtimes=-1, end_call=None): - hooks.AbstractHook.__init__(self, call=call, data=data, + AbstractHook.__init__(self, call=call, data=data, end_call=end_call, mtimes=mtimes) self.name = name @@ -40,12 +40,12 @@ class MessageHook(hooks.AbstractHook): def match(self, msg, server=None): - if not isinstance(msg, message.AbstractMessage): + if not isinstance(msg, nemubot.message.AbstractMessage): return True - elif isinstance(msg, message.Command): + elif isinstance(msg, nemubot.message.Command): return self.is_matching(msg.cmd, msg.to, server) - elif isinstance(msg, message.TextMessage): + elif isinstance(msg, nemubot.message.TextMessage): return self.is_matching(msg.message, msg.to, server) else: return False diff --git a/nemubot/importer.py b/nemubot/importer.py index 3122bd9..1f3a3ef 100644 --- a/nemubot/importer.py +++ b/nemubot/importer.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -# Nemubot is a modulable IRC bot, built around XML configuration files. -# Copyright (C) 2012 Mercier Pierre-Olivier +# 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 @@ -24,16 +24,18 @@ import logging import os import sys -from bot import __version__ -import event -import exception -import hooks -from message import TextMessage -from tools.xmlparser import parse_file, module_state +from nemubot import __version__ +from nemubot.event import ModuleEvent +from nemubot.exception import IRCException +import nemubot.hooks +from nemubot.message import TextMessage +from nemubot.tools.xmlparser import parse_file, module_state logger = logging.getLogger("nemubot.importer") + class ModuleFinder(Finder): + def __init__(self, context, prompt): self.context = context self.prompt = prompt @@ -151,9 +153,9 @@ class ModuleLoader(SourceFileLoader): module.save = lambda: False module.CONF = self.config - module.ModuleEvent = event.ModuleEvent + module.ModuleEvent = ModuleEvent module.ModuleState = module_state.ModuleState - module.IRCException = exception.IRCException + module.IRCException = IRCException # Load dependancies if module.CONF is not None and module.CONF.hasNode("dependson"): @@ -213,7 +215,7 @@ def register_hooks(module, context, prompt): """ # Register decorated functions - for s, h in hooks.last_registered: + for s, h in nemubot.hooks.last_registered: if s == "prompt_cmd": prompt.add_cap_hook(h.name, h.call) elif s == "prompt_list": @@ -222,4 +224,4 @@ def register_hooks(module, context, prompt): s = convert_legacy_store(s) module.REGISTERED_HOOKS.append((s, h)) context.hooks.add_hook(h, s) - hooks.last_registered = [] + nemubot.hooks.last_registered = [] diff --git a/nemubot/message/__init__.py b/nemubot/message/__init__.py index 67ce5f7..bf4feda 100644 --- a/nemubot/message/__init__.py +++ b/nemubot/message/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2014 nemunaire +# 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 @@ -166,10 +166,10 @@ class OwnerCommand(Command): def reload(): - import message.visitor - imp.reload(message.visitor) + import nemubot.message.visitor + imp.reload(nemubot.message.visitor) - import message.printer - imp.reload(message.printer) + import nemubot.message.printer + imp.reload(nemubot.message.printer) - message.printer.reload() + nemubot.message.printer.reload() diff --git a/nemubot/message/printer/IRC.py b/nemubot/message/printer/IRC.py index 1b4670f..9c622ab 100644 --- a/nemubot/message/printer/IRC.py +++ b/nemubot/message/printer/IRC.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2014 nemunaire +# 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 @@ -16,8 +16,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/>. -from message import TextMessage -from message.visitor import AbstractVisitor +from nemubot.message import TextMessage +from nemubot.message.visitor import AbstractVisitor class IRC(AbstractVisitor): diff --git a/nemubot/message/printer/__init__.py b/nemubot/message/printer/__init__.py index 215b464..44eda74 100644 --- a/nemubot/message/printer/__init__.py +++ b/nemubot/message/printer/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2014 nemunaire +# 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 @@ -20,5 +20,5 @@ import imp def reload(): - import message.printer.IRC - imp.reload(message.printer.IRC) + import nemubot.message.printer.IRC + imp.reload(nemubot.message.printer.IRC) diff --git a/nemubot/message/visitor.py b/nemubot/message/visitor.py index 1041a45..a9630c1 100644 --- a/nemubot/message/visitor.py +++ b/nemubot/message/visitor.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2014 nemunaire +# 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 diff --git a/nemubot/networkbot.py b/nemubot/networkbot.py index c4ba4ff..1a253c1 100644 --- a/nemubot/networkbot.py +++ b/nemubot/networkbot.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -# Nemubot is a modulable IRC bot, built around XML configuration files. -# Copyright (C) 2012 Mercier Pierre-Olivier +# 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 @@ -22,8 +22,8 @@ import shlex import urllib.parse import zlib -from server.DCC import DCC -import hooks +from nemubot.server.DCC import DCC +import nemubot.hooks as hooks class NetworkBot: def __init__(self, context, srv, dest, dcc=None): diff --git a/nemubot/prompt/__init__.py b/nemubot/prompt/__init__.py index 2cc90bc..e3ab4d9 100644 --- a/nemubot/prompt/__init__.py +++ b/nemubot/prompt/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -# Nemubot is a modulable IRC bot, built around XML configuration files. -# Copyright (C) 2012 Mercier Pierre-Olivier +# 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 @@ -23,9 +23,9 @@ import shlex import sys import traceback -from .error import PromptError -from .reset import PromptReset -from . import builtins +from nemubot.prompt.error import PromptError +from nemubot.prompt.reset import PromptReset +from nemubot.prompt import builtins class Prompt: @@ -134,5 +134,11 @@ def hotswap(bak): def reload(): - import prompt.builtins - imp.reload(prompt.builtins) + 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 index 30e250d..7edbf5e 100644 --- a/nemubot/prompt/builtins.py +++ b/nemubot/prompt/builtins.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -# Nemubot is a modulable IRC bot, built around XML configuration files. -# Copyright (C) 2012 Mercier Pierre-Olivier +# 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 @@ -18,8 +18,8 @@ import logging -from .reset import PromptReset -from tools.config import load_file +from nemubot.prompt.reset import PromptReset +from nemubot.tools.config import load_file logger = logging.getLogger("nemubot.prompt.builtins") diff --git a/nemubot/prompt/error.py b/nemubot/prompt/error.py index 4ef7ede..3d426d6 100644 --- a/nemubot/prompt/error.py +++ b/nemubot/prompt/error.py @@ -1,7 +1,7 @@ # coding=utf-8 -# Nemubot is a modulable IRC bot, built around XML configuration files. -# Copyright (C) 2012 Mercier Pierre-Olivier +# 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 diff --git a/nemubot/prompt/reset.py b/nemubot/prompt/reset.py index d6bec5f..57da9f8 100644 --- a/nemubot/prompt/reset.py +++ b/nemubot/prompt/reset.py @@ -1,7 +1,7 @@ # coding=utf-8 -# Nemubot is a modulable IRC bot, built around XML configuration files. -# Copyright (C) 2012 Mercier Pierre-Olivier +# 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 diff --git a/nemubot/server/DCC.py b/nemubot/server/DCC.py index 6f85b0b..6b8d8c0 100644 --- a/nemubot/server/DCC.py +++ b/nemubot/server/DCC.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -# Nemubot is a modulable IRC bot, built around XML configuration files. -# Copyright (C) 2012 Mercier Pierre-Olivier +# 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 @@ -25,8 +25,8 @@ import time import threading import traceback -import message -import server +import nemubot.message as message +import nemubot.server as server #Store all used ports PORTS = list() diff --git a/nemubot/server/IRC.py b/nemubot/server/IRC.py index ae01a74..231b41f 100644 --- a/nemubot/server/IRC.py +++ b/nemubot/server/IRC.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2014 nemunaire +# 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 @@ -23,10 +23,10 @@ import re import time import shlex -from channel import Channel -import message -from message.printer.IRC import IRC as IRCPrinter -from server.socket import SocketServer +from nemubot.channel import Channel +import nemubot.message as message +from nemubot.message.printer.IRC import IRC as IRCPrinter +from nemubot.server.socket import SocketServer class IRC(SocketServer): @@ -97,18 +97,18 @@ class IRC(SocketServer): self.logger.error("DCC: unable to connect to %s:%d", ip, port) return "ERRMSG unable to connect to %s:%d" % (ip, port) - import bot + import nemubot self.ctcp_capabilities["ACTION"] = lambda msg, cmds: print ("ACTION receive: %s" % cmds) self.ctcp_capabilities["CLIENTINFO"] = _ctcp_clientinfo #self.ctcp_capabilities["DCC"] = _ctcp_dcc - self.ctcp_capabilities["FINGER"] = lambda msg, cmds: "VERSION nemubot v%s" % bot.__version__ - self.ctcp_capabilities["NEMUBOT"] = lambda msg, cmds: "NEMUBOT %s" % bot.__version__ + 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" % bot.__version__ + 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 diff --git a/nemubot/server/__init__.py b/nemubot/server/__init__.py index 92f2989..0031b02 100644 --- a/nemubot/server/__init__.py +++ b/nemubot/server/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2014 nemunaire +# 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 @@ -154,8 +154,8 @@ class AbstractServer(io.IOBase): def reload(): - import server.socket - imp.reload(server.socket) + import nemubot.server.socket + imp.reload(nemubot.server.socket) - import server.IRC - imp.reload(server.IRC) + import nemubot.server.IRC + imp.reload(nemubot.server.IRC) diff --git a/nemubot/server/socket.py b/nemubot/server/socket.py index bacfdac..2b0d014 100644 --- a/nemubot/server/socket.py +++ b/nemubot/server/socket.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2014 nemunaire +# 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 @@ -19,7 +19,7 @@ import ssl import socket -from server import AbstractServer +from nemubot.server import AbstractServer class SocketServer(AbstractServer): diff --git a/nemubot/tools/__init__.py b/nemubot/tools/__init__.py index 036b9ad..8ba5534 100644 --- a/nemubot/tools/__init__.py +++ b/nemubot/tools/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2014 nemunaire +# 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 @@ -20,16 +20,22 @@ import imp def reload(): - import tools.countdown - imp.reload(tools.countdown) + import nemubot.tools.config + imp.reload(nemubot.tools.config) - import tools.date - imp.reload(tools.date) + import nemubot.tools.countdown + imp.reload(nemubot.tools.countdown) - import tools.web - imp.reload(tools.web) + import nemubot.tools.date + imp.reload(nemubot.tools.date) - import tools.xmlparser - imp.reload(tools.xmlparser) - import tools.xmlparser.node - imp.reload(tools.xmlparser.node) + 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) diff --git a/nemubot/tools/config.py b/nemubot/tools/config.py index 11d4e58..cc94209 100644 --- a/nemubot/tools/config.py +++ b/nemubot/tools/config.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -# Nemubot is a modulable IRC bot, built around XML configuration files. -# Copyright (C) 2012 Mercier Pierre-Olivier +# 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 @@ -19,7 +19,7 @@ import logging import os -from tools.xmlparser import parse_file +from nemubot.tools.xmlparser import parse_file logger = logging.getLogger("nemubot.tools.config") @@ -80,7 +80,7 @@ def _load_server(config, xmlnode): # Bind the protocol asked to the corresponding implementation if "protocol" not in xmlnode or xmlnode["protocol"] == "irc": - from server.IRC import IRC as IRCServer + from nemubot.server.IRC import IRC as IRCServer srvcls = IRCServer else: raise Exception("Unhandled protocol '%s'" % diff --git a/nemubot/tools/countdown.py b/nemubot/tools/countdown.py index 4f66b21..a53476d 100644 --- a/nemubot/tools/countdown.py +++ b/nemubot/tools/countdown.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2014 nemunaire +# 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 diff --git a/nemubot/tools/date.py b/nemubot/tools/date.py index cc4481b..9ce357b 100644 --- a/nemubot/tools/date.py +++ b/nemubot/tools/date.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2014 nemunaire +# 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 diff --git a/nemubot/tools/human.py b/nemubot/tools/human.py index 620e167..478914e 100644 --- a/nemubot/tools/human.py +++ b/nemubot/tools/human.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -# Nemubot is a modulable IRC bot, built around XML configuration files. -# Copyright (C) 2012 Mercier Pierre-Olivier +# 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 diff --git a/nemubot/tools/web.py b/nemubot/tools/web.py index ef9f0cc..dc8a188 100644 --- a/nemubot/tools/web.py +++ b/nemubot/tools/web.py @@ -1,7 +1,7 @@ # coding=utf-8 -# Nemubot is a modulable IRC bot, built around XML configuration files. -# Copyright (C) 2012 Mercier Pierre-Olivier +# 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 @@ -25,9 +25,9 @@ from urllib.parse import quote from urllib.parse import urlparse from urllib.request import urlopen -from bot import __version__ -from exception import IRCException -from tools.xmlparser import parse_string +from nemubot import __version__ +from nemubot.exception import IRCException +from nemubot.tools.xmlparser import parse_string def isURL(url): diff --git a/nemubot/tools/xmlparser/__init__.py b/nemubot/tools/xmlparser/__init__.py index 502d198..3e39335 100644 --- a/nemubot/tools/xmlparser/__init__.py +++ b/nemubot/tools/xmlparser/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -# Nemubot is a modulable IRC bot, built around XML configuration files. -# Copyright (C) 2012 Mercier Pierre-Olivier +# 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 @@ -19,12 +19,13 @@ import logging import xml.sax -from . import node as module_state +from nemubot.tools.xmlparser import node as module_state logger = logging.getLogger("nemubot.tools.xmlparser") class ModuleStatesFile(xml.sax.ContentHandler): + def startDocument(self): self.root = None self.stack = list() diff --git a/nemubot/tools/xmlparser/node.py b/nemubot/tools/xmlparser/node.py index 5202b33..6031f76 100644 --- a/nemubot/tools/xmlparser/node.py +++ b/nemubot/tools/xmlparser/node.py @@ -1,5 +1,21 @@ # 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/>. + import xml.sax from datetime import datetime, timezone import logging From 7c7b63634b4638dea81da54ac1a4a1411edce63b Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 3 Jan 2015 20:34:44 +0100 Subject: [PATCH 239/674] [wip] in modules, changes import to reflect new directory structure --- modules/alias.py | 4 ++-- modules/birthday.py | 8 ++++---- modules/bonneannee.py | 4 ++-- modules/books.py | 4 ++-- modules/cmd_server.py | 12 ++++++------ modules/conjugaison.py | 10 ++++------ modules/cve.py | 3 +-- modules/ddg/DDGSearch.py | 4 ++-- modules/ddg/UrbanDictionnary.py | 2 +- modules/ddg/WFASearch.py | 2 +- modules/ddg/__init__.py | 2 +- modules/events.py | 8 ++++---- modules/github.py | 4 ++-- modules/imdb.py | 4 ++-- modules/man.py | 2 +- modules/mapquest.py | 4 ++-- modules/mediawiki.py | 4 ++-- modules/more.py | 6 +++--- modules/networking/__init__.py | 2 +- modules/networking/isup.py | 2 +- modules/networking/page.py | 4 ++-- modules/networking/w3c.py | 4 +++- modules/networking/watchWebsite.py | 7 ++++--- modules/networking/whois.py | 5 +++-- modules/nextstop/__init__.py | 2 +- modules/reddit.py | 4 ++-- modules/rnd.py | 2 +- modules/sap.py | 6 +++--- modules/sleepytime.py | 2 +- modules/sms.py | 2 +- modules/speak.py | 6 +++--- modules/spell/__init__.py | 2 +- modules/syno.py | 4 ++-- modules/tpb.py | 8 ++++---- modules/translate.py | 4 ++-- modules/velib.py | 4 ++-- modules/weather.py | 6 +++--- modules/worldcup.py | 4 ++-- modules/ycc.py | 6 +++--- 39 files changed, 87 insertions(+), 86 deletions(-) diff --git a/modules/alias.py b/modules/alias.py index 61234c1..6df4fcb 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -7,8 +7,8 @@ import sys from datetime import datetime, timezone import shlex -from hooks import hook -from message import TextMessage, Command +from nemubot.hooks import hook +from nemubot.message import TextMessage, Command nemubotversion = 3.4 diff --git a/modules/birthday.py b/modules/birthday.py index c48ace5..45c08b3 100644 --- a/modules/birthday.py +++ b/modules/birthday.py @@ -6,10 +6,10 @@ import re import sys from datetime import date, datetime -from hooks import hook -from tools.countdown import countdown_format -from tools.date import extractDate -from tools.xmlparser.node import ModuleState +from nemubot.hooks import hook +from nemubot.tools.countdown import countdown_format +from nemubot.tools.date import extractDate +from nemubot.tools.xmlparser.node import ModuleState nemubotversion = 3.4 diff --git a/modules/bonneannee.py b/modules/bonneannee.py index f29b6c4..174072c 100644 --- a/modules/bonneannee.py +++ b/modules/bonneannee.py @@ -4,8 +4,8 @@ from datetime import datetime, timezone -from hooks import hook -from tools.countdown import countdown_format +from nemubot.hooks import hook +from nemubot.tools.countdown import countdown_format nemubotversion = 3.4 diff --git a/modules/books.py b/modules/books.py index de4a9bf..b7f820c 100644 --- a/modules/books.py +++ b/modules/books.py @@ -4,8 +4,8 @@ import urllib -from hooks import hook -from tools import web +from nemubot.hooks import hook +from nemubot.tools import web nemubotversion = 3.4 diff --git a/modules/cmd_server.py b/modules/cmd_server.py index 449f4da..30c08c3 100644 --- a/modules/cmd_server.py +++ b/modules/cmd_server.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -# Nemubot is a modulable IRC bot, built around XML configuration files. -# Copyright (C) 2012 Mercier Pierre-Olivier +# 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 @@ -19,10 +19,10 @@ import traceback import sys -from prompt.error import PromptError -from hooks import hook -from message import TextMessage -from networkbot import NetworkBot +from nemubot.prompt.error import PromptError +from nemubot.hooks import hook +from nemubot.message import TextMessage +from nemubot.networkbot import NetworkBot nemubotversion = 3.4 NODATA = True diff --git a/modules/conjugaison.py b/modules/conjugaison.py index 4347603..50068b6 100644 --- a/modules/conjugaison.py +++ b/modules/conjugaison.py @@ -2,15 +2,13 @@ """Find french conjugaison""" +from collections import defaultdict import re -import traceback -import sys from urllib.parse import quote -from hooks import hook -from tools import web -from tools.web import striphtml -from collections import defaultdict +from nemubot.hooks import hook +from nemubot.tools import web +from nemubot.tools.web import striphtml nemubotversion = 3.4 diff --git a/modules/cve.py b/modules/cve.py index bfd6c98..e02bc55 100644 --- a/modules/cve.py +++ b/modules/cve.py @@ -1,7 +1,7 @@ import urllib.request from bs4 import BeautifulSoup import pprint -from hooks import hook +from nemubot.hooks import hook from more import Response nemubotversion = 3.4 @@ -34,4 +34,3 @@ def get_cve_desc(msg): desc = soup.body.findAll('td') return Response(desc[DESC_INDEX].text, msg.channel) - diff --git a/modules/ddg/DDGSearch.py b/modules/ddg/DDGSearch.py index 7a79c14..e681d8d 100644 --- a/modules/ddg/DDGSearch.py +++ b/modules/ddg/DDGSearch.py @@ -2,8 +2,8 @@ from urllib.parse import quote -from tools import web -from tools.xmlparser import parse_string +from nemubot.tools import web +from nemubot.tools.xmlparser import parse_string class DDGSearch: diff --git a/modules/ddg/UrbanDictionnary.py b/modules/ddg/UrbanDictionnary.py index 439b313..25faf39 100644 --- a/modules/ddg/UrbanDictionnary.py +++ b/modules/ddg/UrbanDictionnary.py @@ -2,7 +2,7 @@ from urllib.parse import quote -from tools import web +from nemubot.tools import web class UrbanDictionnary: diff --git a/modules/ddg/WFASearch.py b/modules/ddg/WFASearch.py index f465165..f20bbe2 100644 --- a/modules/ddg/WFASearch.py +++ b/modules/ddg/WFASearch.py @@ -2,7 +2,7 @@ from urllib.parse import quote -from tools import web +from nemubot.tools import web class WFASearch: diff --git a/modules/ddg/__init__.py b/modules/ddg/__init__.py index cc61eb5..94e3587 100644 --- a/modules/ddg/__init__.py +++ b/modules/ddg/__init__.py @@ -4,7 +4,7 @@ import imp -from hooks import hook +from nemubot.hooks import hook nemubotversion = 3.4 diff --git a/modules/events.py b/modules/events.py index ce2dae3..6fbee00 100644 --- a/modules/events.py +++ b/modules/events.py @@ -10,10 +10,10 @@ import time import threading import traceback -from event import ModuleEvent -from hooks import hook -from tools.date import extractDate -from tools.countdown import countdown_format, countdown +from nemubot.event import ModuleEvent +from nemubot.hooks import hook +from nemubot.tools.date import extractDate +from nemubot.tools.countdown import countdown_format, countdown nemubotversion = 3.4 diff --git a/modules/github.py b/modules/github.py index d22b832..82a6e75 100644 --- a/modules/github.py +++ b/modules/github.py @@ -5,8 +5,8 @@ import re from urllib.parse import quote -from hooks import hook -from tools import web +from nemubot.hooks import hook +from nemubot.tools import web nemubotversion = 3.4 diff --git a/modules/imdb.py b/modules/imdb.py index 7ae4163..552d98e 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -5,8 +5,8 @@ import re import urllib.parse -from hooks import hook -from tools import web +from nemubot.hooks import hook +from nemubot.tools import web nemubotversion = 3.4 diff --git a/modules/man.py b/modules/man.py index 2e897a3..9dc12a8 100644 --- a/modules/man.py +++ b/modules/man.py @@ -6,7 +6,7 @@ import subprocess import re import os -from hooks import hook +from nemubot.hooks import hook nemubotversion = 3.4 diff --git a/modules/mapquest.py b/modules/mapquest.py index 4f51d49..6ff8a2f 100644 --- a/modules/mapquest.py +++ b/modules/mapquest.py @@ -5,7 +5,7 @@ import re from urllib.parse import quote -from tools import web +from nemubot.tools import web nemubotversion = 3.4 @@ -20,7 +20,7 @@ def load(context): "http://developer.mapquest.com/") return None - from hooks.messagehook import MessageHook + from nemubot.hooks.messagehook import MessageHook add_hook("cmd_hook", MessageHook(cmd_geocode, "geocode")) diff --git a/modules/mediawiki.py b/modules/mediawiki.py index e483a60..6a9232a 100644 --- a/modules/mediawiki.py +++ b/modules/mediawiki.py @@ -6,8 +6,8 @@ import json import re import urllib.parse -from hooks import hook -from tools import web +from nemubot.hooks import hook +from nemubot.tools import web nemubotversion = 3.4 diff --git a/modules/more.py b/modules/more.py index a8dbd4f..72c5864 100644 --- a/modules/more.py +++ b/modules/more.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Nemubot is a smart and modulable IM bot. -# Copyright (C) 2012-2014 nemunaire +# 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 @@ -20,8 +20,8 @@ import logging -from message import TextMessage, DirectAsk -from hooks import hook +from nemubot.message import TextMessage, DirectAsk +from nemubot.hooks import hook nemubotversion = 3.4 diff --git a/modules/networking/__init__.py b/modules/networking/__init__.py index 3371a09..d3e515e 100644 --- a/modules/networking/__init__.py +++ b/modules/networking/__init__.py @@ -2,7 +2,7 @@ """Various network tools (w3m, w3c validator, curl, traceurl, ...)""" -from hooks import hook +from nemubot.hooks import hook nemubotversion = 3.4 diff --git a/modules/networking/isup.py b/modules/networking/isup.py index f400381..f20276c 100644 --- a/modules/networking/isup.py +++ b/modules/networking/isup.py @@ -1,6 +1,6 @@ import urllib -from tools.web import getJSON +from nemubot.tools.web import getJSON def isup(url): """Determine if the given URL is up or not diff --git a/modules/networking/page.py b/modules/networking/page.py index df561e0..3155e58 100644 --- a/modules/networking/page.py +++ b/modules/networking/page.py @@ -4,8 +4,8 @@ import subprocess import tempfile import urllib -from bot import __version__ -from tools import web +from nemubot import __version__ +from nemubot.tools import web def load(CONF, add_hook): diff --git a/modules/networking/w3c.py b/modules/networking/w3c.py index db10753..3e145f3 100644 --- a/modules/networking/w3c.py +++ b/modules/networking/w3c.py @@ -1,6 +1,8 @@ import json import urllib +from nemubot import __version__ + def validator(url): """Run the w3c validator on the given URL @@ -15,7 +17,7 @@ def validator(url): raise IRCException("Indiquer une URL valide !") try: - req = urllib.request.Request("http://validator.w3.org/check?uri=%s&output=json" % (urllib.parse.quote(o.geturl())), headers={ 'User-Agent' : "nemubot v3" }) + 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)) diff --git a/modules/networking/watchWebsite.py b/modules/networking/watchWebsite.py index 7415672..2ebc4a2 100644 --- a/modules/networking/watchWebsite.py +++ b/modules/networking/watchWebsite.py @@ -4,12 +4,13 @@ from random import randint import urllib.parse from urllib.parse import urlparse -from hooks import hook -from more import Response -from tools.xmlparser.node import ModuleState +from nemubot.hooks import hook +from nemubot.tools.xmlparser.node import ModuleState nemubotversion = 3.4 +from more import Response + from .atom import Atom from . import page diff --git a/modules/networking/whois.py b/modules/networking/whois.py index 2ec51ed..1e8d6d5 100644 --- a/modules/networking/whois.py +++ b/modules/networking/whois.py @@ -1,8 +1,9 @@ import datetime import urllib +from nemubot.tools.web import getJSON + from more import Response -from tools.web import getJSON URL_WHOIS = "http://www.whoisxmlapi.com/whoisserver/WhoisService?rid=1&domainName=%%s&outputFormat=json&userName=%s&password=%s" @@ -17,7 +18,7 @@ def load(CONF, add_hook): else: URL_WHOIS = URL_WHOIS % (urllib.parse.quote(CONF.getNode("whoisxmlapi")["username"]), urllib.parse.quote(CONF.getNode("whoisxmlapi")["password"])) - from hooks.messagehook import MessageHook + from nemubot.hooks.messagehook import MessageHook add_hook("cmd_hook", MessageHook(cmd_whois, "netwhois")) diff --git a/modules/nextstop/__init__.py b/modules/nextstop/__init__.py index f53aec4..5a54724 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 hooks import hook +from nemubot.hooks import hook from more import Response nemubotversion = 3.4 diff --git a/modules/reddit.py b/modules/reddit.py index 3b47bf0..786e5b0 100644 --- a/modules/reddit.py +++ b/modules/reddit.py @@ -4,11 +4,11 @@ import re -from tools import web +from nemubot.hooks import hook +from nemubot.tools import web nemubotversion = 3.4 -from hooks import hook from more import Response diff --git a/modules/rnd.py b/modules/rnd.py index 9cae89c..ddaa8c2 100644 --- a/modules/rnd.py +++ b/modules/rnd.py @@ -4,7 +4,7 @@ import random -from hooks import hook +from nemubot.hooks import hook nemubotversion = 3.4 diff --git a/modules/sap.py b/modules/sap.py index 81eccea..4067aae 100644 --- a/modules/sap.py +++ b/modules/sap.py @@ -5,9 +5,9 @@ import re import urllib.parse -from hooks import hook -from tools import web -from tools.web import striphtml +from nemubot.hooks import hook +from nemubot.tools import web +from nemubot.tools.web import striphtml nemubotversion = 3.4 diff --git a/modules/sleepytime.py b/modules/sleepytime.py index d082072..bf72249 100644 --- a/modules/sleepytime.py +++ b/modules/sleepytime.py @@ -6,7 +6,7 @@ import re import imp from datetime import datetime, timedelta, timezone -from hooks import hook +from nemubot.hooks import hook nemubotversion = 3.4 diff --git a/modules/sms.py b/modules/sms.py index f96bfad..9d50386 100644 --- a/modules/sms.py +++ b/modules/sms.py @@ -9,7 +9,7 @@ import urllib.error import urllib.request import urllib.parse -from hooks import hook +from nemubot.hooks import hook nemubotversion = 3.4 diff --git a/modules/speak.py b/modules/speak.py index 2b85035..d8808b8 100644 --- a/modules/speak.py +++ b/modules/speak.py @@ -6,9 +6,9 @@ import re import subprocess from threading import Thread -from hooks import hook -from message import TextMessage -from message.visitor import AbstractVisitor +from nemubot.hooks import hook +from nemubot.message import TextMessage +from nemubot.message.visitor import AbstractVisitor nemubotversion = 3.4 diff --git a/modules/spell/__init__.py b/modules/spell/__init__.py index f63cab5..60bd822 100644 --- a/modules/spell/__init__.py +++ b/modules/spell/__init__.py @@ -5,7 +5,7 @@ import re from urllib.parse import quote -from hooks import hook +from nemubot.hooks import hook from .pyaspell import Aspell from .pyaspell import AspellError diff --git a/modules/syno.py b/modules/syno.py index 5747c29..580ef46 100644 --- a/modules/syno.py +++ b/modules/syno.py @@ -5,8 +5,8 @@ import re from urllib.parse import quote -from hooks import hook -from tools import web +from nemubot.hooks import hook +from nemubot.tools import web nemubotversion = 3.4 diff --git a/modules/tpb.py b/modules/tpb.py index 4cc1187..c1d1ad3 100644 --- a/modules/tpb.py +++ b/modules/tpb.py @@ -1,12 +1,12 @@ from datetime import datetime import urllib -from tools import human -from tools.web import getJSON +from nemubot.hooks import hook +from nemubot.tools import human +from nemubot.tools.web import getJSON nemubotversion = 3.4 -from hooks import hook from more import Response URL_TPBAPI = None @@ -22,7 +22,7 @@ def load(context): else: URL_TPBAPI = CONF.getNode("tpbapi")["url"] - from hooks.messagehook import MessageHook + from nemubot.hooks.messagehook import MessageHook add_hook("cmd_hook", MessageHook(cmd_tpb, "tpb")) diff --git a/modules/translate.py b/modules/translate.py index 6e0b825..538f04e 100644 --- a/modules/translate.py +++ b/modules/translate.py @@ -5,7 +5,7 @@ import re from urllib.parse import quote -from tools import web +from nemubot.tools import web nemubotversion = 3.4 @@ -27,7 +27,7 @@ def load(context): else: URL = URL % CONF.getNode("wrapi")["key"] - from hooks.messagehook import MessageHook + from nemubot.hooks.messagehook import MessageHook add_hook("cmd_hook", MessageHook(cmd_translate, "translate")) diff --git a/modules/velib.py b/modules/velib.py index f45372d..ed22117 100644 --- a/modules/velib.py +++ b/modules/velib.py @@ -4,8 +4,8 @@ import re -from hooks import hook -from tools import web +from nemubot.hooks import hook +from nemubot.tools import web nemubotversion = 3.4 diff --git a/modules/weather.py b/modules/weather.py index 6df1882..a5fbc2d 100644 --- a/modules/weather.py +++ b/modules/weather.py @@ -6,8 +6,8 @@ import datetime import re from urllib.parse import quote -from hooks import hook -from tools import web +from nemubot.hooks import hook +from nemubot.tools import web import mapquest @@ -27,7 +27,7 @@ def load(context): "http://developer.forecast.io/") return None - from hooks.messagehook import MessageHook + from nemubot.hooks.messagehook import MessageHook add_hook("cmd_hook", MessageHook(cmd_weather, "météo")) add_hook("cmd_hook", MessageHook(cmd_alert, "alert")) add_hook("cmd_hook", MessageHook(cmd_coordinates, "coordinates")) diff --git a/modules/worldcup.py b/modules/worldcup.py index 2426c7c..d95d884 100644 --- a/modules/worldcup.py +++ b/modules/worldcup.py @@ -8,12 +8,12 @@ import re from urllib.parse import quote from urllib.request import urlopen +from nemubot.hooks import hook + nemubotversion = 3.4 from more import Response -from hooks import hook - API_URL="http://worldcup.sfg.io/%s" def load(context): diff --git a/modules/ycc.py b/modules/ycc.py index 0a42fc3..60b02f6 100644 --- a/modules/ycc.py +++ b/modules/ycc.py @@ -6,9 +6,9 @@ import re from urllib.parse import urlparse from urllib.parse import quote -from hooks import hook -from message import TextMessage -from tools import web +from nemubot.hooks import hook +from nemubot.message import TextMessage +from nemubot.tools import web nemubotversion = 3.4 From 06c85289e01c965263c65cb658a675a566fe377f Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 4 Jan 2015 23:57:09 +0100 Subject: [PATCH 240/674] Don't import some nemubot module automatically --- modules/alias.py | 1 + modules/birthday.py | 1 + modules/bonneannee.py | 1 + modules/books.py | 1 + modules/conjugaison.py | 1 + modules/events.py | 1 + modules/github.py | 1 + modules/imdb.py | 1 + modules/mapquest.py | 1 + modules/mediawiki.py | 1 + modules/networking/__init__.py | 3 +-- modules/networking/page.py | 1 + modules/networking/w3c.py | 1 + modules/networking/watchWebsite.py | 2 ++ modules/networking/whois.py | 1 + modules/nextstop/__init__.py | 1 + modules/reddit.py | 1 + modules/rnd.py | 1 + modules/sap.py | 1 + modules/sms.py | 1 + modules/spell/__init__.py | 1 + modules/syno.py | 1 + modules/tpb.py | 1 + modules/translate.py | 1 + modules/velib.py | 1 + modules/weather.py | 1 + modules/worldcup.py | 3 ++- modules/ycc.py | 1 + nemubot/importer.py | 6 ------ 29 files changed, 30 insertions(+), 9 deletions(-) diff --git a/modules/alias.py b/modules/alias.py index 6df4fcb..6083d34 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -7,6 +7,7 @@ import sys from datetime import datetime, timezone import shlex +from nemubot.exception import IRCException from nemubot.hooks import hook from nemubot.message import TextMessage, Command diff --git a/modules/birthday.py b/modules/birthday.py index 45c08b3..8137a08 100644 --- a/modules/birthday.py +++ b/modules/birthday.py @@ -6,6 +6,7 @@ import re import sys from datetime import date, datetime +from nemubot.exception import IRCException from nemubot.hooks import hook from nemubot.tools.countdown import countdown_format from nemubot.tools.date import extractDate diff --git a/modules/bonneannee.py b/modules/bonneannee.py index 174072c..2eb9b7e 100644 --- a/modules/bonneannee.py +++ b/modules/bonneannee.py @@ -4,6 +4,7 @@ from datetime import datetime, timezone +from nemubot.event import ModuleEvent from nemubot.hooks import hook from nemubot.tools.countdown import countdown_format diff --git a/modules/books.py b/modules/books.py index b7f820c..5d3bc57 100644 --- a/modules/books.py +++ b/modules/books.py @@ -4,6 +4,7 @@ import urllib +from nemubot.exception import IRCException from nemubot.hooks import hook from nemubot.tools import web diff --git a/modules/conjugaison.py b/modules/conjugaison.py index 50068b6..35faa40 100644 --- a/modules/conjugaison.py +++ b/modules/conjugaison.py @@ -6,6 +6,7 @@ from collections import defaultdict import re from urllib.parse import quote +from nemubot.exception import IRCException from nemubot.hooks import hook from nemubot.tools import web from nemubot.tools.web import striphtml diff --git a/modules/events.py b/modules/events.py index 6fbee00..41f5baf 100644 --- a/modules/events.py +++ b/modules/events.py @@ -10,6 +10,7 @@ import time import threading import traceback +from nemubot.exception import IRCException from nemubot.event import ModuleEvent from nemubot.hooks import hook from nemubot.tools.date import extractDate diff --git a/modules/github.py b/modules/github.py index 82a6e75..a3f0ba8 100644 --- a/modules/github.py +++ b/modules/github.py @@ -5,6 +5,7 @@ import re from urllib.parse import quote +from nemubot.exception import IRCException from nemubot.hooks import hook from nemubot.tools import web diff --git a/modules/imdb.py b/modules/imdb.py index 552d98e..4b381ae 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -5,6 +5,7 @@ import re import urllib.parse +from nemubot.exception import IRCException from nemubot.hooks import hook from nemubot.tools import web diff --git a/modules/mapquest.py b/modules/mapquest.py index 6ff8a2f..7d41f2b 100644 --- a/modules/mapquest.py +++ b/modules/mapquest.py @@ -5,6 +5,7 @@ import re from urllib.parse import quote +from nemubot.exception import IRCException from nemubot.tools import web nemubotversion = 3.4 diff --git a/modules/mediawiki.py b/modules/mediawiki.py index 6a9232a..730911c 100644 --- a/modules/mediawiki.py +++ b/modules/mediawiki.py @@ -6,6 +6,7 @@ import json import re import urllib.parse +from nemubot.exception import IRCException from nemubot.hooks import hook from nemubot.tools import web diff --git a/modules/networking/__init__.py b/modules/networking/__init__.py index d3e515e..c6d960c 100644 --- a/modules/networking/__init__.py +++ b/modules/networking/__init__.py @@ -2,6 +2,7 @@ """Various network tools (w3m, w3c validator, curl, traceurl, ...)""" +from nemubot.exception import IRCException from nemubot.hooks import hook nemubotversion = 3.4 @@ -16,8 +17,6 @@ from . import whois def load(context): for mod in [isup, page, w3c, watchWebsite, whois]: - mod.IRCException = IRCException - mod.ModuleEvent = ModuleEvent mod.add_event = add_event mod.del_event = del_event mod.save = save diff --git a/modules/networking/page.py b/modules/networking/page.py index 3155e58..7e5c10e 100644 --- a/modules/networking/page.py +++ b/modules/networking/page.py @@ -5,6 +5,7 @@ import tempfile import urllib from nemubot import __version__ +from nemubot.exception import IRCException from nemubot.tools import web diff --git a/modules/networking/w3c.py b/modules/networking/w3c.py index 3e145f3..d9de178 100644 --- a/modules/networking/w3c.py +++ b/modules/networking/w3c.py @@ -2,6 +2,7 @@ import json import urllib from nemubot import __version__ +from nemubot.exception import IRCException def validator(url): """Run the w3c validator on the given URL diff --git a/modules/networking/watchWebsite.py b/modules/networking/watchWebsite.py index 2ebc4a2..7035b4b 100644 --- a/modules/networking/watchWebsite.py +++ b/modules/networking/watchWebsite.py @@ -4,6 +4,8 @@ from random import randint import urllib.parse from urllib.parse import urlparse +from nemubot.event import ModuleEvent +from nemubot.exception import IRCException from nemubot.hooks import hook from nemubot.tools.xmlparser.node import ModuleState diff --git a/modules/networking/whois.py b/modules/networking/whois.py index 1e8d6d5..3e81e38 100644 --- a/modules/networking/whois.py +++ b/modules/networking/whois.py @@ -1,6 +1,7 @@ import datetime import urllib +from nemubot.exception import IRCException from nemubot.tools.web import getJSON from more import Response diff --git a/modules/nextstop/__init__.py b/modules/nextstop/__init__.py index 5a54724..9f1abe5 100644 --- a/modules/nextstop/__init__.py +++ b/modules/nextstop/__init__.py @@ -2,6 +2,7 @@ """Informe les usagers des prochains passages des transports en communs de la RATP""" +from nemubot.exception import IRCException from nemubot.hooks import hook from more import Response diff --git a/modules/reddit.py b/modules/reddit.py index 786e5b0..fddaadd 100644 --- a/modules/reddit.py +++ b/modules/reddit.py @@ -4,6 +4,7 @@ import re +from nemubot.exception import IRCException from nemubot.hooks import hook from nemubot.tools import web diff --git a/modules/rnd.py b/modules/rnd.py index ddaa8c2..644507f 100644 --- a/modules/rnd.py +++ b/modules/rnd.py @@ -4,6 +4,7 @@ import random +from nemubot.exception import IRCException from nemubot.hooks import hook nemubotversion = 3.4 diff --git a/modules/sap.py b/modules/sap.py index 4067aae..0110c26 100644 --- a/modules/sap.py +++ b/modules/sap.py @@ -5,6 +5,7 @@ import re import urllib.parse +from nemubot.exception import IRCException from nemubot.hooks import hook from nemubot.tools import web from nemubot.tools.web import striphtml diff --git a/modules/sms.py b/modules/sms.py index 9d50386..5eb3c2b 100644 --- a/modules/sms.py +++ b/modules/sms.py @@ -9,6 +9,7 @@ import urllib.error import urllib.request import urllib.parse +from nemubot.exception import IRCException from nemubot.hooks import hook nemubotversion = 3.4 diff --git a/modules/spell/__init__.py b/modules/spell/__init__.py index 60bd822..025cd51 100644 --- a/modules/spell/__init__.py +++ b/modules/spell/__init__.py @@ -5,6 +5,7 @@ import re from urllib.parse import quote +from nemubot.exception import IRCException from nemubot.hooks import hook from .pyaspell import Aspell diff --git a/modules/syno.py b/modules/syno.py index 580ef46..603277b 100644 --- a/modules/syno.py +++ b/modules/syno.py @@ -5,6 +5,7 @@ import re from urllib.parse import quote +from nemubot.exception import IRCException from nemubot.hooks import hook from nemubot.tools import web diff --git a/modules/tpb.py b/modules/tpb.py index c1d1ad3..b525210 100644 --- a/modules/tpb.py +++ b/modules/tpb.py @@ -1,6 +1,7 @@ from datetime import datetime import urllib +from nemubot.exception import IRCException from nemubot.hooks import hook from nemubot.tools import human from nemubot.tools.web import getJSON diff --git a/modules/translate.py b/modules/translate.py index 538f04e..0b73b90 100644 --- a/modules/translate.py +++ b/modules/translate.py @@ -5,6 +5,7 @@ import re from urllib.parse import quote +from nemubot.exception import IRCException from nemubot.tools import web nemubotversion = 3.4 diff --git a/modules/velib.py b/modules/velib.py index ed22117..be196ad 100644 --- a/modules/velib.py +++ b/modules/velib.py @@ -4,6 +4,7 @@ import re +from nemubot.exception import IRCException from nemubot.hooks import hook from nemubot.tools import web diff --git a/modules/weather.py b/modules/weather.py index a5fbc2d..f8aa522 100644 --- a/modules/weather.py +++ b/modules/weather.py @@ -6,6 +6,7 @@ import datetime import re from urllib.parse import quote +from nemubot.exception import IRCException from nemubot.hooks import hook from nemubot.tools import web diff --git a/modules/worldcup.py b/modules/worldcup.py index d95d884..9ae06cd 100644 --- a/modules/worldcup.py +++ b/modules/worldcup.py @@ -8,6 +8,7 @@ import re from urllib.parse import quote from urllib.request import urlopen +from nemubot.exception import IRCException from nemubot.hooks import hook nemubotversion = 3.4 @@ -17,7 +18,7 @@ from more import Response API_URL="http://worldcup.sfg.io/%s" def load(context): - from event import ModuleEvent + from nemubot.event import ModuleEvent 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)) diff --git a/modules/ycc.py b/modules/ycc.py index 60b02f6..97cde3f 100644 --- a/modules/ycc.py +++ b/modules/ycc.py @@ -6,6 +6,7 @@ import re from urllib.parse import urlparse from urllib.parse import quote +from nemubot.exception import IRCException from nemubot.hooks import hook from nemubot.message import TextMessage from nemubot.tools import web diff --git a/nemubot/importer.py b/nemubot/importer.py index 1f3a3ef..4003225 100644 --- a/nemubot/importer.py +++ b/nemubot/importer.py @@ -25,8 +25,6 @@ import os import sys from nemubot import __version__ -from nemubot.event import ModuleEvent -from nemubot.exception import IRCException import nemubot.hooks from nemubot.message import TextMessage from nemubot.tools.xmlparser import parse_file, module_state @@ -153,10 +151,6 @@ class ModuleLoader(SourceFileLoader): module.save = lambda: False module.CONF = self.config - module.ModuleEvent = ModuleEvent - module.ModuleState = module_state.ModuleState - module.IRCException = IRCException - # Load dependancies if module.CONF is not None and module.CONF.hasNode("dependson"): module.MODS = dict() From 38aea5dd372ef16c9329edc143896eaf486f02e1 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 5 Jan 2015 02:49:21 +0100 Subject: [PATCH 241/674] Lock the data directory to avoid concurent modification of XML files --- nemubot/__init__.py | 54 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/nemubot/__init__.py b/nemubot/__init__.py index a69ec0b..bf6cf74 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -20,6 +20,7 @@ from datetime import datetime, timedelta, timezone import imp import ipaddress import logging +import os from queue import Queue import re from select import select @@ -64,7 +65,8 @@ class Bot(threading.Thread): # Context paths self.modules_paths = modules_paths - self.data_path = data_path + self.data_path = None + self.set_data_path(data_path) # Keep global context: servers and modules self.servers = dict() @@ -177,6 +179,52 @@ class Bot(threading.Thread): logger.exception("Uncatched exception on server read") + # Data path + + def set_data_path(self, path): + """Check if the given path is valid and unlock, + then lock the directory and set the variable + + Argument: + path -- the location + """ + + lock_file = os.path.join(path, ".used_by_nemubot") + + if os.path.isdir(path): + if not os.path.exists(lock_file): + if self.data_path is not None: + self.close_data_path(self.data_path) + self.data_path = path + + with open(lock_file, 'w') as lf: + lf.write(str(os.getpid())) + return True + + else: + with open(lock_file, 'r') as lf: + pid = lf.readline() + raise Exception("Data dir already locked, by PID %s" % pid) + return False + + + def close_data_path(self, path=None): + """Release a locked path + + Argument: + path -- the location, self.data_path if None + """ + + if path is None: + path = self.data_path + + lock_file = os.path.join(path, ".used_by_nemubot") + if os.path.isdir(path) and os.path.exists(lock_file): + os.unlink(lock_file) + return True + return False + + # Events methods def add_event(self, evt, eid=None, module_src=None): @@ -406,6 +454,9 @@ class Bot(threading.Thread): def quit(self): """Save and unload modules and disconnect servers""" + + self.close_data_path() + if self.event_timer is not None: logger.info("Stop the event timer...") self.event_timer.cancel() @@ -440,6 +491,7 @@ def hotswap(bak): bak.stop = True if bak.event_timer is not None: bak.event_timer.cancel() + bak.close_data_path() new = Bot(str(bak.ip), bak.modules_paths, bak.data_path) new.servers = bak.servers From 2b9c810e88f0d89883bb65b4367e045c846ad3c3 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 5 Jan 2015 03:07:41 +0100 Subject: [PATCH 242/674] Fix quit --- bin/nemubot | 21 +++++++++------------ nemubot/prompt/__init__.py | 2 +- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/bin/nemubot b/bin/nemubot index c59fee9..0c3d21e 100755 --- a/bin/nemubot +++ b/bin/nemubot @@ -24,7 +24,7 @@ import os import sys import nemubot -import nemubot.prompt as prompt +import nemubot.prompt from nemubot.prompt.builtins import load_file from nemubot.prompt.reset import PromptReset from nemubot.importer import ModuleFinder @@ -98,7 +98,7 @@ if __name__ == "__main__": context.noautoconnect = True # Load the prompt - prmpt = prompt.Prompt() + prmpt = nemubot.prompt.Prompt() # Register the hook for futur import sys.meta_path.append(ModuleFinder(context, prmpt)) @@ -116,26 +116,24 @@ if __name__ == "__main__": print ("Nemubot v%s ready, my PID is %i!" % (nemubot.__version__, os.getpid())) - context.start() while True: try: - prmpt.run(context) + context.start() + if prmpt.run(context): + break except PromptReset as e: if e.type == "quit": break try: - # Reload context - imp.reload(bot) - context = nemubot.hotswap(context) - # Reload prompt - imp.reload(prompt) - prmpt = prompt.hotswap(prmpt) # Reload all other modules + imp.reload(nemubot) + imp.reload(nemubot.prompt) nemubot.reload() + context = nemubot.hotswap(context) + prmpt = nemubot.prompt.hotswap(prmpt) print("\033[1;32mContext reloaded\033[0m, now in Nemubot %s" % nemubot.__version__) - context.start() except: logger.exception("\033[1;31mUnable to reload the prompt due to " "errors.\033[0m Fix them before trying to reload " @@ -143,5 +141,4 @@ if __name__ == "__main__": context.quit() print("\nWaiting for other threads shuts down...") - sys.exit(0) diff --git a/nemubot/prompt/__init__.py b/nemubot/prompt/__init__.py index e3ab4d9..f3e60da 100644 --- a/nemubot/prompt/__init__.py +++ b/nemubot/prompt/__init__.py @@ -123,7 +123,7 @@ class Prompt: print("") except EOFError: print("quit") - raise PromptReset("quit") + return True def hotswap(bak): From e225f3b8d711fad0c96173d5f80f54ae596b525f Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 5 Jan 2015 10:18:40 +0100 Subject: [PATCH 243/674] import ModuleState when needed --- modules/alias.py | 1 + modules/events.py | 3 ++- modules/sms.py | 1 + modules/spell/__init__.py | 1 + modules/weather.py | 1 + modules/worldcup.py | 1 + 6 files changed, 7 insertions(+), 1 deletion(-) diff --git a/modules/alias.py b/modules/alias.py index 6083d34..3365141 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -10,6 +10,7 @@ import shlex from nemubot.exception import IRCException from nemubot.hooks import hook from nemubot.message import TextMessage, Command +from nemubot.tools.xmlparser.node import ModuleState nemubotversion = 3.4 diff --git a/modules/events.py b/modules/events.py index 41f5baf..0731cc5 100644 --- a/modules/events.py +++ b/modules/events.py @@ -13,8 +13,9 @@ import traceback from nemubot.exception import IRCException from nemubot.event import ModuleEvent from nemubot.hooks import hook -from nemubot.tools.date import extractDate from nemubot.tools.countdown import countdown_format, countdown +from nemubot.tools.date import extractDate +from nemubot.tools.xmlparser.node import ModuleState nemubotversion = 3.4 diff --git a/modules/sms.py b/modules/sms.py index 5eb3c2b..dd467f7 100644 --- a/modules/sms.py +++ b/modules/sms.py @@ -11,6 +11,7 @@ import urllib.parse from nemubot.exception import IRCException from nemubot.hooks import hook +from nemubot.tools.xmlparser.node import ModuleState nemubotversion = 3.4 diff --git a/modules/spell/__init__.py b/modules/spell/__init__.py index 025cd51..9e2faba 100644 --- a/modules/spell/__init__.py +++ b/modules/spell/__init__.py @@ -7,6 +7,7 @@ from urllib.parse import quote from nemubot.exception import IRCException from nemubot.hooks import hook +from nemubot.tools.xmlparser.node import ModuleState from .pyaspell import Aspell from .pyaspell import AspellError diff --git a/modules/weather.py b/modules/weather.py index f8aa522..e41bdb2 100644 --- a/modules/weather.py +++ b/modules/weather.py @@ -9,6 +9,7 @@ from urllib.parse import quote from nemubot.exception import IRCException from nemubot.hooks import hook from nemubot.tools import web +from nemubot.tools.xmlparser.node import ModuleState import mapquest diff --git a/modules/worldcup.py b/modules/worldcup.py index 9ae06cd..0a9c1cb 100644 --- a/modules/worldcup.py +++ b/modules/worldcup.py @@ -10,6 +10,7 @@ from urllib.request import urlopen from nemubot.exception import IRCException from nemubot.hooks import hook +from nemubot.tools.xmlparser.node import ModuleState nemubotversion = 3.4 From e7d37991b3e6a80d862394089a0fb3adbcf8b660 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 21 Jan 2015 21:18:34 +0100 Subject: [PATCH 244/674] Allow print and print_debug with multiple arguments --- nemubot/importer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nemubot/importer.py b/nemubot/importer.py index 4003225..205efd1 100644 --- a/nemubot/importer.py +++ b/nemubot/importer.py @@ -88,11 +88,11 @@ class ModuleLoader(SourceFileLoader): def prnt(*args): print("[%s]" % module.__name__, *args) - module.logger.info(*args) + module.logger.info(" ".join(args)) def prnt_dbg(*args): if module.DEBUG: print("{%s}" % module.__name__, *args) - module.logger.debug(*args) + module.logger.debug(" ".join(args)) def mod_save(): fpath = os.path.join(self.context.data_path, module.__name__ + ".xml") From 55b1eb50750671d7bd114ccbdb88b5242959f9c0 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 9 Feb 2015 17:23:07 +0100 Subject: [PATCH 245/674] Allow data_path to be None (don't load or save data) --- nemubot/__init__.py | 5 +++-- nemubot/importer.py | 14 ++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/nemubot/__init__.py b/nemubot/__init__.py index bf6cf74..09f3da0 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -45,7 +45,7 @@ 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_path="./datas/", verbosity=0): + data_path=None, verbosity=0): """Initialize the bot context Keyword arguments: @@ -66,7 +66,8 @@ class Bot(threading.Thread): # Context paths self.modules_paths = modules_paths self.data_path = None - self.set_data_path(data_path) + if data_path is not None: + self.set_data_path(data_path) # Keep global context: servers and modules self.servers = dict() diff --git a/nemubot/importer.py b/nemubot/importer.py index 205efd1..b303a34 100644 --- a/nemubot/importer.py +++ b/nemubot/importer.py @@ -139,13 +139,15 @@ class ModuleLoader(SourceFileLoader): module.del_event = del_event if not hasattr(module, "NODATA"): - data_file = os.path.join(self.context.data_path, - module.__name__ + ".xml") - if os.path.isfile(data_file): - module.DATAS = parse_file(data_file) + module.DATAS = module_state.ModuleState("nemubotstate") + if self.context.data_path is not None: + data_file = os.path.join(self.context.data_path, + module.__name__ + ".xml") + if os.path.isfile(data_file): + module.DATAS = parse_file(data_file) + module.save = mod_save else: - module.DATAS = module_state.ModuleState("nemubotstate") - module.save = mod_save + module.save = lambda: False else: module.DATAS = None module.save = lambda: False From e7fd7c5ec470154d610b2f67349d55c7ffe5b3d0 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 9 Feb 2015 23:07:30 +0100 Subject: [PATCH 246/674] Arrange IRC server construction reorder constructor argument to a more logical order on_connect can be a simple string or a callable --- nemubot/server/IRC.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/nemubot/server/IRC.py b/nemubot/server/IRC.py index 231b41f..c1df9ca 100644 --- a/nemubot/server/IRC.py +++ b/nemubot/server/IRC.py @@ -33,18 +33,18 @@ class IRC(SocketServer): """Concrete implementation of a connexion to an IRC server""" - def __init__(self, owner, nick="nemubot", host="localhost", port=6667, - ssl=False, username=None, password=None, realname="Nemubot", - encoding="utf-8", caps=None, channels=list(), - on_connect=None): + def __init__(self, host="localhost", port=6667, ssl=False, owner=None, + nick="nemubot", username=None, password=None, + realname="Nemubot", encoding="utf-8", caps=None, + channels=list(), on_connect=None): """Prepare a connection with an IRC server Keyword arguments: - owner -- bot's owner - nick -- bot's nick 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 @@ -60,7 +60,7 @@ class IRC(SocketServer): self.owner = owner self.realname = realname - self.id = self.username + "@" + host + ":" + port + self.id = self.username + "@" + host + ":" + str(port) self.printer = IRCPrinter SocketServer.__init__(self, host=host, port=port, ssl=ssl) @@ -128,8 +128,13 @@ class IRC(SocketServer): def _on_connect(msg): # First, send user defined command if on_connect is not None: - for oc in on_connect(): - self.write(oc) + 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 isinstance(chn, tuple): From f66d724d4060ac322b30af89a118abc0bb9216b9 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 10 Feb 2015 00:30:04 +0100 Subject: [PATCH 247/674] Introducing data stores --- bin/nemubot | 5 ++- nemubot/__init__.py | 69 +++++++---------------------- nemubot/datastore/__init__.py | 18 ++++++++ nemubot/datastore/abstract.py | 61 ++++++++++++++++++++++++++ nemubot/datastore/xml.py | 81 +++++++++++++++++++++++++++++++++++ nemubot/importer.py | 19 ++------ 6 files changed, 181 insertions(+), 72 deletions(-) create mode 100644 nemubot/datastore/__init__.py create mode 100644 nemubot/datastore/abstract.py create mode 100644 nemubot/datastore/xml.py diff --git a/bin/nemubot b/bin/nemubot index 0c3d21e..0650e9f 100755 --- a/bin/nemubot +++ b/bin/nemubot @@ -24,6 +24,7 @@ import os import sys import nemubot +from nemubot import datastore import nemubot.prompt from nemubot.prompt.builtins import load_file from nemubot.prompt.reset import PromptReset @@ -91,8 +92,8 @@ if __name__ == "__main__": logger.error("%s is not a directory", path) # Create bot context - context = nemubot.Bot(modules_paths=modules_paths, data_path=args.data_path, - verbosity=args.verbose) + context = nemubot.Bot(modules_paths=modules_paths, data_store=datastore.XML(args.data_path), + verbosity=args.verbose) if args.no_connect: context.noautoconnect = True diff --git a/nemubot/__init__.py b/nemubot/__init__.py index 09f3da0..394e11d 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -32,6 +32,7 @@ __version__ = '4.0.dev0' __author__ = 'nemunaire' from nemubot.consumer import Consumer, EventConsumer, MessageConsumer +from nemubot import datastore from nemubot.event import ModuleEvent from nemubot.hooks.messagehook import MessageHook from nemubot.hooks.manager import HooksManager @@ -45,13 +46,13 @@ 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_path=None, verbosity=0): + data_store=datastore.Abstract(), verbosity=0): """Initialize the bot context Keyword arguments: ip -- The external IP of the bot (default: 127.0.0.1) modules_paths -- Paths to all directories where looking for module - data_path -- Path to directory where store bot context data + data_store -- An instance of the nemubot datastore for bot's modules """ threading.Thread.__init__(self) @@ -65,9 +66,8 @@ class Bot(threading.Thread): # Context paths self.modules_paths = modules_paths - self.data_path = None - if data_path is not None: - self.set_data_path(data_path) + self.datastore = data_store + self.datastore.open() # Keep global context: servers and modules self.servers = dict() @@ -180,52 +180,6 @@ class Bot(threading.Thread): logger.exception("Uncatched exception on server read") - # Data path - - def set_data_path(self, path): - """Check if the given path is valid and unlock, - then lock the directory and set the variable - - Argument: - path -- the location - """ - - lock_file = os.path.join(path, ".used_by_nemubot") - - if os.path.isdir(path): - if not os.path.exists(lock_file): - if self.data_path is not None: - self.close_data_path(self.data_path) - self.data_path = path - - with open(lock_file, 'w') as lf: - lf.write(str(os.getpid())) - return True - - else: - with open(lock_file, 'r') as lf: - pid = lf.readline() - raise Exception("Data dir already locked, by PID %s" % pid) - return False - - - def close_data_path(self, path=None): - """Release a locked path - - Argument: - path -- the location, self.data_path if None - """ - - if path is None: - path = self.data_path - - lock_file = os.path.join(path, ".used_by_nemubot") - if os.path.isdir(path) and os.path.exists(lock_file): - os.unlink(lock_file) - return True - return False - - # Events methods def add_event(self, evt, eid=None, module_src=None): @@ -408,6 +362,13 @@ class Bot(threading.Thread): if module.__name__ in self.modules: self.unload_module(module.__name__) + # Overwrite print built-in + def prnt(*args): + print("[%s]" % module.__name__, *args) + if hasattr(module, "logger"): + module.logger.info(" ".join(args)) + module.print = prnt + self.modules[module.__name__] = module return True @@ -456,7 +417,7 @@ class Bot(threading.Thread): def quit(self): """Save and unload modules and disconnect servers""" - self.close_data_path() + self.datastore.close() if self.event_timer is not None: logger.info("Stop the event timer...") @@ -492,9 +453,9 @@ def hotswap(bak): bak.stop = True if bak.event_timer is not None: bak.event_timer.cancel() - bak.close_data_path() + bak.datastore.close() - new = Bot(str(bak.ip), bak.modules_paths, bak.data_path) + new = Bot(str(bak.ip), bak.modules_paths, bak.datastore) new.servers = bak.servers new.modules = bak.modules new.modules_configuration = bak.modules_configuration diff --git a/nemubot/datastore/__init__.py b/nemubot/datastore/__init__.py new file mode 100644 index 0000000..323a160 --- /dev/null +++ b/nemubot/datastore/__init__.py @@ -0,0 +1,18 @@ +# 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.datastore.abstract import Abstract +from nemubot.datastore.xml import XML diff --git a/nemubot/datastore/abstract.py b/nemubot/datastore/abstract.py new file mode 100644 index 0000000..42b7592 --- /dev/null +++ b/nemubot/datastore/abstract.py @@ -0,0 +1,61 @@ +# 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.tools.xmlparser import module_state + + +class Abstract: + + """Abstract implementation of a module data store, that always return an + empty set""" + + def open(self): + return + + def close(self): + return + + def load(self, module): + """Load data for the given module + + Argument: + module -- the module name of data to load + + Return: + The loaded data + """ + + return module_state.ModuleState("nemubotstate") + + def save(self, module, data): + """Load data for the given module + + Argument: + module -- the module name of data to load + data -- the new data to save + + Return: + Saving status + """ + + return True + + def __enter__(self): + self.open() + return self + + def __exit__(self, type, value, traceback): + self.close() diff --git a/nemubot/datastore/xml.py b/nemubot/datastore/xml.py new file mode 100644 index 0000000..2518af2 --- /dev/null +++ b/nemubot/datastore/xml.py @@ -0,0 +1,81 @@ +# 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 os + +from nemubot.datastore.abstract import Abstract +from nemubot.tools.xmlparser import parse_file + + +class XML(Abstract): + + """A concrete implementation of a data store that relies on XML files""" + + def __init__(self, basedir): + self.basedir = basedir + + def open(self): + """Lock the directory""" + + if os.path.isdir(self.basedir): + lock_file = os.path.join(self.basedir, ".used_by_nemubot") + if not os.path.exists(lock_file): + with open(lock_file, 'w') as lf: + lf.write(str(os.getpid())) + return True + + else: + with open(lock_file, 'r') as lf: + pid = lf.readline() + raise Exception("Data dir already locked, by PID %s" % pid) + return False + + def close(self): + """Release a locked path""" + + lock_file = os.path.join(self.basedir, ".used_by_nemubot") + if os.path.isdir(self.basedir) and os.path.exists(lock_file): + os.unlink(lock_file) + return True + return False + + def _get_data_file_path(self, module): + """Get the path to the module data file""" + + return os.path.join(self.basedir, module + ".xml") + + def load(self, module): + """Load data for the given module + + Argument: + module -- the module name of data to load + """ + + data_file = self._get_data_file_path(module) + if os.path.isfile(data_file): + return parse_file(data_file) + else: + return Abstract.load(self, module) + + def save(self, module, data): + """Load data for the given module + + Argument: + module -- the module name of data to load + data -- the new data to save + """ + + return data.save(self._get_data_file_path(module)) diff --git a/nemubot/importer.py b/nemubot/importer.py index b303a34..869e5e1 100644 --- a/nemubot/importer.py +++ b/nemubot/importer.py @@ -86,18 +86,13 @@ class ModuleLoader(SourceFileLoader): module.__LOADED__ = True module.logger = logging.getLogger("nemubot.module." + fullname) - def prnt(*args): - print("[%s]" % module.__name__, *args) - module.logger.info(" ".join(args)) def prnt_dbg(*args): if module.DEBUG: print("{%s}" % module.__name__, *args) module.logger.debug(" ".join(args)) def mod_save(): - fpath = os.path.join(self.context.data_path, module.__name__ + ".xml") - module.print_debug("Saving DATAS to " + fpath) - module.DATAS.save(fpath) + self.context.datastore.save(module.__name__, module.DATAS) def send_response(server, res): if server in self.context.servers: @@ -129,7 +124,6 @@ class ModuleLoader(SourceFileLoader): module.REGISTERED_HOOKS = list() module.REGISTERED_EVENTS = list() module.DEBUG = self.context.verbosity > 0 - module.print = prnt module.print_debug = prnt_dbg module.send_response = send_response module.add_hook = add_hook @@ -139,15 +133,8 @@ class ModuleLoader(SourceFileLoader): module.del_event = del_event if not hasattr(module, "NODATA"): - module.DATAS = module_state.ModuleState("nemubotstate") - if self.context.data_path is not None: - data_file = os.path.join(self.context.data_path, - module.__name__ + ".xml") - if os.path.isfile(data_file): - module.DATAS = parse_file(data_file) - module.save = mod_save - else: - module.save = lambda: False + module.DATAS = self.context.datastore.load(module.__name__) + module.save = mod_save else: module.DATAS = None module.save = lambda: False From bafc14bd79423280a075343a89305747f9b701bd Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 10 Feb 2015 00:42:38 +0100 Subject: [PATCH 248/674] add_server can now be used before context start --- nemubot/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nemubot/__init__.py b/nemubot/__init__.py index 394e11d..e59543e 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -320,7 +320,7 @@ class Bot(threading.Thread): c.start() - def add_server(self, srv, autoconnect=False): + def add_server(self, srv, autoconnect=True): """Add a new server to the context Arguments: @@ -330,7 +330,8 @@ class Bot(threading.Thread): if srv.id not in self.servers: self.servers[srv.id] = srv - if autoconnect and not hasattr(self, "noautoconnect"): + if (autoconnect and not hasattr(self, "noautoconnect") and + hasattr(self, "stop") and not self.stop): srv.open() return True From 1f5364c387e95d558b08022214ff29b3d018f708 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 10 Feb 2015 03:46:50 +0100 Subject: [PATCH 249/674] Reduce importance of importer; new tiny context for each module, instead of having entire bot context --- bin/nemubot | 2 +- nemubot/__init__.py | 42 +++++++---- nemubot/consumer.py | 2 +- nemubot/importer.py | 152 +++------------------------------------ nemubot/modulecontext.py | 125 ++++++++++++++++++++++++++++++++ 5 files changed, 165 insertions(+), 158 deletions(-) create mode 100644 nemubot/modulecontext.py diff --git a/bin/nemubot b/bin/nemubot index 0650e9f..8f90c33 100755 --- a/bin/nemubot +++ b/bin/nemubot @@ -102,7 +102,7 @@ if __name__ == "__main__": prmpt = nemubot.prompt.Prompt() # Register the hook for futur import - sys.meta_path.append(ModuleFinder(context, prmpt)) + sys.meta_path.append(ModuleFinder(context.modules_paths, context.add_module)) # Load requested configuration files for path in args.files: diff --git a/nemubot/__init__.py b/nemubot/__init__.py index e59543e..af02327 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -34,10 +34,14 @@ __author__ = 'nemunaire' from nemubot.consumer import Consumer, EventConsumer, MessageConsumer from nemubot import datastore from nemubot.event import ModuleEvent +import nemubot.hooks from nemubot.hooks.messagehook import MessageHook from nemubot.hooks.manager import HooksManager +from nemubot.modulecontext import ModuleContext from nemubot.networkbot import NetworkBot +context = ModuleContext(None, None) + logger = logging.getLogger("nemubot") @@ -228,7 +232,7 @@ class Bot(threading.Thread): # Register the event in the source module if module_src is not None: - module_src.REGISTERED_EVENTS.append(evt.id) + module_src.__nemubot_context__.events.append(evt.id) evt.module_src = module_src logger.info("New event registered: %s -> %s", evt.id, evt) @@ -260,7 +264,7 @@ class Bot(threading.Thread): self.events.remove(self.events[0]) self._update_event_timer() if module_src is not None: - module_src.REGISTERED_EVENTS.remove(id) + module_src.__nemubot_context__.events.remove(id) return True for evt in self.events: @@ -268,7 +272,7 @@ class Bot(threading.Thread): self.events.remove(evt) if module_src is not None: - module_src.REGISTERED_EVENTS.remove(evt.id) + module_src.__nemubot_context__.events.remove(evt.id) return True return False @@ -330,8 +334,7 @@ class Bot(threading.Thread): if srv.id not in self.servers: self.servers[srv.id] = srv - if (autoconnect and not hasattr(self, "noautoconnect") and - hasattr(self, "stop") and not self.stop): + if autoconnect and not hasattr(self, "noautoconnect"): srv.open() return True @@ -370,23 +373,36 @@ class Bot(threading.Thread): module.logger.info(" ".join(args)) module.print = prnt + # Create module context + module.__nemubot_context__ = ModuleContext(self, module) + + # Replace imported context by real one + for attr in module.__dict__: + if attr != "__nemubot_context__" and type(module.__dict__[attr]) == ModuleContext: + module.__dict__[attr] = module.__nemubot_context__ + + # Register decorated functions + for s, h in nemubot.hooks.last_registered: + module.__nemubot_context__.add_hook(s, h) + nemubot.hooks.last_registered = [] + + # Save a reference to the module self.modules[module.__name__] = module + + # Launch the module + if hasattr(module, "load"): + module.load(module.__nemubot_context__) + return True def unload_module(self, name): """Unload a module""" if name in self.modules: - self.modules[name].print_debug("Unloading module %s" % name) - self.modules[name].save() + self.modules[name].print("Unloading module %s" % name) if hasattr(self.modules[name], "unload"): self.modules[name].unload(self) - # Remove registered hooks - for (s, h) in self.modules[name].REGISTERED_HOOKS: - self.hooks.del_hook(h, s) - # Remove registered events - for e in self.modules[name].REGISTERED_EVENTS: - self.del_event(e) + self.modules[name].__nemubot_context__.unload() # Remove from the dict del self.modules[name] logger.info("Module `%s' successfully unloaded.", name) diff --git a/nemubot/consumer.py b/nemubot/consumer.py index 371f69b..2594601 100644 --- a/nemubot/consumer.py +++ b/nemubot/consumer.py @@ -203,7 +203,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.REGISTERED_EVENTS.remove(self.evt.id) + self.evt.module_src.__nemubot_context__.events.remove(self.evt.id) diff --git a/nemubot/importer.py b/nemubot/importer.py index 869e5e1..5984cb9 100644 --- a/nemubot/importer.py +++ b/nemubot/importer.py @@ -34,21 +34,21 @@ logger = logging.getLogger("nemubot.importer") class ModuleFinder(Finder): - def __init__(self, context, prompt): - self.context = context - self.prompt = prompt + def __init__(self, modules_paths, add_module): + self.modules_paths = modules_paths + self.add_module = add_module def find_module(self, fullname, path=None): # print ("looking for", fullname, "in", path) # Search only for new nemubot modules (packages init) if path is None: - for mpath in self.context.modules_paths: + for mpath in self.modules_paths: # print ("looking for", fullname, "in", mpath) if os.path.isfile(os.path.join(mpath, fullname + ".py")): - return ModuleLoader(self.context, self.prompt, fullname, + 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")): - return ModuleLoader(self.context, self.prompt, fullname, + return ModuleLoader(self.add_module, fullname, os.path.join( os.path.join(mpath, fullname), "__init__.py")) @@ -58,153 +58,19 @@ class ModuleFinder(Finder): class ModuleLoader(SourceFileLoader): - def __init__(self, context, prompt, fullname, path): - self.context = context - self.prompt = prompt - - if fullname in self.context.modules_configuration: - self.config = self.context.modules_configuration[fullname] - else: - self.config = None - + def __init__(self, add_module, fullname, path): + self.add_module = add_module SourceFileLoader.__init__(self, fullname, path) def load_module(self, fullname): module = SourceFileLoader.load_module(self, fullname) - # Check that is a valid nemubot module - if not hasattr(module, "nemubotversion"): - raise ImportError("Module `%s' is not a nemubot module." % - fullname) - # Check module version - if LooseVersion(__version__) < LooseVersion(str(module.nemubotversion)): - raise ImportError("Module `%s' is not compatible with this " - "version." % fullname) - - # Set module common functions and data - module.__LOADED__ = True - module.logger = logging.getLogger("nemubot.module." + fullname) - - def prnt_dbg(*args): - if module.DEBUG: - print("{%s}" % module.__name__, *args) - module.logger.debug(" ".join(args)) - - def mod_save(): - self.context.datastore.save(module.__name__, module.DATAS) - - def send_response(server, res): - if server in self.context.servers: - r = res.next_response() - if r.server is not None: - return self.context.servers[r.server].send_response(r) - else: - return self.context.servers[server].send_response(r) - else: - module.logger.error("Try to send a message to the unknown server: %s", server) - return False - - def add_hook(store, hook): - store = convert_legacy_store(store) - module.REGISTERED_HOOKS.append((store, hook)) - return self.context.hooks.add_hook(hook, store) - def del_hook(store, hook): - store = convert_legacy_store(store) - module.REGISTERED_HOOKS.remove((store, hook)) - return self.context.hooks.del_hook(hook, store) - def add_event(evt, eid=None): - return self.context.add_event(evt, eid, module_src=module) - def add_event_eid(evt, eid): - return add_event(evt, eid) - def del_event(evt): - return self.context.del_event(evt, module_src=module) - - # Set module common functions and datas - module.REGISTERED_HOOKS = list() - module.REGISTERED_EVENTS = list() - module.DEBUG = self.context.verbosity > 0 - module.print_debug = prnt_dbg - module.send_response = send_response - module.add_hook = add_hook - module.del_hook = del_hook - module.add_event = add_event - module.add_event_eid = add_event_eid - module.del_event = del_event - - if not hasattr(module, "NODATA"): - module.DATAS = self.context.datastore.load(module.__name__) - module.save = mod_save - else: - module.DATAS = None - module.save = lambda: False - module.CONF = self.config - - # Load dependancies - if module.CONF is not None and module.CONF.hasNode("dependson"): - module.MODS = dict() - for depend in module.CONF.getNodes("dependson"): - for md in MODS: - if md.name == depend["name"]: - mod.MODS[md.name] = md - break - if depend["name"] not in module.MODS: - logger.error("In module `%s', module `%s' require by this " - "module but is not loaded.", module.__name__, - depend["name"]) - return - # Add the module to the global modules list - if self.context.add_module(module): - - # Launch the module - if hasattr(module, "load"): - module.load(self.context) - - # Register hooks - register_hooks(module, self.context, self.prompt) - + if self.add_module(module): logger.info("Module '%s' successfully loaded.", module.__name__) else: logger.error("An error occurs while importing `%s'.", module.__name__) raise ImportError("An error occurs while importing `%s'." % module.__name__) return module - - -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_TextMessage" - elif old == "all_post": - return "post" - elif old == "all_pre": - return "pre" - else: - print("UNKNOWN store:", old) - return old - - -def register_hooks(module, context, prompt): - """Register all available hooks - - Arguments: - module -- the loaded Python module - context -- bot context - prompt -- the current Prompt instance - """ - - # Register decorated functions - for s, h in nemubot.hooks.last_registered: - if s == "prompt_cmd": - prompt.add_cap_hook(h.name, h.call) - elif s == "prompt_list": - prompt.add_list_hook(h.name, h.call) - else: - s = convert_legacy_store(s) - module.REGISTERED_HOOKS.append((s, h)) - context.hooks.add_hook(h, s) - nemubot.hooks.last_registered = [] diff --git a/nemubot/modulecontext.py b/nemubot/modulecontext.py new file mode 100644 index 0000000..5ee8a08 --- /dev/null +++ b/nemubot/modulecontext.py @@ -0,0 +1,125 @@ +# 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.tools.xmlparser import module_state + + +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_TextMessage" + elif old == "all_post": + return "post" + elif old == "all_pre": + return "pre" + else: + print("UNKNOWN store:", old) + return old + + +class ModuleContext: + + def __init__(self, context, module): + """Initialize the module context + + arguments: + context -- the bot context + module -- the module + """ + + # 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: + self.config = None + + self.hooks = list() + self.events = list() + self.debug = context.verbosity > 0 if context is not None else False + + # Define some callbacks + if context is not None: + # Load module data + self.data = context.datastore.load(module.__name__) + + def add_hook(store, hook): + store = convert_legacy_store(store) + self.hooks.append((store, hook)) + return context.hooks.add_hook(hook, store) + def del_hook(store, hook): + store = convert_legacy_store(store) + self.hooks.remove((store, hook)) + return context.hooks.del_hook(hook, store) + 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): + if server in context.servers: + r = res.next_response() + if r.server is not None: + return context.servers[r.server].send_response(r) + else: + return context.servers[server].send_response(r) + else: + module.logger.error("Try to send a message to the unknown server: %s", server) + return False + + else: + 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 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): + print(res) + + def save(): + context.datastore.save(module.__name__, self.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 + + + def unload(self): + """Perform actions for unloading the module""" + + # Remove registered hooks + for (s, h) in self.hooks: + self.del_hook(s, h) + + # Remove registered events + for e in self.events: + self.del_event(e) + + self.save() From 28005e5654454eae67361adde034274afea155ea Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 11 Feb 2015 18:12:39 +0100 Subject: [PATCH 250/674] Convert modules to new importer --- modules/alias.py | 50 +++++++++---------- modules/birthday.py | 22 ++++----- modules/bonneannee.py | 13 ++--- modules/books.py | 11 +++-- modules/conjugaison.py | 2 - modules/cve.py | 1 - modules/ddg/__init__.py | 4 +- modules/events.py | 78 +++++++++++++++--------------- modules/imdb.py | 3 -- modules/mapquest.py | 6 +-- modules/mediawiki.py | 5 -- modules/networking/__init__.py | 15 +++--- modules/networking/watchWebsite.py | 2 +- modules/sms.py | 22 ++++----- modules/speak.py | 5 +- modules/spell/__init__.py | 22 ++++----- modules/syno.py | 5 +- modules/tpb.py | 6 +-- modules/translate.py | 6 +-- modules/velib.py | 11 ++--- modules/weather.py | 34 ++++++------- modules/worldcup.py | 27 +++++------ modules/ycc.py | 1 - 23 files changed, 165 insertions(+), 186 deletions(-) diff --git a/modules/alias.py b/modules/alias.py index 3365141..050e2d6 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -7,6 +7,7 @@ import sys from datetime import datetime, timezone import shlex +from nemubot import context from nemubot.exception import IRCException from nemubot.hooks import hook from nemubot.message import TextMessage, Command @@ -19,13 +20,12 @@ from more import Response def load(context): """Load this module""" - global DATAS - if not DATAS.hasNode("aliases"): - DATAS.addChild(ModuleState("aliases")) - DATAS.getNode("aliases").setIndex("alias") - if not DATAS.hasNode("variables"): - DATAS.addChild(ModuleState("variables")) - DATAS.getNode("variables").setIndex("name") + if not context.data.hasNode("aliases"): + context.data.addChild(ModuleState("aliases")) + context.data.getNode("aliases").setIndex("alias") + if not context.data.hasNode("variables"): + context.data.addChild(ModuleState("variables")) + context.data.getNode("variables").setIndex("name") def help_full(): @@ -37,7 +37,7 @@ def set_variable(name, value, creator): var["name"] = name var["value"] = value var["creator"] = creator - DATAS.getNode("variables").addChild(var) + context.data.getNode("variables").addChild(var) def get_variable(name, msg=None): @@ -47,8 +47,8 @@ def get_variable(name, msg=None): return msg.channel elif name == "date": return datetime.now(timezone.utc).strftime("%c") - elif name in DATAS.getNode("variables").index: - return DATAS.getNode("variables").index[name]["value"] + elif name in context.data.getNode("variables").index: + return context.data.getNode("variables").index[name]["value"] else: return "" @@ -59,7 +59,7 @@ def cmd_set(msg): set_variable(msg.cmds[1], " ".join(msg.cmds[2:]), msg.nick) res = Response("Variable \$%s définie." % msg.cmds[1], channel=msg.channel) - save() + context.save() return res return Response("!set prend au minimum deux arguments : " "le nom de la variable et sa valeur.", @@ -71,7 +71,7 @@ def cmd_listalias(msg): if len(msg.cmds) > 1: res = list() for user in msg.cmds[1:]: - als = [x["alias"] for x in DATAS.getNode("aliases").index.values() if x["creator"] == user] + als = [x["alias"] for x in context.data.getNode("aliases").index.values() if x["creator"] == user] if len(als) > 0: res.append("Alias créés par %s : %s" % (user, ", ".join(als))) else: @@ -79,7 +79,7 @@ def cmd_listalias(msg): return Response(" ; ".join(res), channel=msg.channel) else: return Response("Alias connus : %s." % - ", ".join(DATAS.getNode("aliases").index.keys()), + ", ".join(context.data.getNode("aliases").index.keys()), channel=msg.channel) @@ -88,7 +88,7 @@ def cmd_listvars(msg): if len(msg.cmds) > 1: res = list() for user in msg.cmds[1:]: - als = [x["alias"] for x in DATAS.getNode("variables").index.values() if x["creator"] == user] + als = [x["alias"] for x in context.data.getNode("variables").index.values() if x["creator"] == user] if len(als) > 0: res.append("Variables créées par %s : %s" % (user, ", ".join(als))) else: @@ -96,7 +96,7 @@ def cmd_listvars(msg): return Response(" ; ".join(res), channel=msg.channel) else: return Response("Variables connues : %s." % - ", ".join(DATAS.getNode("variables").index.keys()), + ", ".join(context.data.getNode("variables").index.keys()), channel=msg.channel) @@ -107,9 +107,9 @@ def cmd_alias(msg): for alias in msg.cmds[1:]: if alias[0] == "!": alias = alias[1:] - if alias in DATAS.getNode("aliases").index: + if alias in context.data.getNode("aliases").index: res.append(Response("!%s correspond à %s" % - (alias, DATAS.getNode("aliases").index[alias]["origin"]), + (alias, context.data.getNode("aliases").index[alias]["origin"]), channel=msg.channel)) else: res.append(Response("!%s n'est pas un alias" % alias, @@ -127,9 +127,9 @@ def cmd_unalias(msg): for alias in msg.cmds[1:]: if alias[0] == "!" and len(alias) > 1: alias = alias[1:] - if alias in DATAS.getNode("aliases").index: - if DATAS.getNode("aliases").index[alias]["creator"] == msg.nick or msg.frm_owner: - DATAS.getNode("aliases").delChild(DATAS.getNode("aliases").index[alias]) + if alias in context.data.getNode("aliases").index: + if context.data.getNode("aliases").index[alias]["creator"] == msg.nick or msg.frm_owner: + context.data.getNode("aliases").delChild(context.data.getNode("aliases").index[alias]) res.append(Response("%s a bien été supprimé" % alias, channel=msg.channel)) else: @@ -167,8 +167,8 @@ def replace_variables(cnt, msg=None): @hook("pre_Command") def treat_alias(msg): - if msg.cmd in DATAS.getNode("aliases").index: - txt = DATAS.getNode("aliases").index[msg.cmd]["origin"] + 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:] @@ -190,16 +190,16 @@ def parseask(msg): global ALIAS if re.match(".*(set|cr[ée]{2}|nouvel(le)?) alias.*", msg.text) is not None: result = re.match(".*alias !?([^ ]+) (pour|=|:) (.+)$", msg.text) - if result.group(1) in DATAS.getNode("aliases").index or result.group(3).find("alias") >= 0: + if result.group(1) in context.data.getNode("aliases").index or result.group(3).find("alias") >= 0: raise IRCException("cet alias est déjà défini.") else: alias = ModuleState("alias") alias["alias"] = result.group(1) alias["origin"] = result.group(3) alias["creator"] = msg.nick - DATAS.getNode("aliases").addChild(alias) + context.data.getNode("aliases").addChild(alias) res = Response("Nouvel alias %s défini avec succès." % result.group(1), channel=msg.channel) - save() + context.save() return res return None diff --git a/modules/birthday.py b/modules/birthday.py index 8137a08..337b20f 100644 --- a/modules/birthday.py +++ b/modules/birthday.py @@ -6,6 +6,7 @@ import re import sys from datetime import date, datetime +from nemubot import context from nemubot.exception import IRCException from nemubot.hooks import hook from nemubot.tools.countdown import countdown_format @@ -18,8 +19,7 @@ from more import Response def load(context): - global DATAS - DATAS.setIndex("name", "birthday") + context.data.setIndex("name", "birthday") def help_full(): @@ -38,10 +38,10 @@ def findName(msg): matches = [] - if name in DATAS.index: + if name in context.data.index: matches.append(name) else: - for k in DATAS.index.keys(): + for k in context.data.index.keys(): if k.find(name) == 0: matches.append(k) return (matches, name) @@ -52,13 +52,13 @@ def cmd_anniv(msg): (matches, name) = findName(msg) if len(matches) == 1: name = matches[0] - tyd = DATAS.index[name].getDate("born") + tyd = context.data.index[name].getDate("born") tyd = datetime(date.today().year, tyd.month, tyd.day) if (tyd.day == datetime.today().day and tyd.month == datetime.today().month): return Response(countdown_format( - DATAS.index[name].getDate("born"), "", + context.data.index[name].getDate("born"), "", "C'est aujourd'hui l'anniversaire de %s !" " Il a %s. Joyeux anniversaire :)" % (name, "%s")), msg.channel) @@ -81,7 +81,7 @@ def cmd_age(msg): (matches, name) = findName(msg) if len(matches) == 1: name = matches[0] - d = DATAS.index[name].getDate("born") + d = context.data.index[name].getDate("born") return Response(countdown_format(d, "%s va naître dans %s." % (name, "%s"), @@ -107,14 +107,14 @@ def parseask(msg): 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 - if nick.lower() in DATAS.index: - DATAS.index[nick.lower()]["born"] = extDate + if nick.lower() in context.data.index: + context.data.index[nick.lower()]["born"] = extDate else: ms = ModuleState("birthday") ms.setAttribute("name", nick.lower()) ms.setAttribute("born", extDate) - DATAS.addChild(ms) - save() + context.data.addChild(ms) + context.save() return Response("ok, c'est noté, %s est né le %s" % (nick, extDate.strftime("%A %d %B %Y à %H:%M")), msg.channel, diff --git a/modules/bonneannee.py b/modules/bonneannee.py index 2eb9b7e..d1eef72 100644 --- a/modules/bonneannee.py +++ b/modules/bonneannee.py @@ -4,6 +4,7 @@ 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 @@ -17,7 +18,7 @@ yrn = datetime.now(timezone.utc).year + 1 def load(context): - if not CONF or not CONF.hasNode("sayon"): + if not context.config or not context.config.hasNode("sayon"): print("You can append in your configuration some balise to " "automaticaly wish an happy new year on some channels like:\n" "<sayon hostid=\"nemubot@irc.freenode.net:6667\" " @@ -26,19 +27,19 @@ def load(context): def bonneannee(): txt = "Bonne année %d !" % yrn print(txt) - if CONF and CONF.hasNode("sayon"): - for sayon in CONF.getNodes("sayon"): + if context.config and context.config.hasNode("sayon"): + for sayon in context.config.getNodes("sayon"): if "hostid" not in sayon or "channel" not in sayon: print("Error: missing hostif or channel") continue srv = sayon["hostid"] chan = sayon["channel"] - send_response(srv, Response(txt, chan)) + context.send_response(srv, Response(txt, chan)) d = datetime(yrn, 1, 1, 0, 0, 0, 0, timezone.utc) - datetime.now(timezone.utc) - add_event(ModuleEvent(interval=0, offset=d.total_seconds(), - call=bonneannee)) + context.add_event(ModuleEvent(interval=0, offset=d.total_seconds(), + call=bonneannee)) @hook("cmd_hook", "newyear") diff --git a/modules/books.py b/modules/books.py index 5d3bc57..f5c0c7f 100644 --- a/modules/books.py +++ b/modules/books.py @@ -4,6 +4,7 @@ import urllib +from nemubot import context from nemubot.exception import IRCException from nemubot.hooks import hook from nemubot.tools import web @@ -14,7 +15,7 @@ from more import Response def load(context): - if not CONF or not CONF.hasNode("goodreadsapi") or not CONF.getNode("goodreadsapi").hasAttribute("key"): + if not context.config or not context.config.hasNode("goodreadsapi") or not context.config.getNode("goodreadsapi").hasAttribute("key"): print ("You need a Goodreads API key in order to use this " "module. Add it to the module configuration file:\n<goodreadsapi" " key=\"XXXXXXXXXXXXXXXX\" />\nGet one at " @@ -25,7 +26,7 @@ def load(context): def get_book(title): """Retrieve a book from its title""" response = web.getXML("https://www.goodreads.com/book/title.xml?key=%s&title=%s" % - (CONF.getNode("goodreadsapi")["key"], urllib.parse.quote(title))) + (context.config.getNode("goodreadsapi")["key"], urllib.parse.quote(title))) if response is not None and response.hasNode("book"): return response.getNode("book") else: @@ -35,7 +36,7 @@ def get_book(title): 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" % - (CONF.getNode("goodreadsapi")["key"], urllib.parse.quote(title))) + (context.config.getNode("goodreadsapi")["key"], urllib.parse.quote(title))) if response is not None and response.hasNode("search"): return response.getNode("search").getNode("results").getNodes("work") else: @@ -45,10 +46,10 @@ def search_books(title): 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), CONF.getNode("goodreadsapi")["key"])) + (urllib.parse.quote(name), context.config.getNode("goodreadsapi")["key"])) if response is not None and response.hasNode("author") and response.getNode("author").hasAttribute("id"): response = web.getXML("https://www.goodreads.com/author/show/%s.xml?key=%s" % - (urllib.parse.quote(response.getNode("author")["id"]), CONF.getNode("goodreadsapi")["key"])) + (urllib.parse.quote(response.getNode("author")["id"]), context.config.getNode("goodreadsapi")["key"])) if response is not None and response.hasNode("author"): return response.getNode("author") return None diff --git a/modules/conjugaison.py b/modules/conjugaison.py index 35faa40..656118c 100644 --- a/modules/conjugaison.py +++ b/modules/conjugaison.py @@ -44,7 +44,6 @@ def cmd_conjug(msg): "sa conjugaison!") tens = ' '.join(msg.cmds[1:-1]) - print_debug(tens) verb = msg.cmds[-1] @@ -60,7 +59,6 @@ def cmd_conjug(msg): def get_conjug(verb, stringTens): url = ("http://leconjugueur.lefigaro.fr/conjugaison/verbe/%s.html" % quote(verb.encode("ISO-8859-1"))) - print_debug(url) page = web.getURLContent(url) if page is not None: diff --git a/modules/cve.py b/modules/cve.py index e02bc55..5f6435d 100644 --- a/modules/cve.py +++ b/modules/cve.py @@ -1,6 +1,5 @@ import urllib.request from bs4 import BeautifulSoup -import pprint from nemubot.hooks import hook from more import Response diff --git a/modules/ddg/__init__.py b/modules/ddg/__init__.py index 94e3587..63ef340 100644 --- a/modules/ddg/__init__.py +++ b/modules/ddg/__init__.py @@ -4,6 +4,7 @@ import imp +from nemubot import context from nemubot.hooks import hook nemubotversion = 3.4 @@ -15,8 +16,7 @@ from . import UrbanDictionnary from . import WFASearch def load(context): - global CONF - WFASearch.CONF = CONF + WFASearch.CONF = context.config def reload(): imp.reload(DDGSearch) diff --git a/modules/events.py b/modules/events.py index 0731cc5..29a5918 100644 --- a/modules/events.py +++ b/modules/events.py @@ -10,6 +10,7 @@ import time import threading import traceback +from nemubot import context from nemubot.exception import IRCException from nemubot.event import ModuleEvent from nemubot.hooks import hook @@ -22,28 +23,25 @@ nemubotversion = 3.4 from more import Response def help_full (): - return "This module store a lot of events: ny, we, " + (", ".join(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())) + "\n!eventslist: gets list of timer\n!start /something/: launch a timer" def load(context): - global DATAS #Define the index - DATAS.setIndex("name") + context.data.setIndex("name") - for evt in DATAS.index.keys(): - if DATAS.index[evt].hasAttribute("end"): - event = ModuleEvent(call=fini, call_data=dict(strend=DATAS.index[evt])) - if DATAS.index[evt]["server"] not in context.servers: - print("WARNING: registering event for a unexistant server: %s, please connect to it." % DATAS.index[evt]["server"]) - event._end = DATAS.index[evt].getDate("end") - idt = add_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: - DATAS.index[evt]["_id"] = idt + context.data.index[evt]["_id"] = idt def fini(d, strend): - send_response(strend["server"], Response("%s arrivé à échéance." % strend["name"], channel=strend["channel"], nick=strend["proprio"])) - DATAS.delChild(DATAS.index[strend["name"]]) - save() + 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"]]) + context.save() @hook("cmd_hook", "goûter") def cmd_gouter(msg): @@ -68,7 +66,7 @@ def start_countdown(msg): """!start /something/: launch a timer""" if len(msg.cmds) < 2: raise IRCException("indique le nom d'un événement à chronométrer") - if msg.cmds[1] in DATAS.index: + if msg.cmds[1] in context.data.index: raise IRCException("%s existe déjà." % msg.cmds[1]) strnd = ModuleState("strend") @@ -77,7 +75,7 @@ def start_countdown(msg): strnd["proprio"] = msg.nick strnd["start"] = msg.date strnd["name"] = msg.cmds[1] - DATAS.addChild(strnd) + context.data.addChild(strnd) evt = ModuleEvent(call=fini, call_data=dict(strend=strnd)) @@ -106,9 +104,9 @@ def start_countdown(msg): else: strnd["end"] = datetime(now.year, now.month, now.day + 1, hou, minu, sec, timezone.utc) evt._end = strnd.getDate("end") - strnd["_id"] = add_event(evt) + strnd["_id"] = context.add_event(evt) except: - DATAS.delChild(strnd) + context.data.delChild(strnd) raise IRCException("Mauvais format de date pour l'événement %s. Il n'a pas été créé." % msg.cmds[1]) elif result1 is not None and len(result1) > 0: @@ -127,11 +125,11 @@ def start_countdown(msg): else: strnd["end"] += timedelta(seconds=int(t)) evt._end = strnd.getDate("end") - eid = add_event(evt) + eid = context.add_event(evt) if eid is not None: strnd["_id"] = eid - save() + context.save() if "end" in strnd: return Response("%s commencé le %s et se terminera le %s." % (msg.cmds[1], msg.date.strftime("%A %d %B %Y à %H:%M:%S"), @@ -148,16 +146,16 @@ def end_countdown(msg): if len(msg.cmds) < 2: raise IRCException("quel événement terminer ?") - if msg.cmds[1] in DATAS.index: - if DATAS.index[msg.cmds[1]]["proprio"] == msg.nick or (msg.cmds[0] == "forceend" and msg.frm_owner): - duration = countdown(msg.date - DATAS.index[msg.cmds[1]].getDate("start")) - del_event(DATAS.index[msg.cmds[1]]["_id"]) - DATAS.delChild(DATAS.index[msg.cmds[1]]) - save() + if msg.cmds[1] in context.data.index: + if context.data.index[msg.cmds[1]]["proprio"] == msg.nick or (msg.cmds[0] == "forceend" and msg.frm_owner): + duration = countdown(msg.date - context.data.index[msg.cmds[1]].getDate("start")) + context.del_event(context.data.index[msg.cmds[1]]["_id"]) + context.data.delChild(context.data.index[msg.cmds[1]]) + context.save() return Response("%s a duré %s." % (msg.cmds[1], duration), channel=msg.channel, nick=msg.nick) else: - raise IRCException("Vous ne pouvez pas terminer le compteur %s, créé par %s." % (msg.cmds[1], DATAS.index[msg.cmds[1]]["proprio"])) + raise IRCException("Vous ne pouvez pas terminer le compteur %s, créé par %s." % (msg.cmds[1], context.data.index[msg.cmds[1]]["proprio"])) else: return Response("%s n'est pas un compteur connu."% (msg.cmds[1]), channel=msg.channel, nick=msg.nick) @@ -167,31 +165,31 @@ def liste(msg): if len(msg.cmds) > 1: res = list() for user in msg.cmds[1:]: - cmptr = [x["name"] for x in DATAS.index.values() if x["proprio"] == user] + cmptr = [x["name"] for x in context.data.index.values() if x["proprio"] == user] if len(cmptr) > 0: res.append("Compteurs créés par %s : %s" % (user, ", ".join(cmptr))) else: res.append("%s n'a pas créé de compteur" % user) return Response(" ; ".join(res), channel=msg.channel) else: - return Response("Compteurs connus : %s." % ", ".join(DATAS.index.keys()), channel=msg.channel) + return Response("Compteurs connus : %s." % ", ".join(context.data.index.keys()), channel=msg.channel) @hook("cmd_default") def parseanswer(msg): - if msg.cmds[0] in DATAS.index: + if msg.cmds[0] in context.data.index: res = Response(channel=msg.channel) # Avoid message starting by ! which can be interpreted as command by other bots if msg.cmds[0][0] == "!": res.nick = msg.nick - if DATAS.index[msg.cmds[0]].name == "strend": - if DATAS.index[msg.cmds[0]].hasAttribute("end"): - res.append_message("%s commencé il y a %s et se terminera dans %s." % (msg.cmds[0], countdown(msg.date - DATAS.index[msg.cmds[0]].getDate("start")), countdown(DATAS.index[msg.cmds[0]].getDate("end") - msg.date))) + if context.data.index[msg.cmds[0]].name == "strend": + if context.data.index[msg.cmds[0]].hasAttribute("end"): + res.append_message("%s commencé il y a %s et se terminera dans %s." % (msg.cmds[0], countdown(msg.date - context.data.index[msg.cmds[0]].getDate("start")), countdown(context.data.index[msg.cmds[0]].getDate("end") - msg.date))) else: - res.append_message("%s commencé il y a %s." % (msg.cmds[0], countdown(msg.date - DATAS.index[msg.cmds[0]].getDate("start")))) + res.append_message("%s commencé il y a %s." % (msg.cmds[0], countdown(msg.date - context.data.index[msg.cmds[0]].getDate("start")))) else: - res.append_message(countdown_format(DATAS.index[msg.cmds[0]].getDate("start"), DATAS.index[msg.cmds[0]]["msg_before"], DATAS.index[msg.cmds[0]]["msg_after"])) + res.append_message(countdown_format(context.data.index[msg.cmds[0]].getDate("start"), context.data.index[msg.cmds[0]]["msg_before"], context.data.index[msg.cmds[0]]["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) @@ -202,7 +200,7 @@ def parseask(msg): name = re.match("^.*!([^ \"'@!]+).*$", msg.text) if name is None: raise IRCException("il faut que tu attribues une commande à l'événement.") - if name.group(1) in DATAS.index: + if name.group(1) in context.data.index: raise IRCException("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) @@ -231,8 +229,8 @@ def parseask(msg): evt["start"] = extDate evt["msg_after"] = msg_after evt["msg_before"] = msg_before - DATAS.addChild(evt) - save() + context.data.addChild(evt) + context.save() return Response("Nouvel événement !%s ajouté avec succès." % name.group(1), channel=msg.channel) @@ -243,8 +241,8 @@ def parseask(msg): evt["proprio"] = msg.nick evt["name"] = name.group(1) evt["msg_before"] = texts.group (2) - DATAS.addChild(evt) - save() + context.data.addChild(evt) + context.save() return Response("Nouvelle commande !%s ajoutée avec succès." % name.group(1), channel=msg.channel) diff --git a/modules/imdb.py b/modules/imdb.py index 4b381ae..fb1f0b3 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -34,8 +34,6 @@ def get_movie(title=None, year=None, imdbid=None, fullplot=True, tomatoes=False) if tomatoes: url += "tomatoes=true&" - print_debug(url) - # Make the request data = web.getJSON(url) @@ -55,7 +53,6 @@ def find_movies(title): # Built URL url = "http://www.omdbapi.com/?s=%s" % urllib.parse.quote(title) - print_debug(url) # Make the request data = web.getJSON(url) diff --git a/modules/mapquest.py b/modules/mapquest.py index 7d41f2b..17e842b 100644 --- a/modules/mapquest.py +++ b/modules/mapquest.py @@ -14,7 +14,7 @@ from more import Response def load(context): - if not CONF or not CONF.hasNode("mapquestapi") or not CONF.getNode("mapquestapi").hasAttribute("key"): + if not context.config or not context.config.hasNode("mapquestapi") or not context.config.getNode("mapquestapi").hasAttribute("key"): print ("You need a MapQuest API key in order to use this " "module. Add it to the module configuration file:\n<mapquestapi" " key=\"XXXXXXXXXXXXXXXX\" />\nRegister at " @@ -22,7 +22,7 @@ def load(context): return None from nemubot.hooks.messagehook import MessageHook - add_hook("cmd_hook", MessageHook(cmd_geocode, "geocode")) + context.add_hook("cmd_hook", MessageHook(cmd_geocode, "geocode")) def help_full(): @@ -31,7 +31,7 @@ def help_full(): def geocode(location): obj = web.getJSON("http://open.mapquestapi.com/geocoding/v1/address?key=%s&location=%s" % - (CONF.getNode("mapquestapi")["key"], quote(location))) + (context.config.getNode("mapquestapi")["key"], quote(location))) if "results" in obj and "locations" in obj["results"][0]: for loc in obj["results"][0]["locations"]: diff --git a/modules/mediawiki.py b/modules/mediawiki.py index 730911c..dc461bb 100644 --- a/modules/mediawiki.py +++ b/modules/mediawiki.py @@ -19,7 +19,6 @@ def get_namespaces(site, ssl=False): # Built URL url = "http%s://%s/w/api.php?format=json&action=query&meta=siteinfo&siprop=namespaces" % ( "s" if ssl else "", site) - print_debug(url) # Make the request data = web.getJSON(url) @@ -34,7 +33,6 @@ def get_raw_page(site, term, ssl=False): # 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)) - print_debug(url) # Make the request data = web.getJSON(url) @@ -50,7 +48,6 @@ def get_unwikitextified(site, wikitext, ssl=False): # Built URL url = "http%s://%s/w/api.php?format=json&action=expandtemplates&text=%s" % ( "s" if ssl else "", site, urllib.parse.quote(wikitext)) - print_debug(url) # Make the request data = web.getJSON(url) @@ -107,7 +104,6 @@ def opensearch(site, term, ssl=False): # Built URL url = "http%s://%s/w/api.php?format=xml&action=opensearch&search=%s" % ( "s" if ssl else "", site, urllib.parse.quote(term)) - print_debug(url) # Make the request response = web.getXML(url) @@ -123,7 +119,6 @@ def search(site, term, ssl=False): # 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)) - print_debug(url) # Make the request data = web.getJSON(url) diff --git a/modules/networking/__init__.py b/modules/networking/__init__.py index c6d960c..0caab06 100644 --- a/modules/networking/__init__.py +++ b/modules/networking/__init__.py @@ -17,15 +17,14 @@ from . import whois def load(context): for mod in [isup, page, w3c, watchWebsite, whois]: - mod.add_event = add_event - mod.del_event = del_event - mod.save = save + mod.add_event = context.add_event + mod.del_event = context.del_event + mod.save = context.save mod.print = print - mod.print_debug = print_debug - mod.send_response = send_response - page.load(CONF, add_hook) - watchWebsite.load(DATAS) - whois.load(CONF, add_hook) + mod.send_response = context.send_response + page.load(context.config, context.add_hook) + watchWebsite.load(context.data) + whois.load(context.config, context.add_hook) def help_full(): diff --git a/modules/networking/watchWebsite.py b/modules/networking/watchWebsite.py index 7035b4b..47b1b17 100644 --- a/modules/networking/watchWebsite.py +++ b/modules/networking/watchWebsite.py @@ -185,7 +185,7 @@ def start_watching(site, offset=0): """ o = urlparse(site["url"], "http") - print_debug("Add %s event for site: %s" % (site["type"], o.netloc)) + #print_debug("Add %s event for site: %s" % (site["type"], o.netloc)) evt = ModuleEvent(func=lambda url: page.render(url, None), cmp_data=site["lastcontent"], diff --git a/modules/sms.py b/modules/sms.py index dd467f7..add430b 100644 --- a/modules/sms.py +++ b/modules/sms.py @@ -9,6 +9,7 @@ import urllib.error import urllib.request import urllib.parse +from nemubot import context from nemubot.exception import IRCException from nemubot.hooks import hook from nemubot.tools.xmlparser.node import ModuleState @@ -18,8 +19,7 @@ nemubotversion = 3.4 from more import Response def load(context): - global DATAS - DATAS.setIndex("name", "phone") + context.data.setIndex("name", "phone") def help_full(): return "!sms /who/[,/who/[,...]] message: send a SMS to /who/." @@ -55,20 +55,20 @@ def cmd_sms(msg): # Check dests cur_epoch = time.mktime(time.localtime()); for u in msg.cmds[1].split(","): - if u not in DATAS.index: + if u not in context.data.index: raise IRCException("Désolé, je sais pas comment envoyer de SMS à %s." % u) - elif cur_epoch - float(DATAS.index[u]["lastuse"]) < 42: + 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) # Go! fails = list() for u in msg.cmds[1].split(","): - DATAS.index[u]["lastuse"] = cur_epoch + 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, DATAS.index[u]["user"], DATAS.index[u]["key"], " ".join(msg.cmds[2:])) + test = send_sms(frm, context.data.index[u]["user"], context.data.index[u]["key"], " ".join(msg.cmds[2:])) if test is not None: fails.append( "%s: %s" % (u, test) ) @@ -96,16 +96,16 @@ def parseask(msg): if test is not None: return Response("je n'ai pas pu enregistrer tes identifiants : %s" % test, msg.channel, msg.nick) - if msg.nick in DATAS.index: - DATAS.index[msg.nick]["user"] = apiuser - DATAS.index[msg.nick]["key"] = apikey + if msg.nick in context.data.index: + context.data.index[msg.nick]["user"] = apiuser + context.data.index[msg.nick]["key"] = apikey else: ms = ModuleState("phone") ms.setAttribute("name", msg.nick) ms.setAttribute("user", apiuser) ms.setAttribute("key", apikey) ms.setAttribute("lastuse", 0) - DATAS.addChild(ms) - save() + context.data.addChild(ms) + context.save() return Response("ok, c'est noté. Je t'ai envoyé un SMS pour tester ;)", msg.channel, msg.nick) diff --git a/modules/speak.py b/modules/speak.py index d8808b8..f5c88c2 100644 --- a/modules/speak.py +++ b/modules/speak.py @@ -20,12 +20,12 @@ SMILEY = list() CORRECTIONS = list() def load(context): - for smiley in CONF.getNodes("smiley"): + for smiley in context.config.getNodes("smiley"): if smiley.hasAttribute("txt") and smiley.hasAttribute("mood"): SMILEY.append((smiley.getAttribute("txt"), smiley.getAttribute("mood"))) print ("%d smileys loaded" % len(SMILEY)) - for correct in CONF.getNodes("correction"): + for correct in context.config.getNodes("correction"): if correct.hasAttribute("bad") and correct.hasAttribute("good"): CORRECTIONS.append((" " + (correct.getAttribute("bad") + " "), (" " + correct.getAttribute("good") + " "))) print ("%d corrections loaded" % len(CORRECTIONS)) @@ -38,7 +38,6 @@ class Speaker(Thread): while not queue.empty(): sentence = queue.get_nowait() lang = "fr" - print_debug(sentence) subprocess.call(["espeak", "-v", lang, "--", sentence]) queue.task_done() diff --git a/modules/spell/__init__.py b/modules/spell/__init__.py index 9e2faba..0953fae 100644 --- a/modules/spell/__init__.py +++ b/modules/spell/__init__.py @@ -5,6 +5,7 @@ import re from urllib.parse import quote +from nemubot import context from nemubot.exception import IRCException from nemubot.hooks import hook from nemubot.tools.xmlparser.node import ModuleState @@ -20,8 +21,7 @@ def help_full(): return "!spell [<lang>] <word>: give the correct spelling of <word> in <lang=fr>." def load(context): - global DATAS - DATAS.setIndex("name", "score") + context.data.setIndex("name", "score") @hook("cmd_hook", "spell") def cmd_spell(msg): @@ -50,27 +50,25 @@ def cmd_spell(msg): return Response(strRes, channel=msg.channel, nick=msg.nick) def add_score(nick, t): - global DATAS - if nick not in DATAS.index: + if nick not in context.data.index: st = ModuleState("score") st["name"] = nick - DATAS.addChild(st) + context.data.addChild(st) - if DATAS.index[nick].hasAttribute(t): - DATAS.index[nick][t] = DATAS.index[nick].getInt(t) + 1 + if context.data.index[nick].hasAttribute(t): + context.data.index[nick][t] = context.data.index[nick].getInt(t) + 1 else: - DATAS.index[nick][t] = 1 - save() + context.data.index[nick][t] = 1 + context.save() @hook("cmd_hook", "spellscore") def cmd_score(msg): - global DATAS res = list() unknown = list() if len(msg.cmds) > 1: for cmd in msg.cmds[1:]: - if cmd in DATAS.index: - res.append(Response("%s: %s" % (cmd, " ; ".join(["%s: %d" % (a, DATAS.index[cmd].getInt(a)) for a in DATAS.index[cmd].attributes.keys() if a != "name"])), channel=msg.channel)) + 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)) else: unknown.append(cmd) else: diff --git a/modules/syno.py b/modules/syno.py index 603277b..281b947 100644 --- a/modules/syno.py +++ b/modules/syno.py @@ -21,18 +21,17 @@ def help_full(): def load(context): global lang_binding - if not CONF or not CONF.hasNode("bighugelabs") or not CONF.getNode("bighugelabs").hasAttribute("key"): + if not context.config or not context.config.hasNode("bighugelabs") or not context.config.getNode("bighugelabs").hasAttribute("key"): print ("You need a NigHugeLabs API key in order to have english " "theasorus. Add it to the module configuration file:\n<bighugelabs" " key=\"XXXXXXXXXXXXXXXX\" />\nRegister at " "https://words.bighugelabs.com/getkey.php") else: - lang_binding["en"] = lambda word: get_english_synos(CONF.getNode("bighugelabs")["key"], word) + lang_binding["en"] = lambda word: get_english_synos(context.config.getNode("bighugelabs")["key"], word) def get_french_synos(word): url = "http://www.crisco.unicaen.fr/des/synonymes/" + quote(word.encode("ISO-8859-1")) - print_debug(url) page = web.getURLContent(url) best = list(); synos = list(); anton = list() diff --git a/modules/tpb.py b/modules/tpb.py index b525210..6ea4a2d 100644 --- a/modules/tpb.py +++ b/modules/tpb.py @@ -15,16 +15,16 @@ URL_TPBAPI = None def load(context): global URL_TPBAPI - if not CONF or not CONF.hasNode("tpbapi") or not CONF.getNode("tpbapi").hasAttribute("url"): + if not context.config or not context.config.hasNode("tpbapi") or not context.config.getNode("tpbapi").hasAttribute("url"): print ("You need a TPB API in order to use the !tpb feature. Add it to " "the module configuration file:\n" "<tpbapi url=\"http://tpbapi.org/\" />\nSample API: " "https://gist.github.com/colona/07a925f183cfb47d5f20") else: - URL_TPBAPI = CONF.getNode("tpbapi")["url"] + URL_TPBAPI = context.config.getNode("tpbapi")["url"] from nemubot.hooks.messagehook import MessageHook - add_hook("cmd_hook", MessageHook(cmd_tpb, "tpb")) + context.add_hook("cmd_hook", MessageHook(cmd_tpb, "tpb")) def cmd_tpb(msg): diff --git a/modules/translate.py b/modules/translate.py index 0b73b90..8cc5bac 100644 --- a/modules/translate.py +++ b/modules/translate.py @@ -19,17 +19,17 @@ URL = "http://api.wordreference.com/0.8/%s/json/%%s%%s/%%s" def load(context): global URL - if not CONF or not CONF.hasNode("wrapi") or not CONF.getNode("wrapi").hasAttribute("key"): + if not context.config or not context.config.hasNode("wrapi") or not context.config.getNode("wrapi").hasAttribute("key"): print ("You need a WordReference API key in order to use this module." " Add it to the module configuration file:\n<wrapi key=\"XXXXX\"" " />\nRegister at " "http://www.wordreference.com/docs/APIregistration.aspx") return None else: - URL = URL % CONF.getNode("wrapi")["key"] + URL = URL % context.config.getNode("wrapi")["key"] from nemubot.hooks.messagehook import MessageHook - add_hook("cmd_hook", MessageHook(cmd_translate, "translate")) + context.add_hook("cmd_hook", MessageHook(cmd_translate, "translate")) def help_full(): diff --git a/modules/velib.py b/modules/velib.py index be196ad..00105c9 100644 --- a/modules/velib.py +++ b/modules/velib.py @@ -4,6 +4,7 @@ import re +from nemubot import context from nemubot.exception import IRCException from nemubot.hooks import hook from nemubot.tools import web @@ -14,8 +15,7 @@ from more import Response def load(context): - global DATAS - DATAS.setIndex("name", "station") + context.data.setIndex("name", "station") # evt = ModuleEvent(station_available, "42706", # (lambda a, b: a != b), None, 60, @@ -30,7 +30,7 @@ def help_full(): def station_status(station): """Gets available and free status of a given station""" - response = web.getXML(CONF.getNode("server")["url"] + station) + response = web.getXML(context.config.getNode("server")["url"] + station) if response is not None: available = response.getNode("available").getContent() if available is not None and len(available) > 0: @@ -72,7 +72,6 @@ def print_station_status(msg, station): @hook("cmd_hook", "velib") def ask_stations(msg): """Hook entry from !velib""" - global DATAS if len(msg.cmds) > 5: raise IRCException("demande-moi moins de stations à la fois.") @@ -80,9 +79,9 @@ def ask_stations(msg): for station in msg.cmds[1:]: if re.match("^[0-9]{4,5}$", station): return print_station_status(msg, station) - elif station in DATAS.index: + elif station in context.data.index: return print_station_status(msg, - DATAS.index[station]["number"]) + context.data.index[station]["number"]) else: raise IRCException("numéro de station invalide.") diff --git a/modules/weather.py b/modules/weather.py index e41bdb2..a3d5daf 100644 --- a/modules/weather.py +++ b/modules/weather.py @@ -6,6 +6,7 @@ import datetime import re from urllib.parse import quote +from nemubot import context from nemubot.exception import IRCException from nemubot.hooks import hook from nemubot.tools import web @@ -19,10 +20,9 @@ from more import Response def load(context): - global DATAS - DATAS.setIndex("name", "city") + context.data.setIndex("name", "city") - if not CONF or not CONF.hasNode("darkskyapi") or not CONF.getNode("darkskyapi").hasAttribute("key"): + if not context.config or not context.config.hasNode("darkskyapi") or not context.config.getNode("darkskyapi").hasAttribute("key"): print ("You need a Dark-Sky API key in order to use this " "module. Add it to the module configuration file:\n<darkskyapi" " key=\"XXXXXXXXXXXXXXXX\" />\nRegister at " @@ -30,9 +30,9 @@ def load(context): return None from nemubot.hooks.messagehook import MessageHook - add_hook("cmd_hook", MessageHook(cmd_weather, "météo")) - add_hook("cmd_hook", MessageHook(cmd_alert, "alert")) - add_hook("cmd_hook", MessageHook(cmd_coordinates, "coordinates")) + context.add_hook("cmd_hook", MessageHook(cmd_weather, "météo")) + context.add_hook("cmd_hook", MessageHook(cmd_alert, "alert")) + context.add_hook("cmd_hook", MessageHook(cmd_coordinates, "coordinates")) def help_full (): @@ -111,10 +111,10 @@ def treat_coord(msg): except ValueError: pass - if city in DATAS.index: + if city in context.data.index: coords = list() - coords.append(DATAS.index[city]["lat"]) - coords.append(DATAS.index[city]["long"]) + coords.append(context.data.index[city]["lat"]) + coords.append(context.data.index[city]["long"]) return city, coords, specific else: @@ -132,7 +132,7 @@ def treat_coord(msg): def get_json_weather(coords): - wth = web.getJSON("https://api.forecast.io/forecast/%s/%s,%s" % (CONF.getNode("darkskyapi")["key"], float(coords[0]), float(coords[1]))) + wth = web.getJSON("https://api.forecast.io/forecast/%s/%s,%s" % (context.config.getNode("darkskyapi")["key"], float(coords[0]), float(coords[1]))) # First read flags if "darksky-unavailable" in wth["flags"]: @@ -146,10 +146,10 @@ def cmd_coordinates(msg): raise IRCException("indique-moi un nom de ville.") j = msg.args[0].lower() - if j not in DATAS.index: + if j not in context.data.index: raise IRCException("%s n'est pas une ville connue" % msg.args[0]) - coords = DATAS.index[j] + 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) @@ -227,15 +227,15 @@ def parseask(msg): gps_lat = res.group("lat").replace(",", ".") gps_long = res.group("long").replace(",", ".") - if city_name in DATAS.index: - DATAS.index[city_name]["lat"] = gps_lat - DATAS.index[city_name]["long"] = gps_long + if city_name in context.data.index: + context.data.index[city_name]["lat"] = gps_lat + context.data.index[city_name]["long"] = gps_long else: ms = ModuleState("city") ms.setAttribute("name", city_name) ms.setAttribute("lat", gps_lat) ms.setAttribute("long", gps_long) - DATAS.addChild(ms) - save() + 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) diff --git a/modules/worldcup.py b/modules/worldcup.py index 0a9c1cb..712dba4 100644 --- a/modules/worldcup.py +++ b/modules/worldcup.py @@ -8,6 +8,7 @@ import re from urllib.parse import quote from urllib.request import urlopen +from nemubot import context from nemubot.exception import IRCException from nemubot.hooks import hook from nemubot.tools.xmlparser.node import ModuleState @@ -20,7 +21,7 @@ API_URL="http://worldcup.sfg.io/%s" def load(context): from nemubot.event import ModuleEvent - 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=lambda url: urlopen(url, timeout=10).read().decode(), func_data=API_URL % "matches/current?by_date=DESC", call=current_match_new_action, interval=30)) def help_full (): @@ -28,31 +29,29 @@ def help_full (): def start_watch(msg): - global DATAS w = ModuleState("watch") w["server"] = msg.server w["channel"] = msg.channel w["proprio"] = msg.nick w["start"] = datetime.now(timezone.utc) - DATAS.addChild(w) - save() + context.data.addChild(w) + context.save() raise IRCException("This channel is now watching world cup events!") @hook("cmd_hook", "watch_worldcup") def cmd_watch(msg): - global DATAS # Get current state node = None - for n in DATAS.getChilds(): + for n in context.data.getChilds(): if n["server"] == msg.server and n["channel"] == msg.channel: node = n break if len(msg.cmds) >= 2: if msg.cmds[1] == "stop" and node is not None: - DATAS.delChild(node) - save() + context.data.delChild(node) + context.save() raise IRCException("This channel will not anymore receives world cup events.") elif msg.cmds[1] == "start" and node is None: start_watch(msg) @@ -62,14 +61,12 @@ def cmd_watch(msg): if node is None: start_watch(msg) else: - DATAS.delChild(node) - save() + context.data.delChild(node) + context.save() raise IRCException("This channel will not anymore receives world cup events.") def current_match_new_action(match_str, osef): - global DATAS - - 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)) + 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) @@ -81,8 +78,8 @@ def current_match_new_action(match_str, osef): if len(events) > 0: msg += " ; à la " + txt_event(events[0]) - for n in DATAS.getChilds(): - send_response(n["server"], Response(msg, channel=n["channel"])) + for n in context.data.getChilds(): + context.send_response(n["server"], Response(msg, channel=n["channel"])) def is_int(s): try: diff --git a/modules/ycc.py b/modules/ycc.py index 97cde3f..fba6650 100644 --- a/modules/ycc.py +++ b/modules/ycc.py @@ -30,7 +30,6 @@ def reduce(url): """ snd_url = "http://ycc.fr/redirection/create/" + quote(url, "/:%@&=?") - print_debug(snd_url) return web.getURLContent(snd_url) From 46268cb2cf5e4570ef3e08440291e802d9e16b05 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 12 Feb 2015 23:55:47 +0100 Subject: [PATCH 251/674] [networking] Fix variable name conflict --- modules/networking/page.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/networking/page.py b/modules/networking/page.py index 7e5c10e..aade22e 100644 --- a/modules/networking/page.py +++ b/modules/networking/page.py @@ -108,10 +108,10 @@ def traceURL(url, stack=None): stack.append('stack overflow :(') return stack - _, status, _, headers = headers(url) + _, status, _, heads = headers(url) if status == http.client.FOUND or status == http.client.MOVED_PERMANENTLY or status == http.client.SEE_OTHER: - for h, c in headers: + for h, c in heads: if h == "Location": url = c if url in stack: From dacb618069c08ea21689662feefbd66f4cbdd2e7 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 12 Feb 2015 23:57:21 +0100 Subject: [PATCH 252/674] Increasing version number due to significant changes --- nemubot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nemubot/__init__.py b/nemubot/__init__.py index af02327..f693b22 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -28,7 +28,7 @@ import threading import time import uuid -__version__ = '4.0.dev0' +__version__ = '4.0.dev1' __author__ = 'nemunaire' from nemubot.consumer import Consumer, EventConsumer, MessageConsumer From 2e7a4ad132a921535772669e55c7f0894671f74e Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 18 Feb 2015 01:48:02 +0100 Subject: [PATCH 253/674] Save timestamp in UTC format --- nemubot/__init__.py | 2 ++ nemubot/tools/xmlparser/node.py | 12 +++++------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/nemubot/__init__.py b/nemubot/__init__.py index f693b22..315d863 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -215,6 +215,8 @@ class Bot(threading.Thread): except ValueError: evt.id = eid + # TODO: mutex here plz + # Add the event in its place t = evt.current i = 0 # sentinel diff --git a/nemubot/tools/xmlparser/node.py b/nemubot/tools/xmlparser/node.py index 6031f76..6074ec3 100644 --- a/nemubot/tools/xmlparser/node.py +++ b/nemubot/tools/xmlparser/node.py @@ -16,10 +16,10 @@ # 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 xml.sax +import calendar from datetime import datetime, timezone import logging -import time +import xml.sax logger = logging.getLogger("nemubot.tools.xmlparser.node") @@ -82,13 +82,11 @@ class ModuleState: return source else: try: - return datetime.fromtimestamp(float(source), timezone.utc) + return datetime.utcfromtimestamp(float(source)).replace(tzinfo=timezone.utc) except ValueError: while True: try: - return datetime.fromtimestamp(time.mktime( - time.strptime(source[:19], "%Y-%m-%d %H:%M:%S")), - timezone.utc) + return time.strptime(source[:19], "%Y-%m-%d %H:%M:%S").replace(tzinfo=timezone.utc) except ImportError: pass @@ -202,7 +200,7 @@ class ModuleState: for att in self.attributes.keys(): if att[0] != "_": # Don't save attribute starting by _ if isinstance(self.attributes[att], datetime): - attribs[att] = str(time.mktime( + attribs[att] = str(calendar.timegm( self.attributes[att].timetuple())) else: attribs[att] = str(self.attributes[att]) From e588c300449263b2b750f3f51d31934480e8cc2e Mon Sep 17 00:00:00 2001 From: Nemunaire <nemunaire@nemunai.re> Date: Sat, 21 Feb 2015 13:51:40 +0100 Subject: [PATCH 254/674] Optimize imports --- bin/nemubot | 20 +- modules/cmd_server.py | 5 +- modules/mapquest.py | 1 + nemubot/__init__.py | 465 +--------------------------- nemubot/bot.py | 465 ++++++++++++++++++++++++++++ nemubot/consumer.py | 1 - nemubot/datastore/abstract.py | 4 +- nemubot/datastore/xml.py | 2 +- nemubot/exception.py | 6 +- nemubot/hooks/__init__.py | 9 +- nemubot/hooks/messagehook.py | 1 - nemubot/importer.py | 8 - nemubot/message/__init__.py | 3 +- nemubot/message/printer/__init__.py | 5 +- nemubot/modulecontext.py | 5 +- nemubot/networkbot.py | 239 -------------- nemubot/prompt/__init__.py | 10 +- nemubot/prompt/builtins.py | 12 +- nemubot/server/IRC.py | 5 +- nemubot/server/__init__.py | 135 +------- nemubot/server/abstract.py | 147 +++++++++ nemubot/server/socket.py | 10 +- nemubot/tools/__init__.py | 5 +- nemubot/tools/config.py | 7 +- nemubot/tools/countdown.py | 10 +- nemubot/tools/date.py | 3 +- nemubot/tools/web.py | 59 +++- nemubot/tools/xmlparser/__init__.py | 3 - nemubot/tools/xmlparser/node.py | 11 +- 29 files changed, 731 insertions(+), 925 deletions(-) create mode 100644 nemubot/bot.py delete mode 100644 nemubot/networkbot.py create mode 100644 nemubot/server/abstract.py diff --git a/bin/nemubot b/bin/nemubot index 8f90c33..ae88c0c 100755 --- a/bin/nemubot +++ b/bin/nemubot @@ -17,21 +17,12 @@ # 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 argparse -import imp -import logging import os import sys -import nemubot -from nemubot import datastore -import nemubot.prompt -from nemubot.prompt.builtins import load_file -from nemubot.prompt.reset import PromptReset -from nemubot.importer import ModuleFinder - if __name__ == "__main__": # Parse command line arguments + import argparse parser = argparse.ArgumentParser() parser.add_argument("-a", "--no-connect", action="store_true", @@ -59,11 +50,14 @@ if __name__ == "__main__": args = parser.parse_args() + import nemubot + if args.version: print(nemubot.__version__) sys.exit(0) # Setup loggin interface + import logging logger = logging.getLogger("nemubot") formatter = logging.Formatter( @@ -92,6 +86,7 @@ if __name__ == "__main__": logger.error("%s is not a directory", path) # Create bot context + from nemubot import datastore context = nemubot.Bot(modules_paths=modules_paths, data_store=datastore.XML(args.data_path), verbosity=args.verbose) @@ -99,14 +94,17 @@ if __name__ == "__main__": context.noautoconnect = True # Load the prompt + import nemubot.prompt prmpt = nemubot.prompt.Prompt() # Register the hook for futur import + from nemubot.importer import ModuleFinder sys.meta_path.append(ModuleFinder(context.modules_paths, context.add_module)) # Load requested configuration files for path in args.files: if os.path.isfile(path): + from nemubot.prompt.builtins import load_file load_file(path, context) else: logger.error("%s is not a readable file", path) @@ -117,6 +115,7 @@ if __name__ == "__main__": print ("Nemubot v%s ready, my PID is %i!" % (nemubot.__version__, os.getpid())) + from nemubot.prompt.reset import PromptReset while True: try: context.start() @@ -127,6 +126,7 @@ if __name__ == "__main__": break try: + import imp # Reload all other modules imp.reload(nemubot) imp.reload(nemubot.prompt) diff --git a/modules/cmd_server.py b/modules/cmd_server.py index 30c08c3..0134f8f 100644 --- a/modules/cmd_server.py +++ b/modules/cmd_server.py @@ -19,10 +19,7 @@ import traceback import sys -from nemubot.prompt.error import PromptError from nemubot.hooks import hook -from nemubot.message import TextMessage -from nemubot.networkbot import NetworkBot nemubotversion = 3.4 NODATA = True @@ -37,6 +34,7 @@ def getserver(toks, context, prompt, mandatory=False, **kwargs): 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.") @@ -144,6 +142,7 @@ def send(toks, **kwargs): % (toks[1], srv.id)) return 3 + from nemubot.message import TextMessage srv.send_response(TextMessage(" ".join(toks[2:]), server=None, to=[toks[1]])) return 0 diff --git a/modules/mapquest.py b/modules/mapquest.py index 17e842b..6e36f15 100644 --- a/modules/mapquest.py +++ b/modules/mapquest.py @@ -5,6 +5,7 @@ import re from urllib.parse import quote +from nemubot import context from nemubot.exception import IRCException from nemubot.tools import web diff --git a/nemubot/__init__.py b/nemubot/__init__.py index 315d863..7a93012 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -16,476 +16,15 @@ # 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, timedelta, timezone -import imp -import ipaddress -import logging -import os -from queue import Queue -import re -from select import select -import threading -import time -import uuid - __version__ = '4.0.dev1' __author__ = 'nemunaire' -from nemubot.consumer import Consumer, EventConsumer, MessageConsumer -from nemubot import datastore -from nemubot.event import ModuleEvent -import nemubot.hooks -from nemubot.hooks.messagehook import MessageHook -from nemubot.hooks.manager import HooksManager from nemubot.modulecontext import ModuleContext -from nemubot.networkbot import NetworkBot - context = ModuleContext(None, None) -logger = logging.getLogger("nemubot") - - -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): - """Initialize the bot context - - Keyword arguments: - ip -- The external IP of the bot (default: 127.0.0.1) - modules_paths -- Paths to all directories where looking for module - data_store -- An instance of the nemubot datastore for bot's modules - """ - - threading.Thread.__init__(self) - - logger.info("Initiate nemubot v%s", __version__) - - self.verbosity = verbosity - - # External IP for accessing this bot - self.ip = ipaddress.ip_address(ip) - - # Context paths - self.modules_paths = modules_paths - self.datastore = data_store - self.datastore.open() - - # Keep global context: servers and modules - self.servers = dict() - self.modules = dict() - self.modules_configuration = dict() - - # Events - self.events = list() - self.event_timer = None - - # Own hooks - self.hooks = HooksManager() - - def in_ping(msg): - if re.match("^ *(m[' ]?entends?[ -]+tu|h?ear me|do you copy|ping)", msg.message, re.I) is not None: - return msg.respond("pong") - self.hooks.add_hook(MessageHook(in_ping), "in", "DirectAsk") - - def _help_msg(msg): - """Parse and response to help messages""" - from more import Response - res = Response(channel=msg.frm) - if len(msg.args) > 1: - if msg.args[0] in self.modules: - if len(msg.args) > 2: - if hasattr(self.modules[msg.args[0]], "HELP_cmd"): - res.append_message(self.modules[msg.args[0]].HELP_cmd(msg.args[1])) - else: - res.append_message("No help for command %s in module %s" % (msg.args[1], msg.args[0])) - elif hasattr(self.modules[msg.args[0]], "help_full"): - res.append_message(self.modules[msg.args[0]].help_full()) - else: - res.append_message("No help for module %s" % msg.args[0]) - else: - res.append_message("No module named %s" % msg.args[0]) - else: - res.append_message("Pour me demander quelque chose, commencez " - "votre message par mon nom ; je réagis " - "également à certaine commandes commençant par" - " !. Pour plus d'informations, envoyez le " - "message \"!more\".") - res.append_message("Mon code source est libre, publié sous " - "licence AGPL (http://www.gnu.org/licenses/). " - "Vous pouvez le consulter, le dupliquer, " - "envoyer des rapports de bogues ou bien " - "contribuer au projet sur GitHub : " - "http://github.com/nemunaire/nemubot/") - res.append_message(title="Pour plus de détails sur un module, " - "envoyez \"!help nomdumodule\". Voici la liste" - " de tous les modules disponibles localement", - message=["\x03\x02%s\x03\x02 (%s)" % (im, self.modules[im].__doc__) for im in self.modules if self.modules[im].__doc__]) - return res - self.hooks.add_hook(MessageHook(_help_msg, "help"), "in", "Command") - - # Other known bots, making a bots network - self.network = dict() - - # Messages to be treated - self.cnsr_queue = Queue() - self.cnsr_thrd = list() - self.cnsr_thrd_size = -1 - - - def run(self): - from nemubot.server import _rlist, _wlist, _xlist - - self.stop = False - while not self.stop: - try: - rl, wl, xl = select(_rlist, _wlist, _xlist, 0.1) - except: - logger.error("Something went wrong in select") - fnd_smth = False - # Looking for invalid server - for r in _rlist: - if not hasattr(r, "fileno") or not isinstance(r.fileno(), int): - _rlist.remove(r) - logger.error("Found invalid object in _rlist: " + r) - fnd_smth = True - for w in _wlist: - if not hasattr(r, "fileno") or not isinstance(w.fileno(), int): - _wlist.remove(w) - logger.error("Found invalid object in _wlist: " + w) - fnd_smth = True - for x in _xlist: - if not hasattr(r, "fileno") or not isinstance(x.fileno(), int): - _xlist.remove(x) - logger.error("Found invalid object in _xlist: " + x) - fnd_smth = True - if not fnd_smth: - logger.exception("Can't continue, sorry") - self.stop = True - continue - - for x in xl: - try: - x.exception() - except: - logger.exception("Uncatched exception on server exception") - for w in wl: - try: - w.write_select() - except: - logger.exception("Uncatched exception on server write") - for r in rl: - for i in r.read(): - try: - self.receive_message(r, i) - except: - logger.exception("Uncatched exception on server read") - - - # Events methods - - def add_event(self, evt, eid=None, module_src=None): - """Register an event and return its identifiant for futur update - - Return: - None if the event is not in the queue (eg. if it has been executed during the call) or - returns the event ID. - - Argument: - evt -- The event object to add - - Keyword arguments: - eid -- The desired event ID (object or string UUID) - module_src -- The module to which the event is attached to - """ - - # Generate the event id if no given - if eid is None: - eid = uuid.uuid1() - - # Fill the id field of the event - if type(eid) is uuid.UUID: - evt.id = str(eid) - else: - # Ok, this is quite useless... - try: - evt.id = str(uuid.UUID(eid)) - except ValueError: - evt.id = eid - - # TODO: mutex here plz - - # Add the event in its place - t = evt.current - i = 0 # sentinel - for i in range(0, len(self.events)): - if self.events[i].current > t: - break - self.events.insert(i, evt) - - if i == 0: - # First event changed, reset timer - self._update_event_timer() - if len(self.events) <= 0 or self.events[i] != evt: - # Our event has been executed and removed from queue - return None - - # Register the event in the source module - if module_src is not None: - module_src.__nemubot_context__.events.append(evt.id) - evt.module_src = module_src - - logger.info("New event registered: %s -> %s", evt.id, evt) - return evt.id - - - def del_event(self, evt, module_src=None): - """Find and remove an event from list - - Return: - True if the event has been found and removed, False else - - Argument: - evt -- The ModuleEvent object to remove or just the event identifier - - Keyword arguments: - module_src -- The module to which the event is attached to (ignored if evt is a ModuleEvent) - """ - - logger.info("Removing event: %s from %s", evt, module_src) - - if type(evt) is ModuleEvent: - id = evt.id - module_src = evt.module_src - else: - id = evt - - if len(self.events) > 0 and id == self.events[0].id: - self.events.remove(self.events[0]) - self._update_event_timer() - if module_src is not None: - module_src.__nemubot_context__.events.remove(id) - return True - - for evt in self.events: - if evt.id == id: - self.events.remove(evt) - - if module_src is not None: - module_src.__nemubot_context__.events.remove(evt.id) - return True - return False - - - def _update_event_timer(self): - """(Re)launch the timer to end with the closest event""" - - # Reset the timer if this is the first item - if self.event_timer is not None: - self.event_timer.cancel() - - if len(self.events) > 0: - logger.debug("Update timer: next event in %d seconds", - self.events[0].time_left.seconds) - if datetime.now(timezone.utc) + timedelta(seconds=5) >= self.events[0].current: - while datetime.now(timezone.utc) < self.events[0].current: - time.sleep(0.6) - self._end_event_timer() - else: - self.event_timer = threading.Timer( - self.events[0].time_left.seconds + 1, self._end_event_timer) - self.event_timer.start() - else: - logger.debug("Update timer: no timer left") - - - def _end_event_timer(self): - """Function called at the end of the event timer""" - - while len(self.events) > 0 and datetime.now(timezone.utc) >= self.events[0].current: - evt = self.events.pop(0) - self.cnsr_queue.put_nowait(EventConsumer(evt)) - self._launch_consumers() - - self._update_event_timer() - - - # Consumers methods - - def _launch_consumers(self): - """Launch new consumer threads if necessary""" - - while self.cnsr_queue.qsize() > self.cnsr_thrd_size: - # Next launch if two more items in queue - self.cnsr_thrd_size += 2 - - c = Consumer(self) - self.cnsr_thrd.append(c) - c.start() - - - def add_server(self, srv, autoconnect=True): - """Add a new server to the context - - Arguments: - srv -- a concrete AbstractServer instance - autoconnect -- connect after add? - """ - - if srv.id not in self.servers: - self.servers[srv.id] = srv - if autoconnect and not hasattr(self, "noautoconnect"): - srv.open() - return True - - else: - return False - - - # Modules methods - - def import_module(self, name): - """Load a module - - Argument: - name -- name of the module to load - """ - - if name in self.modules: - self.unload_module(name) - tt = __import__(name) - imp.reload(tt) - else: - __import__(name) - - - def add_module(self, module): - """Add a module to the context, if already exists, unload the - old one before""" - # Check if the module already exists - if module.__name__ in self.modules: - self.unload_module(module.__name__) - - # Overwrite print built-in - def prnt(*args): - print("[%s]" % module.__name__, *args) - if hasattr(module, "logger"): - module.logger.info(" ".join(args)) - module.print = prnt - - # Create module context - module.__nemubot_context__ = ModuleContext(self, module) - - # Replace imported context by real one - for attr in module.__dict__: - if attr != "__nemubot_context__" and type(module.__dict__[attr]) == ModuleContext: - module.__dict__[attr] = module.__nemubot_context__ - - # Register decorated functions - for s, h in nemubot.hooks.last_registered: - module.__nemubot_context__.add_hook(s, h) - nemubot.hooks.last_registered = [] - - # Save a reference to the module - self.modules[module.__name__] = module - - # Launch the module - if hasattr(module, "load"): - module.load(module.__nemubot_context__) - - return True - - - def unload_module(self, name): - """Unload a module""" - if name in self.modules: - self.modules[name].print("Unloading module %s" % name) - if hasattr(self.modules[name], "unload"): - self.modules[name].unload(self) - self.modules[name].__nemubot_context__.unload() - # Remove from the dict - del self.modules[name] - logger.info("Module `%s' successfully unloaded.", name) - return True - return False - - - def receive_message(self, srv, msg, private=False, data=None): - """Queued the message for treatment""" - #print("READ", raw_msg) - self.cnsr_queue.put_nowait(MessageConsumer(srv, msg)) - - # Launch a new thread if necessary - self._launch_consumers() - - - def add_networkbot(self, srv, dest, dcc=None): - """Append a new bot into the network""" - id = srv.id + "/" + dest - if id not in self.network: - self.network[id] = NetworkBot(self, srv, dest, dcc) - return self.network[id] - - def send_networkbot(self, srv, cmd, data=None): - for bot in self.network: - if self.network[bot].srv == srv: - self.network[bot].send_cmd(cmd, data) - - 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...") - 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.stop = True - - - # Treatment - - def check_rest_times(self, store, hook): - """Remove from store the hook if it has been executed given time""" - if hook.times == 0: - if isinstance(store, dict): - store[hook.name].remove(hook) - if len(store) == 0: - del store[hook.name] - elif isinstance(store, list): - store.remove(hook) - - -def hotswap(bak): - bak.stop = True - if bak.event_timer is not None: - bak.event_timer.cancel() - 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.network = bak.network - - new._update_event_timer() - return new - def reload(): + import imp + import nemubot.channel imp.reload(nemubot.channel) diff --git a/nemubot/bot.py b/nemubot/bot.py new file mode 100644 index 0000000..5087229 --- /dev/null +++ b/nemubot/bot.py @@ -0,0 +1,465 @@ +# -*- 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/>. + +from datetime import datetime, timedelta, timezone +import logging +import threading + +from nemubot import __version__ +from nemubot.consumer import Consumer, EventConsumer, MessageConsumer +from nemubot import datastore +from nemubot.hooks.messagehook import MessageHook +from nemubot.modulecontext import ModuleContext + +logger = logging.getLogger("nemubot") + + +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): + """Initialize the bot context + + Keyword arguments: + ip -- The external IP of the bot (default: 127.0.0.1) + modules_paths -- Paths to all directories where looking for module + data_store -- An instance of the nemubot datastore for bot's modules + """ + + threading.Thread.__init__(self) + + logger.info("Initiate nemubot v%s", __version__) + + self.verbosity = verbosity + + # External IP for accessing this bot + import ipaddress + self.ip = ipaddress.ip_address(ip) + + # Context paths + self.modules_paths = modules_paths + self.datastore = data_store + self.datastore.open() + + # Keep global context: servers and modules + self.servers = dict() + self.modules = dict() + self.modules_configuration = dict() + + # Events + self.events = list() + self.event_timer = None + + # Own hooks + from nemubot.hooks.manager import HooksManager + self.hooks = HooksManager() + + 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.hooks.add_hook(MessageHook(in_ping), "in", "DirectAsk") + + def _help_msg(msg): + """Parse and response to help messages""" + from more import Response + res = Response(channel=msg.frm) + if len(msg.args) > 1: + if msg.args[0] in self.modules: + if len(msg.args) > 2: + if hasattr(self.modules[msg.args[0]], "HELP_cmd"): + res.append_message(self.modules[msg.args[0]].HELP_cmd(msg.args[1])) + else: + res.append_message("No help for command %s in module %s" % (msg.args[1], msg.args[0])) + elif hasattr(self.modules[msg.args[0]], "help_full"): + res.append_message(self.modules[msg.args[0]].help_full()) + else: + res.append_message("No help for module %s" % msg.args[0]) + else: + res.append_message("No module named %s" % msg.args[0]) + else: + res.append_message("Pour me demander quelque chose, commencez " + "votre message par mon nom ; je réagis " + "également à certaine commandes commençant par" + " !. Pour plus d'informations, envoyez le " + "message \"!more\".") + res.append_message("Mon code source est libre, publié sous " + "licence AGPL (http://www.gnu.org/licenses/). " + "Vous pouvez le consulter, le dupliquer, " + "envoyer des rapports de bogues ou bien " + "contribuer au projet sur GitHub : " + "http://github.com/nemunaire/nemubot/") + res.append_message(title="Pour plus de détails sur un module, " + "envoyez \"!help nomdumodule\". Voici la liste" + " de tous les modules disponibles localement", + message=["\x03\x02%s\x03\x02 (%s)" % (im, self.modules[im].__doc__) for im in self.modules if self.modules[im].__doc__]) + return res + self.hooks.add_hook(MessageHook(_help_msg, "help"), "in", "Command") + + # Messages to be treated + from queue import Queue + self.cnsr_queue = Queue() + self.cnsr_thrd = list() + self.cnsr_thrd_size = -1 + + + def run(self): + from select import select + from nemubot.server import _rlist, _wlist, _xlist + + self.stop = False + while not self.stop: + try: + rl, wl, xl = select(_rlist, _wlist, _xlist, 0.1) + except: + logger.error("Something went wrong in select") + fnd_smth = False + # Looking for invalid server + for r in _rlist: + if not hasattr(r, "fileno") or not isinstance(r.fileno(), int): + _rlist.remove(r) + logger.error("Found invalid object in _rlist: " + r) + fnd_smth = True + for w in _wlist: + if not hasattr(r, "fileno") or not isinstance(w.fileno(), int): + _wlist.remove(w) + logger.error("Found invalid object in _wlist: " + w) + fnd_smth = True + for x in _xlist: + if not hasattr(r, "fileno") or not isinstance(x.fileno(), int): + _xlist.remove(x) + logger.error("Found invalid object in _xlist: " + x) + fnd_smth = True + if not fnd_smth: + logger.exception("Can't continue, sorry") + self.stop = True + continue + + for x in xl: + try: + x.exception() + except: + logger.exception("Uncatched exception on server exception") + for w in wl: + try: + w.write_select() + except: + logger.exception("Uncatched exception on server write") + for r in rl: + for i in r.read(): + try: + self.receive_message(r, i) + except: + logger.exception("Uncatched exception on server read") + + + # Events methods + + def add_event(self, evt, eid=None, module_src=None): + """Register an event and return its identifiant for futur update + + Return: + None if the event is not in the queue (eg. if it has been executed during the call) or + returns the event ID. + + Argument: + evt -- The event object to add + + Keyword arguments: + eid -- The desired event ID (object or string UUID) + module_src -- The module to which the event is attached to + """ + + import uuid + + # Generate the event id if no given + if eid is None: + eid = uuid.uuid1() + + # Fill the id field of the event + if type(eid) is uuid.UUID: + evt.id = str(eid) + else: + # Ok, this is quite useless... + try: + evt.id = str(uuid.UUID(eid)) + except ValueError: + evt.id = eid + + # TODO: mutex here plz + + # Add the event in its place + t = evt.current + i = 0 # sentinel + for i in range(0, len(self.events)): + if self.events[i].current > t: + break + self.events.insert(i, evt) + + if i == 0: + # First event changed, reset timer + self._update_event_timer() + if len(self.events) <= 0 or self.events[i] != evt: + # Our event has been executed and removed from queue + return None + + # Register the event in the source module + if module_src is not None: + module_src.__nemubot_context__.events.append(evt.id) + evt.module_src = module_src + + logger.info("New event registered: %s -> %s", evt.id, evt) + return evt.id + + + def del_event(self, evt, module_src=None): + """Find and remove an event from list + + Return: + True if the event has been found and removed, False else + + Argument: + evt -- The ModuleEvent object to remove or just the event identifier + + Keyword arguments: + module_src -- The module to which the event is attached to (ignored if evt is a ModuleEvent) + """ + + logger.info("Removing event: %s from %s", evt, module_src) + + from nemubot.event import ModuleEvent + if type(evt) is ModuleEvent: + id = evt.id + module_src = evt.module_src + else: + id = evt + + if len(self.events) > 0 and id == self.events[0].id: + self.events.remove(self.events[0]) + self._update_event_timer() + if module_src is not None: + module_src.__nemubot_context__.events.remove(id) + return True + + for evt in self.events: + if evt.id == id: + self.events.remove(evt) + + if module_src is not None: + module_src.__nemubot_context__.events.remove(evt.id) + return True + return False + + + def _update_event_timer(self): + """(Re)launch the timer to end with the closest event""" + + # Reset the timer if this is the first item + if self.event_timer is not None: + self.event_timer.cancel() + + if len(self.events) > 0: + logger.debug("Update timer: next event in %d seconds", + self.events[0].time_left.seconds) + if datetime.now(timezone.utc) + timedelta(seconds=5) >= self.events[0].current: + import time + while datetime.now(timezone.utc) < self.events[0].current: + time.sleep(0.6) + self._end_event_timer() + else: + self.event_timer = threading.Timer( + self.events[0].time_left.seconds + 1, self._end_event_timer) + self.event_timer.start() + else: + logger.debug("Update timer: no timer left") + + + def _end_event_timer(self): + """Function called at the end of the event timer""" + + while len(self.events) > 0 and datetime.now(timezone.utc) >= self.events[0].current: + evt = self.events.pop(0) + self.cnsr_queue.put_nowait(EventConsumer(evt)) + self._launch_consumers() + + self._update_event_timer() + + + # Consumers methods + + def _launch_consumers(self): + """Launch new consumer threads if necessary""" + + while self.cnsr_queue.qsize() > self.cnsr_thrd_size: + # Next launch if two more items in queue + self.cnsr_thrd_size += 2 + + c = Consumer(self) + self.cnsr_thrd.append(c) + c.start() + + + def add_server(self, srv, autoconnect=True): + """Add a new server to the context + + Arguments: + srv -- a concrete AbstractServer instance + autoconnect -- connect after add? + """ + + if srv.id not in self.servers: + self.servers[srv.id] = srv + if autoconnect and not hasattr(self, "noautoconnect"): + srv.open() + return True + + else: + return False + + + # Modules methods + + def import_module(self, name): + """Load a module + + Argument: + name -- name of the module to load + """ + + if name in self.modules: + import imp + self.unload_module(name) + tt = __import__(name) + imp.reload(tt) + else: + __import__(name) + + + def add_module(self, module): + """Add a module to the context, if already exists, unload the + old one before""" + # Check if the module already exists + if module.__name__ in self.modules: + self.unload_module(module.__name__) + + # Overwrite print built-in + def prnt(*args): + print("[%s]" % module.__name__, *args) + if hasattr(module, "logger"): + module.logger.info(" ".join(args)) + module.print = prnt + + # Create module context + module.__nemubot_context__ = ModuleContext(self, module) + + # Replace imported context by real one + for attr in module.__dict__: + if attr != "__nemubot_context__" and type(module.__dict__[attr]) == ModuleContext: + module.__dict__[attr] = module.__nemubot_context__ + + # Register decorated functions + import nemubot.hooks + for s, h in nemubot.hooks.last_registered: + module.__nemubot_context__.add_hook(s, h) + nemubot.hooks.last_registered = [] + + # Save a reference to the module + self.modules[module.__name__] = module + + # Launch the module + if hasattr(module, "load"): + module.load(module.__nemubot_context__) + + return True + + + def unload_module(self, name): + """Unload a module""" + if name in self.modules: + self.modules[name].print("Unloading module %s" % name) + if hasattr(self.modules[name], "unload"): + self.modules[name].unload(self) + self.modules[name].__nemubot_context__.unload() + # Remove from the dict + del self.modules[name] + logger.info("Module `%s' successfully unloaded.", name) + return True + return False + + + def receive_message(self, srv, msg, private=False, data=None): + """Queued the message for treatment""" + #print("READ", raw_msg) + self.cnsr_queue.put_nowait(MessageConsumer(srv, msg)) + + # Launch a new thread if necessary + self._launch_consumers() + + + 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...") + 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.stop = True + + + # Treatment + + def check_rest_times(self, store, hook): + """Remove from store the hook if it has been executed given time""" + if hook.times == 0: + if isinstance(store, dict): + store[hook.name].remove(hook) + if len(store) == 0: + del store[hook.name] + elif isinstance(store, list): + store.remove(hook) + + +def hotswap(bak): + bak.stop = True + if bak.event_timer is not None: + bak.event_timer.cancel() + 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/consumer.py b/nemubot/consumer.py index 2594601..8022b72 100644 --- a/nemubot/consumer.py +++ b/nemubot/consumer.py @@ -18,7 +18,6 @@ import logging import queue -import re import threading logger = logging.getLogger("nemubot.consumer") diff --git a/nemubot/datastore/abstract.py b/nemubot/datastore/abstract.py index 42b7592..ad45665 100644 --- a/nemubot/datastore/abstract.py +++ b/nemubot/datastore/abstract.py @@ -14,9 +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/>. -from nemubot.tools.xmlparser import module_state - - class Abstract: """Abstract implementation of a module data store, that always return an @@ -38,6 +35,7 @@ class Abstract: The loaded data """ + from nemubot.tools.xmlparser import module_state return module_state.ModuleState("nemubotstate") def save(self, module, data): diff --git a/nemubot/datastore/xml.py b/nemubot/datastore/xml.py index 2518af2..7bbe155 100644 --- a/nemubot/datastore/xml.py +++ b/nemubot/datastore/xml.py @@ -17,7 +17,6 @@ import os from nemubot.datastore.abstract import Abstract -from nemubot.tools.xmlparser import parse_file class XML(Abstract): @@ -66,6 +65,7 @@ class XML(Abstract): data_file = self._get_data_file_path(module) if os.path.isfile(data_file): + from nemubot.tools.xmlparser import parse_file return parse_file(data_file) else: return Abstract.load(self, module) diff --git a/nemubot/exception.py b/nemubot/exception.py index 88b17a9..04c142e 100644 --- a/nemubot/exception.py +++ b/nemubot/exception.py @@ -16,9 +16,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/>. -from nemubot.message import TextMessage, DirectAsk - - class IRCException(Exception): def __init__(self, message, personnal=True): @@ -28,8 +25,11 @@ class IRCException(Exception): def fill_response(self, msg): if self.personnal: + from nemubot.message import DirectAsk return DirectAsk(msg.frm, self.message, server=msg.server, to=msg.to_response) + else: + from nemubot.message import TextMessage return TextMessage(self.message, server=msg.server, to=msg.to_response) diff --git a/nemubot/hooks/__init__.py b/nemubot/hooks/__init__.py index e067774..bfd26a2 100644 --- a/nemubot/hooks/__init__.py +++ b/nemubot/hooks/__init__.py @@ -16,11 +16,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/>. -import imp - -from nemubot.exception import IRCException - - def call_game(call, *args, **kargs): """TODO""" l = list() @@ -54,6 +49,8 @@ class AbstractHook: def run(self, data1, *args): """Run the hook""" + + from nemubot.exception import IRCException self.times -= 1 try: @@ -81,6 +78,8 @@ def hook(store, *args, **kargs): def reload(): + import imp + import nemubot.hooks.manager imp.reload(nemubot.hooks.manager) diff --git a/nemubot/hooks/messagehook.py b/nemubot/hooks/messagehook.py index cd78458..939286d 100644 --- a/nemubot/hooks/messagehook.py +++ b/nemubot/hooks/messagehook.py @@ -18,7 +18,6 @@ import re -from nemubot.exception import IRCException from nemubot.hooks import AbstractHook import nemubot.message diff --git a/nemubot/importer.py b/nemubot/importer.py index 5984cb9..4bb990e 100644 --- a/nemubot/importer.py +++ b/nemubot/importer.py @@ -16,18 +16,10 @@ # 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 distutils.version import LooseVersion from importlib.abc import Finder from importlib.machinery import SourceFileLoader -import imp import logging import os -import sys - -from nemubot import __version__ -import nemubot.hooks -from nemubot.message import TextMessage -from nemubot.tools.xmlparser import parse_file, module_state logger = logging.getLogger("nemubot.importer") diff --git a/nemubot/message/__init__.py b/nemubot/message/__init__.py index bf4feda..232f557 100644 --- a/nemubot/message/__init__.py +++ b/nemubot/message/__init__.py @@ -17,7 +17,6 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. from datetime import datetime, timezone -import imp class AbstractMessage: @@ -166,6 +165,8 @@ class OwnerCommand(Command): def reload(): + import imp + import nemubot.message.visitor imp.reload(nemubot.message.visitor) diff --git a/nemubot/message/printer/__init__.py b/nemubot/message/printer/__init__.py index 44eda74..f906b35 100644 --- a/nemubot/message/printer/__init__.py +++ b/nemubot/message/printer/__init__.py @@ -16,9 +16,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 imp - - def reload(): + import imp + import nemubot.message.printer.IRC imp.reload(nemubot.message.printer.IRC) diff --git a/nemubot/modulecontext.py b/nemubot/modulecontext.py index 5ee8a08..7abdefb 100644 --- a/nemubot/modulecontext.py +++ b/nemubot/modulecontext.py @@ -14,9 +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/>. -from nemubot.tools.xmlparser import module_state - - def convert_legacy_store(old): if old == "cmd_hook" or old == "cmd_rgxp" or old == "cmd_default": return "in_Command" @@ -84,6 +81,8 @@ class ModuleContext: return False else: + from nemubot.tools.xmlparser import module_state + self.data = module_state.ModuleState("nemubotstate") def add_hook(store, hook): diff --git a/nemubot/networkbot.py b/nemubot/networkbot.py deleted file mode 100644 index 1a253c1..0000000 --- a/nemubot/networkbot.py +++ /dev/null @@ -1,239 +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/>. - -import json -import random -import shlex -import urllib.parse -import zlib - -from nemubot.server.DCC import DCC -import nemubot.hooks as hooks - -class NetworkBot: - def __init__(self, context, srv, dest, dcc=None): - # General informations - self.context = context - self.srv = srv - self.dest = dest - - self.dcc = dcc # DCC connection to the other bot - if self.dcc is not None: - self.dcc.closing_event = self.closing_event - - self.hooks = list() - self.REGISTERED_HOOKS = list() - - # Tags monitor - self.my_tag = random.randint(0,255) - self.inc_tag = 0 - self.tags = dict() - - @property - def id(self): - return self.dcc.id - @property - def sender(self): - if self.dcc is not None: - return self.dcc.sender - return None - @property - def nick(self): - if self.dcc is not None: - return self.dcc.nick - return None - @property - def realname(self): - if self.dcc is not None: - return self.dcc.realname - return None - @property - def owner(self): - return self.srv.owner - - def isDCC(self, someone): - """Abstract implementation""" - return True - - def accepted_channel(self, chan, sender=None): - return True - - def send_cmd(self, cmd, data=None): - """Create a tag and send the command""" - # First, define a tag - self.inc_tag = (self.inc_tag + 1) % 256 - while self.inc_tag in self.tags: - self.inc_tag = (self.inc_tag + 1) % 256 - tag = ("%c%c" % (self.my_tag, self.inc_tag)).encode() - - self.tags[tag] = (cmd, data) - - # Send the command with the tag - self.send_response_final(tag, cmd) - - def send_response(self, res, tag): - self.send_response_final(tag, [res.sender, res.channel, res.nick, res.nomore, res.title, res.more, res.count, json.dumps(res.messages)]) - - def msg_treated(self, tag): - self.send_ack(tag) - - def send_response_final(self, tag, msg): - """Send a response with a tag""" - if isinstance(msg, list): - cnt = b'' - for i in msg: - if i is None: - cnt += b' ""' - elif isinstance(i, int): - cnt += (' %d' % i).encode() - elif isinstance(i, float): - cnt += (' %f' % i).encode() - else: - cnt += b' "' + urllib.parse.quote(i).encode() + b'"' - if False and len(cnt) > 10: - cnt = b' Z ' + zlib.compress(cnt) - print (cnt) - self.dcc.send_dcc_raw(tag + cnt) - else: - for line in msg.split("\n"): - self.dcc.send_dcc_raw(tag + b' ' + line.encode()) - - def send_ack(self, tag): - """Acknowledge a command""" - if tag in self.tags: - del self.tags[tag] - self.send_response_final(tag, "ACK") - - def connect(self): - """Making the connexion with dest through srv""" - if self.dcc is None or not self.dcc.connected: - self.dcc = DCC(self.srv, self.dest) - self.dcc.closing_event = self.closing_event - self.dcc.treatement = self.hello - self.dcc.send_dcc("NEMUBOT###") - else: - self.send_cmd("FETCH") - - def disconnect(self, reason=""): - """Close the connection and remove the bot from network list""" - del self.context.network[self.dcc.id] - self.dcc.send_dcc("DISCONNECT :%s" % reason) - self.dcc.disconnect() - - def hello(self, line): - if line == b'NEMUBOT###': - self.dcc.treatement = self.treat_msg - self.send_cmd("MYTAG %c" % self.my_tag) - self.send_cmd("FETCH") - elif line != b'Hello ' + self.srv.nick.encode() + b'!': - self.disconnect("Sorry, I think you were a bot") - - def treat_msg(self, line, cmd=None): - words = line.split(b' ') - - # Ignore invalid commands - if len(words) >= 2: - tag = words[0] - - # Is it a response? - if tag in self.tags: - # Is it compressed content? - if words[1] == b'Z': - #print (line) - line = zlib.decompress(line[len(tag) + 3:]) - self.response(line, tag, [urllib.parse.unquote(arg) for arg in shlex.split(line[len(tag) + 1:].decode())], self.tags[tag]) - else: - cmd = words[1] - if len(words) > 2: - args = shlex.split(line[len(tag) + len(cmd) + 2:].decode()) - args = [urllib.parse.unquote(arg) for arg in args] - else: - args = list() - #print ("request:", line) - self.request(tag, cmd, args) - - def closing_event(self): - for lvl in self.hooks: - lvl.clear() - - def response(self, line, tag, args, t): - (cmds, data) = t - #print ("response for", cmds, ":", args) - - if isinstance(cmds, list): - cmd = cmds[0] - else: - cmd = cmds - cmds = list(cmd) - - if args[0] == 'ACK': # Acknowledge a command - del self.tags[tag] - - elif cmd == "FETCH" and len(args) >= 5: - level = int(args[1]) - while len(self.hooks) <= level: - self.hooks.append(hooks.MessagesHook(self.context, self)) - - if args[2] == "": args[2] = None - if args[3] == "": args[3] = None - if args[4] == "": args[4] = list() - else: args[4] = args[4].split(',') - - self.hooks[level].add_hook(args[0], hooks.Hook(self.exec_hook, args[2], None, args[3], args[4]), self) - - elif cmd == "HOOK" and len(args) >= 8: - # Rebuild the response - if args[1] == '': args[1] = None - if args[2] == '': args[2] = None - if args[3] == '': args[3] = None - if args[4] == '': args[4] = None - if args[5] == '': args[5] = None - if args[6] == '': args[6] = None - res = Response(args[0], channel=args[1], nick=args[2], nomore=args[3], title=args[4], more=args[5], count=args[6]) - for msg in json.loads(args[7]): - res.append_message(msg) - if len(res.messages) <= 1: - res.alone = True - self.srv.send_response(res, None) - - - def request(self, tag, cmd, args): - # Parse - if cmd == b'MYTAG' and len(args) > 0: # Inform about choosen tag - while args[0] == self.my_tag: - self.my_tag = random.randint(0,255) - self.send_ack(tag) - - elif cmd == b'FETCH': # Get known commands - for name in ["cmd_hook", "ask_hook", "msg_hook"]: - elts = self.context.create_cache(name) - for elt in elts: - (hooks, lvl, store, bot) = elts[elt] - for h in hooks: - self.send_response_final(tag, [name, lvl, elt, h.regexp, ','.join(h.channels)]) - self.send_ack(tag) - - elif (cmd == b'HOOK' or cmd == b'"HOOK"') and len(args) > 0: # Action requested - self.context.receive_message(self, args[0].encode(), True, tag) - - elif (cmd == b'NOMORE' or cmd == b'"NOMORE"') and len(args) > 0: # Reset !more feature - if args[0] in self.srv.moremessages: - del self.srv.moremessages[args[0]] - - def exec_hook(self, msg): - self.send_cmd(["HOOK", msg.raw]) diff --git a/nemubot/prompt/__init__.py b/nemubot/prompt/__init__.py index f3e60da..c491d99 100644 --- a/nemubot/prompt/__init__.py +++ b/nemubot/prompt/__init__.py @@ -16,15 +16,10 @@ # 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 readline import shlex import sys import traceback -from nemubot.prompt.error import PromptError -from nemubot.prompt.reset import PromptReset from nemubot.prompt import builtins @@ -102,6 +97,9 @@ class Prompt: 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 " % @@ -134,6 +132,8 @@ def hotswap(bak): def reload(): + import imp + import nemubot.prompt.builtins imp.reload(nemubot.prompt.builtins) diff --git a/nemubot/prompt/builtins.py b/nemubot/prompt/builtins.py index 7edbf5e..e78c3dd 100644 --- a/nemubot/prompt/builtins.py +++ b/nemubot/prompt/builtins.py @@ -16,16 +16,10 @@ # 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 - -from nemubot.prompt.reset import PromptReset -from nemubot.tools.config import load_file - -logger = logging.getLogger("nemubot.prompt.builtins") - - 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": @@ -67,6 +61,8 @@ def liste(toks, context, prompt): 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: diff --git a/nemubot/server/IRC.py b/nemubot/server/IRC.py index c1df9ca..27c656c 100644 --- a/nemubot/server/IRC.py +++ b/nemubot/server/IRC.py @@ -16,11 +16,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 calendar from datetime import datetime, timezone -import ipaddress import re -import time import shlex from nemubot.channel import Channel @@ -82,6 +79,7 @@ class IRC(SocketServer): 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) @@ -333,6 +331,7 @@ class IRCMessage: # Treat special tags if key == "time": + import calendar, time value = datetime.fromtimestamp(calendar.timegm(time.strptime(value, "%Y-%m-%dT%H:%M:%S.%fZ")), timezone.utc) # Store tag diff --git a/nemubot/server/__init__.py b/nemubot/server/__init__.py index 0031b02..6770796 100644 --- a/nemubot/server/__init__.py +++ b/nemubot/server/__init__.py @@ -16,11 +16,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/>. -import io -import imp -import logging -import socket -import queue # Lists for select _rlist = [] @@ -28,132 +23,12 @@ _wlist = [] _xlist = [] -# Extends from IOBase in order to be compatible with select function -class AbstractServer(io.IOBase): - - """An abstract server: handle communication with an IM server""" - - def __init__(self, send_callback=None): - """Initialize an abstract server - - Keyword argument: - send_callback -- Callback when developper want to send a message - """ - - 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._sending_queue = queue.Queue() - if send_callback is not None: - self._send_callback = send_callback - else: - self._send_callback = self._write_select - - - # Open/close - - def __enter__(self): - self.open() - return self - - - def __exit__(self, type, value, traceback): - self.close() - - - 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 - - - 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) - 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 - - - # Writes - - def write(self, message): - """Asynchronymously send a message to the server using send_callback - - Argument: - message -- message to send - """ - - self._send_callback(message) - - - def write_select(self): - """Internal function used by the select function""" - try: - _wlist.remove(self) - while not self._sending_queue.empty(): - self._write(self._sending_queue.get_nowait()) - self._sending_queue.task_done() - - except queue.Empty: - 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 Queue", message) - if self not in _wlist: - _wlist.append(self) - - - def send_response(self, response): - """Send a formated Message class - - Argument: - response -- message to send - """ - - if response is None: - return - - elif isinstance(response, list): - for r in response: - self.send_response(r) - - else: - vprnt = self.printer() - response.accept(vprnt) - self.write(vprnt.pp) - - - # Exceptions - - def exception(self): - """Exception occurs in fd""" - self.logger.warning("Unhandle file descriptor exception on server %s", - self.id) - - def reload(): + import imp + + import nemubot.server.abstract + imp.reload(nemubot.server.abstract) + import nemubot.server.socket imp.reload(nemubot.server.socket) diff --git a/nemubot/server/abstract.py b/nemubot/server/abstract.py new file mode 100644 index 0000000..127fa15 --- /dev/null +++ b/nemubot/server/abstract.py @@ -0,0 +1,147 @@ +# -*- 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/>. + +import io +import logging +import queue + +from nemubot.server import _rlist, _wlist, _xlist + +# Extends from IOBase in order to be compatible with select function +class AbstractServer(io.IOBase): + + """An abstract server: handle communication with an IM server""" + + def __init__(self, send_callback=None): + """Initialize an abstract server + + Keyword argument: + send_callback -- Callback when developper want to send a message + """ + + 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._sending_queue = queue.Queue() + if send_callback is not None: + self._send_callback = send_callback + else: + self._send_callback = self._write_select + + + # Open/close + + def __enter__(self): + self.open() + return self + + + def __exit__(self, type, value, traceback): + self.close() + + + 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 + + + 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) + 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 + + + # Writes + + def write(self, message): + """Asynchronymously send a message to the server using send_callback + + Argument: + message -- message to send + """ + + self._send_callback(message) + + + def write_select(self): + """Internal function used by the select function""" + try: + _wlist.remove(self) + while not self._sending_queue.empty(): + self._write(self._sending_queue.get_nowait()) + self._sending_queue.task_done() + + except queue.Empty: + 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 Queue", message) + if self not in _wlist: + _wlist.append(self) + + + def send_response(self, response): + """Send a formated Message class + + Argument: + response -- message to send + """ + + if response is None: + return + + elif isinstance(response, list): + for r in response: + self.send_response(r) + + else: + vprnt = self.printer() + response.accept(vprnt) + self.write(vprnt.pp) + + + # Exceptions + + def exception(self): + """Exception occurs in fd""" + self.logger.warning("Unhandle file descriptor exception on server %s", + self.id) diff --git a/nemubot/server/socket.py b/nemubot/server/socket.py index 2b0d014..e229d5e 100644 --- a/nemubot/server/socket.py +++ b/nemubot/server/socket.py @@ -16,10 +16,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 ssl -import socket - -from nemubot.server import AbstractServer +from nemubot.server.abstract import AbstractServer class SocketServer(AbstractServer): @@ -49,11 +46,14 @@ class SocketServer(AbstractServer): # Open/close def _open(self): + import os + import socket # Create the socket self.socket = socket.socket() # Wrap the socket for SSL if self.ssl: + import ssl ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1) self.socket = ctx.wrap_socket(self.socket) @@ -71,6 +71,8 @@ class SocketServer(AbstractServer): def _close(self): + import socket + self._sending_queue.join() if self.connected: try: diff --git a/nemubot/tools/__init__.py b/nemubot/tools/__init__.py index 8ba5534..95be66a 100644 --- a/nemubot/tools/__init__.py +++ b/nemubot/tools/__init__.py @@ -16,10 +16,9 @@ # 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 - - def reload(): + import imp + import nemubot.tools.config imp.reload(nemubot.tools.config) diff --git a/nemubot/tools/config.py b/nemubot/tools/config.py index cc94209..488179b 100644 --- a/nemubot/tools/config.py +++ b/nemubot/tools/config.py @@ -17,9 +17,6 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import logging -import os - -from nemubot.tools.xmlparser import parse_file logger = logging.getLogger("nemubot.tools.config") @@ -97,7 +94,11 @@ def load_file(filename, context): 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! diff --git a/nemubot/tools/countdown.py b/nemubot/tools/countdown.py index a53476d..58bdc55 100644 --- a/nemubot/tools/countdown.py +++ b/nemubot/tools/countdown.py @@ -16,10 +16,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/>. -from datetime import datetime, timezone -import time - - def countdown(delta, resolution=5): sec = delta.seconds hours, remainder = divmod(sec, 3600) @@ -82,10 +78,15 @@ def countdown_format(date, msg_before, msg_after, tz=None): """Replace in a text %s by a sentence incidated the remaining time before/after an event""" if tz is not None: + import os oldtz = os.environ['TZ'] os.environ['TZ'] = tz + + import time time.tzset() + from datetime import datetime, timezone + # Calculate time before the date try: if datetime.now(timezone.utc) > date: @@ -103,6 +104,7 @@ def countdown_format(date, msg_before, msg_after, tz=None): delta = date - datetime.now() if tz is not None: + import os os.environ['TZ'] = oldtz return sentence_c % countdown(delta) diff --git a/nemubot/tools/date.py b/nemubot/tools/date.py index 9ce357b..da46756 100644 --- a/nemubot/tools/date.py +++ b/nemubot/tools/date.py @@ -18,7 +18,6 @@ # Extraction/Format text -from datetime import datetime, date import re xtrdt = re.compile(r'''^.*? (?P<day>[0-9]{1,4}) .+? @@ -71,6 +70,7 @@ def extractDate(msg): second = result.group("second") if year is None: + from datetime import date year = date.today().year if hour is None: hour = 0 @@ -84,6 +84,7 @@ def extractDate(msg): minute = int(minute) + 1 second = 0 + from datetime import datetime return datetime(int(year), int(month), int(day), int(hour), int(minute), int(second)) else: diff --git a/nemubot/tools/web.py b/nemubot/tools/web.py index dc8a188..b7960c5 100644 --- a/nemubot/tools/web.py +++ b/nemubot/tools/web.py @@ -16,18 +16,9 @@ # 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 html.entities import name2codepoint -import http.client -import json -import re -import socket -from urllib.parse import quote from urllib.parse import urlparse -from urllib.request import urlopen -from nemubot import __version__ from nemubot.exception import IRCException -from nemubot.tools.xmlparser import parse_string def isURL(url): @@ -70,11 +61,19 @@ def getPassword(url): # Get real pages def getURLContent(url, timeout=15): - """Return page content corresponding to URL or None if any error occurs""" + """Return page content corresponding to URL or None if any error occurs + + Arguments: + url -- the URL to get + timeout -- maximum number of seconds to wait before returning an exception + """ + o = urlparse(url) if o.netloc == "": o = urlparse("http://" + url) + import http.client + if o.scheme == "http": conn = http.client.HTTPConnection(o.hostname, port=o.port, timeout=timeout) @@ -85,7 +84,10 @@ def getURLContent(url, timeout=15): conn = http.client.HTTPConnection(o.hostname, port=80, timeout=timeout) else: return None + + import socket try: + from nemubot import __version__ if o.query != '': conn.request("GET", o.path + "?" + o.query, None, {"User-agent": "Nemubot v%s" % __version__}) @@ -141,16 +143,31 @@ def getURLContent(url, timeout=15): def getXML(url, timeout=15): - """Get content page and return XML parsed content""" + """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 + """ + cnt = getURLContent(url, timeout) if cnt is None: return None else: + from nemubot.tools.xmlparser import parse_string return parse_string(cnt.encode()) def getJSON(url, timeout=15): - """Get content page and return JSON content""" + """Get content page and return JSON content + + Arguments: + url -- the URL to get + timeout -- maximum number of seconds to wait before returning an exception + """ + + import json + cnt = getURLContent(url, timeout) if cnt is None: return None @@ -161,13 +178,27 @@ def getJSON(url, timeout=15): # Other utils def htmlentitydecode(s): - """Decode htmlentities""" + """Decode htmlentities + + Argument: + s -- The string to decode + """ + + import re + from html.entities import name2codepoint + return re.sub('&(%s);' % '|'.join(name2codepoint), lambda m: chr(name2codepoint[m.group(1)]), s) def striphtml(data): - """Remove HTML tags from text""" + """Remove HTML tags from text + + Argument: + data -- the string to strip + """ + + import re p = re.compile(r'<.*?>') return htmlentitydecode(p.sub('', data) .replace("(", "/(") diff --git a/nemubot/tools/xmlparser/__init__.py b/nemubot/tools/xmlparser/__init__.py index 3e39335..f8506fa 100644 --- a/nemubot/tools/xmlparser/__init__.py +++ b/nemubot/tools/xmlparser/__init__.py @@ -16,13 +16,10 @@ # 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 xml.sax from nemubot.tools.xmlparser import node as module_state -logger = logging.getLogger("nemubot.tools.xmlparser") - class ModuleStatesFile(xml.sax.ContentHandler): diff --git a/nemubot/tools/xmlparser/node.py b/nemubot/tools/xmlparser/node.py index 6074ec3..a070e22 100644 --- a/nemubot/tools/xmlparser/node.py +++ b/nemubot/tools/xmlparser/node.py @@ -16,10 +16,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 calendar -from datetime import datetime, timezone import logging -import xml.sax logger = logging.getLogger("nemubot.tools.xmlparser.node") @@ -78,14 +75,17 @@ class ModuleState: else: return None + from datetime import datetime if isinstance(source, datetime): return source else: + from datetime import timezone try: return datetime.utcfromtimestamp(float(source)).replace(tzinfo=timezone.utc) except ValueError: while True: try: + import time return time.strptime(source[:19], "%Y-%m-%d %H:%M:%S").replace(tzinfo=timezone.utc) except ImportError: pass @@ -140,6 +140,7 @@ class ModuleState: def setAttribute(self, name, value): """DOM like method""" + from datetime import datetime if (isinstance(value, datetime) or isinstance(value, str) or isinstance(value, int) or isinstance(value, float)): self.attributes[name] = value @@ -196,14 +197,17 @@ class ModuleState: def save_node(self, gen): """Serialize this node as a XML node""" + from datetime import datetime attribs = {} for att in self.attributes.keys(): if att[0] != "_": # Don't save attribute starting by _ if isinstance(self.attributes[att], datetime): + import calendar attribs[att] = str(calendar.timegm( self.attributes[att].timetuple())) else: attribs[att] = str(self.attributes[att]) + import xml.sax attrs = xml.sax.xmlreader.AttributesImpl(attribs) try: @@ -220,6 +224,7 @@ class ModuleState: def save(self, filename): """Save the current node as root node in a XML file""" with open(filename, "w") as f: + import xml.sax gen = xml.sax.saxutils.XMLGenerator(f, "utf-8") gen.startDocument() self.save_node(gen) From 806ff1d4a0926cedd8502fb51a3f638a9ccc6d65 Mon Sep 17 00:00:00 2001 From: Nemunaire <nemunaire@nemunai.re> Date: Thu, 5 Mar 2015 21:42:46 +0100 Subject: [PATCH 255/674] Move main code to __main__.py --- bin/nemubot | 126 +------------------------------------ nemubot/__init__.py | 2 +- nemubot/__main__.py | 149 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 153 insertions(+), 124 deletions(-) create mode 100644 nemubot/__main__.py diff --git a/bin/nemubot b/bin/nemubot index ae88c0c..97746f1 100755 --- a/bin/nemubot +++ b/bin/nemubot @@ -17,129 +17,9 @@ # 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 sys +from nemubot.__main__ import main + if __name__ == "__main__": - # Parse command line arguments - import argparse - parser = argparse.ArgumentParser() - - parser.add_argument("-a", "--no-connect", action="store_true", - help="disable auto-connect to servers at startup") - - parser.add_argument("-v", "--verbose", action="count", - default=0, - help="verbosity level") - - parser.add_argument("-V", "--version", action="store_true", - help="display nemubot version and exit") - - parser.add_argument("-M", "--modules-path", nargs='*', - default=["./modules/"], - help="directory to use as modules store") - - parser.add_argument("-m", "--module", nargs='*', - help="load given modules") - - parser.add_argument("-D", "--data-path", default="./datas/", - help="path to use to save bot data") - - parser.add_argument('files', metavar='FILE', nargs='*', - help="configuration files to load") - - args = parser.parse_args() - - import nemubot - - if args.version: - print(nemubot.__version__) - sys.exit(0) - - # Setup loggin interface - import logging - logger = logging.getLogger("nemubot") - - formatter = logging.Formatter( - '%(asctime)s %(name)s %(levelname)s %(message)s') - - ch = logging.StreamHandler() - ch.setFormatter(formatter) - if args.verbose > 1: - ch.setLevel(logging.DEBUG) - else: - ch.setLevel(logging.INFO) - logger.addHandler(ch) - - fh = logging.FileHandler('./nemubot.log') - fh.setFormatter(formatter) - fh.setLevel(logging.DEBUG) - logger.addHandler(fh) - - # Add modules dir paths - modules_paths = list() - for path in args.modules_path: - if os.path.isdir(path): - modules_paths.append( - os.path.realpath(os.path.abspath(path))) - else: - logger.error("%s is not a directory", path) - - # Create bot context - from nemubot import datastore - context = nemubot.Bot(modules_paths=modules_paths, data_store=datastore.XML(args.data_path), - verbosity=args.verbose) - - 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 - sys.meta_path.append(ModuleFinder(context.modules_paths, context.add_module)) - - # Load requested configuration files - for path in args.files: - if os.path.isfile(path): - from nemubot.prompt.builtins import load_file - load_file(path, context) - else: - logger.error("%s is not a readable file", path) - - if args.module: - for module in args.module: - __import__(module) - - print ("Nemubot v%s ready, my PID is %i!" % (nemubot.__version__, - os.getpid())) - from nemubot.prompt.reset import PromptReset - while True: - try: - context.start() - if prmpt.run(context): - break - except PromptReset as e: - if e.type == "quit": - break - - try: - import imp - # Reload all other modules - imp.reload(nemubot) - imp.reload(nemubot.prompt) - nemubot.reload() - context = nemubot.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() - print("\nWaiting for other threads shuts down...") - sys.exit(0) + main() diff --git a/nemubot/__init__.py b/nemubot/__init__.py index 7a93012..c51e68e 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -16,7 +16,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/>. -__version__ = '4.0.dev1' +__version__ = '4.0.dev2' __author__ = 'nemunaire' from nemubot.modulecontext import ModuleContext diff --git a/nemubot/__main__.py b/nemubot/__main__.py new file mode 100644 index 0000000..b35cbde --- /dev/null +++ b/nemubot/__main__.py @@ -0,0 +1,149 @@ +# -*- 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/>. + +def main(): + import os + import sys + + # Parse command line arguments + import argparse + parser = argparse.ArgumentParser() + + parser.add_argument("-a", "--no-connect", action="store_true", + help="disable auto-connect to servers at startup") + + parser.add_argument("-v", "--verbose", action="count", + default=0, + help="verbosity level") + + parser.add_argument("-V", "--version", action="store_true", + help="display nemubot version and exit") + + parser.add_argument("-M", "--modules-path", nargs='*', + default=["./modules/"], + help="directory to use as modules store") + + parser.add_argument("-m", "--module", nargs='*', + help="load given modules") + + parser.add_argument("-D", "--data-path", default="./datas/", + help="path to use to save bot data") + + parser.add_argument('files', metavar='FILE', nargs='*', + help="configuration files to load") + + args = parser.parse_args() + + import nemubot + + if args.version: + print(nemubot.__version__) + sys.exit(0) + + # Setup loggin interface + import logging + logger = logging.getLogger("nemubot") + + formatter = logging.Formatter( + '%(asctime)s %(name)s %(levelname)s %(message)s') + + ch = logging.StreamHandler() + ch.setFormatter(formatter) + if args.verbose > 1: + ch.setLevel(logging.DEBUG) + else: + ch.setLevel(logging.INFO) + logger.addHandler(ch) + + fh = logging.FileHandler('./nemubot.log') + fh.setFormatter(formatter) + fh.setLevel(logging.DEBUG) + logger.addHandler(fh) + + # Add modules dir paths + modules_paths = list() + for path in args.modules_path: + if os.path.isdir(path): + modules_paths.append( + os.path.realpath(os.path.abspath(path))) + else: + logger.error("%s is not a directory", path) + + # Create bot context + from nemubot import datastore + from nemubot.bot import Bot + context = Bot(modules_paths=modules_paths, data_store=datastore.XML(args.data_path), + verbosity=args.verbose) + + 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 + sys.meta_path.append(ModuleFinder(context.modules_paths, context.add_module)) + + # Load requested configuration files + for path in args.files: + if os.path.isfile(path): + from nemubot.tools.config import load_file + load_file(path, context) + else: + logger.error("%s is not a readable file", path) + + if args.module: + 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 + + 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() + print("\nWaiting for other threads shuts down...") + sys.exit(0) + +if __name__ == "__main__": + main() From 06bc0a7693e12a4bd028664b0274008b0dc24f8f Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 22 Feb 2015 15:29:10 +0100 Subject: [PATCH 256/674] IRC: allow empty host as ZNC seems to send empty one sometimes --- nemubot/server/IRC.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nemubot/server/IRC.py b/nemubot/server/IRC.py index 27c656c..43d116d 100644 --- a/nemubot/server/IRC.py +++ b/nemubot/server/IRC.py @@ -274,7 +274,7 @@ mgx = re.compile(b'''^(?:@(?P<tags>[^ ]+)\ )? (?::(?P<prefix> (?P<nick>[^!@ ]+) (?: !(?P<user>[^@ ]+))? - (?:@(?P<host>[^ ]+))? + (?:@(?P<host>[^ ]*))? )\ )? (?P<command>(?:[a-zA-Z]+|[0-9]{3})) (?P<params>(?:\ [^:][^ ]*)*)(?:\ :(?P<trailing>.*))? From c984493c795a2791e93e056dec891794d39fa56c Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 10 Mar 2015 22:11:54 +0100 Subject: [PATCH 257/674] Place events to a separate directory --- nemubot/{event.py => event/__init__.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename nemubot/{event.py => event/__init__.py} (100%) diff --git a/nemubot/event.py b/nemubot/event/__init__.py similarity index 100% rename from nemubot/event.py rename to nemubot/event/__init__.py From 3d1a8ff2ba88ccc68ea39d983905fb5e265f6c79 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 14 Mar 2015 01:14:35 +0100 Subject: [PATCH 258/674] [mediawiki] improve output --- modules/mediawiki.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/modules/mediawiki.py b/modules/mediawiki.py index dc461bb..9fb4661 100644 --- a/modules/mediawiki.py +++ b/modules/mediawiki.py @@ -56,14 +56,21 @@ def get_unwikitextified(site, wikitext, ssl=False): def strip_model(cnt): - # Strip models at begin and end: mostly useless - cnt = re.sub(r"^(({{([^{]|\s|({{(.|\s|{{.*?}})*?}})*?)*?}}|\[\[(.|\s|\[\[.*?\]\])*?\]\])\s*)+", "", cnt) + # Strip models at begin: mostly useless + cnt = re.sub(r"^(({{([^{]|\s|({{(.|\s|{{.*?}})*?}})*?)*?}}|\[\[(.|\s|\[\[.*?\]\])*?\]\])\s*)+", "", cnt, flags=re.DOTALL) + + # Remove new line from models + for full in re.findall(r"{{.*?}}", cnt, flags=re.DOTALL): + cnt = cnt.replace(full, full.replace("\n", " "), 1) + + # Remove new line after titles + cnt, _ = re.subn(r"((?P<title>==+)\s*(.*?)\s*(?P=title))\n+", r"\1", cnt) # Strip HTML comments - cnt = re.sub(r"<!--.*?-->", "", cnt) + cnt = re.sub(r"<!--.*?-->", "", cnt, flags=re.DOTALL) # Strip ref - cnt = re.sub(r"<ref.*?/ref>", "", cnt) + cnt = re.sub(r"<ref.*?/ref>", "", cnt, flags=re.DOTALL) return cnt @@ -92,7 +99,7 @@ def parse_wikitext(site, cnt, namespaces=dict(), ssl=False): def irc_format(cnt): - cnt, _ = re.subn(r"(?P<title>==+)\s*(.*?)\s*(?P=title)\n*", "\x03\x16" + r"\2" + " :\x03\x16 ", cnt) + cnt, _ = re.subn(r"(?P<title>==+)\s*(.*?)\s*(?P=title)", "\x03\x16" + r"\2" + " :\x03\x16 ", cnt) return cnt.replace("'''", "\x03\x02").replace("''", "\x03\x1f") From 4d7d1ccab28e86f354227ac2b9cb6405ed5b9496 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 16 Mar 2015 06:45:51 +0100 Subject: [PATCH 259/674] Add unittest for IRCMessage --- nemubot/server/IRC.py | 2 +- nemubot/server/test_IRC.py | 50 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 nemubot/server/test_IRC.py diff --git a/nemubot/server/IRC.py b/nemubot/server/IRC.py index 43d116d..d698fcd 100644 --- a/nemubot/server/IRC.py +++ b/nemubot/server/IRC.py @@ -330,7 +330,7 @@ class IRCMessage: """ # Treat special tags - if key == "time": + 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) diff --git a/nemubot/server/test_IRC.py b/nemubot/server/test_IRC.py new file mode 100644 index 0000000..e22d190 --- /dev/null +++ b/nemubot/server/test_IRC.py @@ -0,0 +1,50 @@ +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_irc_string(False) + msg2 = IRC.IRCMessage(bst1.encode()) + + bst2 = msg2.to_irc_string(False) + msg3 = IRC.IRCMessage(bst2.encode()) + + bst3 = msg3.to_irc_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() From 418ff4f519c9e16f30d039ccc3fa92caffa2e1b8 Mon Sep 17 00:00:00 2001 From: Nemunaire <nemunaire@nemunai.re> Date: Mon, 6 Apr 2015 13:01:02 +0200 Subject: [PATCH 260/674] Datastore: add a method to create a new empty tree --- nemubot/__main__.py | 2 +- nemubot/datastore/abstract.py | 10 ++++++++-- nemubot/modulecontext.py | 3 +-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/nemubot/__main__.py b/nemubot/__main__.py index b35cbde..5d0ef6a 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -142,7 +142,7 @@ def main(): "the prompt.") context.quit() - print("\nWaiting for other threads shuts down...") + print("Waiting for other threads shuts down...") sys.exit(0) if __name__ == "__main__": diff --git a/nemubot/datastore/abstract.py b/nemubot/datastore/abstract.py index ad45665..6162d52 100644 --- a/nemubot/datastore/abstract.py +++ b/nemubot/datastore/abstract.py @@ -19,6 +19,13 @@ class Abstract: """Abstract implementation of a module data store, that always return an empty set""" + def new(self): + """Initialize a new empty storage tree + """ + + from nemubot.tools.xmlparser import module_state + return module_state.ModuleState("nemubotstate") + def open(self): return @@ -35,8 +42,7 @@ class Abstract: The loaded data """ - from nemubot.tools.xmlparser import module_state - return module_state.ModuleState("nemubotstate") + return self.new() def save(self, module, data): """Load data for the given module diff --git a/nemubot/modulecontext.py b/nemubot/modulecontext.py index 7abdefb..4c6193f 100644 --- a/nemubot/modulecontext.py +++ b/nemubot/modulecontext.py @@ -80,9 +80,8 @@ class ModuleContext: module.logger.error("Try to send a message to the unknown server: %s", server) return False - else: + else: # Used when using outside of nemubot from nemubot.tools.xmlparser import module_state - self.data = module_state.ModuleState("nemubotstate") def add_hook(store, hook): From 1b9395ca37da12340c02ed67c0806d9b3d2a77a3 Mon Sep 17 00:00:00 2001 From: Nemunaire <nemunaire@nemunai.re> Date: Mon, 6 Apr 2015 11:31:39 +0200 Subject: [PATCH 261/674] Doc --- nemubot/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nemubot/__init__.py b/nemubot/__init__.py index c51e68e..444886a 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -23,6 +23,9 @@ from nemubot.modulecontext import ModuleContext context = ModuleContext(None, None) def reload(): + """Reload code of all Python modules used by nemubot + """ + import imp import nemubot.channel From 57bbca4e7a3755f0e8337707e116fd4654bc7637 Mon Sep 17 00:00:00 2001 From: Nemunaire <nemunaire@nemunai.re> Date: Mon, 6 Apr 2015 11:32:29 +0200 Subject: [PATCH 262/674] Raise an exception when unable to open datastore, instead of returning False --- datas/datas | 0 nemubot/datastore/xml.py | 22 +++++++++++----------- 2 files changed, 11 insertions(+), 11 deletions(-) delete mode 100644 datas/datas diff --git a/datas/datas b/datas/datas deleted file mode 100644 index e69de29..0000000 diff --git a/nemubot/datastore/xml.py b/nemubot/datastore/xml.py index 7bbe155..67cd9f0 100644 --- a/nemubot/datastore/xml.py +++ b/nemubot/datastore/xml.py @@ -29,18 +29,18 @@ class XML(Abstract): def open(self): """Lock the directory""" - if os.path.isdir(self.basedir): - lock_file = os.path.join(self.basedir, ".used_by_nemubot") - if not os.path.exists(lock_file): - with open(lock_file, 'w') as lf: - lf.write(str(os.getpid())) - return True + if not os.path.isdir(self.basedir): + os.mkdir(self.basedir) - else: - with open(lock_file, 'r') as lf: - pid = lf.readline() - raise Exception("Data dir already locked, by PID %s" % pid) - return False + lock_file = os.path.join(self.basedir, ".used_by_nemubot") + if os.path.exists(lock_file): + with open(lock_file, 'r') as lf: + pid = lf.readline() + raise Exception("Data dir already locked, by PID %s" % pid) + + with open(lock_file, 'w') as lf: + lf.write(str(os.getpid())) + return True def close(self): """Release a locked path""" From c8d495d508a716c33d7eaff5199fc645eebbc2b3 Mon Sep 17 00:00:00 2001 From: Nemunaire <nemunaire@nemunai.re> Date: Mon, 6 Apr 2015 09:50:13 +0200 Subject: [PATCH 263/674] Split messages class into multiple files --- modules/alias.py | 2 +- modules/cmd_server.py | 6 +- modules/more.py | 6 +- modules/speak.py | 30 +++--- modules/ycc.py | 6 +- nemubot/exception.py | 6 +- nemubot/hooks/messagehook.py | 4 +- nemubot/message/__init__.py | 165 ++++----------------------------- nemubot/message/abstract.py | 90 ++++++++++++++++++ nemubot/message/command.py | 43 +++++++++ nemubot/message/directask.py | 39 ++++++++ nemubot/message/printer/IRC.py | 36 +++---- nemubot/message/text.py | 41 ++++++++ nemubot/modulecontext.py | 2 +- nemubot/server/IRC.py | 2 +- 15 files changed, 280 insertions(+), 198 deletions(-) create mode 100644 nemubot/message/abstract.py create mode 100644 nemubot/message/command.py create mode 100644 nemubot/message/directask.py create mode 100644 nemubot/message/text.py diff --git a/modules/alias.py b/modules/alias.py index 050e2d6..ef03172 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -10,7 +10,7 @@ import shlex from nemubot import context from nemubot.exception import IRCException from nemubot.hooks import hook -from nemubot.message import TextMessage, Command +from nemubot.message import Command from nemubot.tools.xmlparser.node import ModuleState nemubotversion = 3.4 diff --git a/modules/cmd_server.py b/modules/cmd_server.py index 0134f8f..8fdadb5 100644 --- a/modules/cmd_server.py +++ b/modules/cmd_server.py @@ -142,9 +142,9 @@ def send(toks, **kwargs): % (toks[1], srv.id)) return 3 - from nemubot.message import TextMessage - srv.send_response(TextMessage(" ".join(toks[2:]), server=None, - to=[toks[1]])) + from nemubot.message import Text + srv.send_response(Text(" ".join(toks[2:]), server=None, + to=[toks[1]])) return 0 diff --git a/modules/more.py b/modules/more.py index 72c5864..302d20e 100644 --- a/modules/more.py +++ b/modules/more.py @@ -20,7 +20,7 @@ import logging -from nemubot.message import TextMessage, DirectAsk +from nemubot.message import Text, DirectAsk from nemubot.hooks import hook nemubotversion = 3.4 @@ -120,8 +120,8 @@ class Response: self.get_message(maxlen - len(self.nick) - 2), server=None, to=self.receivers) else: - return TextMessage(self.get_message(maxlen), - server=None, to=self.receivers) + return Text(self.get_message(maxlen), + server=None, to=self.receivers) def get_message(self, maxlen): if self.alone and len(self.messages) > 1: diff --git a/modules/speak.py b/modules/speak.py index f5c88c2..972aa13 100644 --- a/modules/speak.py +++ b/modules/speak.py @@ -7,7 +7,7 @@ import subprocess from threading import Thread from nemubot.hooks import hook -from nemubot.message import TextMessage +from nemubot.message import Text from nemubot.message.visitor import AbstractVisitor nemubotversion = 3.4 @@ -51,7 +51,7 @@ class SpeakerVisitor(AbstractVisitor): self.last = last - def visit_TextMessage(self, msg): + def visit_Text(self, msg): force = (self.last is None) if force or msg.date - self.last.date > timedelta(0, 500): @@ -86,27 +86,27 @@ class SpeakerVisitor(AbstractVisitor): def visit_DirectAsk(self, msg): - res = TextMessage("%s: %s" % (msg.designated, msg.message), - server=msg.server, date=msg.date, - to=msg.to, frm=msg.frm) + res = Text("%s: %s" % (msg.designated, msg.message), + server=msg.server, date=msg.date, + to=msg.to, frm=msg.frm) res.accept(self) def visit_Command(self, msg): - res = TextMessage("Bang %s%s%s" % (msg.cmd, - " " if len(msg.args) else "", - " ".join(msg.args)), - server=msg.server, date=msg.date, - to=msg.to, frm=msg.frm) + res = Text("Bang %s%s%s" % (msg.cmd, + " " if len(msg.args) else "", + " ".join(msg.args)), + server=msg.server, date=msg.date, + to=msg.to, frm=msg.frm) res.accept(self) def visit_OwnerCommand(self, msg): - res = TextMessage("Owner Bang %s%s%s" % (msg.cmd, - " " if len(msg.args) else "", - " ".join(msg.args)), - server=msg.server, date=msg.date, - to=msg.to, frm=msg.frm) + res = Text("Owner Bang %s%s%s" % (msg.cmd, + " " if len(msg.args) else "", + " ".join(msg.args)), + server=msg.server, date=msg.date, + to=msg.to, frm=msg.frm) res.accept(self) diff --git a/modules/ycc.py b/modules/ycc.py index fba6650..c05296f 100644 --- a/modules/ycc.py +++ b/modules/ycc.py @@ -8,7 +8,7 @@ from urllib.parse import quote from nemubot.exception import IRCException from nemubot.hooks import hook -from nemubot.message import TextMessage +from nemubot.message import Text from nemubot.tools import web nemubotversion = 3.4 @@ -37,8 +37,8 @@ def gen_response(res, msg, srv): if res is None: raise IRCException("mauvaise URL : %s" % srv) else: - return TextMessage("URL pour %s : %s" % (srv, res), server=None, - to=msg.to_response) + return Text("URL pour %s : %s" % (srv, res), server=None, + to=msg.to_response) @hook("cmd_hook", "ycc") diff --git a/nemubot/exception.py b/nemubot/exception.py index 04c142e..93e6a53 100644 --- a/nemubot/exception.py +++ b/nemubot/exception.py @@ -30,6 +30,6 @@ class IRCException(Exception): server=msg.server, to=msg.to_response) else: - from nemubot.message import TextMessage - return TextMessage(self.message, - server=msg.server, to=msg.to_response) + from nemubot.message import Text + return Text(self.message, + server=msg.server, to=msg.to_response) diff --git a/nemubot/hooks/messagehook.py b/nemubot/hooks/messagehook.py index 939286d..8f76114 100644 --- a/nemubot/hooks/messagehook.py +++ b/nemubot/hooks/messagehook.py @@ -39,12 +39,12 @@ class MessageHook(AbstractHook): def match(self, msg, server=None): - if not isinstance(msg, nemubot.message.AbstractMessage): + 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.TextMessage): + elif isinstance(msg, nemubot.message.Text): return self.is_matching(msg.message, msg.to, server) else: return False diff --git a/nemubot/message/__init__.py b/nemubot/message/__init__.py index 232f557..31d7313 100644 --- a/nemubot/message/__init__.py +++ b/nemubot/message/__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,157 +14,28 @@ # 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 - - -class AbstractMessage: - - """This class represents an abstract message""" - - def __init__(self, server, date=None, to=None, to_response=None, frm=None): - """Initialize an abstract message - - Arguments: - server -- the servir identifier - date -- time of the message reception, default: now - to -- list of recipients - to_response -- if channel(s) where send the response differ - frm -- the sender - """ - - self.server = server - self.date = datetime.now(timezone.utc) if date is None else date - self.to = to if to is not None else list() - self._to_response = (to_response if (to_response is None or - isinstance(to_response, list)) - else [ to_response ]) - self.frm = frm # None allowed when it designate this bot - - self.frm_owner = False # Filled later, in consumer - - - @property - def to_response(self): - if self._to_response is not None: - return self._to_response - else: - 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 - return self.to_response[0] - - @property - def nick(self): - # TODO: this is for legacy modules - return self.frm - - - def accept(self, visitor): - visitor.visit(self) - - - def export_args(self, without=list()): - if not isinstance(without, list): - without = [ without ] - - ret = { - "server": self.server, - "date": self.date, - "to": self.to, - "to_response": self._to_response, - "frm": self.frm - } - - for w in without: - if w in ret: - del ret[w] - - return ret - - -class TextMessage(AbstractMessage): - - """This class represent a simple message send to someone""" - - def __init__(self, message, *args, **kargs): - """Initialize a message with no particular specificity - - Argument: - message -- the parsed message - """ - - AbstractMessage.__init__(self, *args, **kargs) - - self.message = message - - def __str__(self): - return self.message - - @property - def text(self): - # TODO: this is for legacy modules - return self.message - - -class DirectAsk(TextMessage): - - """This class represents a message to this bot""" - - def __init__(self, designated, *args, **kargs): - """Initialize a message to a specific person - - Argument: - designated -- the user designated by the message - """ - - TextMessage.__init__(self, *args, **kargs) - - self.designated = designated - - def respond(self, message): - return DirectAsk(self.frm, - message, - server=self.server, - to=self.to_response) - - -class Command(AbstractMessage): - - """This class represents a specialized TextMessage""" - - def __init__(self, cmd, args=None, *nargs, **kargs): - AbstractMessage.__init__(self, *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 - - -class OwnerCommand(Command): - - """This class represents a special command incomming from the owner""" - - pass +from nemubot.message.abstract import Abstract +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) diff --git a/nemubot/message/abstract.py b/nemubot/message/abstract.py new file mode 100644 index 0000000..e308e0c --- /dev/null +++ b/nemubot/message/abstract.py @@ -0,0 +1,90 @@ +# 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, timezone + + +class Abstract: + + """This class represents an abstract message""" + + def __init__(self, server, date=None, to=None, to_response=None, frm=None): + """Initialize an abstract message + + Arguments: + server -- the servir identifier + date -- time of the message reception, default: now + to -- list of recipients + to_response -- if channel(s) where send the response differ + frm -- the sender + """ + + self.server = server + self.date = datetime.now(timezone.utc) if date is None else date + self.to = to if to is not None else list() + self._to_response = (to_response if (to_response is None or + isinstance(to_response, list)) + else [ to_response ]) + self.frm = frm # None allowed when it designate this bot + + self.frm_owner = False # Filled later, in consumer + + + @property + def to_response(self): + if self._to_response is not None: + return self._to_response + else: + 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 + return self.to_response[0] + + @property + def nick(self): + # TODO: this is for legacy modules + return self.frm + + + def accept(self, visitor): + visitor.visit(self) + + + def export_args(self, without=list()): + if not isinstance(without, list): + without = [ without ] + + ret = { + "server": self.server, + "date": self.date, + "to": self.to, + "to_response": self._to_response, + "frm": self.frm + } + + for w in without: + if w in ret: + del ret[w] + + return ret diff --git a/nemubot/message/command.py b/nemubot/message/command.py new file mode 100644 index 0000000..842b19f --- /dev/null +++ b/nemubot/message/command.py @@ -0,0 +1,43 @@ +# 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 Command(Abstract): + + """This class represents a specialized TextMessage""" + + def __init__(self, cmd, args=None, *nargs, **kargs): + Abstract.__init__(self, *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 + + +class OwnerCommand(Command): + + """This class represents a special command incomming from the owner""" + + pass diff --git a/nemubot/message/directask.py b/nemubot/message/directask.py new file mode 100644 index 0000000..03c7902 --- /dev/null +++ b/nemubot/message/directask.py @@ -0,0 +1,39 @@ +# 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.text import Text + + +class DirectAsk(Text): + + """This class represents a message to this bot""" + + def __init__(self, designated, *args, **kargs): + """Initialize a message to a specific person + + Argument: + designated -- the user designated by the message + """ + + Text.__init__(self, *args, **kargs) + + self.designated = designated + + def respond(self, message): + return DirectAsk(self.frm, + message, + server=self.server, + to=self.to_response) diff --git a/nemubot/message/printer/IRC.py b/nemubot/message/printer/IRC.py index 9c622ab..1386e45 100644 --- a/nemubot/message/printer/IRC.py +++ b/nemubot/message/printer/IRC.py @@ -16,7 +16,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 nemubot.message import TextMessage +from nemubot.message import Text from nemubot.message.visitor import AbstractVisitor @@ -26,7 +26,7 @@ class IRC(AbstractVisitor): self.pp = "" - def visit_TextMessage(self, msg): + def visit_Text(self, msg): self.pp += "PRIVMSG %s :" % ",".join(msg.to) if isinstance(msg.message, str): self.pp += msg.message @@ -40,31 +40,31 @@ class IRC(AbstractVisitor): # Avoid nick starting message when discussing on user channel if len(others) != len(msg.to): - res = TextMessage(msg.message, - server=msg.server, date=msg.date, - to=msg.to, frm=msg.frm) + res = Text(msg.message, + server=msg.server, date=msg.date, + to=msg.to, frm=msg.frm) res.accept(self) if len(others): - res = TextMessage("%s: %s" % (msg.designated, msg.message), - server=msg.server, date=msg.date, - to=others, frm=msg.frm) + res = Text("%s: %s" % (msg.designated, msg.message), + server=msg.server, date=msg.date, + to=others, frm=msg.frm) res.accept(self) def visit_Command(self, msg): - res = TextMessage("!%s%s%s" % (msg.cmd, - " " if len(msg.args) else "", - " ".join(msg.args)), - server=msg.server, date=msg.date, - to=msg.to, frm=msg.frm) + res = Text("!%s%s%s" % (msg.cmd, + " " if len(msg.args) else "", + " ".join(msg.args)), + server=msg.server, date=msg.date, + to=msg.to, frm=msg.frm) res.accept(self) def visit_OwnerCommand(self, msg): - res = TextMessage("`%s%s%s" % (msg.cmd, - " " if len(msg.args) else "", - " ".join(msg.args)), - server=msg.server, date=msg.date, - to=msg.to, frm=msg.frm) + res = Text("`%s%s%s" % (msg.cmd, + " " if len(msg.args) else "", + " ".join(msg.args)), + server=msg.server, date=msg.date, + to=msg.to, frm=msg.frm) res.accept(self) diff --git a/nemubot/message/text.py b/nemubot/message/text.py new file mode 100644 index 0000000..ec90a36 --- /dev/null +++ b/nemubot/message/text.py @@ -0,0 +1,41 @@ +# 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 Text(Abstract): + + """This class represent a simple message send to someone""" + + def __init__(self, message, *args, **kargs): + """Initialize a message with no particular specificity + + Argument: + message -- the parsed message + """ + + Abstract.__init__(self, *args, **kargs) + + self.message = message + + def __str__(self): + return self.message + + @property + def text(self): + # TODO: this is for legacy modules + return self.message diff --git a/nemubot/modulecontext.py b/nemubot/modulecontext.py index 4c6193f..625d526 100644 --- a/nemubot/modulecontext.py +++ b/nemubot/modulecontext.py @@ -20,7 +20,7 @@ def convert_legacy_store(old): 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_TextMessage" + return "in_Text" elif old == "all_post": return "post" elif old == "all_pre": diff --git a/nemubot/server/IRC.py b/nemubot/server/IRC.py index d698fcd..718a9bc 100644 --- a/nemubot/server/IRC.py +++ b/nemubot/server/IRC.py @@ -432,6 +432,6 @@ class IRCMessage: # Normal message else: - return message.TextMessage(message=text, **common_args) + return message.Text(message=text, **common_args) return None From 002f2463a314ee0aae1266bc3c5849d9d0cd7620 Mon Sep 17 00:00:00 2001 From: Nemunaire <nemunaire@nemunai.re> Date: Fri, 17 Apr 2015 08:43:03 +0200 Subject: [PATCH 264/674] Extract hooks --- modules/mapquest.py | 5 +- modules/networking/whois.py | 4 +- modules/tpb.py | 4 +- modules/translate.py | 5 +- modules/weather.py | 11 ++-- nemubot/bot.py | 6 +- nemubot/hooks/__init__.py | 66 ++++--------------- nemubot/hooks/abstract.py | 69 ++++++++++++++++++++ nemubot/hooks/{messagehook.py => message.py} | 10 ++- 9 files changed, 104 insertions(+), 76 deletions(-) create mode 100644 nemubot/hooks/abstract.py rename nemubot/hooks/{messagehook.py => message.py} (90%) diff --git a/modules/mapquest.py b/modules/mapquest.py index 6e36f15..b921af0 100644 --- a/modules/mapquest.py +++ b/modules/mapquest.py @@ -22,8 +22,9 @@ def load(context): "http://developer.mapquest.com/") return None - from nemubot.hooks.messagehook import MessageHook - context.add_hook("cmd_hook", MessageHook(cmd_geocode, "geocode")) + import nemubot.hooks + context.add_hook("cmd_hook", + nemubot.hooks.Message(cmd_geocode, "geocode")) def help_full(): diff --git a/modules/networking/whois.py b/modules/networking/whois.py index 3e81e38..8b95e40 100644 --- a/modules/networking/whois.py +++ b/modules/networking/whois.py @@ -19,8 +19,8 @@ def load(CONF, add_hook): else: URL_WHOIS = URL_WHOIS % (urllib.parse.quote(CONF.getNode("whoisxmlapi")["username"]), urllib.parse.quote(CONF.getNode("whoisxmlapi")["password"])) - from nemubot.hooks.messagehook import MessageHook - add_hook("cmd_hook", MessageHook(cmd_whois, "netwhois")) + import nemubot.hooks + add_hook("cmd_hook", nemubot.hooks.Message(cmd_whois, "netwhois")) def extractdate(str): diff --git a/modules/tpb.py b/modules/tpb.py index 6ea4a2d..241ad31 100644 --- a/modules/tpb.py +++ b/modules/tpb.py @@ -23,8 +23,8 @@ def load(context): else: URL_TPBAPI = context.config.getNode("tpbapi")["url"] - from nemubot.hooks.messagehook import MessageHook - context.add_hook("cmd_hook", MessageHook(cmd_tpb, "tpb")) + from nemubot.hooks.message import Message + context.add_hook("cmd_hook", Message(cmd_tpb, "tpb")) def cmd_tpb(msg): diff --git a/modules/translate.py b/modules/translate.py index 8cc5bac..922d685 100644 --- a/modules/translate.py +++ b/modules/translate.py @@ -28,8 +28,9 @@ def load(context): else: URL = URL % context.config.getNode("wrapi")["key"] - from nemubot.hooks.messagehook import MessageHook - context.add_hook("cmd_hook", MessageHook(cmd_translate, "translate")) + import nemubot.hooks + context.add_hook("cmd_hook", + nemubot.hooks.Message(cmd_translate, "translate")) def help_full(): diff --git a/modules/weather.py b/modules/weather.py index a3d5daf..74e0107 100644 --- a/modules/weather.py +++ b/modules/weather.py @@ -29,10 +29,13 @@ def load(context): "http://developer.forecast.io/") return None - from nemubot.hooks.messagehook import MessageHook - context.add_hook("cmd_hook", MessageHook(cmd_weather, "météo")) - context.add_hook("cmd_hook", MessageHook(cmd_alert, "alert")) - context.add_hook("cmd_hook", MessageHook(cmd_coordinates, "coordinates")) + import nemubot.hooks + context.add_hook("cmd_hook", + nemubot.hooks.Message(cmd_weather, "météo")) + context.add_hook("cmd_hook", + nemubot.hooks.Message(cmd_alert, "alert")) + context.add_hook("cmd_hook", + nemubot.hooks.Message(cmd_coordinates, "coordinates")) def help_full (): diff --git a/nemubot/bot.py b/nemubot/bot.py index 5087229..cae24a0 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -23,7 +23,7 @@ import threading from nemubot import __version__ from nemubot.consumer import Consumer, EventConsumer, MessageConsumer from nemubot import datastore -from nemubot.hooks.messagehook import MessageHook +import nemubot.hooks from nemubot.modulecontext import ModuleContext logger = logging.getLogger("nemubot") @@ -75,7 +75,7 @@ class Bot(threading.Thread): 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.hooks.add_hook(MessageHook(in_ping), "in", "DirectAsk") + self.hooks.add_hook(nemubot.hooks.Message(in_ping), "in", "DirectAsk") def _help_msg(msg): """Parse and response to help messages""" @@ -111,7 +111,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.hooks.add_hook(MessageHook(_help_msg, "help"), "in", "Command") + self.hooks.add_hook(nemubot.hooks.Message(_help_msg, "help"), "in", "Command") # Messages to be treated from queue import Queue diff --git a/nemubot/hooks/__init__.py b/nemubot/hooks/__init__.py index bfd26a2..15af034 100644 --- a/nemubot/hooks/__init__.py +++ b/nemubot/hooks/__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,55 +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/>. -def call_game(call, *args, **kargs): - """TODO""" - l = list() - d = kargs - - for a in args: - if a is not None: - if isinstance(a, dict): - d.update(a) - else: - l.append(a) - - return call(*l, **d) - - -class AbstractHook: - - """Abstract class for Hook implementation""" - - def __init__(self, call, data=None, mtimes=-1, end_call=None): - self.call = call - self.data = data - - self.times = mtimes - self.end_call = end_call - - - def match(self, data1, server): - return NotImplemented - - - def run(self, data1, *args): - """Run the hook""" - - from nemubot.exception import IRCException - self.times -= 1 - - try: - ret = call_game(self.call, data1, self.data, *args) - except IRCException as e: - ret = e.fill_response(data1) - finally: - if self.times == 0: - self.call_end(ret) - - return ret - - -from nemubot.hooks.messagehook import MessageHook +from nemubot.hooks.abstract import Abstract +from nemubot.hooks.message import Message last_registered = [] @@ -72,16 +23,21 @@ last_registered = [] def hook(store, *args, **kargs): """Function used as a decorator for module loading""" def sec(call): - last_registered.append((store, MessageHook(call, *args, **kargs))) + last_registered.append((store, Message(call, *args, **kargs))) return call return sec def reload(): + global Abstract, 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 + import nemubot.hooks.manager imp.reload(nemubot.hooks.manager) - - import nemubot.hooks.messagehook - imp.reload(nemubot.hooks.messagehook) diff --git a/nemubot/hooks/abstract.py b/nemubot/hooks/abstract.py new file mode 100644 index 0000000..b0101e4 --- /dev/null +++ b/nemubot/hooks/abstract.py @@ -0,0 +1,69 @@ +# 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 call_game(call, *args, **kargs): + """With given args, try to determine the right call to make + + Arguments: + call -- the function to call + *args -- unamed arguments to pass, dictionnaries contains are placed into kargs + **kargs -- named arguments + """ + + l = list() + d = kargs + + for a in args: + if a is not None: + if isinstance(a, dict): + d.update(a) + else: + l.append(a) + + return call(*l, **d) + + +class Abstract: + + """Abstract class for Hook implementation""" + + def __init__(self, call, data=None, mtimes=-1, end_call=None): + self.call = call + self.data = data + + self.times = mtimes + self.end_call = end_call + + + def match(self, data1, server): + return NotImplemented + + + def run(self, data1, *args): + """Run the hook""" + + from nemubot.exception import IRCException + self.times -= 1 + + try: + ret = call_game(self.call, data1, self.data, *args) + except IRCException as e: + ret = e.fill_response(data1) + finally: + if self.times == 0: + self.call_end(ret) + + return ret diff --git a/nemubot/hooks/messagehook.py b/nemubot/hooks/message.py similarity index 90% rename from nemubot/hooks/messagehook.py rename to nemubot/hooks/message.py index 8f76114..65a2c0f 100644 --- a/nemubot/hooks/messagehook.py +++ b/nemubot/hooks/message.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Nemubot is a smart and modulable IM bot. # Copyright (C) 2012-2015 Mercier Pierre-Olivier # @@ -18,19 +16,19 @@ import re -from nemubot.hooks import AbstractHook +from nemubot.hooks.abstract import Abstract import nemubot.message -class MessageHook(AbstractHook): +class Message(Abstract): """Class storing hook information, specialized for a generic Message""" def __init__(self, call, name=None, data=None, regexp=None, channels=list(), server=None, mtimes=-1, end_call=None): - AbstractHook.__init__(self, call=call, data=data, - end_call=end_call, mtimes=mtimes) + Abstract.__init__(self, call=call, data=data, + end_call=end_call, mtimes=mtimes) self.name = name self.regexp = regexp From 48ebc1b1f5ff277d55714dcb744395c22adcbed5 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 15 Apr 2015 09:54:25 +0200 Subject: [PATCH 265/674] [speak] Avoid saying multiple identical message --- modules/speak.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/speak.py b/modules/speak.py index 972aa13..c08b2bd 100644 --- a/modules/speak.py +++ b/modules/speak.py @@ -120,6 +120,8 @@ def append_message(msg): if hasattr(msg, "message") and msg.message.find("TYPING ") == 0: return + if last is not None and last.message == msg.message: + return vprnt = SpeakerVisitor(last) msg.accept(vprnt) From 8bcceb641fb6db9b6de637a5f7f17440e30ba6c4 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 26 Mar 2015 07:45:33 +0100 Subject: [PATCH 266/674] Add a logger to module context on init --- nemubot/bot.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nemubot/bot.py b/nemubot/bot.py index cae24a0..28b5980 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -369,6 +369,9 @@ class Bot(threading.Thread): # Create module context module.__nemubot_context__ = ModuleContext(self, module) + if not hasattr(module, "logger"): + module.logger = logging.getLogger("nemubot.module." + module.__name__) + # Replace imported context by real one for attr in module.__dict__: if attr != "__nemubot_context__" and type(module.__dict__[attr]) == ModuleContext: From 2644d1bc02a7a459f87b6f2a8ec1a4b910bc1078 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 2 Apr 2015 19:57:26 +0200 Subject: [PATCH 267/674] [xmlparser] Fix date extraction when using old format --- nemubot/tools/xmlparser/node.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nemubot/tools/xmlparser/node.py b/nemubot/tools/xmlparser/node.py index a070e22..3606ac7 100644 --- a/nemubot/tools/xmlparser/node.py +++ b/nemubot/tools/xmlparser/node.py @@ -85,8 +85,8 @@ class ModuleState: except ValueError: while True: try: - import time - return time.strptime(source[:19], "%Y-%m-%d %H:%M:%S").replace(tzinfo=timezone.utc) + import calendar, time + return datetime.utcfromtimestamp(calendar.timegm(time.strptime(source[:19], "%Y-%m-%d %H:%M:%S"))).replace(tzinfo=timezone.utc) except ImportError: pass From f19dd81a0da56ecc209fd3f335a75d5d1ee910b7 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 22 Apr 2015 15:59:22 +0200 Subject: [PATCH 268/674] Update TODO item --- modules/networking/page.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/networking/page.py b/modules/networking/page.py index aade22e..fa03bb5 100644 --- a/modules/networking/page.py +++ b/modules/networking/page.py @@ -10,7 +10,7 @@ from nemubot.tools import web def load(CONF, add_hook): - # check w3m exists + # TODO: check w3m exists pass From 0f2f14ddda4035642a5a8cd7037d2249edc9a930 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 22 Apr 2015 17:30:02 +0200 Subject: [PATCH 269/674] XML datastore: new directory locking procedure This new procedure use fcntl functions to lock the file during the life of the datastore instance. Now, locked directory error is not displayed if if nemubot is not correctly closed. --- nemubot/datastore/xml.py | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/nemubot/datastore/xml.py b/nemubot/datastore/xml.py index 67cd9f0..e17b4aa 100644 --- a/nemubot/datastore/xml.py +++ b/nemubot/datastore/xml.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/>. -import os +import fcntl, os from nemubot.datastore.abstract import Abstract @@ -32,22 +32,35 @@ class XML(Abstract): if not os.path.isdir(self.basedir): os.mkdir(self.basedir) - lock_file = os.path.join(self.basedir, ".used_by_nemubot") - if os.path.exists(lock_file): - with open(lock_file, 'r') as lf: + lock_path = os.path.join(self.basedir, ".used_by_nemubot") + + self.lock_file = open(lock_path, 'a+') + ok = True + try: + fcntl.lockf(self.lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB) + except OSError: + ok = False + + if not ok: + with open(lock_path, 'r') as lf: pid = lf.readline() raise Exception("Data dir already locked, by PID %s" % pid) - with open(lock_file, 'w') as lf: - lf.write(str(os.getpid())) + self.lock_file.truncate() + self.lock_file.write(str(os.getpid())) + self.lock_file.flush() + return True def close(self): """Release a locked path""" - lock_file = os.path.join(self.basedir, ".used_by_nemubot") - if os.path.isdir(self.basedir) and os.path.exists(lock_file): - os.unlink(lock_file) + if hasattr(self, "lock_file"): + self.lock_file.close() + lock_path = os.path.join(self.basedir, ".used_by_nemubot") + if os.path.isdir(self.basedir) and os.path.exists(lock_path): + os.unlink(lock_path) + del self.lock_file return True return False From 0fb58f0ff26adfcffe057807975d91eec672365e Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 24 Apr 2015 13:05:21 +0200 Subject: [PATCH 270/674] Use expat parser instead of SAX to parse XML files --- nemubot/tools/xmlparser/__init__.py | 24 ++++++++++++++---------- nemubot/tools/xmlparser/node.py | 2 +- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/nemubot/tools/xmlparser/__init__.py b/nemubot/tools/xmlparser/__init__.py index f8506fa..4617b57 100644 --- a/nemubot/tools/xmlparser/__init__.py +++ b/nemubot/tools/xmlparser/__init__.py @@ -16,14 +16,14 @@ # 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 xml.sax +import xml.parsers.expat from nemubot.tools.xmlparser import node as module_state -class ModuleStatesFile(xml.sax.ContentHandler): +class ModuleStatesFile: - def startDocument(self): + def __init__(self): self.root = None self.stack = list() @@ -31,7 +31,7 @@ class ModuleStatesFile(xml.sax.ContentHandler): cur = module_state.ModuleState(name) for name in attrs.keys(): - cur.setAttribute(name, attrs.getValue(name)) + cur.setAttribute(name, attrs[name]) self.stack.append(cur) @@ -49,14 +49,18 @@ class ModuleStatesFile(xml.sax.ContentHandler): def parse_file(filename): - parser = xml.sax.make_parser() - mod = ModuleStatesFile() - parser.setContentHandler(mod) - parser.parse(open(filename, "r")) - return mod.root + with open(filename, "r") as f: + return parse_string(f.read()) def parse_string(string): + p = xml.parsers.expat.ParserCreate() mod = ModuleStatesFile() - xml.sax.parseString(string, mod) + + p.StartElementHandler = mod.startElement + p.EndElementHandler = mod.endElement + p.CharacterDataHandler = mod.characters + + p.Parse(string, 1) + return mod.root diff --git a/nemubot/tools/xmlparser/node.py b/nemubot/tools/xmlparser/node.py index 3606ac7..39ddc22 100644 --- a/nemubot/tools/xmlparser/node.py +++ b/nemubot/tools/xmlparser/node.py @@ -224,7 +224,7 @@ class ModuleState: def save(self, filename): """Save the current node as root node in a XML file""" with open(filename, "w") as f: - import xml.sax + import xml.sax.saxutils gen = xml.sax.saxutils.XMLGenerator(f, "utf-8") gen.startDocument() self.save_node(gen) From 40ff3d6edafcaff0bc9ac52706c23cf1835613b5 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 26 Apr 2015 08:02:37 +0200 Subject: [PATCH 271/674] Socket connection can now be made in IPv6 --- nemubot/server/socket.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/nemubot/server/socket.py b/nemubot/server/socket.py index e229d5e..7f46325 100644 --- a/nemubot/server/socket.py +++ b/nemubot/server/socket.py @@ -48,17 +48,9 @@ class SocketServer(AbstractServer): def _open(self): import os import socket - # Create the socket - self.socket = socket.socket() - - # Wrap the socket for SSL - if self.ssl: - import ssl - ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1) - self.socket = ctx.wrap_socket(self.socket) try: - self.socket.connect((self.host, self.port)) # Connect to server + 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: self.socket = None @@ -67,6 +59,12 @@ class SocketServer(AbstractServer): os.strerror(e.errno)) 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 True From 65b5f6b0564513ab2a2c58773362bca08d095c42 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 28 Apr 2015 17:02:00 +0200 Subject: [PATCH 272/674] [mediawiki] Improve parsing of recursive templates --- modules/mediawiki.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/mediawiki.py b/modules/mediawiki.py index 9fb4661..ab498af 100644 --- a/modules/mediawiki.py +++ b/modules/mediawiki.py @@ -57,7 +57,7 @@ def get_unwikitextified(site, wikitext, ssl=False): def strip_model(cnt): # Strip models at begin: mostly useless - cnt = re.sub(r"^(({{([^{]|\s|({{(.|\s|{{.*?}})*?}})*?)*?}}|\[\[(.|\s|\[\[.*?\]\])*?\]\])\s*)+", "", cnt, flags=re.DOTALL) + cnt = re.sub(r"^(({{([^{]|\s|({{([^{]|\s|{{.*?}})*?}})*?)*?}}|\[\[([^[]|\s|\[\[.*?\]\])*?\]\])\s*)+", "", cnt, flags=re.DOTALL) # Remove new line from models for full in re.findall(r"{{.*?}}", cnt, flags=re.DOTALL): From 9c78e9df1d0557d85bc475c701129a47ba2067f2 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 11 Apr 2015 15:20:55 +0200 Subject: [PATCH 273/674] [cve] Merge multiple lines --- modules/cve.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/cve.py b/modules/cve.py index 5f6435d..bc4e58c 100644 --- a/modules/cve.py +++ b/modules/cve.py @@ -19,7 +19,7 @@ def get_cve_desc(msg): cve_id = '' - if msg.cmds[1][:3] == 'cve' : + if msg.cmds[1][:3].lower() == 'cve': cve_id = msg.cmds[1] else: @@ -32,4 +32,4 @@ def get_cve_desc(msg): desc = soup.body.findAll('td') - return Response(desc[DESC_INDEX].text, msg.channel) + return Response(desc[DESC_INDEX].text.replace("\n", " ") + " Moar at " + search_url, msg.channel) From 7d051f7b35189cb042769455d2996e83786fdde6 Mon Sep 17 00:00:00 2001 From: Maxence <max@23.tf> Date: Sat, 23 May 2015 19:58:55 +0200 Subject: [PATCH 274/674] Added a La Poste tracking module --- README.md | 2 +- modules/laposte.py | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 modules/laposte.py diff --git a/README.md b/README.md index c0d796e..e93cbaf 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ An extremely modulable IRC bot, built around XML configuration files! *nemubot* requires at least Python 3.3 to work. -Some modules (like `cve` or `nextstop`) require the +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/modules/laposte.py b/modules/laposte.py new file mode 100644 index 0000000..4e6e750 --- /dev/null +++ b/modules/laposte.py @@ -0,0 +1,40 @@ +import urllib.request +import urllib.parse +from bs4 import BeautifulSoup +from nemubot.hooks import hook +from more import Response + +nemubotversion = 3.4 + +def help_full(): + return "Traquez vos courriers La Poste en utilisant la commande: !laposte <tracking number>\nCe service se base sur http://www.csuivi.courrier.laposte.fr/suivi/index" + + +@hook("cmd_hook", "laposte") +def get_tracking_info(msg): + data = urllib.parse.urlencode({'id': msg.cmds[1]}) + laposte_baseurl = "http://www.part.csuivi.courrier.laposte.fr/suivi/index" + + 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 + and soup.find(class_='resultat_rech_simple_table').thead.tr): + field = search_res.find('td') + poste_id = field.get_text() + + field = field.find_next('td') + poste_type = field.get_text() + + 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 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.lower(), poste_id.strip(), poste_status.lower(), poste_location, poste_date), msg.channel) + return Response("L'identifiant recherché semble incorrect, merci de vérifier son exactitude.", msg.channel) + From 9120cf56c2950ef59c6cfdd9299f5e259770b2b2 Mon Sep 17 00:00:00 2001 From: Maxence <dev@23.tf> Date: Sat, 23 May 2015 23:46:46 +0200 Subject: [PATCH 275/674] Updated SAP transaction lookup module --- modules/sap.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/modules/sap.py b/modules/sap.py index 0110c26..5806708 100644 --- a/modules/sap.py +++ b/modules/sap.py @@ -4,6 +4,8 @@ import re import urllib.parse +import urllib.request +from bs4 import BeautifulSoup from nemubot.exception import IRCException from nemubot.hooks import hook @@ -16,7 +18,7 @@ from more import Response def help_full(): - return "!tcode <transaction code|keywords>" + return "Retrieve SAP transaction codes and details using tcodes or keywords: !tcode <transaction code|keywords>" @hook("cmd_hook", "tcode") @@ -27,19 +29,17 @@ def cmd_tcode(msg): url = ("http://www.tcodesearch.com/tcodes/search?q=%s" % urllib.parse.quote(msg.cmds[1])) + page = web.getURLContent(url) + soup = BeautifulSoup(page) res = Response(channel=msg.channel, nomore="No more transaction code", count=" (%d more tcodes)") - if page is not None: - index = (page.index('<div id="searchresults">') + - len('<div id="searchresults">')) - end = page[index:].index('</div>')+index - strscope = page[index:end] - for tcode in re.finditer('<strong> ([a-zA-Z0-9_]*)</strong> - ([^\n]*)\n', strscope): - res.append_message("\x02%s\x0F - %s" % (tcode.group(1), - striphtml(tcode.group(2)))) + + search_res = soup.find("", {'id':'searchresults'}) + for item in search_res.find_all('dd'): + res.append_message(item.get_text().split('\n')[1].strip()) return res From 481b1974c357f9a9f40766a60b8a289cf0305d01 Mon Sep 17 00:00:00 2001 From: Maxence <dev@23.tf> Date: Sun, 24 May 2015 11:06:37 +0200 Subject: [PATCH 276/674] [laposte] handling no arguments --- modules/laposte.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/laposte.py b/modules/laposte.py index 4e6e750..70ae045 100644 --- a/modules/laposte.py +++ b/modules/laposte.py @@ -1,7 +1,9 @@ import urllib.request import urllib.parse from bs4 import BeautifulSoup + from nemubot.hooks import hook +from nemubot.exception import IRCException from more import Response nemubotversion = 3.4 @@ -12,6 +14,9 @@ def help_full(): @hook("cmd_hook", "laposte") def get_tracking_info(msg): + if len(msg.cmds) < 2: + raise IRCException("Renseignez un identifiant d'envoi,") + data = urllib.parse.urlencode({'id': msg.cmds[1]}) laposte_baseurl = "http://www.part.csuivi.courrier.laposte.fr/suivi/index" From fc1bc135df427b46b9bdc9a6beabb8d05308a3ce Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 22 May 2015 23:56:24 +0200 Subject: [PATCH 277/674] Importer: now compatible with Python 3.4 --- nemubot/importer.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/nemubot/importer.py b/nemubot/importer.py index 4bb990e..d5688d5 100644 --- a/nemubot/importer.py +++ b/nemubot/importer.py @@ -55,14 +55,24 @@ class ModuleLoader(SourceFileLoader): SourceFileLoader.__init__(self, fullname, path) - def load_module(self, fullname): - module = SourceFileLoader.load_module(self, fullname) - + def _load(self, module, name): # Add the module to the global modules list if self.add_module(module): - logger.info("Module '%s' successfully loaded.", module.__name__) + logger.info("Module '%s' successfully loaded.", name) else: - logger.error("An error occurs while importing `%s'.", module.__name__) + logger.error("An error occurs while importing `%s'.", name) raise ImportError("An error occurs while importing `%s'." - % module.__name__) + % name) return module + + + # Python 3.4 + def exec_module(self, module): + super(ModuleLoader, self).exec_module(module) + self._load(module, module.__spec__.name) + + + # Python 3.3 + def load_module(self, fullname): + module = super(ModuleLoader, self).load_module(fullname) + return self._load(module, module.__name__) From 500e3a6e0164b15a25ef93024d1b25af189dd8c0 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 23 May 2015 00:33:15 +0200 Subject: [PATCH 278/674] Compatibly with Python 3.4 --- nemubot/bot.py | 12 +++++++----- nemubot/modulecontext.py | 11 +++++++---- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/nemubot/bot.py b/nemubot/bot.py index 28b5980..65d8221 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -355,13 +355,15 @@ class Bot(threading.Thread): def add_module(self, module): """Add a module to the context, if already exists, unload the old one before""" + module_name = module.__spec__.name if hasattr(module, "__spec__") else module.__name__ + # Check if the module already exists - if module.__name__ in self.modules: - self.unload_module(module.__name__) + if module_name in self.modules: + self.unload_module(module_name) # Overwrite print built-in def prnt(*args): - print("[%s]" % module.__name__, *args) + print("[%s]" % module_name, *args) if hasattr(module, "logger"): module.logger.info(" ".join(args)) module.print = prnt @@ -370,7 +372,7 @@ class Bot(threading.Thread): module.__nemubot_context__ = ModuleContext(self, module) if not hasattr(module, "logger"): - module.logger = logging.getLogger("nemubot.module." + module.__name__) + module.logger = logging.getLogger("nemubot.module." + module_name) # Replace imported context by real one for attr in module.__dict__: @@ -384,7 +386,7 @@ class Bot(threading.Thread): nemubot.hooks.last_registered = [] # Save a reference to the module - self.modules[module.__name__] = module + self.modules[module_name] = module # Launch the module if hasattr(module, "load"): diff --git a/nemubot/modulecontext.py b/nemubot/modulecontext.py index 625d526..731dfba 100644 --- a/nemubot/modulecontext.py +++ b/nemubot/modulecontext.py @@ -40,10 +40,13 @@ class ModuleContext: module -- the module """ + if module is not None: + module_name = module.__spec__.name if hasattr(module, "__spec__") 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__] + module_name in context.modules_configuration): + self.config = context.modules_configuration[module_name] else: self.config = None @@ -54,7 +57,7 @@ class ModuleContext: # Define some callbacks if context is not None: # Load module data - self.data = context.datastore.load(module.__name__) + self.data = context.datastore.load(module_name) def add_hook(store, hook): store = convert_legacy_store(store) @@ -99,7 +102,7 @@ class ModuleContext: print(res) def save(): - context.datastore.save(module.__name__, self.data) + context.datastore.save(module_name, self.data) self.add_hook = add_hook self.del_hook = del_hook From 4be9f781041f60fa93949ce86acb33d9f279a186 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 26 May 2015 07:26:13 +0200 Subject: [PATCH 279/674] [whois] New module from nbr23 bot --- modules/whois.py | 101 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 modules/whois.py diff --git a/modules/whois.py b/modules/whois.py new file mode 100644 index 0000000..5d4638e --- /dev/null +++ b/modules/whois.py @@ -0,0 +1,101 @@ +# coding=utf-8 + +import re + +from nemubot import context +from nemubot.exception import IRCException +from nemubot.hooks import hook +from nemubot.tools.xmlparser.node import ModuleState + +nemubotversion = 3.4 + +from more import Response + +PASSWD_FILE = None + +def load(context): + global PASSWD_FILE + if not context.config or not context.config.hasAttribute("passwd"): + print("No passwd file given") + return None + PASSWD_FILE = context.config["passwd"] + + if not context.data.hasNode("aliases"): + 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("cmd_hook", + nemubot.hooks.Message(cmd_whois, "whois")) + +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 get_promo(self): + return self.home.split("/")[2].replace("_", " ") + + def get_photo(self): + if self.login in context.data.getNode("pics").index: + return context.data.getNode("pics").index[self.login]["url"] + else: + return "https://static.acu.epita.fr/photos/%s" % self.login + + +def found_login(login): + if login in context.data.getNode("aliases").index: + login = context.data.getNode("aliases").index[login]["to"] + + login_ = login + ":" + 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 + +def cmd_whois(msg): + if len(msg.args) < 1: + raise IRCException("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: + 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 "")) + else: + res.append_message("Unknown %s :(" % srch) + return res + +@hook("ask_default") +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: + 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 + if nick in context.data.getNode("aliases").index: + context.data.getNode("aliases").index[nick]["to"] = login + else: + ms = ModuleState("alias") + ms.setAttribute("from", nick) + ms.setAttribute("to", login) + context.data.getNode("aliases").addChild(ms) + context.save() + return Response("ok, c'est noté, %s est %s" + % (nick, login), + channel=msg.channel, + nick=msg.nick) From fc500bc853cf8018e0af6f220cd979eba8d8519f Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 27 May 2015 02:16:48 +0200 Subject: [PATCH 280/674] Tools.Web: fix charset detection on webpages --- 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 b7960c5..4f02289 100644 --- a/nemubot/tools/web.py +++ b/nemubot/tools/web.py @@ -118,7 +118,7 @@ def getURLContent(url, timeout=15): if cntype is not None: lcharset = res.getheader("Content-Type").split(";") if len(lcharset) > 1: - for c in charset: + for c in lcharset: ch = c.split("=") if ch[0].strip().lower() == "charset" and len(ch) > 1: cha = ch[1].split(".") @@ -132,7 +132,7 @@ def getURLContent(url, timeout=15): conn.close() if res.status == http.client.OK or res.status == http.client.SEE_OTHER: - return data.decode(charset) + return data.decode(charset).strip() elif ((res.status == http.client.FOUND or res.status == http.client.MOVED_PERMANENTLY) and res.getheader("Location") != url): From b3274c0dc70935ae778aa62e910a0ead3fe5940c Mon Sep 17 00:00:00 2001 From: Bob <bob@23.tf> Date: Fri, 29 May 2015 13:54:00 +0200 Subject: [PATCH 281/674] New module youtube-title: that retrieve title from a youtube link --- modules/youtube-title.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 modules/youtube-title.py diff --git a/modules/youtube-title.py b/modules/youtube-title.py new file mode 100644 index 0000000..5142115 --- /dev/null +++ b/modules/youtube-title.py @@ -0,0 +1,27 @@ +import urllib.request +from bs4 import BeautifulSoup +from nemubot.exception import IRCException +from nemubot.hooks import hook +from nemubot.tools.web import getURLContent +from more import Response + +nemubotversion = 3.4 + +def help_tiny(): + return "Return the video title from a youtube link" + +def help_full(): + return "No help " + +@hook("cmd_hook", "yt") +def get_info_yt(msg): + if len(msg.args) <= 0: + raise IRCException("Please provide an URL from youtube.com") + + res = list() + for url in msg.args: + req = getURLContent(url) + soup = BeautifulSoup(req) + desc = soup.body.find(id='eow-title') + res.append(desc.text.strip()) + return Response(res, channel=msg.channel, nomore="No more description") From 63a6654331834ef08b1dadc3c246f2cdef7a59a4 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 30 May 2015 15:17:46 +0200 Subject: [PATCH 282/674] [yt] Improve module: track last video URL --- modules/youtube-title.py | 73 ++++++++++++++++++++++++++++++++-------- 1 file changed, 59 insertions(+), 14 deletions(-) diff --git a/modules/youtube-title.py b/modules/youtube-title.py index 5142115..24cb82b 100644 --- a/modules/youtube-title.py +++ b/modules/youtube-title.py @@ -1,27 +1,72 @@ -import urllib.request +import re +from urllib.parse import urlparse from bs4 import BeautifulSoup + from nemubot.exception import IRCException from nemubot.hooks import hook from nemubot.tools.web import getURLContent from more import Response +"""Get information of youtube videos""" + nemubotversion = 3.4 -def help_tiny(): - return "Return the video title from a youtube link" - def help_full(): - return "No help " + return "!yt [<url>]: with an argument, get information about the given link; without arguments, use the latest youtube link seen on the current channel." + +LAST_URLS = dict() @hook("cmd_hook", "yt") def get_info_yt(msg): - if len(msg.args) <= 0: - raise IRCException("Please provide an URL from youtube.com") + links = list() - res = list() - for url in msg.args: - req = getURLContent(url) - soup = BeautifulSoup(req) - desc = soup.body.find(id='eow-title') - res.append(desc.text.strip()) - return Response(res, channel=msg.channel, nomore="No more description") + if len(msg.args) <= 0: + global LAST_URLS + 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!") + else: + for url in msg.args: + links.append(url) + + titles = list() + descrip = list() + for url in links: + if not re.findall("([a-zA-Z0-9+.-]+:(?://)?[^ ]+)", url): + url = "http://youtube.com/watch?v=" + url + soup = BeautifulSoup(getURLContent(url)) + shortlink = soup.head.find("link", rel="shortlink") + titl = soup.body.find(id='eow-title') + titles.append("%s : %s" % (shortlink["href"], titl.text.strip())) + desc = soup.body.find(id='eow-description') + descrip.append(desc.text.strip()) + res = Response(channel=msg.channel) + if len(titles) > 0: + res.append_message(titles) + for d in descrip: + res.append_message(d) + return res + + +@hook("msg_default") +def parselisten(msg): + parseresponse(msg) + return None + + +@hook("all_post") +def parseresponse(msg): + global LAST_URLS + urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?[^ ]+)", msg.text) + for url in urls: + o = urlparse(url) + if o.scheme != "": + if o.netloc == "" and len(o.path) < 10: + continue + if (o.netloc == "youtube.com" or o.netloc == "www.youtube.com" or + o.netloc == "youtu.be" or o.netloc == "www.youtu.be"): + if msg.channel not in LAST_URLS: + LAST_URLS[msg.channel] = list() + LAST_URLS[msg.channel].append(url) + return msg From 889e3762540e143d39a9d9553d089f843359d0f0 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 28 May 2015 07:18:07 +0200 Subject: [PATCH 283/674] [whois] Module try to find a recent photo --- modules/whois.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/modules/whois.py b/modules/whois.py index 5d4638e..f59b541 100644 --- a/modules/whois.py +++ b/modules/whois.py @@ -10,6 +10,7 @@ from nemubot.tools.xmlparser.node import ModuleState nemubotversion = 3.4 from more import Response +from networking.page import headers PASSWD_FILE = None @@ -48,8 +49,12 @@ class Login: def get_photo(self): if self.login in context.data.getNode("pics").index: return context.data.getNode("pics").index[self.login]["url"] - else: - return "https://static.acu.epita.fr/photos/%s" % self.login + for url in [ "https://static.acu.epita.fr/photos/%s", "https://static.acu.epita.fr/photos/%s/%%s" % self.gid, "https://intra-bocal.epitech.net/trombi/%s.jpg", "http://pub.23.tf/p/%s/%%s.jpg" % self.gid ]: + url = url % self.login + _, status, _, _ = headers(url) + if status == 200: + return url + return None def found_login(login): From 8d91ad31fb3d5b6fce1b420b3c1725962d441c75 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 29 May 2015 19:28:07 +0200 Subject: [PATCH 284/674] [whois] Module try to find a recent photo --- modules/whois.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/modules/whois.py b/modules/whois.py index f59b541..03091a4 100644 --- a/modules/whois.py +++ b/modules/whois.py @@ -84,6 +84,25 @@ def cmd_whois(msg): res.append_message("Unknown %s :(" % srch) return res +@hook("cmd_hook", "nicks") +def cmd_nicks(msg): + if len(msg.args) < 1: + raise IRCException("Provide a login") + nick = found_login(msg.args[0]) + if nick is None: + nick = msg.args[0] + else: + nick = nick.login + + nicks = [] + for alias in context.data.getNode("aliases").getChilds(): + if alias["to"] == nick: + nicks.append(alias["from"]) + if len(nicks) >= 1: + return Response("%s is also known as %s." % (nick, ", ".join(nicks)), channel=msg.channel) + else: + return Response("%s has no known alias." % nick, channel=msg.channel) + @hook("ask_default") 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) From 04023e945e0acc1aaf7e01adb19ceaef03b3f8f2 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 31 May 2015 16:55:43 +0200 Subject: [PATCH 285/674] Fix module parseresponse when more.Response is used as response --- modules/more.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/modules/more.py b/modules/more.py index 302d20e..062b6f7 100644 --- a/modules/more.py +++ b/modules/more.py @@ -103,6 +103,17 @@ class Response: else: return self.rawtitle + @property + def text(self): + if len(self.messages) < 1: + return self.nomore + else: + for msg in self.messages: + if isinstance(msg, list): + return ", ".join(msg) + else: + return msg + def pop(self): self.messages.pop(0) self.elt = 0 From 19ad2d7a32baf49950f67112e1012f3c4d379cbf Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 1 Jun 2015 17:06:13 +0200 Subject: [PATCH 286/674] [github] use msg.args instead of deprecated msg.cmds; fixes #80 --- modules/github.py | 47 +++++++++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/modules/github.py b/modules/github.py index a3f0ba8..ab40589 100644 --- a/modules/github.py +++ b/modules/github.py @@ -67,10 +67,10 @@ def info_commit(repo, commit=None): @hook("cmd_hook", "github") def cmd_github(msg): - if len(msg.cmds) < 2: + if not len(msg.args): raise IRCException("indicate a repository name to search") - repos = info_repos(" ".join(msg.cmds[1:])) + repos = info_repos(" ".join(msg.args)) res = Response(channel=msg.channel, nomore="No more repository", @@ -95,12 +95,12 @@ def cmd_github(msg): @hook("cmd_hook", "github_user") def cmd_github(msg): - if len(msg.cmds) < 2: + if not len(msg.args): raise IRCException("indicate a user name to search") res = Response(channel=msg.channel, nomore="No more user") - user = info_user(" ".join(msg.cmds[1:])) + user = info_user(" ".join(msg.args)) if "login" in user: if user["repos"]: @@ -128,21 +128,21 @@ def cmd_github(msg): @hook("cmd_hook", "github_issue") def cmd_github(msg): - if len(msg.cmds) < 2: + if not len(msg.args): raise IRCException("indicate a repository to view its issues") issue = None - if len(msg.cmds) > 2: - li = re.match("^#?([0-9]+)$", msg.cmds[1]) - ri = re.match("^#?([0-9]+)$", msg.cmds[-1]) - if li is not None: - issue = msg.cmds[1] - del msg.cmds[1] - elif ri is not None: - issue = msg.cmds[-1] - del msg.cmds[-1] - repo = " ".join(msg.cmds[1:]) + li = re.match("^#?([0-9]+)$", msg.args[0]) + ri = re.match("^#?([0-9]+)$", msg.args[-1]) + if li is not None: + issue = msg.args[0] + del msg.args[0] + elif ri is not None: + issue = msg.args[-1] + del msg.args[-1] + + repo = " ".join(msg.args) count = " (%d more issues)" if issue is None else None res = Response(channel=msg.channel, nomore="No more issue", count=count) @@ -166,19 +166,18 @@ def cmd_github(msg): @hook("cmd_hook", "github_commit") def cmd_github(msg): - if len(msg.cmds) < 2: + if not len(msg.args): raise IRCException("indicate a repository to view its commits") commit = None - if len(msg.cmds) > 2: - if re.match("^[a-fA-F0-9]+$", msg.cmds[1]): - commit = msg.cmds[1] - del msg.cmds[1] - elif re.match("^[a-fA-F0-9]+$", msg.cmds[-1]): - commit = msg.cmds[-1] - del msg.cmds[-1] + if re.match("^[a-fA-F0-9]+$", msg.args[0]): + commit = msg.args[0] + del msg.args[0] + elif re.match("^[a-fA-F0-9]+$", msg.args[-1]): + commit = msg.args[-1] + del msg.args[-1] - repo = " ".join(msg.cmds[1:]) + repo = " ".join(msg.args) count = " (%d more commits)" if commit is None else None res = Response(channel=msg.channel, nomore="No more commit", count=count) From 91688875d4a59ea3e9e74924d67c099d8fcdf5ca Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 1 Jun 2015 17:14:54 +0200 Subject: [PATCH 287/674] [github] can use #ID as ID when looking for a particular issue --- modules/github.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/github.py b/modules/github.py index ab40589..b8aa9d2 100644 --- a/modules/github.py +++ b/modules/github.py @@ -136,10 +136,10 @@ def cmd_github(msg): li = re.match("^#?([0-9]+)$", msg.args[0]) ri = re.match("^#?([0-9]+)$", msg.args[-1]) if li is not None: - issue = msg.args[0] + issue = li.group(1) del msg.args[0] elif ri is not None: - issue = msg.args[-1] + issue = ri.group(1) del msg.args[-1] repo = " ".join(msg.args) From c1858fff3ac698e632add00e6e8fd8dce5ce202b Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 2 Jun 2015 22:24:45 +0200 Subject: [PATCH 288/674] Catch exception during module loading: just skip the module registration --- nemubot/bot.py | 12 +++++++----- nemubot/importer.py | 8 ++------ nemubot/tools/config.py | 7 ++++++- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/nemubot/bot.py b/nemubot/bot.py index 65d8221..544e5f0 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -385,14 +385,16 @@ class Bot(threading.Thread): module.__nemubot_context__.add_hook(s, h) nemubot.hooks.last_registered = [] - # Save a reference to the module - self.modules[module_name] = module - # Launch the module if hasattr(module, "load"): - module.load(module.__nemubot_context__) + try: + module.load(module.__nemubot_context__) + except: + module.__nemubot_context__.unload() + raise - return True + # Save a reference to the module + self.modules[module_name] = module def unload_module(self, name): diff --git a/nemubot/importer.py b/nemubot/importer.py index d5688d5..4d4b6a7 100644 --- a/nemubot/importer.py +++ b/nemubot/importer.py @@ -57,12 +57,8 @@ class ModuleLoader(SourceFileLoader): def _load(self, module, name): # Add the module to the global modules list - if self.add_module(module): - logger.info("Module '%s' successfully loaded.", name) - else: - logger.error("An error occurs while importing `%s'.", name) - raise ImportError("An error occurs while importing `%s'." - % name) + self.add_module(module) + logger.info("Module '%s' successfully loaded.", name) return module diff --git a/nemubot/tools/config.py b/nemubot/tools/config.py index 488179b..608da56 100644 --- a/nemubot/tools/config.py +++ b/nemubot/tools/config.py @@ -117,7 +117,12 @@ def load_file(filename, context): for mod in config.getNodes("module"): context.modules_configuration[mod["name"]] = mod if get_boolean(mod, "autoload", default=True): - __import__(mod["name"]) + 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"): From 2e55ba5671cc9ff9307202e31ecdbc94cf3e34d6 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 3 Jun 2015 21:05:28 +0200 Subject: [PATCH 289/674] [ddg/wolframalpha] extract wolframalpha module and dusting --- bot_sample.xml | 4 +- modules/ddg/__init__.py | 30 ------------ modules/{ddg/WFASearch.py => wolframalpha.py} | 48 +++++++++++++++---- 3 files changed, 39 insertions(+), 43 deletions(-) rename modules/{ddg/WFASearch.py => wolframalpha.py} (60%) diff --git a/bot_sample.xml b/bot_sample.xml index 0867bb6..ce821d2 100644 --- a/bot_sample.xml +++ b/bot_sample.xml @@ -11,9 +11,7 @@ --> <!-- - <module name="ddg"> - <wfaapi key="YOUR-APIKEY" /> - </module> + <module name="wolframalpha" apikey="YOUR-APIKEY" /> --> <module name="cmd_server" /> diff --git a/modules/ddg/__init__.py b/modules/ddg/__init__.py index 63ef340..75ee56a 100644 --- a/modules/ddg/__init__.py +++ b/modules/ddg/__init__.py @@ -13,17 +13,6 @@ from more import Response from . import DDGSearch from . import UrbanDictionnary -from . import WFASearch - -def load(context): - WFASearch.CONF = context.config - -def reload(): - imp.reload(DDGSearch) - imp.reload(UrbanDictionnary) - imp.reload(WFASearch) - imp.reload(Wikipedia) - @hook("cmd_hook", "define") def define(msg): @@ -76,22 +65,3 @@ def udsearch(msg): res.append_message(d) return res - - -@hook("cmd_hook", "calculate") -def calculate(msg): - if len(msg.cmds) <= 1: - return Response("Indicate a calcul to compute", - msg.channel, nick=msg.nick) - - s = WFASearch.WFASearch(' '.join(msg.cmds[1:])) - - 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) diff --git a/modules/ddg/WFASearch.py b/modules/wolframalpha.py similarity index 60% rename from modules/ddg/WFASearch.py rename to modules/wolframalpha.py index f20bbe2..f3bc072 100644 --- a/modules/ddg/WFASearch.py +++ b/modules/wolframalpha.py @@ -2,22 +2,32 @@ from urllib.parse import quote +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 + +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"): + raise ImportError ("You need a Wolfram|Alpha API key in order to use " + "this module. Add it to the module configuration: " + "\n<module name=\"wolframalpha\" " + "apikey=\"XXXXXX-XXXXXXXXXX\" />\n" + "Register at http://products.wolframalpha.com/api/") + URL_API = URL_API % quote(context.config["apikey"]).replace("%", "%%") + class WFASearch: def __init__(self, terms): self.terms = terms - try: - url = ("http://api.wolframalpha.com/v2/query?input=%s&appid=%s" % - (quote(terms), CONF.getNode("wfaapi")["key"])) - self.wfares = web.getXML(url) - except (TypeError, KeyError): - print ("You need a Wolfram|Alpha API key in order to use this " - "module. Add it to the module configuration file:\n<wfaapi" - " key=\"XXXXXX-XXXXXXXXXX\" />\nRegister at " - "http://products.wolframalpha.com/api/") - self.wfares = None + self.wfares = web.getXML(URL_API % quote(terms)) @property def success(self): @@ -69,3 +79,21 @@ class WFASearch: subnode.getFirstNode("plaintext").getContent()) except IndexError: pass + + +@hook("cmd_hook", "calculate") +def calculate(msg): + if not len(msg.args): + raise IRCException("Indicate a calcul to compute") + + s = WFASearch(' '.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) From d95de8c195658326be938e1df63bdae8728f941e Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 3 Jun 2015 22:07:06 +0200 Subject: [PATCH 290/674] In some modules, raise ImportError to avoid module loading on errors --- modules/books.py | 25 ++++++++++---------- modules/mapquest.py | 33 ++++++++++++-------------- modules/networking/__init__.py | 5 +++- modules/networking/whois.py | 17 +++++++------- modules/sap.py | 6 ++--- modules/syno.py | 26 ++++++++++---------- modules/tpb.py | 26 +++++++++----------- modules/translate.py | 43 ++++++++++++++++------------------ modules/velib.py | 10 ++++++-- modules/weather.py | 30 ++++++++++-------------- 10 files changed, 108 insertions(+), 113 deletions(-) diff --git a/modules/books.py b/modules/books.py index f5c0c7f..5d9ed82 100644 --- a/modules/books.py +++ b/modules/books.py @@ -9,18 +9,17 @@ from nemubot.exception import IRCException from nemubot.hooks import hook from nemubot.tools import web -nemubotversion = 3.4 +nemubotversion = 4.0 from more import Response def load(context): - if not context.config or not context.config.hasNode("goodreadsapi") or not context.config.getNode("goodreadsapi").hasAttribute("key"): - print ("You need a Goodreads API key in order to use this " - "module. Add it to the module configuration file:\n<goodreadsapi" - " key=\"XXXXXXXXXXXXXXXX\" />\nGet one at " - "https://www.goodreads.com/api/keys") - return None + if not context.config or not context.config.getAttribute("goodreadskey"): + raise ImportError("You need a Goodreads API key in order to use this " + "module. Add it to the module configuration file:\n" + "<module name=\"books\" goodreadskey=\"XXXXXX\" />\n" + "Get one at https://www.goodreads.com/api/keys") def get_book(title): @@ -57,10 +56,10 @@ def search_author(name): @hook("cmd_hook", "book") def cmd_book(msg): - if len(msg.cmds) < 2: + if not len(msg.args): raise IRCException("please give me a title to search") - book = get_book(" ".join(msg.cmds[1:])) + book = get_book(" ".join(msg.args)) if book is None: raise IRCException("unable to find book named like this") res = Response(channel=msg.channel) @@ -72,10 +71,10 @@ def cmd_book(msg): @hook("cmd_hook", "search_books") def cmd_books(msg): - if len(msg.cmds) < 2: + if not len(msg.args): raise IRCException("please give me a title to search") - title = " ".join(msg.cmds[1:]) + title = " ".join(msg.args) res = Response(channel=msg.channel, title="%s" % (title), count=" (%d more books)") @@ -88,10 +87,10 @@ def cmd_books(msg): @hook("cmd_hook", "author_books") def cmd_author(msg): - if len(msg.cmds) < 2: + if not len(msg.args): raise IRCException("please give me an author to search") - ath = search_author(" ".join(msg.cmds[1:])) + ath = search_author(" ".join(msg.args)) return Response([b.getNode("title").getContent() for b in ath.getNode("books").getNodes("book")], channel=msg.channel, title=ath.getNode("name").getContent()) diff --git a/modules/mapquest.py b/modules/mapquest.py index b921af0..95952ab 100644 --- a/modules/mapquest.py +++ b/modules/mapquest.py @@ -1,30 +1,28 @@ # coding=utf-8 -"""The mapquest module""" +"""Transform name location to GPS coordinates""" import re from urllib.parse import quote -from nemubot import context from nemubot.exception import IRCException +from nemubot.hooks import hook from nemubot.tools import web -nemubotversion = 3.4 +nemubotversion = 4.0 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.hasNode("mapquestapi") or not context.config.getNode("mapquestapi").hasAttribute("key"): - print ("You need a MapQuest API key in order to use this " - "module. Add it to the module configuration file:\n<mapquestapi" - " key=\"XXXXXXXXXXXXXXXX\" />\nRegister at " - "http://developer.mapquest.com/") - return None - - import nemubot.hooks - context.add_hook("cmd_hook", - nemubot.hooks.Message(cmd_geocode, "geocode")) + if not context.config or not context.config.hasAttribute("apikey"): + 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/") + global URL_API + URL_API = URL_API % context.config["apikey"].replace("%", "%%") def help_full(): @@ -32,8 +30,7 @@ def help_full(): def geocode(location): - obj = web.getJSON("http://open.mapquestapi.com/geocoding/v1/address?key=%s&location=%s" % - (context.config.getNode("mapquestapi")["key"], quote(location))) + obj = web.getJSON(URL_API % quote(location)) if "results" in obj and "locations" in obj["results"][0]: for loc in obj["results"][0]["locations"]: @@ -46,15 +43,15 @@ def where(loc): "{adminArea1}".format(**loc)).strip() +@hook("cmd_hook", "geocode") def cmd_geocode(msg): - if len(msg.cmds) < 2: + if not len(msg.args): raise IRCException("indicate a name") - locname = ' '.join(msg.cmds[1:]) res = Response(channel=msg.channel, nick=msg.nick, nomore="No more geocode", count=" (%s more geocode)") - for loc in geocode(locname): + for loc in geocode(' '.join(msg.args)): res.append_message("%s is at %s,%s (%s precision)" % (where(loc), loc["latLng"]["lat"], diff --git a/modules/networking/__init__.py b/modules/networking/__init__.py index 0caab06..c080cbe 100644 --- a/modules/networking/__init__.py +++ b/modules/networking/__init__.py @@ -24,7 +24,10 @@ def load(context): mod.send_response = context.send_response page.load(context.config, context.add_hook) watchWebsite.load(context.data) - whois.load(context.config, context.add_hook) + try: + whois.load(context.config, context.add_hook) + except ImportError: + logger.exception("Unable to load netwhois module") def help_full(): diff --git a/modules/networking/whois.py b/modules/networking/whois.py index 8b95e40..9ff4a3f 100644 --- a/modules/networking/whois.py +++ b/modules/networking/whois.py @@ -12,15 +12,16 @@ def load(CONF, add_hook): global URL_WHOIS if not CONF or not CONF.hasNode("whoisxmlapi") or not CONF.getNode("whoisxmlapi").hasAttribute("username") or not CONF.getNode("whoisxmlapi").hasAttribute("password"): - print ("You need a WhoisXML API account in order to use the " - "!netwhois feature. Add it to the module configuration file:\n" - "<whoisxmlapi username=\"XX\" password=\"XXX\" />\nRegister at " - "http://www.whoisxmlapi.com/newaccount.php") - else: - URL_WHOIS = URL_WHOIS % (urllib.parse.quote(CONF.getNode("whoisxmlapi")["username"]), urllib.parse.quote(CONF.getNode("whoisxmlapi")["password"])) + raise ImportError("You need a WhoisXML API account in order to use " + "the !netwhois feature. Add it to the module " + "configuration file:\n<whoisxmlapi username=\"XX\" " + "password=\"XXX\" />\nRegister at " + "http://www.whoisxmlapi.com/newaccount.php") - import nemubot.hooks - add_hook("cmd_hook", nemubot.hooks.Message(cmd_whois, "netwhois")) + 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")) def extractdate(str): diff --git a/modules/sap.py b/modules/sap.py index 5806708..19b0f67 100644 --- a/modules/sap.py +++ b/modules/sap.py @@ -12,7 +12,7 @@ from nemubot.hooks import hook from nemubot.tools import web from nemubot.tools.web import striphtml -nemubotversion = 3.4 +nemubotversion = 4.0 from more import Response @@ -23,12 +23,12 @@ def help_full(): @hook("cmd_hook", "tcode") def cmd_tcode(msg): - if len(msg.cmds) != 2: + if not len(msg.args): raise IRCException("indicate a transaction code or " "a keyword to search!") url = ("http://www.tcodesearch.com/tcodes/search?q=%s" % - urllib.parse.quote(msg.cmds[1])) + urllib.parse.quote(msg.args[0])) page = web.getURLContent(url) soup = BeautifulSoup(page) diff --git a/modules/syno.py b/modules/syno.py index 281b947..0bd8ce4 100644 --- a/modules/syno.py +++ b/modules/syno.py @@ -9,7 +9,7 @@ from nemubot.exception import IRCException from nemubot.hooks import hook from nemubot.tools import web -nemubotversion = 3.4 +nemubotversion = 4.0 from more import Response @@ -21,13 +21,13 @@ def help_full(): def load(context): global lang_binding - if not context.config or not context.config.hasNode("bighugelabs") or not context.config.getNode("bighugelabs").hasAttribute("key"): - print ("You need a NigHugeLabs API key in order to have english " - "theasorus. Add it to the module configuration file:\n<bighugelabs" - " key=\"XXXXXXXXXXXXXXXX\" />\nRegister at " - "https://words.bighugelabs.com/getkey.php") + if not context.config or not context.config.hasAttribute("bighugelabskey"): + logger.error("You need a NigHugeLabs API key in order to have english " + "theasorus. Add it to the module configuration file:\n" + "<module name=\"syno\" bighugelabskey=\"XXXXXXXXXXXXXXXX\"" + " />\nRegister at https://words.bighugelabs.com/getkey.php") else: - lang_binding["en"] = lambda word: get_english_synos(context.config.getNode("bighugelabs")["key"], word) + lang_binding["en"] = lambda word: get_english_synos(context.config["bighugelabskey"], word) def get_french_synos(word): @@ -75,18 +75,18 @@ lang_binding = { 'fr': get_french_synos } @hook("cmd_hook", "synonymes", data="synonymes") @hook("cmd_hook", "antonymes", data="antonymes") def go(msg, what): - if len(msg.cmds) < 2: + if not len(msg.args): raise IRCException("de quel mot veux-tu connaître la liste des synonymes ?") # Detect lang - if msg.cmds[1] in lang_binding: - func = lang_binding[msg.cmds[1]] - word = ' '.join(msg.cmds[2:]) + 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.cmds[1:]) + word = ' '.join(msg.args) # TODO: depreciate usage without lang - #raise IRCException("language %s is not handled yet." % msg.cmds[1]) + #raise IRCException("language %s is not handled yet." % msg.args[0]) try: best, synos, anton = func(word) diff --git a/modules/tpb.py b/modules/tpb.py index 241ad31..76711bf 100644 --- a/modules/tpb.py +++ b/modules/tpb.py @@ -6,32 +6,28 @@ from nemubot.hooks import hook from nemubot.tools import human from nemubot.tools.web import getJSON -nemubotversion = 3.4 +nemubotversion = 4.0 from more import Response URL_TPBAPI = None def load(context): + if not context.config or not context.config.hasAttribute("url"): + raise ImportError("You need a TPB API in order to use the !tpb feature" + ". Add it to the module configuration file:\n<module" + "name=\"tpb\" url=\"http://tpbapi.org/\" />\nSample " + "API: " + "https://gist.github.com/colona/07a925f183cfb47d5f20") global URL_TPBAPI + URL_TPBAPI = context.config["url"] - if not context.config or not context.config.hasNode("tpbapi") or not context.config.getNode("tpbapi").hasAttribute("url"): - print ("You need a TPB API in order to use the !tpb feature. Add it to " - "the module configuration file:\n" - "<tpbapi url=\"http://tpbapi.org/\" />\nSample API: " - "https://gist.github.com/colona/07a925f183cfb47d5f20") - else: - URL_TPBAPI = context.config.getNode("tpbapi")["url"] - - from nemubot.hooks.message import Message - context.add_hook("cmd_hook", Message(cmd_tpb, "tpb")) - - +@hook("cmd_hook", "tpb") def cmd_tpb(msg): - if len(msg.cmds) < 1: + if not len(msg.args): raise IRCException("indicate an item to search!") - torrents = getJSON(URL_TPBAPI + urllib.parse.quote(" ".join(msg.cmds[1:]))) + torrents = getJSON(URL_TPBAPI + urllib.parse.quote(" ".join(msg.args))) res = Response(channel=msg.channel, nomore="No more torrents", count=" (%d more torrents)") diff --git a/modules/translate.py b/modules/translate.py index 922d685..a0d8dc2 100644 --- a/modules/translate.py +++ b/modules/translate.py @@ -6,9 +6,10 @@ import re from urllib.parse import quote from nemubot.exception import IRCException +from nemubot.hooks import hook from nemubot.tools import web -nemubotversion = 3.4 +nemubotversion = 4.0 from more import Response @@ -18,46 +19,42 @@ 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"): + raise ImportError("You need a WordReference API key in order to use " + "this module. Add it to the module configuration " + "file:\n<module name=\"translate\" wrapikey=\"XXXXX\"" + " />\nRegister at http://" + "www.wordreference.com/docs/APIregistration.aspx") global URL - if not context.config or not context.config.hasNode("wrapi") or not context.config.getNode("wrapi").hasAttribute("key"): - print ("You need a WordReference API key in order to use this module." - " Add it to the module configuration file:\n<wrapi key=\"XXXXX\"" - " />\nRegister at " - "http://www.wordreference.com/docs/APIregistration.aspx") - return None - else: - URL = URL % context.config.getNode("wrapi")["key"] - - import nemubot.hooks - context.add_hook("cmd_hook", - nemubot.hooks.Message(cmd_translate, "translate")) + URL = URL % context.config["wrapikey"] def help_full(): return "!translate [lang] <term>[ <term>[...]]: Found translation of <term> from/to english to/from <lang>. Data © WordReference.com" +@hook("cmd_hook", "translate") def cmd_translate(msg): - if len(msg.cmds) < 2: + if not len(msg.args): raise IRCException("which word would you translate?") - if len(msg.cmds) > 3 and msg.cmds[1] in LANG and msg.cmds[2] in LANG: - if msg.cmds[1] != "en" and msg.cmds[2] != "en": + 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") - langFrom = msg.cmds[1] - langTo = msg.cmds[2] - term = ' '.join(msg.cmds[3:]) - elif len(msg.cmds) > 2 and msg.cmds[1] in LANG: - langFrom = msg.cmds[1] + 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.cmds[2:]) + term = ' '.join(msg.args[1:]) else: langFrom = "en" langTo = "fr" - term = ' '.join(msg.cmds[1:]) + term = ' '.join(msg.args) wres = web.getJSON(URL % (langFrom, langTo, quote(term))) diff --git a/modules/velib.py b/modules/velib.py index 00105c9..b7e97e3 100644 --- a/modules/velib.py +++ b/modules/velib.py @@ -9,12 +9,18 @@ from nemubot.exception import IRCException from nemubot.hooks import hook from nemubot.tools import web -nemubotversion = 3.4 +nemubotversion = 4.0 from more import Response +URL_API = None # http://www.velib.paris.fr/service/stationdetails/paris/%s + def load(context): + global URL_API + if not context.config or not context.config.hasAttribute("url"): + raise ImportError("Please provide url attribute in the module configuration") + URL_API = context.config["url"] context.data.setIndex("name", "station") # evt = ModuleEvent(station_available, "42706", @@ -30,7 +36,7 @@ def help_full(): def station_status(station): """Gets available and free status of a given station""" - response = web.getXML(context.config.getNode("server")["url"] + 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: diff --git a/modules/weather.py b/modules/weather.py index 74e0107..19e8f8f 100644 --- a/modules/weather.py +++ b/modules/weather.py @@ -14,28 +14,21 @@ from nemubot.tools.xmlparser.node import ModuleState import mapquest -nemubotversion = 3.4 +nemubotversion = 4.0 from more import Response +URL_DSAPI = "https://api.forecast.io/forecast/%s/%%s,%%s" def load(context): + if not context.config or not context.config.hasAttribute("darkskyapikey"): + 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/") context.data.setIndex("name", "city") - - if not context.config or not context.config.hasNode("darkskyapi") or not context.config.getNode("darkskyapi").hasAttribute("key"): - print ("You need a Dark-Sky API key in order to use this " - "module. Add it to the module configuration file:\n<darkskyapi" - " key=\"XXXXXXXXXXXXXXXX\" />\nRegister at " - "http://developer.forecast.io/") - return None - - import nemubot.hooks - context.add_hook("cmd_hook", - nemubot.hooks.Message(cmd_weather, "météo")) - context.add_hook("cmd_hook", - nemubot.hooks.Message(cmd_alert, "alert")) - context.add_hook("cmd_hook", - nemubot.hooks.Message(cmd_coordinates, "coordinates")) + global URL_DSAPI + URL_DSAPI = URL_DSAPI % context.config["darkskyapikey"] def help_full (): @@ -135,7 +128,7 @@ def treat_coord(msg): def get_json_weather(coords): - wth = web.getJSON("https://api.forecast.io/forecast/%s/%s,%s" % (context.config.getNode("darkskyapi")["key"], float(coords[0]), float(coords[1]))) + wth = web.getJSON(URL_DSAPI % (float(coords[0]), float(coords[1]))) # First read flags if "darksky-unavailable" in wth["flags"]: @@ -144,6 +137,7 @@ def get_json_weather(coords): return wth +@hook("cmd_hook", "coordinates") def cmd_coordinates(msg): if len(msg.args) < 1: raise IRCException("indique-moi un nom de ville.") @@ -156,6 +150,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") def cmd_alert(msg): loc, coords, specific = treat_coord(msg) wth = get_json_weather(coords) @@ -169,6 +164,7 @@ def cmd_alert(msg): return res +@hook("cmd_hook", "météo") def cmd_weather(msg): loc, coords, specific = treat_coord(msg) wth = get_json_weather(coords) From c86031ea3238cb5c4678cbd681a2c32d3552f553 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 4 Jun 2015 06:46:39 +0200 Subject: [PATCH 291/674] Can use print with non string --- nemubot/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nemubot/bot.py b/nemubot/bot.py index 544e5f0..5fd4e3b 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -365,7 +365,7 @@ class Bot(threading.Thread): def prnt(*args): print("[%s]" % module_name, *args) if hasattr(module, "logger"): - module.logger.info(" ".join(args)) + module.logger.info(" ".join([str(s) for s in args])) module.print = prnt # Create module context From f4a80e0fda58049f2e690febcfc667806950ce2b Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 5 Jun 2015 06:59:11 +0200 Subject: [PATCH 292/674] New function in ModuleContext: call_hook --- modules/more.py | 12 ++++++++++++ nemubot/message/abstract.py | 7 +++++-- nemubot/modulecontext.py | 13 +++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/modules/more.py b/modules/more.py index 062b6f7..2aeed91 100644 --- a/modules/more.py +++ b/modules/more.py @@ -19,6 +19,7 @@ """Progressive display of very long messages""" import logging +import sys from nemubot.message import Text, DirectAsk from nemubot.hooks import hook @@ -134,6 +135,17 @@ class Response: return Text(self.get_message(maxlen), server=None, to=self.receivers) + def __str__(self): + ret = [] + if len(self.messages): + for msg in self.messages: + if isinstance(msg, list): + ret.append(", ".join(msg)) + else: + ret.append(msg) + ret.append(self.nomore) + return "\n".join(ret) + def get_message(self, maxlen): if self.alone and len(self.messages) > 1: self.alone = False diff --git a/nemubot/message/abstract.py b/nemubot/message/abstract.py index e308e0c..3c69c8d 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, date=None, to=None, to_response=None, frm=None): + def __init__(self, server=None, date=None, to=None, to_response=None, frm=None): """Initialize an abstract message Arguments: @@ -59,7 +59,10 @@ class Abstract: @property def channel(self): # TODO: this is for legacy modules - return self.to_response[0] + if self.to_response is not None and len(self.to_response) > 0: + return self.to_response[0] + else: + return None @property def nick(self): diff --git a/nemubot/modulecontext.py b/nemubot/modulecontext.py index 731dfba..23ca9e4 100644 --- a/nemubot/modulecontext.py +++ b/nemubot/modulecontext.py @@ -67,6 +67,15 @@ class ModuleContext: store = convert_legacy_store(store) self.hooks.remove((store, hook)) return context.hooks.del_hook(hook, store) + def call_hook(store, msg): + for h in context.hooks.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_event(evt, eid=None): return context.add_event(evt, eid, module_src=module) def del_event(evt): @@ -93,6 +102,9 @@ class ModuleContext: 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? + return None def add_event(evt, eid=None): return context.add_event(evt, eid, module_src=module) def del_event(evt): @@ -110,6 +122,7 @@ class ModuleContext: self.del_event = del_event self.save = save self.send_response = send_response + self.call_hook = call_hook def unload(self): From f786dd1d4314e665492291cf373cf3f0365a6a50 Mon Sep 17 00:00:00 2001 From: Bob <bob@23.tf> Date: Mon, 8 Jun 2015 09:46:01 +0200 Subject: [PATCH 293/674] ctfs plugins --- modules/ctfs.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 modules/ctfs.py diff --git a/modules/ctfs.py b/modules/ctfs.py new file mode 100644 index 0000000..bdd26e6 --- /dev/null +++ b/modules/ctfs.py @@ -0,0 +1,37 @@ +import urllib.request +from bs4 import BeautifulSoup +from hooks import hook +from more import Response + +nemubotversion = 3.4 + +def help_tiny(): + return "No help" + +def help_full(): + return "No help " + +@hook("cmd_hook", "ctfs") +def get_info_yt(msg): + req = urllib.request.Request('https://ctftime.org/event/list/upcoming', + data=None, + headers={ + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.47 Safari/537.36' + }) + + ctf = '' + url = urllib.request.urlopen(req) + soup = BeautifulSoup(url) + desc = soup.body.find_all('td') + i = 0 + for result in desc: + ctf += result.text.replace('\n', ' ') + ctf += ' ' + i += 1 + if not (i % 5): + ctf += '\n' + res = Response(channel=msg.channel, nomore="No more description") + res.append_message(ctf) + return res + + From 381cf13432cd6f58e8f6fa4ff69de10eb74a0781 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 6 Jun 2015 12:15:15 +0200 Subject: [PATCH 294/674] [networking] Allow anyone to remove a watch --- modules/networking/watchWebsite.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/networking/watchWebsite.py b/modules/networking/watchWebsite.py index 47b1b17..b94bfd6 100644 --- a/modules/networking/watchWebsite.py +++ b/modules/networking/watchWebsite.py @@ -46,8 +46,8 @@ def del_site(url, nick, channel, frm_owner): site = DATAS.index[url] 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.") +# if not (nick == a["nick"] or frm_owner): +# raise IRCException("you cannot unwatch this URL.") site.delChild(a) if not site.hasNode("alert"): del_event(site["_evt_id"]) From 859b32abb7d652874108ed13e1cc4d4522d198c9 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 8 Jun 2015 14:57:21 +0200 Subject: [PATCH 295/674] [ctfs] Improve module --- modules/ctfs.py | 46 +++++++++++++++++----------------------------- 1 file changed, 17 insertions(+), 29 deletions(-) diff --git a/modules/ctfs.py b/modules/ctfs.py index bdd26e6..819bfb4 100644 --- a/modules/ctfs.py +++ b/modules/ctfs.py @@ -1,37 +1,25 @@ -import urllib.request from bs4 import BeautifulSoup -from hooks import hook + +from nemubot.hooks import hook +from nemubot.tools.web import getURLContent from more import Response -nemubotversion = 3.4 +"""List upcoming CTFs""" -def help_tiny(): - return "No help" - -def help_full(): - return "No help " +nemubotversion = 4.0 @hook("cmd_hook", "ctfs") def get_info_yt(msg): - req = urllib.request.Request('https://ctftime.org/event/list/upcoming', - data=None, - headers={ - 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.47 Safari/537.36' - }) - - ctf = '' - url = urllib.request.urlopen(req) - soup = BeautifulSoup(url) - desc = soup.body.find_all('td') - i = 0 - for result in desc: - ctf += result.text.replace('\n', ' ') - ctf += ' ' - i += 1 - if not (i % 5): - ctf += '\n' - res = Response(channel=msg.channel, nomore="No more description") - res.append_message(ctf) + soup = BeautifulSoup(getURLContent('https://ctftime.org/event/list/upcoming')) + res = Response(channel=msg.channel, nomore="No more upcoming CTF") + 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([x.text.replace("\n", " ").strip() for x in n])) + except: + import sys + import traceback + exc_type, exc_value, _ = sys.exc_info() + sys.stderr.write(traceback.format_exception_only(exc_type, exc_value)[0]) return res - - From c0e6b26b0c2801c014ffb069b9b4f7110637163a Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 7 Jun 2015 15:49:01 +0200 Subject: [PATCH 296/674] [alias] Fix variable replacement in aliases --- modules/alias.py | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/modules/alias.py b/modules/alias.py index ef03172..c2fa54f 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -146,23 +146,20 @@ def cmd_unalias(msg): def replace_variables(cnt, msg=None): - cnt = cnt.split(' ') + if isinstance(cnt, list): + return [replace_variables(c, msg) for c in cnt] + unsetCnt = list() - for i in range(0, len(cnt)): - if i not in unsetCnt: - res = re.match("^([^$]*)(\\\\)?\\$([a-zA-Z0-9]+)(.*)$", cnt[i]) - if res is not None: - try: - varI = int(res.group(3)) - unsetCnt.append(varI) - cnt[i] = res.group(1) + msg.cmds[varI] + res.group(4) - except: - if res.group(2) != "": - cnt[i] = res.group(1) + "$" + res.group(3) + res.group(4) - else: - cnt[i] = (res.group(1) + get_variable(res.group(3), msg) + - res.group(4)) - return " ".join(cnt) + for res in re.findall("\\$\{(?P<name>[a-zA-Z0-9]+)\}", cnt): + try: + varI = int(res) - 1 + cnt = cnt.replace("${%s}" % res, msg.args[varI], 1) + unsetCnt.append(varI) + except: + cnt = cnt.replace("${%s}" % res, get_variable(res), 1) + for u in sorted(unsetCnt, reverse=True): + msg.args.pop(u) + return cnt @hook("pre_Command") @@ -176,7 +173,7 @@ def treat_alias(msg): args = shlex.split(txt) except ValueError: args = txt.split(' ') - nmsg = Command(args[0], args[1:] + msg.args, **msg.export_args()) + nmsg = Command(args[0], replace_variables(args[1:], msg) + msg.args, **msg.export_args()) # Avoid infinite recursion if msg.cmd != nmsg.cmd: From 9cf4b9becb17cf8409f528c518dff8c2a1fb56f8 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 9 Jun 2015 18:48:32 +0200 Subject: [PATCH 297/674] Fix bot close Tell consumer to stop their work Avoid error when select on closed fd --- nemubot/bot.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/nemubot/bot.py b/nemubot/bot.py index 5fd4e3b..23bab0e 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -133,23 +133,23 @@ class Bot(threading.Thread): fnd_smth = False # Looking for invalid server for r in _rlist: - if not hasattr(r, "fileno") or not isinstance(r.fileno(), int): + if not hasattr(r, "fileno") or not isinstance(r.fileno(), int) or r.fileno() == -1: _rlist.remove(r) logger.error("Found invalid object in _rlist: " + r) fnd_smth = True for w in _wlist: - if not hasattr(r, "fileno") or not isinstance(w.fileno(), int): + if not hasattr(w, "fileno") or not isinstance(w.fileno(), int) or w.fileno() == -1: _wlist.remove(w) logger.error("Found invalid object in _wlist: " + w) fnd_smth = True for x in _xlist: - if not hasattr(r, "fileno") or not isinstance(x.fileno(), int): + if not hasattr(x, "fileno") or not isinstance(x.fileno(), int) or x.fileno() == -1: _xlist.remove(x) logger.error("Found invalid object in _xlist: " + x) fnd_smth = True if not fnd_smth: logger.exception("Can't continue, sorry") - self.stop = True + self.quit() continue for x in xl: @@ -429,6 +429,11 @@ class Bot(threading.Thread): logger.info("Stop the event timer...") self.event_timer.cancel() + 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: From 3ac151f888a92d60e39c324ca2c244e592c95cde Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 10 Jun 2015 01:41:13 +0200 Subject: [PATCH 298/674] [books] Fix API calls --- modules/books.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/books.py b/modules/books.py index 5d9ed82..c8c6817 100644 --- a/modules/books.py +++ b/modules/books.py @@ -25,7 +25,7 @@ def load(context): 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.getNode("goodreadsapi")["key"], urllib.parse.quote(title))) + (context.config["goodreadskey"], urllib.parse.quote(title))) if response is not None and response.hasNode("book"): return response.getNode("book") else: @@ -35,7 +35,7 @@ def get_book(title): 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.getNode("goodreadsapi")["key"], urllib.parse.quote(title))) + (context.config["goodreadskey"], urllib.parse.quote(title))) if response is not None and response.hasNode("search"): return response.getNode("search").getNode("results").getNodes("work") else: @@ -45,10 +45,10 @@ def search_books(title): 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.getNode("goodreadsapi")["key"])) + (urllib.parse.quote(name), context.config["goodreadskey"])) if response is not None and response.hasNode("author") and response.getNode("author").hasAttribute("id"): response = web.getXML("https://www.goodreads.com/author/show/%s.xml?key=%s" % - (urllib.parse.quote(response.getNode("author")["id"]), context.config.getNode("goodreadsapi")["key"])) + (urllib.parse.quote(response.getNode("author")["id"]), context.config["goodreadskey"])) if response is not None and response.hasNode("author"): return response.getNode("author") return None From 73082e4109c3183cc8a058b97cd37c7f1c9db47a Mon Sep 17 00:00:00 2001 From: Max <max@23.tf> Date: Tue, 9 Jun 2015 23:59:57 +0100 Subject: [PATCH 299/674] Updated nextstop module --- modules/nextstop/__init__.py | 10 ++++++++++ modules/nextstop/external | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/modules/nextstop/__init__.py b/modules/nextstop/__init__.py index 9f1abe5..3b112f6 100644 --- a/modules/nextstop/__init__.py +++ b/modules/nextstop/__init__.py @@ -40,3 +40,13 @@ def ask_ratp(msg): else: raise IRCException("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): + if len(msg.cmds) == 3: + transport = msg.cmds[1] + cause = msg.cmds[2] + 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.") diff --git a/modules/nextstop/external b/modules/nextstop/external index e5675c6..7e550a5 160000 --- a/modules/nextstop/external +++ b/modules/nextstop/external @@ -1 +1 @@ -Subproject commit e5675c631665dfbdaba55a0be66708a07d157408 +Subproject commit 7e550a5684d21efeaac06edde4276e7946e7f188 From ef73516ceb5d752a2260c9bc7b3d171bb6a949c9 Mon Sep 17 00:00:00 2001 From: Max <max@23.tf> Date: Wed, 10 Jun 2015 00:23:57 +0100 Subject: [PATCH 300/674] Updated submodule --- modules/nextstop/external | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/nextstop/external b/modules/nextstop/external index 7e550a5..0817530 160000 --- a/modules/nextstop/external +++ b/modules/nextstop/external @@ -1 +1 @@ -Subproject commit 7e550a5684d21efeaac06edde4276e7946e7f188 +Subproject commit 0817530fdf846469ac5930754261ca19ac753e3d From cacf216e3678b21bd408480e602cb029573e9981 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 11 Jun 2015 00:06:55 +0200 Subject: [PATCH 301/674] [alias] Fix #85 --- modules/alias.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/alias.py b/modules/alias.py index c2fa54f..63dc8db 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -184,10 +184,9 @@ def treat_alias(msg): @hook("ask_default") def parseask(msg): - global ALIAS if re.match(".*(set|cr[ée]{2}|nouvel(le)?) alias.*", msg.text) is not None: result = re.match(".*alias !?([^ ]+) (pour|=|:) (.+)$", msg.text) - if result.group(1) in context.data.getNode("aliases").index or result.group(3).find("alias") >= 0: + if result.group(1) in context.data.getNode("aliases").index: raise IRCException("cet alias est déjà défini.") else: alias = ModuleState("alias") From ed6da06271bb643171f49b23c90b820335e67509 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 11 Jun 2015 00:09:33 +0200 Subject: [PATCH 302/674] [alias] Fix #83 --- modules/alias.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/alias.py b/modules/alias.py index 63dc8db..ab44ffc 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -177,7 +177,8 @@ def treat_alias(msg): # Avoid infinite recursion if msg.cmd != nmsg.cmd: - return nmsg + # Also return origin message, if it can be treated as well + return [msg, nmsg] return msg From 889b129da3ef241ed7213c80287c8b4eedc7a8bd Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 10 Jun 2015 15:25:41 +0200 Subject: [PATCH 303/674] [alias] Fix #86 --- modules/alias.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/modules/alias.py b/modules/alias.py index ab44ffc..17f58ac 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -108,16 +108,13 @@ def cmd_alias(msg): if alias[0] == "!": alias = alias[1:] if alias in context.data.getNode("aliases").index: - res.append(Response("!%s correspond à %s" % - (alias, context.data.getNode("aliases").index[alias]["origin"]), - channel=msg.channel)) + res.append("!%s correspond à %s" % (alias, context.data.getNode("aliases").index[alias]["origin"])) else: - res.append(Response("!%s n'est pas un alias" % alias, - channel=msg.channel)) - return res + res.append("!%s n'est pas un alias" % alias) + return Response(res, channel=msg.channel, nick=msg.nick) else: return Response("!alias prend en argument l'alias à étendre.", - channel=msg.channel) + channel=msg.channel, nick=msg.nick) @hook("cmd_hook", "unalias") From 5f29fe81677dda50bd984db4e75384bdf1dad278 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 10 Jun 2015 20:42:24 +0200 Subject: [PATCH 304/674] [alias] Allow anyone to remove an alias --- modules/alias.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/modules/alias.py b/modules/alias.py index 17f58ac..2caac93 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -125,14 +125,9 @@ def cmd_unalias(msg): if alias[0] == "!" and len(alias) > 1: alias = alias[1:] if alias in context.data.getNode("aliases").index: - if context.data.getNode("aliases").index[alias]["creator"] == msg.nick or msg.frm_owner: - context.data.getNode("aliases").delChild(context.data.getNode("aliases").index[alias]) - res.append(Response("%s a bien été supprimé" % alias, - channel=msg.channel)) - else: - res.append(Response("Vous n'êtes pas le createur de " - "l'alias %s." % alias, - channel=msg.channel)) + context.data.getNode("aliases").delChild(context.data.getNode("aliases").index[alias]) + res.append(Response("%s a bien été supprimé" % alias, + channel=msg.channel)) else: res.append(Response("%s n'est pas un alias" % alias, channel=msg.channel)) From 8a7ca25d6f0e70125a075fdef9f3aae973e2c3d7 Mon Sep 17 00:00:00 2001 From: Ivan Delalande <colona@ycc.fr> Date: Thu, 11 Jun 2015 07:06:44 +0200 Subject: [PATCH 305/674] Change !yt to use youtube-dl youtube-dl does a good job at extracting all the information from hundreds of website, presenting it in a standardized way and staying up-to-date. Signed-off-by: Ivan Delalande <colona@ycc.fr> --- modules/youtube-title.py | 66 ++++++++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 22 deletions(-) diff --git a/modules/youtube-title.py b/modules/youtube-title.py index 24cb82b..2fb759c 100644 --- a/modules/youtube-title.py +++ b/modules/youtube-title.py @@ -1,6 +1,5 @@ -import re from urllib.parse import urlparse -from bs4 import BeautifulSoup +import re, json, subprocess from nemubot.exception import IRCException from nemubot.hooks import hook @@ -14,8 +13,45 @@ nemubotversion = 3.4 def help_full(): return "!yt [<url>]: with an argument, get information about the given link; without arguments, use the latest youtube link seen on the current channel." +def _get_ytdl(links): + cmd = 'youtube-dl -j --'.split() + cmd.extend(links) + res = [] + with subprocess.Popen(cmd, stdout=subprocess.PIPE) as p: + if p.wait() > 0: + raise IRCException("Error while retrieving video information.") + for line in p.stdout.read().split(b"\n"): + localres = '' + if not line: + continue + info = json.loads(line.decode('utf-8')) + if info.get('fulltitle'): + localres += info['fulltitle'] + elif info.get('title'): + localres += info['title'] + else: + continue + if info.get('duration'): + d = info['duration'] + localres += ' [{0}:{1:06.3f}]'.format(int(d/60), d%60) + if info.get('age_limit'): + localres += ' [-{}]'.format(info['age_limit']) + if info.get('uploader'): + localres += ' by {}'.format(info['uploader']) + if info.get('upload_date'): + localres += ' on {}'.format(info['upload_date']) + if info.get('description'): + localres += ': ' + info['description'] + if info.get('webpage_url'): + localres += ' | ' + info['webpage_url'] + res.append(localres) + if not res: + raise IRCException("No video information to retrieve about this. Sorry!") + return res + LAST_URLS = dict() + @hook("cmd_hook", "yt") def get_info_yt(msg): links = list() @@ -30,22 +66,10 @@ def get_info_yt(msg): for url in msg.args: links.append(url) - titles = list() - descrip = list() - for url in links: - if not re.findall("([a-zA-Z0-9+.-]+:(?://)?[^ ]+)", url): - url = "http://youtube.com/watch?v=" + url - soup = BeautifulSoup(getURLContent(url)) - shortlink = soup.head.find("link", rel="shortlink") - titl = soup.body.find(id='eow-title') - titles.append("%s : %s" % (shortlink["href"], titl.text.strip())) - desc = soup.body.find(id='eow-description') - descrip.append(desc.text.strip()) + data = _get_ytdl(links) res = Response(channel=msg.channel) - if len(titles) > 0: - res.append_message(titles) - for d in descrip: - res.append_message(d) + for msg in data: + res.append_message(msg) return res @@ -64,9 +88,7 @@ def parseresponse(msg): if o.scheme != "": if o.netloc == "" and len(o.path) < 10: continue - if (o.netloc == "youtube.com" or o.netloc == "www.youtube.com" or - o.netloc == "youtu.be" or o.netloc == "www.youtu.be"): - if msg.channel not in LAST_URLS: - LAST_URLS[msg.channel] = list() - LAST_URLS[msg.channel].append(url) + if msg.channel not in LAST_URLS: + LAST_URLS[msg.channel] = list() + LAST_URLS[msg.channel].append(url) return msg From ab2eb405ca4488fc6164f537baa69e06f660aba1 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 12 Jun 2015 21:19:44 +0200 Subject: [PATCH 306/674] XML datastore: add file rotation for backup purpose --- nemubot/datastore/xml.py | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/nemubot/datastore/xml.py b/nemubot/datastore/xml.py index e17b4aa..2a4fcb7 100644 --- a/nemubot/datastore/xml.py +++ b/nemubot/datastore/xml.py @@ -14,7 +14,9 @@ # 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 fcntl, os +import fcntl +import os +import xml.parsers.expat from nemubot.datastore.abstract import Abstract @@ -23,8 +25,17 @@ class XML(Abstract): """A concrete implementation of a data store that relies on XML files""" - def __init__(self, basedir): + def __init__(self, basedir, rotate=True): + """Initialize the datastore + + Arguments: + basedir -- path to directory containing XML files + rotate -- auto-backup files? + """ + self.basedir = basedir + self.rotate = rotate + self.nb_save = 0 def open(self): """Lock the directory""" @@ -83,6 +94,22 @@ class XML(Abstract): else: return Abstract.load(self, module) + def _rotate(self, path): + """Backup given path + + Argument: + path -- location of the file to backup + """ + + self.nb_save += 1 + + for i in range(10): + if self.nb_save % (1 << i) == 0: + src = path + "." + str(i-1) if i != 0 else path + dst = path + "." + str(i) + if os.path.isfile(src): + os.rename(src, dst) + def save(self, module, data): """Load data for the given module @@ -91,4 +118,9 @@ class XML(Abstract): data -- the new data to save """ - return data.save(self._get_data_file_path(module)) + path = self._get_data_file_path(module) + + if self.rotate: + self._rotate(path) + + return data.save(path) From c7706bfc97b086d0213db4d023aa9917c3d44b51 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 13 Jun 2015 14:00:04 +0200 Subject: [PATCH 307/674] XML datastore: load will now automatically try to load backup --- nemubot/datastore/xml.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/nemubot/datastore/xml.py b/nemubot/datastore/xml.py index 2a4fcb7..46dca70 100644 --- a/nemubot/datastore/xml.py +++ b/nemubot/datastore/xml.py @@ -15,11 +15,14 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import fcntl +import logging import os import xml.parsers.expat from nemubot.datastore.abstract import Abstract +logger = logging.getLogger("nemubot.datastore.xml") + class XML(Abstract): @@ -88,11 +91,28 @@ class XML(Abstract): """ data_file = self._get_data_file_path(module) + + # Try to load original file if os.path.isfile(data_file): from nemubot.tools.xmlparser import parse_file - return parse_file(data_file) - else: - return Abstract.load(self, module) + try: + return parse_file(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) + + logger.warn("Restoring from backup: %s", path) + + return cnt + except xml.parsers.expat.ExpatError: + continue + + # Default case: initialize a new empty datastore + return Abstract.load(self, module) def _rotate(self, path): """Backup given path From 0e76a6ed2a9f6428f26620b482b62036e2baa7f3 Mon Sep 17 00:00:00 2001 From: Max <max@23.tf> Date: Sun, 14 Jun 2015 15:17:48 +0100 Subject: [PATCH 308/674] Added json parsing module --- modules/jsonbot.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 modules/jsonbot.py diff --git a/modules/jsonbot.py b/modules/jsonbot.py new file mode 100644 index 0000000..fca0b4e --- /dev/null +++ b/modules/jsonbot.py @@ -0,0 +1,35 @@ +from bs4 import BeautifulSoup + +from nemubot.hooks import hook +from nemubot.exception import IRCException +from nemubot.tools import web +from more import Response +import json + +nemubotversion = 3.4 + +def help_full(): + return "Retrieves data from json" + + +@hook("cmd_hook", "json") +def get_hn_info(msg): + if len(msg.cmds) < 2: + raise IRCException("Please specify a url and a list of JSON keys.") + + request_data = web.getURLContent(msg.cmds[1]) + if not request_data: + raise IRCException("Please specify a valid url.") + json_data = json.loads(request_data) + + if len(msg.cmds) == 2: + raise IRCException("Please specify the keys to return (%s)" % ", ".join(json_data.keys())) + + tags = msg.cmds[2].split(',') + response = "" + for tag in tags: + if not tag in json_data.keys(): + raise IRCException("The key '%s' was not found in the JSON retrieved." % tag) + response += tag + ": " + str(json_data[tag]) + "\n" + + return Response(response, channel=msg.channel, nomore="No more content", count=" (%d more lines)") From 92895a7b1de520a447882e5adb5ad16ea010577b Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 14 Jun 2015 19:34:04 +0200 Subject: [PATCH 309/674] XML parser: perform atomic save by moving a temporary file after the serialization --- nemubot/tools/xmlparser/node.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/nemubot/tools/xmlparser/node.py b/nemubot/tools/xmlparser/node.py index 39ddc22..5f8a509 100644 --- a/nemubot/tools/xmlparser/node.py +++ b/nemubot/tools/xmlparser/node.py @@ -222,10 +222,21 @@ class ModuleState: "XML node: %s with %s", self.name, attrs) def save(self, filename): - """Save the current node as root node in a XML file""" - with open(filename, "w") as f: + """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 26515677b8e8303464108983394baec2cd337d34 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 24 Jun 2015 19:50:23 +0200 Subject: [PATCH 310/674] Don't add new event after main thread stop --- nemubot/bot.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/nemubot/bot.py b/nemubot/bot.py index 23bab0e..307fe0f 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -187,6 +187,10 @@ 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 @@ -357,6 +361,10 @@ 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 b44464d255ca30f41ccf56bc644ddfb4257c63cb Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 25 Jun 2015 01:55:29 +0200 Subject: [PATCH 311/674] [networking] Log error when unable to restart a watch --- modules/networking/__init__.py | 3 +++ modules/networking/watchWebsite.py | 17 +++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/modules/networking/__init__.py b/modules/networking/__init__.py index c080cbe..e3d7dda 100644 --- a/modules/networking/__init__.py +++ b/modules/networking/__init__.py @@ -2,9 +2,12 @@ """Various network tools (w3m, w3c validator, curl, traceurl, ...)""" +import logging + from nemubot.exception import IRCException from nemubot.hooks import hook +logger = logging.getLogger("nemubot.module.networking") nemubotversion = 3.4 from more import Response diff --git a/modules/networking/watchWebsite.py b/modules/networking/watchWebsite.py index b94bfd6..e063a56 100644 --- a/modules/networking/watchWebsite.py +++ b/modules/networking/watchWebsite.py @@ -1,5 +1,6 @@ """Alert on changes on websites""" +import logging from random import randint import urllib.parse from urllib.parse import urlparse @@ -9,6 +10,7 @@ from nemubot.exception import IRCException from nemubot.hooks import hook from nemubot.tools.xmlparser.node import ModuleState +logger = logging.getLogger("nemubot.module.networking.watchWebsite") nemubotversion = 3.4 from more import Response @@ -187,9 +189,12 @@ def start_watching(site, offset=0): o = urlparse(site["url"], "http") #print_debug("Add %s event for site: %s" % (site["type"], o.netloc)) - evt = ModuleEvent(func=lambda url: page.render(url, None), - cmp_data=site["lastcontent"], - func_data=site["url"], offset=offset, - interval=site.getInt("time"), - call=alert_change, call_data=site) - site["_evt_id"] = add_event(evt) + try: + evt = ModuleEvent(func=lambda url: page.render(url, None), + cmp_data=site["lastcontent"], + func_data=site["url"], offset=offset, + interval=site.getInt("time"), + call=alert_change, call_data=site) + site["_evt_id"] = add_event(evt) + except IRCException: + logger.exception("Unable to watch %s", site["url"]) From d90c44de49e9450ffc5665d0534e4ecd58395d42 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 26 Jun 2015 07:59:40 +0200 Subject: [PATCH 312/674] [nextstop] Update submodule --- modules/nextstop/external | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/nextstop/external b/modules/nextstop/external index 0817530..060aad3 160000 --- a/modules/nextstop/external +++ b/modules/nextstop/external @@ -1 +1 @@ -Subproject commit 0817530fdf846469ac5930754261ca19ac753e3d +Subproject commit 060aad3f42afe4185180e0ba952b72dc1cb4cac6 From 487cb13e14ab36b04395835df0eaac9a6eefb9be Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 27 Jun 2015 18:14:45 +0200 Subject: [PATCH 313/674] [weather] fix some hypothetical errors --- modules/weather.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/weather.py b/modules/weather.py index 19e8f8f..a36306e 100644 --- a/modules/weather.py +++ b/modules/weather.py @@ -131,7 +131,7 @@ def get_json_weather(coords): wth = web.getJSON(URL_DSAPI % (float(coords[0]), float(coords[1]))) # First read flags - if "darksky-unavailable" in wth["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.") return wth From 0c960e984aded5bef5a8dabf28c2f551258478bb Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 1 Jul 2015 18:15:35 +0200 Subject: [PATCH 314/674] [networking] fix watch pages that aren't text/html --- modules/networking/page.py | 25 +++++++++++++++---------- modules/networking/watchWebsite.py | 12 +++++++++++- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/modules/networking/page.py b/modules/networking/page.py index fa03bb5..fc5b809 100644 --- a/modules/networking/page.py +++ b/modules/networking/page.py @@ -74,6 +74,20 @@ def fetch(url, onNone=_onNoneDefault): raise IRCException(e.strerror) +def _render(cnt): + """Render the page contained in cnt as HTML page""" + if cnt is None: + return None + + with tempfile.NamedTemporaryFile() as fp: + fp.write(cnt.encode()) + + args = ["w3m", "-T", "text/html", "-dump"] + args.append(fp.name) + with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc: + return proc.stdout.read().decode() + + def render(url, onNone=_onNoneDefault): """Use w3m to render the given url @@ -81,16 +95,7 @@ def render(url, onNone=_onNoneDefault): url -- the URL to render """ - with tempfile.NamedTemporaryFile() as fp: - cnt = fetch(url, onNone) - if cnt is None: - return None - fp.write(cnt.encode()) - - args = ["w3m", "-T", "text/html", "-dump"] - args.append(fp.name) - with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc: - return proc.stdout.read().decode() + return _render(fetch(url, onNone)) def traceURL(url, stack=None): diff --git a/modules/networking/watchWebsite.py b/modules/networking/watchWebsite.py index e063a56..32bf79f 100644 --- a/modules/networking/watchWebsite.py +++ b/modules/networking/watchWebsite.py @@ -176,6 +176,16 @@ def alert_change(content, site): save() +def fwatch(url): + cnt = page.fetch(url, None) + if cnt is not None: + render = page._render(cnt) + if render is None or render == "": + return cnt + return render + return None + + def start_watching(site, offset=0): """Launch the event watching given site @@ -190,7 +200,7 @@ def start_watching(site, offset=0): #print_debug("Add %s event for site: %s" % (site["type"], o.netloc)) try: - evt = ModuleEvent(func=lambda url: page.render(url, None), + evt = ModuleEvent(func=fwatch, cmp_data=site["lastcontent"], func_data=site["url"], offset=offset, interval=site.getInt("time"), From ea05b3014c13b95022bee953b418c4579676bcf6 Mon Sep 17 00:00:00 2001 From: Max <max@23.tf> Date: Thu, 2 Jul 2015 22:49:17 +0100 Subject: [PATCH 315/674] [jsonbot] now supports json lists --- modules/jsonbot.py | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/modules/jsonbot.py b/modules/jsonbot.py index fca0b4e..a784849 100644 --- a/modules/jsonbot.py +++ b/modules/jsonbot.py @@ -11,9 +11,39 @@ nemubotversion = 3.4 def help_full(): return "Retrieves data from json" +def getRequestedTags(tags, data): + response = "" + if isinstance(data, list): + for element in data: + repdata = getRequestedTags(tags, element) + if repdata: + if response: + response = response + "\n" + repdata + else: + response = repdata + else: + for tag in tags: + if tag in data.keys(): + if response: + response += ", " + tag + ": " + str(data[tag]) + else: + response = tag + ": " + str(data[tag]) + return response + +def getJsonKeys(data): + if isinstance(data, list): + pkeys = [] + for element in data: + keys = getJsonKeys(element) + for key in keys: + if not key in pkeys: + pkeys.append(key) + return pkeys + else: + return data.keys() @hook("cmd_hook", "json") -def get_hn_info(msg): +def get_json_info(msg): if len(msg.cmds) < 2: raise IRCException("Please specify a url and a list of JSON keys.") @@ -23,13 +53,9 @@ def get_hn_info(msg): json_data = json.loads(request_data) if len(msg.cmds) == 2: - raise IRCException("Please specify the keys to return (%s)" % ", ".join(json_data.keys())) + raise IRCException("Please specify the keys to return (%s)" % ", ".join(getJsonKeys(json_data))) tags = msg.cmds[2].split(',') - response = "" - for tag in tags: - if not tag in json_data.keys(): - raise IRCException("The key '%s' was not found in the JSON retrieved." % tag) - response += tag + ": " + str(json_data[tag]) + "\n" + response = getRequestedTags(tags, json_data) return Response(response, channel=msg.channel, nomore="No more content", count=" (%d more lines)") From 3cad59108685688a2241b447e99a219e585ebc3a Mon Sep 17 00:00:00 2001 From: Max <max@23.tf> Date: Wed, 1 Jul 2015 01:19:15 +0100 Subject: [PATCH 316/674] [imdb] Fixed grammar --- modules/imdb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/imdb.py b/modules/imdb.py index fb1f0b3..1dd7a2a 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -92,7 +92,7 @@ def cmd_imdb(msg): res.append_message("\x02rating\x0F: %s (%s votes); \x02plot\x0F: %s" % (data['imdbRating'], data['imdbVotes'], data['Plot'])) - res.append_message("%s \x02from\x0F %s \x02released on\x0F %s; \x02genre:\x0F %s; \x02directed by:\x0F %s; \x02writed by:\x0F %s; \x02main actors:\x0F %s" + 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'])) return res From dc681fdc35da5d256b6b6a5a3245ca682f54ef5f Mon Sep 17 00:00:00 2001 From: Max <max@23.tf> Date: Fri, 3 Jul 2015 00:05:01 +0100 Subject: [PATCH 317/674] [jsonbot] now taking all cmd args --- modules/jsonbot.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/modules/jsonbot.py b/modules/jsonbot.py index a784849..6339a52 100644 --- a/modules/jsonbot.py +++ b/modules/jsonbot.py @@ -16,11 +16,10 @@ def getRequestedTags(tags, data): if isinstance(data, list): for element in data: repdata = getRequestedTags(tags, element) - if repdata: - if response: - response = response + "\n" + repdata - else: - response = repdata + if response: + response = response + "\n" + repdata + else: + response = repdata else: for tag in tags: if tag in data.keys(): @@ -55,7 +54,7 @@ def get_json_info(msg): if len(msg.cmds) == 2: raise IRCException("Please specify the keys to return (%s)" % ", ".join(getJsonKeys(json_data))) - tags = msg.cmds[2].split(',') + tags = ','.join(msg.cmds[2:]).split(',') response = getRequestedTags(tags, json_data) return Response(response, channel=msg.channel, nomore="No more content", count=" (%d more lines)") From 3b8195b81bcc800934f0e7d093b61186edd1834a Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 2 Jul 2015 20:17:22 +0200 Subject: [PATCH 318/674] [alias] Variable replacement can operate on slice list. In addition to ${1}, ${2}, ... you can now use slice: ${1:5} or ${1:}. The effect is to join arguments with a space character. --- modules/alias.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/modules/alias.py b/modules/alias.py index 2caac93..e249b72 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -142,12 +142,23 @@ def replace_variables(cnt, msg=None): return [replace_variables(c, msg) for c in cnt] unsetCnt = list() - for res in re.findall("\\$\{(?P<name>[a-zA-Z0-9]+)\}", cnt): - try: - varI = int(res) - 1 - cnt = cnt.replace("${%s}" % res, msg.args[varI], 1) + for res in re.findall("\\$\{(?P<name>[a-zA-Z0-9:]+)\}", cnt): + rv = re.match("([0-9]+)(:([0-9]*))?", res) + if rv is not None: + varI = int(rv.group(1)) - 1 + print(varI, len(msg.args)) + if varI > len(msg.args): + cnt = cnt.replace("${%s}" % res, "", 1) + elif rv.group(2) is not None: + if rv.group(3) is not None: + varJ = int(rv.group(3)) - 1 + cnt = cnt.replace("${%s}" % res, " ".join(msg.args[varI:varJ]), 1) + else: + cnt = cnt.replace("${%s}" % res, " ".join(msg.args[varI:]), 1) + else: + cnt = cnt.replace("${%s}" % res, msg.args[varI], 1) unsetCnt.append(varI) - except: + else: cnt = cnt.replace("${%s}" % res, get_variable(res), 1) for u in sorted(unsetCnt, reverse=True): msg.args.pop(u) From 6415e4a697187196666dd4094f25af1b6e228dcc Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 3 Jul 2015 02:31:27 +0200 Subject: [PATCH 319/674] [networking] Handle ConnectionError exceptions --- modules/networking/page.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/networking/page.py b/modules/networking/page.py index fc5b809..4e74943 100644 --- a/modules/networking/page.py +++ b/modules/networking/page.py @@ -31,6 +31,8 @@ def headers(url): try: conn.request("HEAD", o.path, None, {"User-agent": "Nemubot v%s" % __version__}) + except ConnectionError as e: + raise IRCException(e.strerror) except socket.timeout: raise IRCException("request timeout") except socket.gaierror: @@ -68,6 +70,8 @@ def fetch(url, onNone=_onNoneDefault): return onNone() else: return None + except ConnectionError as e: + raise IRCException(e.strerror) except socket.timeout: raise IRCException("The request timeout when trying to access the page") except socket.error as e: From ae4a30355475a11d871a69dd1859a748e02e0d96 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 4 Jul 2015 12:39:52 +0200 Subject: [PATCH 320/674] Fix #74 --- nemubot/bot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nemubot/bot.py b/nemubot/bot.py index 307fe0f..95f8439 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -81,9 +81,9 @@ class Bot(threading.Thread): """Parse and response to help messages""" from more import Response res = Response(channel=msg.frm) - if len(msg.args) > 1: + if len(msg.args) >= 1: if msg.args[0] in self.modules: - if len(msg.args) > 2: + if len(msg.args) >= 2: if hasattr(self.modules[msg.args[0]], "HELP_cmd"): res.append_message(self.modules[msg.args[0]].HELP_cmd(msg.args[1])) else: From 000c67e45e918d419afd556c3ea411a5af0026b2 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 4 Jul 2015 12:52:39 +0200 Subject: [PATCH 321/674] Can return Response in help_full function --- nemubot/bot.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/nemubot/bot.py b/nemubot/bot.py index 95f8439..fccefc5 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -85,11 +85,15 @@ class Bot(threading.Thread): if msg.args[0] in self.modules: if len(msg.args) >= 2: if hasattr(self.modules[msg.args[0]], "HELP_cmd"): - res.append_message(self.modules[msg.args[0]].HELP_cmd(msg.args[1])) + return self.modules[msg.args[0]].HELP_cmd(msg.args[1]) else: res.append_message("No help for command %s in module %s" % (msg.args[1], msg.args[0])) elif hasattr(self.modules[msg.args[0]], "help_full"): - res.append_message(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("No help for module %s" % msg.args[0]) else: From 787a5fd3dafc0d8e91c1779a181b077adcbef2cf Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 5 Jul 2015 13:09:18 +0200 Subject: [PATCH 322/674] Web tool raise more IRCException --- modules/networking/isup.py | 2 +- nemubot/hooks/message.py | 7 +++---- nemubot/tools/web.py | 14 ++++---------- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/modules/networking/isup.py b/modules/networking/isup.py index f20276c..3b68a7f 100644 --- a/modules/networking/isup.py +++ b/modules/networking/isup.py @@ -14,7 +14,7 @@ def isup(url): o = urllib.parse.urlparse("http://" + url) if o.netloc != "": isup = getJSON("http://isitup.org/%s.json" % o.netloc) - if "status_code" in isup and isup["status_code"] == 1: + if isup is not None and "status_code" in isup and isup["status_code"] == 1: return isup["response_time"] return None diff --git a/nemubot/hooks/message.py b/nemubot/hooks/message.py index 65a2c0f..0844358 100644 --- a/nemubot/hooks/message.py +++ b/nemubot/hooks/message.py @@ -24,11 +24,10 @@ class Message(Abstract): """Class storing hook information, specialized for a generic Message""" - def __init__(self, call, name=None, data=None, regexp=None, - channels=list(), server=None, mtimes=-1, end_call=None): + def __init__(self, call, name=None, regexp=None, channels=list(), + server=None, **kargs): - Abstract.__init__(self, call=call, data=data, - end_call=end_call, mtimes=mtimes) + Abstract.__init__(self, call=call, **kargs) self.name = name self.regexp = regexp diff --git a/nemubot/tools/web.py b/nemubot/tools/web.py index 4f02289..23ae713 100644 --- a/nemubot/tools/web.py +++ b/nemubot/tools/web.py @@ -83,7 +83,7 @@ def getURLContent(url, timeout=15): elif o.scheme is None or o.scheme == "": conn = http.client.HTTPConnection(o.hostname, port=80, timeout=timeout) else: - return None + raise IRCException("Invalid URL") import socket try: @@ -94,14 +94,8 @@ def getURLContent(url, timeout=15): else: conn.request("GET", o.path, None, {"User-agent": "Nemubot v%s" % __version__}) - except socket.timeout: - return None - except OSError: # [Errno 113] No route to host - return None - except socket.gaierror: - print ("<tools.web> Unable to receive page %s on %s from %s." - % (o.path, o.netloc, url)) - return None + except OSError as e: + raise IRCException(e.strerror) try: res = conn.getresponse() @@ -109,7 +103,7 @@ def getURLContent(url, timeout=15): cntype = res.getheader("Content-Type") if size > 524288 or (cntype is not None and cntype[:4] != "text" and cntype[:4] != "appl"): - return None + raise IRCException("Content too large to be retrieved") data = res.read(size) From b75c54419f8a4cbdb94f6943d4af8731779458e0 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 6 Jul 2015 22:10:55 +0200 Subject: [PATCH 323/674] [ddg] Dusting + !safeoff handling --- modules/ddg/DDGSearch.py | 6 +++--- modules/ddg/__init__.py | 37 ++++++++++++++++++++----------------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/modules/ddg/DDGSearch.py b/modules/ddg/DDGSearch.py index e681d8d..174e4a5 100644 --- a/modules/ddg/DDGSearch.py +++ b/modules/ddg/DDGSearch.py @@ -8,12 +8,12 @@ from nemubot.tools.xmlparser import parse_string class DDGSearch: - def __init__(self, terms): + def __init__(self, terms, safeoff=False): self.terms = terms self.ddgres = web.getXML( - "https://api.duckduckgo.com/?q=%s&format=xml&no_redirect=1" % - quote(terms), + "https://api.duckduckgo.com/?q=%s&format=xml&no_redirect=1%s" % + (quote(terms), "&kp=-1" if safeoff else ""), timeout=10) @property diff --git a/modules/ddg/__init__.py b/modules/ddg/__init__.py index 75ee56a..e7cfe89 100644 --- a/modules/ddg/__init__.py +++ b/modules/ddg/__init__.py @@ -5,6 +5,7 @@ import imp from nemubot import context +from nemubot.exception import IRCException from nemubot.hooks import hook nemubotversion = 3.4 @@ -16,25 +17,26 @@ from . import UrbanDictionnary @hook("cmd_hook", "define") def define(msg): - if len(msg.cmds) <= 1: - return Response("Indicate a term to define", - msg.channel, nick=msg.nick) + if not len(msg.args): + raise IRCException("Indicate a term to define") - s = DDGSearch.DDGSearch(' '.join(msg.cmds[1:])) + s = DDGSearch.DDGSearch(' '.join(msg.args)) - res = Response(channel=msg.channel) + return Response(s.definition, channel=msg.channel) - res.append_message(s.definition) - - return res @hook("cmd_hook", "search") def search(msg): - if len(msg.cmds) <= 1: - return Response("Indicate a term to search", - msg.channel, nick=msg.nick) + if not len(msg.args): + raise IRCException("Indicate a term to search") - s = DDGSearch.DDGSearch(' '.join(msg.cmds[1:])) + 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)") @@ -47,21 +49,22 @@ def search(msg): for rt in s.relatedTopics: res.append_message(rt) + res.append_message(s.definition) + return res @hook("cmd_hook", "urbandictionnary") def udsearch(msg): - if len(msg.cmds) <= 1: - return Response("Indicate a term to search", - msg.channel, nick=msg.nick) + if not len(msg.args): + raise IRCException("Indicate a term to search") - s = UrbanDictionnary.UrbanDictionnary(' '.join(msg.cmds[1:])) + 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) + res.append_message(d.replace("\n", " ")) return res From 6e226f0fb37f6f09495082d0f7ceb1602a08107b Mon Sep 17 00:00:00 2001 From: Max <max@23.tf> Date: Sun, 5 Jul 2015 16:01:39 +0100 Subject: [PATCH 324/674] [nextstop] Updated nextstop with destination option --- modules/nextstop/__init__.py | 7 +++++-- modules/nextstop/external | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/modules/nextstop/__init__.py b/modules/nextstop/__init__.py index 3b112f6..084a5a9 100644 --- a/modules/nextstop/__init__.py +++ b/modules/nextstop/__init__.py @@ -17,11 +17,14 @@ def help_full (): @hook("cmd_hook", "ratp") def ask_ratp(msg): """Hook entry from !ratp""" - if len(msg.cmds) == 4: + if len(msg.cmds) >= 4: transport = msg.cmds[1] line = msg.cmds[2] station = msg.cmds[3] - times = ratp.getNextStopsAtStation(transport, line, station) + if len(msg.cmds) == 5: + times = ratp.getNextStopsAtStation(transport, line, station, msg.cmds[4]) + else: + 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)) diff --git a/modules/nextstop/external b/modules/nextstop/external index 060aad3..3d5c9b2 160000 --- a/modules/nextstop/external +++ b/modules/nextstop/external @@ -1 +1 @@ -Subproject commit 060aad3f42afe4185180e0ba952b72dc1cb4cac6 +Subproject commit 3d5c9b2d52fbd214f5aaad00e5f3952de919b3e5 From b5e4acdb70fb7d480d56f93ee48aa5e8160c1059 Mon Sep 17 00:00:00 2001 From: Max <max@23.tf> Date: Mon, 6 Jul 2015 23:21:14 +0100 Subject: [PATCH 325/674] [jsonbot] replacing spaces with %20 in queried url --- modules/jsonbot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/jsonbot.py b/modules/jsonbot.py index 6339a52..0757aaa 100644 --- a/modules/jsonbot.py +++ b/modules/jsonbot.py @@ -46,7 +46,7 @@ def get_json_info(msg): if len(msg.cmds) < 2: raise IRCException("Please specify a url and a list of JSON keys.") - request_data = web.getURLContent(msg.cmds[1]) + request_data = web.getURLContent(msg.cmds[1].replace(' ', "%20")) if not request_data: raise IRCException("Please specify a valid url.") json_data = json.loads(request_data) From 4bc8bc3c125c5be4b5dc5aab116eaf01afb88423 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 7 Jul 2015 12:34:00 +0200 Subject: [PATCH 326/674] Dusting modules --- modules/bonneannee.py | 4 ++-- modules/cve.py | 42 ++++++++++++++++++++---------------------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/modules/bonneannee.py b/modules/bonneannee.py index d1eef72..a00efe0 100644 --- a/modules/bonneannee.py +++ b/modules/bonneannee.py @@ -9,7 +9,7 @@ from nemubot.event import ModuleEvent from nemubot.hooks import hook from nemubot.tools.countdown import countdown_format -nemubotversion = 3.4 +nemubotversion = 4.0 from more import Response @@ -54,7 +54,7 @@ def cmd_newyear(msg, yr): @hook("cmd_rgxp", data=yrn, regexp="^[0-9]{4}$") def cmd_timetoyear(msg, cur): - yr = int(msg.cmds[0]) + yr = int(msg.cmd) if yr == cur: return None diff --git a/modules/cve.py b/modules/cve.py index bc4e58c..fd28181 100644 --- a/modules/cve.py +++ b/modules/cve.py @@ -1,35 +1,33 @@ -import urllib.request from bs4 import BeautifulSoup +from urllib.parse import quote + from nemubot.hooks import hook +from nemubot.tools.web import getURLContent from more import Response -nemubotversion = 3.4 +"""CVE description""" -def help_tiny(): - return "CVE description" +nemubotversion = 4.0 -def help_full(): - return "No help " +BASEURL_MITRE = 'http://cve.mitre.org/cgi-bin/cvename.cgi?name=' +def get_cve(cve_id): + search_url = BASEURL_MITRE + quote(cve_id.upper()) + + soup = BeautifulSoup(getURLContent(search_url)) + desc = soup.body.findAll('td') + + return desc[17].text.replace("\n", " ") + " Moar at " + search_url + @hook("cmd_hook", "cve") def get_cve_desc(msg): - DESC_INDEX = 17 - BASEURL_MITRE = 'http://cve.mitre.org/cgi-bin/cvename.cgi?name=' + res = Response(channel=msg.channel) - cve_id = '' + for cve_id in msg.args: + if cve_id[:3].lower() != 'cve': + cve_id = 'cve-' + cve_id - if msg.cmds[1][:3].lower() == 'cve': - cve_id = msg.cmds[1] + res.append_message(get_cve(cve_id)) - else: - cve_id = 'cve-' + msg.cmds[1] - - search_url = BASEURL_MITRE + cve_id.upper() - - url = urllib.request.urlopen(search_url) - soup = BeautifulSoup(url) - - desc = soup.body.findAll('td') - - return Response(desc[DESC_INDEX].text.replace("\n", " ") + " Moar at " + search_url, msg.channel) + return res From 4d51bc1fdab9a8086bb06d8e2bb698234cf7813e Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 10 Jul 2015 23:09:54 +0200 Subject: [PATCH 327/674] Dusting modules --- modules/birthday.py | 6 ++-- modules/conjugaison.py | 6 ++-- modules/events.py | 58 +++++++++++++++++----------------- modules/imdb.py | 8 +++-- modules/jsonbot.py | 8 ++--- modules/laposte.py | 5 ++- modules/man.py | 22 ++++++------- modules/mediawiki.py | 18 +++++------ modules/networking/__init__.py | 32 +++++++++---------- modules/networking/whois.py | 4 +-- modules/nextstop/__init__.py | 22 ++++++------- modules/reddit.py | 4 +-- modules/rnd.py | 10 +++--- modules/sleepytime.py | 6 ++-- modules/sms.py | 8 ++--- modules/spell/__init__.py | 21 ++++++------ modules/velib.py | 6 ++-- modules/worldcup.py | 23 +++++++------- modules/ycc.py | 6 ++-- modules/youtube-title.py | 21 ++++++------ 20 files changed, 147 insertions(+), 147 deletions(-) diff --git a/modules/birthday.py b/modules/birthday.py index 337b20f..372567f 100644 --- a/modules/birthday.py +++ b/modules/birthday.py @@ -30,11 +30,11 @@ def help_full(): def findName(msg): - if (len(msg.cmds) < 2 or msg.cmds[1].lower() == "moi" or - msg.cmds[1].lower() == "me"): + if (not len(msg.args) or msg.args[0].lower() == "moi" or + msg.args[0].lower() == "me"): name = msg.nick.lower() else: - name = msg.cmds[1].lower() + name = msg.args[0].lower() matches = [] diff --git a/modules/conjugaison.py b/modules/conjugaison.py index 656118c..7ef1758 100644 --- a/modules/conjugaison.py +++ b/modules/conjugaison.py @@ -39,13 +39,13 @@ def help_full(): @hook("cmd_hook", "conjugaison") def cmd_conjug(msg): - if len(msg.cmds) < 3: + if len(msg.args) < 2: raise IRCException("donne moi un temps et un verbe, et je te donnerai " "sa conjugaison!") - tens = ' '.join(msg.cmds[1:-1]) + tens = ' '.join(msg.args[:-1]) - verb = msg.cmds[-1] + verb = msg.args[-1] conjug = get_conjug(verb, tens) diff --git a/modules/events.py b/modules/events.py index 29a5918..39ac787 100644 --- a/modules/events.py +++ b/modules/events.py @@ -64,25 +64,25 @@ def cmd_we(msg): @hook("cmd_hook", "start") def start_countdown(msg): """!start /something/: launch a timer""" - if len(msg.cmds) < 2: + if len(msg.args) < 1: raise IRCException("indique le nom d'un événement à chronométrer") - if msg.cmds[1] in context.data.index: - raise IRCException("%s existe déjà." % msg.cmds[1]) + if msg.args[0] in context.data.index: + raise IRCException("%s existe déjà." % msg.args[0]) strnd = ModuleState("strend") strnd["server"] = msg.server strnd["channel"] = msg.channel strnd["proprio"] = msg.nick strnd["start"] = msg.date - strnd["name"] = msg.cmds[1] + strnd["name"] = msg.args[0] context.data.addChild(strnd) evt = ModuleEvent(call=fini, call_data=dict(strend=strnd)) - if len(msg.cmds) > 2: - result1 = re.findall("([0-9]+)([smhdjwyaSMHDJWYA])?", msg.cmds[2]) - result2 = re.match("(.*[^0-9])?([0-3]?[0-9])/([0-1]?[0-9])/((19|20)?[01239][0-9])", msg.cmds[2]) - result3 = re.match("(.*[^0-9])?([0-2]?[0-9]):([0-5]?[0-9])(:([0-5]?[0-9]))?", msg.cmds[2]) + if len(msg.args) > 1: + result1 = re.findall("([0-9]+)([smhdjwyaSMHDJWYA])?", msg.args[1]) + result2 = re.match("(.*[^0-9])?([0-3]?[0-9])/([0-1]?[0-9])/((19|20)?[01239][0-9])", msg.args[1]) + result3 = re.match("(.*[^0-9])?([0-2]?[0-9]):([0-5]?[0-9])(:([0-5]?[0-9]))?", msg.args[1]) if result2 is not None or result3 is not None: try: now = msg.date @@ -107,7 +107,7 @@ def start_countdown(msg): strnd["_id"] = context.add_event(evt) except: context.data.delChild(strnd) - raise IRCException("Mauvais format de date pour l'événement %s. Il n'a pas été créé." % msg.cmds[1]) + raise IRCException("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 @@ -132,39 +132,39 @@ def start_countdown(msg): context.save() if "end" in strnd: return Response("%s commencé le %s et se terminera le %s." % - (msg.cmds[1], msg.date.strftime("%A %d %B %Y à %H:%M:%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) else: - return Response("%s commencé le %s"% (msg.cmds[1], + return Response("%s commencé le %s"% (msg.args[0], msg.date.strftime("%A %d %B %Y à %H:%M:%S")), nick=msg.frm) @hook("cmd_hook", "end") @hook("cmd_hook", "forceend") def end_countdown(msg): - if len(msg.cmds) < 2: + if len(msg.args) < 1: raise IRCException("quel événement terminer ?") - if msg.cmds[1] in context.data.index: - if context.data.index[msg.cmds[1]]["proprio"] == msg.nick or (msg.cmds[0] == "forceend" and msg.frm_owner): - duration = countdown(msg.date - context.data.index[msg.cmds[1]].getDate("start")) - context.del_event(context.data.index[msg.cmds[1]]["_id"]) - context.data.delChild(context.data.index[msg.cmds[1]]) + 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): + 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.cmds[1], duration), + 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.cmds[1], context.data.index[msg.cmds[1]]["proprio"])) + raise IRCException("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.cmds[1]), channel=msg.channel, nick=msg.nick) + return Response("%s n'est pas un compteur connu."% (msg.args[0]), channel=msg.channel, nick=msg.nick) @hook("cmd_hook", "eventslist") def liste(msg): """!eventslist: gets list of timer""" - if len(msg.cmds) > 1: + if len(msg.args): res = list() - for user in msg.cmds[1:]: + for user in msg.args: cmptr = [x["name"] for x in context.data.index.values() if x["proprio"] == user] if len(cmptr) > 0: res.append("Compteurs créés par %s : %s" % (user, ", ".join(cmptr))) @@ -176,20 +176,20 @@ def liste(msg): @hook("cmd_default") def parseanswer(msg): - if msg.cmds[0] in context.data.index: + if msg.cmd in context.data.index: res = Response(channel=msg.channel) # Avoid message starting by ! which can be interpreted as command by other bots - if msg.cmds[0][0] == "!": + if msg.cmd[0] == "!": res.nick = msg.nick - if context.data.index[msg.cmds[0]].name == "strend": - if context.data.index[msg.cmds[0]].hasAttribute("end"): - res.append_message("%s commencé il y a %s et se terminera dans %s." % (msg.cmds[0], countdown(msg.date - context.data.index[msg.cmds[0]].getDate("start")), countdown(context.data.index[msg.cmds[0]].getDate("end") - msg.date))) + 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.cmds[0], countdown(msg.date - context.data.index[msg.cmds[0]].getDate("start")))) + 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.cmds[0]].getDate("start"), context.data.index[msg.cmds[0]]["msg_before"], context.data.index[msg.cmds[0]]["msg_after"])) + 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) diff --git a/modules/imdb.py b/modules/imdb.py index 1dd7a2a..49c4cc9 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -71,10 +71,10 @@ def find_movies(title): @hook("cmd_hook", "imdb") def cmd_imdb(msg): """View movie details with !imdb <title>""" - if len(msg.cmds) < 2: + if not len(msg.args): raise IRCException("precise a movie/serie title!") - title = ' '.join(msg.cmds[1:]) + title = ' '.join(msg.args) if re.match("^tt[0-9]{7}$", title) is not None: data = get_movie(imdbid=title) @@ -100,8 +100,10 @@ def cmd_imdb(msg): @hook("cmd_hook", "imdbs") def cmd_search(msg): """!imdbs <approximative title> to search a movie title""" + if not len(msg.args): + raise IRCException("precise a movie/serie title!") - data = find_movies(' '.join(msg.cmds[1:])) + data = find_movies(' '.join(msg.args)) movies = list() for m in data['Search']: diff --git a/modules/jsonbot.py b/modules/jsonbot.py index 0757aaa..9061d29 100644 --- a/modules/jsonbot.py +++ b/modules/jsonbot.py @@ -43,18 +43,18 @@ def getJsonKeys(data): @hook("cmd_hook", "json") def get_json_info(msg): - if len(msg.cmds) < 2: + if not len(msg.args): raise IRCException("Please specify a url and a list of JSON keys.") - request_data = web.getURLContent(msg.cmds[1].replace(' ', "%20")) + request_data = web.getURLContent(msg.args[0].replace(' ', "%20")) if not request_data: raise IRCException("Please specify a valid url.") json_data = json.loads(request_data) - if len(msg.cmds) == 2: + if len(msg.args) == 1: raise IRCException("Please specify the keys to return (%s)" % ", ".join(getJsonKeys(json_data))) - tags = ','.join(msg.cmds[2:]).split(',') + tags = ','.join(msg.args[1:]).split(',') response = getRequestedTags(tags, json_data) return Response(response, channel=msg.channel, nomore="No more content", count=" (%d more lines)") diff --git a/modules/laposte.py b/modules/laposte.py index 70ae045..966cb93 100644 --- a/modules/laposte.py +++ b/modules/laposte.py @@ -14,10 +14,10 @@ def help_full(): @hook("cmd_hook", "laposte") def get_tracking_info(msg): - if len(msg.cmds) < 2: + if not len(msg.args): raise IRCException("Renseignez un identifiant d'envoi,") - data = urllib.parse.urlencode({'id': msg.cmds[1]}) + data = urllib.parse.urlencode({'id': msg.args[0]}) laposte_baseurl = "http://www.part.csuivi.courrier.laposte.fr/suivi/index" laposte_data = urllib.request.urlopen(laposte_baseurl, data.encode('utf-8')) @@ -42,4 +42,3 @@ def get_tracking_info(msg): 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.lower(), poste_id.strip(), poste_status.lower(), poste_location, poste_date), msg.channel) return Response("L'identifiant recherché semble incorrect, merci de vérifier son exactitude.", msg.channel) - diff --git a/modules/man.py b/modules/man.py index 9dc12a8..31ed9f2 100644 --- a/modules/man.py +++ b/modules/man.py @@ -23,15 +23,15 @@ RGXP_s = re.compile(b'\x1b\\[[0-9]+m') def cmd_man(msg): args = ["man"] num = None - if len(msg.cmds) == 2: - args.append(msg.cmds[1]) - elif len(msg.cmds) >= 3: + if len(msg.args) == 1: + args.append(msg.args[0]) + elif len(msg.args) >= 2: try: - num = int(msg.cmds[1]) + num = int(msg.args[0]) args.append("%d" % num) - args.append(msg.cmds[2]) + args.append(msg.args[1]) except ValueError: - args.append(msg.cmds[1]) + args.append(msg.args[0]) os.unsetenv("LANG") res = Response(channel=msg.channel) @@ -45,16 +45,16 @@ def cmd_man(msg): if len(res.messages) <= 0: if num is not None: res.append_message("There is no entry %s in section %d." % - (msg.cmds[1], num)) + (msg.args[0], num)) else: - res.append_message("There is no man page for %s." % msg.cmds[1]) + res.append_message("There is no man page for %s." % msg.args[0]) return res @hook("cmd_hook", "man") def cmd_whatis(msg): - args = ["whatis", " ".join(msg.cmds[1:])] + args = ["whatis", " ".join(msg.args)] res = Response(channel=msg.channel) with subprocess.Popen(args, @@ -67,8 +67,8 @@ def cmd_whatis(msg): if len(res.messages) <= 0: if num is not None: res.append_message("There is no entry %s in section %d." % - (msg.cmds[1], num)) + (msg.args[0], num)) else: - res.append_message("There is no man page for %s." % msg.cmds[1]) + res.append_message("There is no man page for %s." % msg.args[0]) return res diff --git a/modules/mediawiki.py b/modules/mediawiki.py index ab498af..c7c4e41 100644 --- a/modules/mediawiki.py +++ b/modules/mediawiki.py @@ -139,27 +139,27 @@ def search(site, term, ssl=False): @hook("cmd_hook", "mediawiki") def cmd_mediawiki(msg): """Read an article on a MediaWiki""" - if len(msg.cmds) < 3: + if len(msg.args) < 2: raise IRCException("indicate a domain and a term to search") - site = msg.cmds[1] + site = msg.args[0] ns = get_namespaces(site) - return Response(get_page(site, " ".join(msg.cmds[2:])), - line_treat=lambda line: irc_format(parse_wikitext(msg.cmds[1], line, ns)), + return Response(get_page(site, " ".join(msg.args[1:])), + line_treat=lambda line: irc_format(parse_wikitext(msg.args[0], line, ns)), channel=msg.receivers) @hook("cmd_hook", "search_mediawiki") def cmd_srchmediawiki(msg): """Search an article on a MediaWiki""" - if len(msg.cmds) < 3: + if len(msg.args) < 2: raise IRCException("indicate a domain and a term to search") res = Response(channel=msg.receivers, nomore="No more results", count=" (%d more results)") - for r in search(msg.cmds[1], " ".join(msg.cmds[2:])): + for r in search(msg.args[0], " ".join(msg.args[1:])): res.append_message("%s: %s" % r) return res @@ -167,13 +167,13 @@ def cmd_srchmediawiki(msg): @hook("cmd_hook", "wikipedia") def cmd_wikipedia(msg): - if len(msg.cmds) < 3: + if len(msg.args) < 2: raise IRCException("indicate a lang and a term to search") - site = msg.cmds[1] + ".wikipedia.org" + site = msg.args[0] + ".wikipedia.org" ns = get_namespaces(site) - return Response(get_page(site, " ".join(msg.cmds[2:])), + return Response(get_page(site, " ".join(msg.args[1:])), line_treat=lambda line: irc_format(parse_wikitext(site, line, ns)), channel=msg.receivers) diff --git a/modules/networking/__init__.py b/modules/networking/__init__.py index e3d7dda..8d62c54 100644 --- a/modules/networking/__init__.py +++ b/modules/networking/__init__.py @@ -39,10 +39,10 @@ def help_full(): @hook("cmd_hook", "curly") def cmd_curly(msg): - if len(msg.cmds) < 2: + if not len(msg.args): raise IRCException("Indicate the URL to visit.") - url = " ".join(msg.cmds[1:]) + url = " ".join(msg.args) version, status, reason, headers = page.headers(url) 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) @@ -50,20 +50,20 @@ def cmd_curly(msg): @hook("cmd_hook", "curl") def cmd_curl(msg): - if len(msg.cmds) < 2: + if not len(msg.args): raise IRCException("Indicate the URL to visit.") res = Response(channel=msg.channel) - for m in page.fetch(" ".join(msg.cmds[1:])).split("\n"): + for m in page.fetch(" ".join(msg.args)).split("\n"): res.append_message(m) return res @hook("cmd_hook", "w3m") def cmd_w3m(msg): - if len(msg.cmds) > 1: + if len(msg.args): res = Response(channel=msg.channel) - for line in page.render(" ".join(msg.cmds[1:])).split("\n"): + for line in page.render(" ".join(msg.args)).split("\n"): res.append_message(line) return res else: @@ -72,9 +72,9 @@ def cmd_w3m(msg): @hook("cmd_hook", "traceurl") def cmd_traceurl(msg): - if 1 < len(msg.cmds) < 6: + if 1 < len(msg.args) < 5: res = list() - for url in msg.cmds[1:]: + for url in msg.args: trace = page.traceURL(url) res.append(Response(trace, channel=msg.channel, title="TraceURL")) return res @@ -84,9 +84,9 @@ def cmd_traceurl(msg): @hook("cmd_hook", "isup") def cmd_isup(msg): - if 1 < len(msg.cmds) < 6: + if 1 < len(msg.args) < 5: res = list() - for url in msg.cmds[1:]: + for url in msg.args: rep = isup.isup(url) if rep: res.append(Response("%s is up (response time: %ss)" % (url, rep), channel=msg.channel)) @@ -99,10 +99,10 @@ def cmd_isup(msg): @hook("cmd_hook", "w3c") def cmd_w3c(msg): - if len(msg.cmds) < 2: + if not len(msg.args): raise IRCException("Indicate an URL to validate!") - headers, validator = w3c.validator(msg.cmds[1]) + headers, validator = w3c.validator(msg.args[0]) res = Response(channel=msg.channel, nomore="No more error") @@ -121,15 +121,15 @@ def cmd_w3c(msg): @hook("cmd_hook", "watch", data="diff") @hook("cmd_hook", "updown", data="updown") def cmd_watch(msg, diffType="diff"): - if len(msg.cmds) <= 1: + if not len(msg.args): raise IRCException("indicate an URL to watch!") - return watchWebsite.add_site(msg.cmds[1], msg.frm, msg.channel, msg.server, diffType) + return watchWebsite.add_site(msg.args[0], msg.frm, msg.channel, msg.server, diffType) @hook("cmd_hook", "unwatch") def cmd_unwatch(msg): - if len(msg.cmds) <= 1: + if not len(msg.args): raise IRCException("which URL should I stop watching?") - return watchWebsite.del_site(msg.cmds[1], msg.frm, msg.channel, msg.frm_owner) + return watchWebsite.del_site(msg.args[0], msg.frm, msg.channel, msg.frm_owner) diff --git a/modules/networking/whois.py b/modules/networking/whois.py index 9ff4a3f..781dfdc 100644 --- a/modules/networking/whois.py +++ b/modules/networking/whois.py @@ -77,10 +77,10 @@ def whois_entityformat(entity): def cmd_whois(msg): - if len(msg.cmds) < 2: + if not len(msg.args): raise IRCException("Indiquer un domaine ou une IP à whois !") - dom = msg.cmds[1] + dom = msg.args[0] js = getJSON(URL_WHOIS % urllib.parse.quote(dom)) diff --git a/modules/nextstop/__init__.py b/modules/nextstop/__init__.py index 084a5a9..65095b2 100644 --- a/modules/nextstop/__init__.py +++ b/modules/nextstop/__init__.py @@ -17,12 +17,12 @@ def help_full (): @hook("cmd_hook", "ratp") def ask_ratp(msg): """Hook entry from !ratp""" - if len(msg.cmds) >= 4: - transport = msg.cmds[1] - line = msg.cmds[2] - station = msg.cmds[3] - if len(msg.cmds) == 5: - times = ratp.getNextStopsAtStation(transport, line, station, msg.cmds[4]) + 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) @@ -34,8 +34,8 @@ def ask_ratp(msg): title="Prochains passages du %s ligne %s à l'arrêt %s" % (transport, line, stationname), channel=msg.channel) - elif len(msg.cmds) == 3: - stations = ratp.getAllStations(msg.cmds[1], msg.cmds[2]) + elif len(msg.args) == 2: + stations = ratp.getAllStations(msg.args[0], msg.args[1]) if len(stations) == 0: raise IRCException("aucune station trouvée.") @@ -46,9 +46,9 @@ def ask_ratp(msg): @hook("cmd_hook", "ratp_alert") def ratp_alert(msg): - if len(msg.cmds) == 3: - transport = msg.cmds[1] - cause = msg.cmds[2] + 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: diff --git a/modules/reddit.py b/modules/reddit.py index fddaadd..63bb6eb 100644 --- a/modules/reddit.py +++ b/modules/reddit.py @@ -22,14 +22,14 @@ LAST_SUBS = dict() @hook("cmd_hook", "subreddit") def cmd_subreddit(msg): global LAST_SUBS - if len(msg.cmds) <= 1: + if not len(msg.args): 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? " "type !horny or !bored") else: - subs = msg.cmds[1:] + subs = msg.args all_res = list() for osub in subs: diff --git a/modules/rnd.py b/modules/rnd.py index 644507f..6ecf9eb 100644 --- a/modules/rnd.py +++ b/modules/rnd.py @@ -14,9 +14,9 @@ from more import Response @hook("cmd_hook", "choice") def cmd_choice(msg): - if len(msg.cmds) > 1: - return Response(random.choice(msg.cmds[1:]), - channel=msg.channel, - nick=msg.nick) - else: + if not len(msg.args): raise IRCException("indicate some terms to pick!") + + return Response(random.choice(msg.args), + channel=msg.channel, + nick=msg.nick) diff --git a/modules/sleepytime.py b/modules/sleepytime.py index bf72249..aef2db3 100644 --- a/modules/sleepytime.py +++ b/modules/sleepytime.py @@ -21,10 +21,10 @@ def help_full(): @hook("cmd_hook", "sleepytime") def cmd_sleep(msg): - if len(msg.cmds) > 1 and re.match("[0-9]{1,2}[h':.,-]([0-9]{1,2})?[m'\":.,-]?", - msg.cmds[1]) is not None: + if len(msg.args) and re.match("[0-9]{1,2}[h':.,-]([0-9]{1,2})?[m'\":.,-]?", + msg.args[0]) is not None: # First, parse the hour - p = re.match("([0-9]{1,2})[h':.,-]([0-9]{1,2})?[m':.,-]?", msg.cmds[1]) + p = re.match("([0-9]{1,2})[h':.,-]([0-9]{1,2})?[m':.,-]?", msg.args[0]) f = [datetime(datetime.now(timezone.utc).year, datetime.now(timezone.utc).month, datetime.now(timezone.utc).day, diff --git a/modules/sms.py b/modules/sms.py index add430b..91a8623 100644 --- a/modules/sms.py +++ b/modules/sms.py @@ -49,12 +49,12 @@ def send_sms(frm, api_usr, api_key, content): @hook("cmd_hook", "sms") def cmd_sms(msg): - if len(msg.cmds) <= 2: + if not len(msg.args): raise IRCException("À qui veux-tu envoyer ce SMS ?") # Check dests cur_epoch = time.mktime(time.localtime()); - for u in msg.cmds[1].split(","): + 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) elif cur_epoch - float(context.data.index[u]["lastuse"]) < 42: @@ -62,13 +62,13 @@ def cmd_sms(msg): # Go! fails = list() - for u in msg.cmds[1].split(","): + for u in msg.args[0].split(","): 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.cmds[2:])) + test = send_sms(frm, context.data.index[u]["user"], context.data.index[u]["key"], " ".join(msg.args[1:])) if test is not None: fails.append( "%s: %s" % (u, test) ) diff --git a/modules/spell/__init__.py b/modules/spell/__init__.py index 0953fae..af08fde 100644 --- a/modules/spell/__init__.py +++ b/modules/spell/__init__.py @@ -25,13 +25,13 @@ def load(context): @hook("cmd_hook", "spell") def cmd_spell(msg): - if len(msg.cmds) < 2: + if not len(msg.args): raise IRCException("indique une orthographe approximative du mot dont tu veux vérifier l'orthographe.") lang = "fr" strRes = list() - for word in msg.cmds[1:]: - if len(word) <= 2 and len(msg.cmds) > 2: + for word in msg.args: + if len(word) <= 2 and len(msg.args) > 2: lang = word else: try: @@ -65,14 +65,13 @@ def add_score(nick, t): def cmd_score(msg): res = list() unknown = list() - if len(msg.cmds) > 1: - for cmd in msg.cmds[1:]: - 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)) - else: - unknown.append(cmd) - else: - return Response("De qui veux-tu voir les scores ?", channel=msg.channel, nick=msg.nick) + if not len(msg.args): + raise IRCException("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)) + else: + unknown.append(cmd) if len(unknown) > 0: res.append(Response("%s inconnus" % ", ".join(unknown), channel=msg.channel)) diff --git a/modules/velib.py b/modules/velib.py index b7e97e3..bdfc8e0 100644 --- a/modules/velib.py +++ b/modules/velib.py @@ -78,11 +78,11 @@ def print_station_status(msg, station): @hook("cmd_hook", "velib") def ask_stations(msg): """Hook entry from !velib""" - if len(msg.cmds) > 5: + if len(msg.args) > 4: raise IRCException("demande-moi moins de stations à la fois.") - elif len(msg.cmds) > 1: - for station in msg.cmds[1:]: + 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: diff --git a/modules/worldcup.py b/modules/worldcup.py index 712dba4..1cd49dc 100644 --- a/modules/worldcup.py +++ b/modules/worldcup.py @@ -48,12 +48,12 @@ def cmd_watch(msg): node = n break - if len(msg.cmds) >= 2: - if msg.cmds[1] == "stop" and node is not None: + if len(msg.args): + 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.") - elif msg.cmds[1] == "start" and node is None: + elif msg.args[0] == "start" and node is None: start_watch(msg) else: raise IRCException("Use only start or stop as first argument") @@ -180,20 +180,19 @@ def get_matches(url): @hook("cmd_hook", "worldcup") def cmd_worldcup(msg): res = Response(channel=msg.channel, nomore="No more match to display", count=" (%d more matches)") - nb = len(msg.cmds) url = None - if nb == 2: - if msg.cmds[1] == "today" or msg.cmds[1] == "aujourd'hui": + if len(msg.args) == 1: + if msg.args[0] == "today" or msg.args[0] == "aujourd'hui": url = "matches/today?by_date=ASC" - elif msg.cmds[1] == "tomorrow" or msg.cmds[1] == "demain": + elif msg.args[0] == "tomorrow" or msg.args[0] == "demain": url = "matches/tomorrow?by_date=ASC" - elif msg.cmds[1] == "all" or msg.cmds[1] == "tout" or msg.cmds[1] == "tous": + elif msg.args[0] == "all" or msg.args[0] == "tout" or msg.args[0] == "tous": url = "matches/" - elif len(msg.cmds[1]) == 3: - url = "matches/country?fifa_code=%s&by_date=DESC" % msg.cmds[1] - elif is_int(msg.cmds[1]): - url = int(msg.cmds[1]) + 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]) else: raise IRCException("unrecognized request; choose between 'today', 'tomorrow', a FIFA country code or a match identifier") diff --git a/modules/ycc.py b/modules/ycc.py index c05296f..afe415b 100644 --- a/modules/ycc.py +++ b/modules/ycc.py @@ -45,17 +45,17 @@ def gen_response(res, msg, srv): def cmd_ycc(msg): minify = list() - if len(msg.cmds) == 1: + if not len(msg.args): global LAST_URLS if msg.channel in LAST_URLS and len(LAST_URLS[msg.channel]) > 0: minify.append(LAST_URLS[msg.channel].pop()) else: raise IRCException("je n'ai pas d'autre URL à réduire.") - if len(msg.cmds) > 5: + if len(msg.args) > 4: raise IRCException("je ne peux pas réduire autant d'URL d'un seul coup.") else: - minify += msg.cmds[1:] + minify += msg.args res = list() for url in minify: diff --git a/modules/youtube-title.py b/modules/youtube-title.py index 2fb759c..4842d1d 100644 --- a/modules/youtube-title.py +++ b/modules/youtube-title.py @@ -11,7 +11,7 @@ from more import Response nemubotversion = 3.4 def help_full(): - return "!yt [<url>]: with an argument, get information about the given link; without arguments, use the latest youtube link seen on the current channel." + return "!yt [<url>]: with an argument, get information about the given link; without arguments, use the latest link seen on the current channel." def _get_ytdl(links): cmd = 'youtube-dl -j --'.split() @@ -82,13 +82,14 @@ def parselisten(msg): @hook("all_post") def parseresponse(msg): global LAST_URLS - urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?[^ ]+)", msg.text) - for url in urls: - o = urlparse(url) - if o.scheme != "": - if o.netloc == "" and len(o.path) < 10: - continue - if msg.channel not in LAST_URLS: - LAST_URLS[msg.channel] = list() - LAST_URLS[msg.channel].append(url) + if hasattr(msg, "text"): + urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?[^ ]+)", msg.text) + for url in urls: + o = urlparse(url) + if o.scheme != "": + if o.netloc == "" and len(o.path) < 10: + continue + if msg.channel not in LAST_URLS: + LAST_URLS[msg.channel] = list() + LAST_URLS[msg.channel].append(url) return msg From 3c15d35fca9e254a0e7de5d5731a61ab34a4549f Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sat, 11 Jul 2015 00:09:43 +0200 Subject: [PATCH 328/674] [alias] Dusting module --- modules/alias.py | 50 +++++++++++++++++++++++------------------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/modules/alias.py b/modules/alias.py index e249b72..2b7a209 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -28,10 +28,6 @@ def load(context): context.data.getNode("variables").setIndex("name") -def help_full(): - return "TODO" - - def set_variable(name, value, creator): var = ModuleState("variable") var["name"] = name @@ -55,56 +51,58 @@ def get_variable(name, msg=None): @hook("cmd_hook", "set") def cmd_set(msg): - if len(msg.cmds) > 2: - set_variable(msg.cmds[1], " ".join(msg.cmds[2:]), msg.nick) - res = Response("Variable \$%s définie." % msg.cmds[1], - channel=msg.channel) - context.save() - return res - return Response("!set prend au minimum deux arguments : " - "le nom de la variable et sa valeur.", + if len(msg.args) < 2: + raise IRCException("!set prend au minimum deux arguments : " + "le nom de la variable et sa valeur.") + set_variable(msg.args[0], " ".join(msg.args[1:]), msg.nick) + context.save() + return Response("Variable \$%s définie." % msg.args[0], channel=msg.channel) @hook("cmd_hook", "listalias") def cmd_listalias(msg): - if len(msg.cmds) > 1: + if len(msg.args): res = list() - for user in msg.cmds[1:]: + for user in msg.args: als = [x["alias"] for x in context.data.getNode("aliases").index.values() if x["creator"] == user] if len(als) > 0: - res.append("Alias créés par %s : %s" % (user, ", ".join(als))) + res.append("%s's aliases: %s" % (user, ", ".join(als))) else: - res.append("%s n'a pas encore créé d'alias" % user) - return Response(" ; ".join(res), channel=msg.channel) - else: - return Response("Alias connus : %s." % + res.append("%s has never created aliases." % user) + return Response("; ".join(res), channel=msg.channel) + elif len(context.data.getNode("aliases").index): + return Response("Known aliases: %s." % ", ".join(context.data.getNode("aliases").index.keys()), channel=msg.channel) + else: + return Response("There is no alias currently.", channel=msg.channel) @hook("cmd_hook", "listvars") def cmd_listvars(msg): - if len(msg.cmds) > 1: + if len(msg.args): res = list() - for user in msg.cmds[1:]: + for user in msg.args: als = [x["alias"] for x in context.data.getNode("variables").index.values() if x["creator"] == user] if len(als) > 0: res.append("Variables créées par %s : %s" % (user, ", ".join(als))) else: res.append("%s n'a pas encore créé de variable" % user) return Response(" ; ".join(res), channel=msg.channel) - else: + elif len(context.data.getNode("variables").index): return Response("Variables connues : %s." % ", ".join(context.data.getNode("variables").index.keys()), channel=msg.channel) + else: + return Response("No variable are currently stored.", channel=msg.channel) @hook("cmd_hook", "alias") def cmd_alias(msg): - if len(msg.cmds) > 1: + if len(msg.args): res = list() - for alias in msg.cmds[1:]: + for alias in msg.args: if alias[0] == "!": alias = alias[1:] if alias in context.data.getNode("aliases").index: @@ -119,9 +117,9 @@ def cmd_alias(msg): @hook("cmd_hook", "unalias") def cmd_unalias(msg): - if len(msg.cmds) > 1: + if len(msg.args): res = list() - for alias in msg.cmds[1:]: + for alias in msg.args: if alias[0] == "!" and len(alias) > 1: alias = alias[1:] if alias in context.data.getNode("aliases").index: From a1ac7d480d07f9b678f974fd48d6746df6f163c6 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 13 Jul 2015 20:35:40 +0200 Subject: [PATCH 329/674] Split server message parsing from message retrieving --- nemubot/server/IRC.py | 176 +------------------------- nemubot/server/message/IRC.py | 190 +++++++++++++++++++++++++++++ nemubot/server/message/abstract.py | 35 ++++++ nemubot/server/test_IRC.py | 6 +- 4 files changed, 231 insertions(+), 176 deletions(-) create mode 100644 nemubot/server/message/IRC.py create mode 100644 nemubot/server/message/abstract.py diff --git a/nemubot/server/IRC.py b/nemubot/server/IRC.py index 718a9bc..25f37cf 100644 --- a/nemubot/server/IRC.py +++ b/nemubot/server/IRC.py @@ -16,13 +16,12 @@ # 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 +from datetime import datetime import re -import shlex from nemubot.channel import Channel -import nemubot.message as message from nemubot.message.printer.IRC import IRC as IRCPrinter +from nemubot.server.message.IRC import IRC as IRCMessage from nemubot.server.socket import SocketServer @@ -263,175 +262,6 @@ class IRC(SocketServer): if msg.cmd in self.hookscmd: self.hookscmd[msg.cmd](msg) - mes = msg.to_message(self) + mes = msg.to_bot_message(self) if mes is not None: yield mes - - -# Parsing stuff - -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 IRCMessage: - - """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_irc_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_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.id, - "date": self.tags["time"], - "to": receivers, - "to_response": [r if r != srv.nick else self.nick for r in receivers], - "frm": self.nick - } - - # 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(' ') - - return message.Command(cmd=args[0], args=args[1:], **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/IRC.py b/nemubot/server/message/IRC.py new file mode 100644 index 0000000..43dcf49 --- /dev/null +++ b/nemubot/server/message/IRC.py @@ -0,0 +1,190 @@ +# -*- 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/>. + +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.id, + "date": self.tags["time"], + "to": receivers, + "to_response": [r if r != srv.nick else self.nick for r in receivers], + "frm": self.nick + } + + # 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(' ') + + return message.Command(cmd=args[0], args=args[1:], **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/abstract.py b/nemubot/server/message/abstract.py new file mode 100644 index 0000000..35d7105 --- /dev/null +++ b/nemubot/server/message/abstract.py @@ -0,0 +1,35 @@ +# -*- 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 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_message(self): + """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 index e22d190..3553d6b 100644 --- a/nemubot/server/test_IRC.py +++ b/nemubot/server/test_IRC.py @@ -23,13 +23,13 @@ class TestIRCMessage(unittest.TestCase): def test_prettyprint(self): - bst1 = self.msg.to_irc_string(False) + bst1 = self.msg.to_server_message(False) msg2 = IRC.IRCMessage(bst1.encode()) - bst2 = msg2.to_irc_string(False) + bst2 = msg2.to_server_message(False) msg3 = IRC.IRCMessage(bst2.encode()) - bst3 = msg3.to_irc_string(False) + bst3 = msg3.to_server_message(False) self.assertEqual(bst2, bst3) From d2694682874f067b77980448159eb6794a27bb69 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 14 Jul 2015 00:48:56 +0200 Subject: [PATCH 330/674] Let consumer parse the message instead of server --- nemubot/bot.py | 11 ++++++++--- nemubot/consumer.py | 13 ++++++++++++- nemubot/server/IRC.py | 11 ++++++++--- nemubot/server/abstract.py | 6 ++++++ 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/nemubot/bot.py b/nemubot/bot.py index fccefc5..0889ca1 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -423,9 +423,14 @@ class Bot(threading.Thread): return False - def receive_message(self, srv, msg, private=False, data=None): - """Queued the message for treatment""" - #print("READ", raw_msg) + def receive_message(self, srv, msg): + """Queued the message for treatment + + Arguments: + srv -- The server where the message comes from + msg -- The message not parsed, as simple as possible + """ + self.cnsr_queue.put_nowait(MessageConsumer(srv, msg)) # Launch a new thread if necessary diff --git a/nemubot/consumer.py b/nemubot/consumer.py index 8022b72..18e3d47 100644 --- a/nemubot/consumer.py +++ b/nemubot/consumer.py @@ -30,7 +30,8 @@ class MessageConsumer: def __init__(self, srv, msg): self.srv = srv - self.msgs = [ msg ] + self.orig = msg + self.msgs = [ ] self.responses = None @@ -145,6 +146,16 @@ class MessageConsumer: def run(self, context): """Create, parse and treat the message""" + try: + for msg in self.srv.parse(self.orig): + self.msgs.append(msg) + except: + logger.exception("Error occurred during the processing of the %s: " + "%s", type(self.msgs[0]).__name__, self.msgs[0]) + + if len(self.msgs) <= 0: + return + try: for msg in self.msgs: self.first_treat(msg) diff --git a/nemubot/server/IRC.py b/nemubot/server/IRC.py index 25f37cf..5dd81a5 100644 --- a/nemubot/server/IRC.py +++ b/nemubot/server/IRC.py @@ -257,11 +257,16 @@ class IRC(SocketServer): def read(self): for line in SocketServer.read(self): + # 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) - mes = msg.to_bot_message(self) - if mes is not None: - yield mes + yield msg + + + def parse(self, msg): + mes = msg.to_bot_message(self) + if mes is not None: + yield mes diff --git a/nemubot/server/abstract.py b/nemubot/server/abstract.py index 127fa15..dd71ff6 100644 --- a/nemubot/server/abstract.py +++ b/nemubot/server/abstract.py @@ -139,6 +139,12 @@ class AbstractServer(io.IOBase): self.write(vprnt.pp) + # Read + + def parse(self, msg): + raise NotImplemented + + # Exceptions def exception(self): From a00c3542878bb11d605066bf1c078e8ce9a21447 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 15 Jul 2015 07:48:41 +0200 Subject: [PATCH 331/674] Add a factory to help connecting to servers --- nemubot/server/__init__.py | 39 +++++++++++++++++++++++ nemubot/server/factory_test.py | 58 ++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 nemubot/server/factory_test.py diff --git a/nemubot/server/__init__.py b/nemubot/server/__init__.py index 6770796..a171d8f 100644 --- a/nemubot/server/__init__.py +++ b/nemubot/server/__init__.py @@ -23,6 +23,45 @@ _wlist = [] _xlist = [] +def factory(uri): + 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() + + modifiers = o.path.split(",") + target = unquote(modifiers.pop(0)[1:]) + + if o.scheme == "ircs": args["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 + + queries = o.query.split("&") + for q in queries: + key, val = tuple(q.split("=", 1)) + if key == "msg": + args["on_connect"] = [ "PRIVMSG %s :%s" % (target, unquote(val)) ] + elif key == "key": + args["channels"] = [ (target, unquote(val)) ] + elif key == "pass": + args["password"] = unquote(val) + elif key == "charset": + args["encoding"] = unquote(val) + + 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 + + def reload(): import imp diff --git a/nemubot/server/factory_test.py b/nemubot/server/factory_test.py new file mode 100644 index 0000000..1296414 --- /dev/null +++ b/nemubot/server/factory_test.py @@ -0,0 +1,58 @@ +# -*- 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/>. + +import unittest + +from nemubot.server import factory + +class TestFactory(unittest.TestCase): + + def test_IRC1(self): + from nemubot.server.IRC import IRC as IRCServer + + # <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.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.assertEqual(server.host, "host3") + self.assertEqual(server.port, 194) + self.assertTrue(server.ssl) + + +if __name__ == '__main__': + unittest.main() From 0208a5d552fc2fe437d32aaa4347a14d494c081b Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 17 Jul 2015 20:04:37 +0200 Subject: [PATCH 332/674] Allow socket to print messages --- nemubot/message/printer/IRC.py | 49 ++-------------------- nemubot/message/printer/socket.py | 68 +++++++++++++++++++++++++++++++ nemubot/server/IRC.py | 2 +- nemubot/server/socket.py | 2 + 4 files changed, 74 insertions(+), 47 deletions(-) create mode 100644 nemubot/message/printer/socket.py diff --git a/nemubot/message/printer/IRC.py b/nemubot/message/printer/IRC.py index 1386e45..d9a1ffc 100644 --- a/nemubot/message/printer/IRC.py +++ b/nemubot/message/printer/IRC.py @@ -17,54 +17,11 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. from nemubot.message import Text -from nemubot.message.visitor import AbstractVisitor +from nemubot.message.printer.socket import Socket as SocketPrinter -class IRC(AbstractVisitor): - - def __init__(self): - self.pp = "" - +class IRC(SocketPrinter): def visit_Text(self, msg): self.pp += "PRIVMSG %s :" % ",".join(msg.to) - if isinstance(msg.message, str): - self.pp += msg.message - else: - msg.message.accept(self) - self.pp += "\r\n" - - - def visit_DirectAsk(self, msg): - 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): - res = Text(msg.message, - server=msg.server, date=msg.date, - to=msg.to, frm=msg.frm) - res.accept(self) - - if len(others): - res = Text("%s: %s" % (msg.designated, msg.message), - server=msg.server, date=msg.date, - to=others, frm=msg.frm) - res.accept(self) - - - def visit_Command(self, msg): - res = Text("!%s%s%s" % (msg.cmd, - " " if len(msg.args) else "", - " ".join(msg.args)), - server=msg.server, date=msg.date, - to=msg.to, frm=msg.frm) - res.accept(self) - - - def visit_OwnerCommand(self, msg): - res = Text("`%s%s%s" % (msg.cmd, - " " if len(msg.args) else "", - " ".join(msg.args)), - server=msg.server, date=msg.date, - to=msg.to, frm=msg.frm) - res.accept(self) + SocketPrinter.visit_Text(self, msg) diff --git a/nemubot/message/printer/socket.py b/nemubot/message/printer/socket.py new file mode 100644 index 0000000..dfa0115 --- /dev/null +++ b/nemubot/message/printer/socket.py @@ -0,0 +1,68 @@ +# -*- 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/>. + +from nemubot.message import Text +from nemubot.message.visitor import AbstractVisitor + + +class Socket(AbstractVisitor): + + def __init__(self): + self.pp = "" + + + def visit_Text(self, msg): + if isinstance(msg.message, str): + self.pp += msg.message + else: + msg.message.accept(self) + + + def visit_DirectAsk(self, msg): + 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): + res = Text(msg.message, + server=msg.server, date=msg.date, + to=msg.to, frm=msg.frm) + res.accept(self) + + if len(others): + res = Text("%s: %s" % (msg.designated, msg.message), + server=msg.server, date=msg.date, + to=others, frm=msg.frm) + res.accept(self) + + + def visit_Command(self, msg): + res = Text("!%s%s%s" % (msg.cmd, + " " if len(msg.args) else "", + " ".join(msg.args)), + server=msg.server, date=msg.date, + to=msg.to, frm=msg.frm) + res.accept(self) + + + def visit_OwnerCommand(self, msg): + res = Text("`%s%s%s" % (msg.cmd, + " " if len(msg.args) else "", + " ".join(msg.args)), + server=msg.server, date=msg.date, + to=msg.to, frm=msg.frm) + res.accept(self) diff --git a/nemubot/server/IRC.py b/nemubot/server/IRC.py index 5dd81a5..5c26d21 100644 --- a/nemubot/server/IRC.py +++ b/nemubot/server/IRC.py @@ -57,8 +57,8 @@ class IRC(SocketServer): self.realname = realname self.id = self.username + "@" + host + ":" + str(port) - self.printer = IRCPrinter SocketServer.__init__(self, host=host, port=port, ssl=ssl) + self.printer = IRCPrinter self.encoding = encoding diff --git a/nemubot/server/socket.py b/nemubot/server/socket.py index 7f46325..54a1703 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 <http://www.gnu.org/licenses/>. +from nemubot.message.printer.socket import Socket as SocketPrinter from nemubot.server.abstract import AbstractServer @@ -31,6 +32,7 @@ class SocketServer(AbstractServer): self.socket = None self.readbuffer = b'' + self.printer = SocketPrinter def fileno(self): From 88a8e0fe5927eb833acce21446ce755794da2f37 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 20 Jul 2015 07:18:50 +0200 Subject: [PATCH 333/674] web: can make POST request --- nemubot/tools/web.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/nemubot/tools/web.py b/nemubot/tools/web.py index 23ae713..6d49f40 100644 --- a/nemubot/tools/web.py +++ b/nemubot/tools/web.py @@ -60,11 +60,12 @@ def getPassword(url): # Get real pages -def getURLContent(url, timeout=15): +def getURLContent(url, body=None, timeout=15): """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 """ @@ -89,11 +90,15 @@ def getURLContent(url, timeout=15): try: from nemubot import __version__ if o.query != '': - conn.request("GET", o.path + "?" + o.query, - None, {"User-agent": "Nemubot v%s" % __version__}) + conn.request("GET" if body is None else "POST", + o.path + "?" + o.query, + body, + {"User-agent": "Nemubot v%s" % __version__}) else: - conn.request("GET", o.path, None, {"User-agent": - "Nemubot v%s" % __version__}) + conn.request("GET" if body is None else "POST", + o.path, + body, + {"User-agent": "Nemubot v%s" % __version__}) except OSError as e: raise IRCException(e.strerror) @@ -130,7 +135,7 @@ def getURLContent(url, timeout=15): elif ((res.status == http.client.FOUND or res.status == http.client.MOVED_PERMANENTLY) and res.getheader("Location") != url): - return getURLContent(res.getheader("Location"), timeout) + return getURLContent(res.getheader("Location"), timeout=timeout) else: raise IRCException("A HTTP error occurs: %d - %s" % (res.status, http.client.responses[res.status])) @@ -144,7 +149,7 @@ def getXML(url, timeout=15): timeout -- maximum number of seconds to wait before returning an exception """ - cnt = getURLContent(url, timeout) + cnt = getURLContent(url, timeout=timeout) if cnt is None: return None else: @@ -162,7 +167,7 @@ def getJSON(url, timeout=15): import json - cnt = getURLContent(url, timeout) + cnt = getURLContent(url, timeout=timeout) if cnt is None: return None else: From c27540eb87b77788f09a60cbe6698cfd86c4d862 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 20 Jul 2015 07:50:18 +0200 Subject: [PATCH 334/674] web: reduce timeout from 15 to 7 seconds --- 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 6d49f40..050d726 100644 --- a/nemubot/tools/web.py +++ b/nemubot/tools/web.py @@ -60,7 +60,7 @@ def getPassword(url): # Get real pages -def getURLContent(url, body=None, timeout=15): +def getURLContent(url, body=None, timeout=7): """Return page content corresponding to URL or None if any error occurs Arguments: From d1c28fc4a37895f0da8a72cb1377256fb47f8a75 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 21 Jul 2015 00:21:32 +0200 Subject: [PATCH 335/674] [networking] New function to get a list of watched URL --- modules/networking/__init__.py | 9 +++++++++ modules/networking/watchWebsite.py | 14 ++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/modules/networking/__init__.py b/modules/networking/__init__.py index 8d62c54..670c423 100644 --- a/modules/networking/__init__.py +++ b/modules/networking/__init__.py @@ -127,6 +127,15 @@ def cmd_watch(msg, diffType="diff"): return watchWebsite.add_site(msg.args[0], msg.frm, msg.channel, msg.server, diffType) +@hook("cmd_hook", "listwatch") +def cmd_listwatch(msg): + wl = watchWebsite.watchedon(msg.channel) + if len(wl): + return Response(wl, channel=msg.channel, title="URL watched on this channel") + else: + return Response("No URL are currently watched. Use !watch URL to watch one.", channel=msg.channel) + + @hook("cmd_hook", "unwatch") def cmd_unwatch(msg): if not len(msg.args): diff --git a/modules/networking/watchWebsite.py b/modules/networking/watchWebsite.py index 32bf79f..d37a19b 100644 --- a/modules/networking/watchWebsite.py +++ b/modules/networking/watchWebsite.py @@ -36,6 +36,20 @@ def load(datas): #DATAS.delChild(site) +def watchedon(channel): + """Get a list of currently watched URL on the given channel. + """ + + res = list() + for site in DATAS.getNodes("watch"): + if site.hasNode("alert"): + for a in site.getNodes("alert"): + if a["channel"] == channel: + res.append("%s (%s)" % (site["url"], site["type"])) + break + return res + + def del_site(url, nick, channel, frm_owner): """Remove a site from watching list From 5b039edb6276511601383347cc9d3002740bbb8d Mon Sep 17 00:00:00 2001 From: Max <max@23.tf> Date: Mon, 13 Jul 2015 22:20:10 +0100 Subject: [PATCH 336/674] Updated tracking module to support colissimo --- modules/laposte.py | 44 ---------------------------- modules/suivi.py | 71 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 44 deletions(-) delete mode 100644 modules/laposte.py create mode 100644 modules/suivi.py diff --git a/modules/laposte.py b/modules/laposte.py deleted file mode 100644 index 966cb93..0000000 --- a/modules/laposte.py +++ /dev/null @@ -1,44 +0,0 @@ -import urllib.request -import urllib.parse -from bs4 import BeautifulSoup - -from nemubot.hooks import hook -from nemubot.exception import IRCException -from more import Response - -nemubotversion = 3.4 - -def help_full(): - return "Traquez vos courriers La Poste en utilisant la commande: !laposte <tracking number>\nCe service se base sur http://www.csuivi.courrier.laposte.fr/suivi/index" - - -@hook("cmd_hook", "laposte") -def get_tracking_info(msg): - if not len(msg.args): - raise IRCException("Renseignez un identifiant d'envoi,") - - data = urllib.parse.urlencode({'id': msg.args[0]}) - laposte_baseurl = "http://www.part.csuivi.courrier.laposte.fr/suivi/index" - - 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 - and soup.find(class_='resultat_rech_simple_table').thead.tr): - field = search_res.find('td') - poste_id = field.get_text() - - field = field.find_next('td') - poste_type = field.get_text() - - 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 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.lower(), poste_id.strip(), poste_status.lower(), poste_location, poste_date), msg.channel) - return Response("L'identifiant recherché semble incorrect, merci de vérifier son exactitude.", msg.channel) diff --git a/modules/suivi.py b/modules/suivi.py new file mode 100644 index 0000000..c8a8fa3 --- /dev/null +++ b/modules/suivi.py @@ -0,0 +1,71 @@ +import urllib.request +import urllib.parse +from bs4 import BeautifulSoup + +from nemubot.hooks import hook +from nemubot.exception import IRCException +from more import Response + +nemubotversion = 3.4 + +def help_full(): + return "Traquez vos courriers La Poste en utilisant la commande: !laposte <tracking number>\nCe service se base sur http://www.csuivi.courrier.laposte.fr/suivi/index" + +def get_colissimo_info(colissimo_id): + data = urllib.parse.urlencode({'colispart': colissimo_id}) + colissimo_baseurl = "http://www.colissimo.fr/portail_colissimo/suivre.do" + colissimo_data = urllib.request.urlopen(colissimo_baseurl, data.encode('utf-8')) + 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() + site = dataArray.tbody.tr.find(headers="site").get_text().strip() + return (date, libelle, site.strip()) + +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')) + 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): + field = search_res.find('td') + poste_id = field.get_text() + + field = field.find_next('td') + poste_type = field.get_text() + + 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) + + +@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) From 710896f711e0ec17e130f15844a668a5720e7443 Mon Sep 17 00:00:00 2001 From: Max <max@23.tf> Date: Mon, 13 Jul 2015 22:25:40 +0100 Subject: [PATCH 337/674] [suivi] Now using nemubot.tools.web for queries --- modules/suivi.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/modules/suivi.py b/modules/suivi.py index c8a8fa3..deffae6 100644 --- a/modules/suivi.py +++ b/modules/suivi.py @@ -4,17 +4,16 @@ from bs4 import BeautifulSoup from nemubot.hooks import hook from nemubot.exception import IRCException +from nemubot.tools.web import getURLContent from more import Response -nemubotversion = 3.4 +nemubotversion = 4.0 def help_full(): - return "Traquez vos courriers La Poste en utilisant la commande: !laposte <tracking number>\nCe service se base sur http://www.csuivi.courrier.laposte.fr/suivi/index" + return "Traquez vos courriers La Poste ou Colissimo en utilisant la commande: !laposte <tracking number> ou !colissimo <tracking number>\nCe service se base sur http://www.csuivi.courrier.laposte.fr/suivi/index et http://www.colissimo.fr/portail_colissimo/suivre.do" def get_colissimo_info(colissimo_id): - data = urllib.parse.urlencode({'colispart': colissimo_id}) - colissimo_baseurl = "http://www.colissimo.fr/portail_colissimo/suivre.do" - colissimo_data = urllib.request.urlopen(colissimo_baseurl, data.encode('utf-8')) + colissimo_data = getURLContent("http://www.colissimo.fr/portail_colissimo/suivre.do?colispart=%s" % colissimo_id) soup = BeautifulSoup(colissimo_data) dataArray = soup.find(class_='dataArray') @@ -25,10 +24,7 @@ def get_colissimo_info(colissimo_id): return (date, libelle, site.strip()) 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("http://www.part.csuivi.courrier.laposte.fr/suivi/index?id=%s" % laposte_id) soup = BeautifulSoup(laposte_data) search_res = soup.find(class_='resultat_rech_simple_table').tbody.tr if (soup.find(class_='resultat_rech_simple_table').thead @@ -49,7 +45,6 @@ def get_laposte_info(laposte_id): poste_status = field.get_text() return (poste_type.lower(), poste_id.strip(), poste_status.lower(), poste_location, poste_date) - @hook("cmd_hook", "colissimo") def get_colissimo_tracking_info(msg): if not len(msg.args): From 67cd66b922a30a5f8693c773b97c66bfc214306c Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Wed, 22 Jul 2015 20:10:08 +0200 Subject: [PATCH 338/674] [mediawiki] Handle # --- modules/mediawiki.py | 98 ++++++++++++++++++++++++++------------------ 1 file changed, 58 insertions(+), 40 deletions(-) diff --git a/modules/mediawiki.py b/modules/mediawiki.py index c7c4e41..4f0e6f4 100644 --- a/modules/mediawiki.py +++ b/modules/mediawiki.py @@ -15,6 +15,8 @@ nemubotversion = 3.4 from more import Response +# MEDIAWIKI REQUESTS ################################################## + def get_namespaces(site, ssl=False): # Built URL url = "http%s://%s/w/api.php?format=json&action=query&meta=siteinfo&siprop=namespaces" % ( @@ -55,6 +57,39 @@ def get_unwikitextified(site, wikitext, ssl=False): return data["expandtemplates"]["*"] +## Search + +def opensearch(site, term, ssl=False): + # Built URL + url = "http%s://%s/w/api.php?format=xml&action=opensearch&search=%s" % ( + "s" if ssl else "", site, urllib.parse.quote(term)) + + # Make the request + response = web.getXML(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(), + itm.getNode("Url").getContent()) + + +def search(site, term, ssl=False): + # 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)) + + # Make the request + data = web.getJSON(url) + + if data is not None and "query" in data and "search" in data["query"]: + for itm in data["query"]["search"]: + yield (web.striphtml(itm["titlesnippet"].replace("<span class='searchmatch'>", "\x03\x02").replace("</span>", "\x03\x02")), + web.striphtml(itm["snippet"].replace("<span class='searchmatch'>", "\x03\x02").replace("</span>", "\x03\x02"))) + + +# PARSING FUNCTIONS ################################################### + def strip_model(cnt): # Strip models at begin: mostly useless cnt = re.sub(r"^(({{([^{]|\s|({{([^{]|\s|{{.*?}})*?}})*?)*?}}|\[\[([^[]|\s|\[\[.*?\]\])*?\]\])\s*)+", "", cnt, flags=re.DOTALL) @@ -98,42 +133,33 @@ def parse_wikitext(site, cnt, namespaces=dict(), ssl=False): return cnt +# FORMATING FUNCTIONS ################################################# + def irc_format(cnt): cnt, _ = re.subn(r"(?P<title>==+)\s*(.*?)\s*(?P=title)", "\x03\x16" + r"\2" + " :\x03\x16 ", cnt) return cnt.replace("'''", "\x03\x02").replace("''", "\x03\x1f") -def get_page(site, term, ssl=False): - return strip_model(get_raw_page(site, term, ssl)) +def get_page(site, term, ssl=False, subpart=None): + raw = get_raw_page(site, term, ssl) + + if subpart is not None: + 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) -def opensearch(site, term, ssl=False): - # Built URL - url = "http%s://%s/w/api.php?format=xml&action=opensearch&search=%s" % ( - "s" if ssl else "", site, urllib.parse.quote(term)) +# NEMUBOT ############################################################# - # Make the request - response = web.getXML(url) +def mediawiki_response(site, term, receivers): + ns = get_namespaces(site) - 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(), - itm.getNode("Url").getContent()) + terms = term.split("#", 1) - -def search(site, term, ssl=False): - # 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)) - - # Make the request - data = web.getJSON(url) - - if data is not None and "query" in data and "search" in data["query"]: - for itm in data["query"]["search"]: - yield (web.striphtml(itm["titlesnippet"].replace("<span class='searchmatch'>", "\x03\x02").replace("</span>", "\x03\x02")), - web.striphtml(itm["snippet"].replace("<span class='searchmatch'>", "\x03\x02").replace("</span>", "\x03\x02"))) + 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=msg.receivers) @hook("cmd_hook", "mediawiki") @@ -142,13 +168,9 @@ def cmd_mediawiki(msg): if len(msg.args) < 2: raise IRCException("indicate a domain and a term to search") - site = msg.args[0] - - ns = get_namespaces(site) - - return Response(get_page(site, " ".join(msg.args[1:])), - line_treat=lambda line: irc_format(parse_wikitext(msg.args[0], line, ns)), - channel=msg.receivers) + return mediawiki_response(msg.args[0], + " ".join(msg.args[1:]), + msg.receivers) @hook("cmd_hook", "search_mediawiki") @@ -170,10 +192,6 @@ def cmd_wikipedia(msg): if len(msg.args) < 2: raise IRCException("indicate a lang and a term to search") - site = msg.args[0] + ".wikipedia.org" - - ns = get_namespaces(site) - - return Response(get_page(site, " ".join(msg.args[1:])), - line_treat=lambda line: irc_format(parse_wikitext(site, line, ns)), - channel=msg.receivers) + return mediawiki_response(msg.args[0] + ".wikipedia.org", + " ".join(msg.args[1:]), + msg.receivers) From 6147eef19bca138bd90e6840b38c9356be5cbb2e Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Thu, 23 Jul 2015 21:53:20 +0200 Subject: [PATCH 339/674] [mediawiki] Help user find the article he want to read if it doesn't exist --- modules/mediawiki.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/modules/mediawiki.py b/modules/mediawiki.py index 4f0e6f4..08d2a19 100644 --- a/modules/mediawiki.py +++ b/modules/mediawiki.py @@ -157,9 +157,20 @@ def mediawiki_response(site, term, receivers): terms = term.split("#", 1) - 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=msg.receivers) + try: + # 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) + except: + # Try looking at opensearch + os = [x for x, _, _ in opensearch(site, terms[0])] + # Fallback to global search + 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, + title="Article not found, would you mean") @hook("cmd_hook", "mediawiki") From 2b0593a51e8038c3af2eb82f282c51fa131e46d7 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Fri, 24 Jul 2015 22:51:47 +0200 Subject: [PATCH 340/674] Add tool to calculate string distance --- nemubot/tools/human.py | 22 ++++++++++++++++++++++ nemubot/tools/test_human.py | 19 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 nemubot/tools/test_human.py diff --git a/nemubot/tools/human.py b/nemubot/tools/human.py index 478914e..ef6f782 100644 --- a/nemubot/tools/human.py +++ b/nemubot/tools/human.py @@ -36,3 +36,25 @@ def size(size, unit=True): return "%.3f %s" % (size / math.pow(1024,p), units[int(p)]) else: return "%.3f" % (size / math.pow(1024,p)) + + +def word_distance(str1, str2): + """Perform a Damerau-Levenshtein distance on the two given strings""" + + d = [[i + j for j in range(len(str2) + 1)] for i in range(len(str1) + 1)] + + for i in range(0, len(str1)): + for j in range(0, len(str2)): + cost = 0 if str1[i-1] == str2[j-1] else 1 + d[i+1][j+1] = min( + d[i][j+1] + 1, # deletion + d[i+1][j] + 1, # insertion + d[i][j] + cost, # substitution + ) + if i >= 1 and j >= 1 and str1[i] == str2[j-1] and str1[i-1] == str2[j]: + d[i+1][j+1] = min( + d[i+1][j+1], + d[i-1][j-1] + cost, # transposition + ) + + return d[len(str1)][len(str2)] diff --git a/nemubot/tools/test_human.py b/nemubot/tools/test_human.py new file mode 100644 index 0000000..00d2967 --- /dev/null +++ b/nemubot/tools/test_human.py @@ -0,0 +1,19 @@ +import unittest + +from nemubot.tools.human import size, word_distance + +class TestHuman(unittest.TestCase): + + def test_Levenshtein(self): + self.assertEqual(word_distance("", "a"), 1) + self.assertEqual(word_distance("a", ""), 1) + self.assertEqual(word_distance("a", "a"), 0) + self.assertEqual(word_distance("a", "b"), 1) + self.assertEqual(word_distance("aa", "ba"), 1) + self.assertEqual(word_distance("ba", "ab"), 1) + self.assertEqual(word_distance("long", "short"), 4) + self.assertEqual(word_distance("long", "short"), word_distance("short", "long")) + + +if __name__ == '__main__': + unittest.main() From 969210a72386af7bbb0bfe9d821d3837c3c44f14 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Tue, 1 Sep 2015 20:29:11 +0200 Subject: [PATCH 341/674] [networking] Avoid exception when port is not defined on socket error --- modules/networking/page.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/networking/page.py b/modules/networking/page.py index 4e74943..ba1c626 100644 --- a/modules/networking/page.py +++ b/modules/networking/page.py @@ -37,7 +37,7 @@ def headers(url): raise IRCException("request timeout") except socket.gaierror: print ("<tools.web> Unable to receive page %s from %s on %d." - % (o.path, o.hostname, o.port)) + % (o.path, o.hostname, o.port if o.port is not None else 0)) raise IRCException("an unexpected error occurs") try: From e837f9c8e5bcb759d43d0f690b96466588ebd9cb Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Sun, 6 Sep 2015 13:09:56 +0200 Subject: [PATCH 342/674] Improve formating of size function and test it --- nemubot/tools/human.py | 7 +++---- nemubot/tools/test_human.py | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/nemubot/tools/human.py b/nemubot/tools/human.py index ef6f782..b751409 100644 --- a/nemubot/tools/human.py +++ b/nemubot/tools/human.py @@ -32,10 +32,9 @@ def size(size, unit=True): units = ['B','KiB','MiB','GiB','TiB','PiB','EiB','ZiB','YiB'] p = math.floor(math.log(size, 2) / 10) - if unit: - return "%.3f %s" % (size / math.pow(1024,p), units[int(p)]) - else: - return "%.3f" % (size / math.pow(1024,p)) + s = size / math.pow(1024, p) + r = size % math.pow(1024, p) + return (("%.3f" if r else "%.0f") % s) + ((" " + units[int(p)]) if unit else "") def word_distance(str1, str2): diff --git a/nemubot/tools/test_human.py b/nemubot/tools/test_human.py index 00d2967..06b48d8 100644 --- a/nemubot/tools/test_human.py +++ b/nemubot/tools/test_human.py @@ -4,6 +4,23 @@ from nemubot.tools.human import size, word_distance class TestHuman(unittest.TestCase): + def test_size(self): + self.assertEqual(size(42), "42 B") + self.assertEqual(size(42, False), "42") + self.assertEqual(size(1023), "1023 B") + self.assertEqual(size(1024), "1 KiB") + self.assertEqual(size(1024, False), "1") + self.assertEqual(size(1025), "1.001 KiB") + self.assertEqual(size(1025, False), "1.001") + self.assertEqual(size(1024000), "1000 KiB") + self.assertEqual(size(1024000, False), "1000") + self.assertEqual(size(1024 * 1024), "1 MiB") + self.assertEqual(size(1024 * 1024, False), "1") + self.assertEqual(size(1024 * 1024 * 1024), "1 GiB") + self.assertEqual(size(1024 * 1024 * 1024, False), "1") + self.assertEqual(size(1024 * 1024 * 1024 * 1024), "1 TiB") + self.assertEqual(size(1024 * 1024 * 1024 * 1024, False), "1") + def test_Levenshtein(self): self.assertEqual(word_distance("", "a"), 1) self.assertEqual(word_distance("a", ""), 1) From ec9481e65bea8bbd9862531d48c687109f00aa66 Mon Sep 17 00:00:00 2001 From: nemunaire <nemunaire@nemunai.re> Date: Mon, 7 Sep 2015 19:07:43 +0200 Subject: [PATCH 343/674] [networking] Add !title command to display the title of a page --- modules/networking/__init__.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/modules/networking/__init__.py b/modules/networking/__init__.py index 670c423..188dbb5 100644 --- a/modules/networking/__init__.py +++ b/modules/networking/__init__.py @@ -3,6 +3,7 @@ """Various network tools (w3m, w3c validator, curl, traceurl, ...)""" import logging +import re from nemubot.exception import IRCException from nemubot.hooks import hook @@ -37,6 +38,20 @@ def help_full(): return "!traceurl /url/: Follow redirections from /url/." +@hook("cmd_hook", "title") +def cmd_title(msg): + if not len(msg.args): + raise IRCException("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) + else: + return Response("%s: %s" % (url, res.group(1).replace("\n", " ")), channel=msg.channel) + + @hook("cmd_hook", "curly") def cmd_curly(msg): if not len(msg.args): From 9686f36522245e7c164b2528157efe57ec8189b4 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Tue, 8 Sep 2015 20:14:27 +0200 Subject: [PATCH 344/674] Add a function to guess the closest word for a miss input --- nemubot/tools/human.py | 10 ++++++++++ nemubot/tools/test_human.py | 6 +++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/nemubot/tools/human.py b/nemubot/tools/human.py index b751409..588ac1f 100644 --- a/nemubot/tools/human.py +++ b/nemubot/tools/human.py @@ -57,3 +57,13 @@ def word_distance(str1, str2): ) return d[len(str1)][len(str2)] + + +def guess(pattern, expect): + if len(expect): + se = sorted([(e, word_distance(pattern, e)) for e in expect], key=lambda x: x[1]) + _, m = se[0] + for e, wd in se: + if wd > m or wd > 1 + len(pattern) / 4: + break + yield e diff --git a/nemubot/tools/test_human.py b/nemubot/tools/test_human.py index 06b48d8..8ebdd49 100644 --- a/nemubot/tools/test_human.py +++ b/nemubot/tools/test_human.py @@ -1,6 +1,6 @@ import unittest -from nemubot.tools.human import size, word_distance +from nemubot.tools.human import guess, size, word_distance class TestHuman(unittest.TestCase): @@ -31,6 +31,10 @@ class TestHuman(unittest.TestCase): self.assertEqual(word_distance("long", "short"), 4) self.assertEqual(word_distance("long", "short"), word_distance("short", "long")) + def test_guess(self): + self.assertListEqual([g for g in guess("drunk", ["eat", "drink"])], ["drink"]) + self.assertListEqual([g for g in guess("drunk", ["long", "short"])], []) + if __name__ == '__main__': unittest.main() From ac3ed0d492166849996dce2666e708deac362996 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Thu, 10 Sep 2015 01:40:06 +0200 Subject: [PATCH 345/674] [alias] huge refactoring --- modules/alias.py | 268 +++++++++++++++++++++++++++++------------------ 1 file changed, 166 insertions(+), 102 deletions(-) diff --git a/modules/alias.py b/modules/alias.py index 2b7a209..00ae770 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -1,7 +1,7 @@ -# coding=utf-8 - """Create alias of commands""" +# PYTHON STUFFS ####################################################### + import re import sys from datetime import datetime, timezone @@ -13,11 +13,16 @@ from nemubot.hooks import hook from nemubot.message import Command from nemubot.tools.xmlparser.node import ModuleState -nemubotversion = 3.4 - from more import Response +# HELP ################################################################ + +def help_full(): + return "Pour créer un alias, adressez-vous à moi en disant quelque chose comme : \"nouvel alias XX : YY\", où YY sera la commande équivalente à XX. Vous pouvez ajouter des variables comme ${1}, ${2}, ... correspondant aux éventuels arguments.\nDe l'aide supplémentaire existe pour les commandes !alias, !listalias, !unalias, !set et !listvars" + +# LOADING ############################################################# + def load(context): """Load this module""" if not context.data.hasNode("aliases"): @@ -28,18 +33,49 @@ def load(context): context.data.getNode("variables").setIndex("name") -def set_variable(name, value, creator): - var = ModuleState("variable") - var["name"] = name - var["value"] = value - var["creator"] = creator - context.data.getNode("variables").addChild(var) +# MODULE CORE ######################################################### +## Alias management + +def list_alias(channel=None): + """List known aliases. + + Argument: + channel -- optional, if defined, return a list of aliases only defined on this channel, else alias widly defined + """ + + for alias in context.data.getNode("aliases").index.values(): + if (channel is None and "channel" not in alias) or (channel is not None and "channel" in alias and alias["channel"] == channel): + yield alias + +def create_alias(alias, origin, channel=None, creator=None): + """Create or erase an existing alias + """ + + anode = ModuleState("alias") + anode["alias"] = alias + anode["origin"] = origin + if channel is not None: + anode["creator"] = channel + if creator is not None: + anode["creator"] = creator + context.data.getNode("aliases").addChild(anode) + context.save() + + +## Variables management def get_variable(name, msg=None): - if name == "sender" or name == "from" or name == "nick": + """Get the value for the given variable + + Arguments: + name -- The variable identifier + msg -- optional, original message where some variable can be picked + """ + + if msg is not None and (name == "sender" or name == "from" or name == "nick"): return msg.frm - elif name == "chan" or name == "channel": + elif msg is not None and (name == "chan" or name == "channel"): return msg.channel elif name == "date": return datetime.now(timezone.utc).strftime("%c") @@ -49,93 +85,43 @@ def get_variable(name, msg=None): return "" -@hook("cmd_hook", "set") -def cmd_set(msg): - if len(msg.args) < 2: - raise IRCException("!set prend au minimum deux arguments : " - "le nom de la variable et sa valeur.") - set_variable(msg.args[0], " ".join(msg.args[1:]), msg.nick) +def list_variables(user=None): + """List known variables. + + Argument: + user -- optional, if defined, display only variable created by the given user + """ + if user is not None: + return [x for x in context.data.getNode("variables").index.values() if x["creator"] == user] + else: + return context.data.getNode("variables").index.values() + + +def set_variable(name, value, creator): + """Define or erase a variable. + + Arguments: + name -- The variable identifier + value -- Variable value + creator -- User who has created this variable + """ + + var = ModuleState("variable") + var["name"] = name + var["value"] = value + var["creator"] = creator + context.data.getNode("variables").addChild(var) context.save() - return Response("Variable \$%s définie." % msg.args[0], - channel=msg.channel) - - -@hook("cmd_hook", "listalias") -def cmd_listalias(msg): - if len(msg.args): - res = list() - for user in msg.args: - als = [x["alias"] for x in context.data.getNode("aliases").index.values() if x["creator"] == user] - if len(als) > 0: - res.append("%s's aliases: %s" % (user, ", ".join(als))) - else: - res.append("%s has never created aliases." % user) - return Response("; ".join(res), channel=msg.channel) - elif len(context.data.getNode("aliases").index): - return Response("Known aliases: %s." % - ", ".join(context.data.getNode("aliases").index.keys()), - channel=msg.channel) - else: - return Response("There is no alias currently.", channel=msg.channel) - - -@hook("cmd_hook", "listvars") -def cmd_listvars(msg): - if len(msg.args): - res = list() - for user in msg.args: - als = [x["alias"] for x in context.data.getNode("variables").index.values() if x["creator"] == user] - if len(als) > 0: - res.append("Variables créées par %s : %s" % (user, ", ".join(als))) - else: - res.append("%s n'a pas encore créé de variable" % user) - return Response(" ; ".join(res), channel=msg.channel) - elif len(context.data.getNode("variables").index): - return Response("Variables connues : %s." % - ", ".join(context.data.getNode("variables").index.keys()), - channel=msg.channel) - else: - return Response("No variable are currently stored.", channel=msg.channel) - - -@hook("cmd_hook", "alias") -def cmd_alias(msg): - if len(msg.args): - res = list() - for alias in msg.args: - if alias[0] == "!": - alias = alias[1:] - if alias in context.data.getNode("aliases").index: - res.append("!%s correspond à %s" % (alias, context.data.getNode("aliases").index[alias]["origin"])) - else: - res.append("!%s n'est pas un alias" % alias) - return Response(res, channel=msg.channel, nick=msg.nick) - else: - return Response("!alias prend en argument l'alias à étendre.", - channel=msg.channel, nick=msg.nick) - - -@hook("cmd_hook", "unalias") -def cmd_unalias(msg): - if len(msg.args): - res = list() - for alias in msg.args: - if alias[0] == "!" and len(alias) > 1: - alias = alias[1:] - if alias in context.data.getNode("aliases").index: - context.data.getNode("aliases").delChild(context.data.getNode("aliases").index[alias]) - res.append(Response("%s a bien été supprimé" % alias, - channel=msg.channel)) - else: - res.append(Response("%s n'est pas un alias" % alias, - channel=msg.channel)) - return res - else: - return Response("!unalias prend en argument l'alias à supprimer.", - channel=msg.channel) def replace_variables(cnt, msg=None): + """Replace variables contained in the content + + Arguments: + cnt -- content where search variables + msg -- optional message where pick some variables + """ + if isinstance(cnt, list): return [replace_variables(c, msg) for c in cnt] @@ -163,6 +149,86 @@ def replace_variables(cnt, msg=None): return cnt +# MODULE INTERFACE #################################################### + +## Variables management + +@hook("cmd_hook", "listvars") +def cmd_listvars(msg): + if len(msg.args): + res = list() + for user in msg.args: + als = [v["alias"] for v in list_variables(user)] + if len(als) > 0: + res.append("Variables créées par %s : %s" % (user, ", ".join(als))) + else: + res.append("%s n'a pas encore créé de variable" % user) + return Response(" ; ".join(res), channel=msg.channel) + elif len(context.data.getNode("variables").index): + return Response("Variables connues : %s." % + ", ".join(list_variables()), + channel=msg.channel) + else: + return Response("No variable are currently stored.", channel=msg.channel) + + +@hook("cmd_hook", "set") +def cmd_set(msg): + if len(msg.args) < 2: + raise IRCException("!set prend au minimum deux arguments : " + "le nom de la variable et sa valeur.") + set_variable(msg.args[0], " ".join(msg.args[1:]), msg.nick) + return Response("Variable \$%s définie avec succès." % msg.args[0], + channel=msg.channel) + + +## Alias management + +@hook("cmd_hook", "listalias") +def cmd_listalias(msg): + aliases = [a for a in list_alias(None)] + [a for a in list_alias(msg.channel)] + if len(aliases): + return Response([a["alias"] for a in aliases], + channel=msg.channel, + title="Known aliases:") + return Response("There is no alias currently.", channel=msg.channel) + + +@hook("cmd_hook", "alias") +def cmd_alias(msg): + if not len(msg.args): + raise IRCException("!alias prend en argument l'alias à étendre.") + res = list() + for alias in msg.args: + if alias[0] == "!": + alias = alias[1:] + if alias in context.data.getNode("aliases").index: + res.append("!%s correspond à %s" % (alias, context.data.getNode("aliases").index[alias]["origin"])) + else: + res.append("!%s n'est pas un alias" % alias) + return Response(res, channel=msg.channel, nick=msg.nick) + + +@hook("cmd_hook", "unalias") +def cmd_unalias(msg): + if not len(msg.args): + raise IRCException("Quel alias voulez-vous supprimer ?") + res = list() + for alias in msg.args: + if alias[0] == "!" and len(alias) > 1: + alias = alias[1:] + if alias in context.data.getNode("aliases").index: + context.data.getNode("aliases").delChild(context.data.getNode("aliases").index[alias]) + res.append(Response("%s a bien été supprimé" % alias, + channel=msg.channel)) + else: + res.append(Response("%s n'est pas un alias" % alias, + channel=msg.channel)) + return res + + +## Alias replacement + @hook("pre_Command") def treat_alias(msg): if msg.cmd in context.data.getNode("aliases").index: @@ -187,17 +253,15 @@ def treat_alias(msg): @hook("ask_default") def parseask(msg): if re.match(".*(set|cr[ée]{2}|nouvel(le)?) alias.*", msg.text) is not None: - result = re.match(".*alias !?([^ ]+) (pour|=|:) (.+)$", msg.text) + result = re.match(".*alias !?([^ ]+) ?(pour|=|:) ?(.+)$", msg.text) if result.group(1) in context.data.getNode("aliases").index: raise IRCException("cet alias est déjà défini.") else: - alias = ModuleState("alias") - alias["alias"] = result.group(1) - alias["origin"] = result.group(3) - alias["creator"] = msg.nick - context.data.getNode("aliases").addChild(alias) + create_alias(result.group(1), + result.group(3), + channel=msg.channel, + creator=msg.nick) res = Response("Nouvel alias %s défini avec succès." % result.group(1), channel=msg.channel) - context.save() return res return None From beeb5573e1a47e13c4dfd47f7e9153aaaea4f938 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Fri, 11 Sep 2015 08:10:20 +0200 Subject: [PATCH 346/674] Define class variables in __init__ --- nemubot/bot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nemubot/bot.py b/nemubot/bot.py index 0889ca1..d83f22e 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -48,6 +48,7 @@ class Bot(threading.Thread): logger.info("Initiate nemubot v%s", __version__) self.verbosity = verbosity + self.stop = None # External IP for accessing this bot import ipaddress From 9fa8902f1a908984d3cedc422a5be634fa09d43f Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sat, 12 Sep 2015 14:35:40 +0200 Subject: [PATCH 347/674] Invalid fd are < 0, not only -1 --- nemubot/bot.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nemubot/bot.py b/nemubot/bot.py index d83f22e..d5510a7 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -138,17 +138,17 @@ class Bot(threading.Thread): 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() == -1: + 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: " + r) fnd_smth = True for w in _wlist: - if not hasattr(w, "fileno") or not isinstance(w.fileno(), int) or w.fileno() == -1: + 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: " + w) fnd_smth = True for x in _xlist: - if not hasattr(x, "fileno") or not isinstance(x.fileno(), int) or x.fileno() == -1: + 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: " + x) fnd_smth = True From 8988dd0d41d25d1ec6810ced387bc2bea5af2dca Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sun, 13 Sep 2015 11:18:07 +0200 Subject: [PATCH 348/674] =?UTF-8?q?striphtml:=20also=20convert=20=C2=B4=20?= =?UTF-8?q?and=20collapse=20multiple=20space,=20as=20HTML=20display=20do?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nemubot/tools/web.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/nemubot/tools/web.py b/nemubot/tools/web.py index 050d726..9ea0830 100644 --- a/nemubot/tools/web.py +++ b/nemubot/tools/web.py @@ -199,7 +199,10 @@ def striphtml(data): import re p = re.compile(r'<.*?>') - return htmlentitydecode(p.sub('', data) - .replace("(", "/(") - .replace(")", ")/") - .replace(""", "\"")) + r, _ = re.subn(r' +', ' ', htmlentitydecode(p.sub('', data) + .replace("(", "/(") + .replace(")", ")/") + .replace("'", "´") + .replace(""", "\"")) + .replace('\n', ' ')) + return r From 9b2bc27374d7988d6a1b828c1434c31e55a9433a Mon Sep 17 00:00:00 2001 From: nemunaire Date: Mon, 14 Sep 2015 22:28:11 +0200 Subject: [PATCH 349/674] tools.web: restore Python3.3 behavior: don't check server certificate --- nemubot/tools/web.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/nemubot/tools/web.py b/nemubot/tools/web.py index 9ea0830..0cf25fa 100644 --- a/nemubot/tools/web.py +++ b/nemubot/tools/web.py @@ -75,14 +75,25 @@ def getURLContent(url, body=None, timeout=7): import http.client + kwargs = { + 'host': o.hostname, + 'port': o.port, + 'timeout': timeout + } + if o.scheme == "http": - conn = http.client.HTTPConnection(o.hostname, port=o.port, - timeout=timeout) + conn = http.client.HTTPConnection(**kwargs) elif o.scheme == "https": - conn = http.client.HTTPSConnection(o.hostname, port=o.port, - timeout=timeout) + # For Python>3.4, restore the Python 3.3 behavior + import ssl + if hasattr(ssl, "create_default_context"): + kwargs["context"] = ssl.create_default_context() + kwargs["context"].check_hostname = False + kwargs["context"].verify_mode = ssl.CERT_NONE + + conn = http.client.HTTPSConnection(**kwargs) elif o.scheme is None or o.scheme == "": - conn = http.client.HTTPConnection(o.hostname, port=80, timeout=timeout) + conn = http.client.HTTPConnection(**kwargs) else: raise IRCException("Invalid URL") From 760da8ef61093e49dad06b080c05e35082aef4a9 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Tue, 15 Sep 2015 20:15:35 +0200 Subject: [PATCH 350/674] tools.web: don't try to striphtml content that is not str or buffer --- nemubot/tools/web.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nemubot/tools/web.py b/nemubot/tools/web.py index 0cf25fa..8955d5d 100644 --- a/nemubot/tools/web.py +++ b/nemubot/tools/web.py @@ -208,6 +208,9 @@ def striphtml(data): data -- the string to strip """ + if data is None or (not isinstance(data, str) and not isinstance(data, buffer)): + return data + import re p = re.compile(r'<.*?>') r, _ = re.subn(r' +', ' ', htmlentitydecode(p.sub('', data) From e915c4930c02ff3fcff66817e5439b519259d85f Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 15 Sep 2015 23:23:42 +0100 Subject: [PATCH 351/674] =?UTF-8?q?Fix=20#2=20;=20Add=20chronopost=20and?= =?UTF-8?q?=20colis=20priv=C3=A9=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/suivi.py | 59 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/modules/suivi.py b/modules/suivi.py index deffae6..8d4271b 100644 --- a/modules/suivi.py +++ b/modules/suivi.py @@ -19,16 +19,47 @@ def get_colissimo_info(colissimo_id): 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() + libelle = dataArray.tbody.tr.find(headers="Libelle").get_text().replace('\n', '').replace('\t', '').replace('\r', '') 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_data = urllib.request.urlopen(track_baseurl, data.encode('utf-8')) + soup = BeautifulSoup(track_data) + + infoClass = soup.find(class_='numeroColi2') + if infoClass and infoClass.get_text(): + info = infoClass.get_text().split("\n") + if len(info) >= 1: + info = info[1].strip().split("\"") + if len(info) >= 2: + date = info[2] + 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_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() + return status + def get_laposte_info(laposte_id): - laposte_data = getURLContent("http://www.part.csuivi.courrier.laposte.fr/suivi/index?id=%s" % 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')) 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 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() @@ -43,8 +74,28 @@ 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) +@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): @@ -52,7 +103,7 @@ def get_colissimo_tracking_info(msg): 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("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") From d5f07ec338299b47df9ed43bb1d181235205f4da Mon Sep 17 00:00:00 2001 From: nemunaire Date: Wed, 16 Sep 2015 01:42:33 +0200 Subject: [PATCH 352/674] Display a basic error to IM user on uncatched exception --- nemubot/consumer.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/nemubot/consumer.py b/nemubot/consumer.py index 18e3d47..74b875e 100644 --- a/nemubot/consumer.py +++ b/nemubot/consumer.py @@ -170,10 +170,14 @@ class MessageConsumer: # Run post-treatment: from Response to [ Response ] if self.responses is not None and len(self.responses) > 0: self.post_treat(context.hooks) - except: + except BaseException as e: logger.exception("Error occurred during the processing of the %s: " "%s", type(self.msgs[0]).__name__, self.msgs[0]) + from nemubot.message import Text + self.responses.append(Text("Sorry, an error occured (%s). Feel free to open a new issue at https://github.com/nemunaire/nemubot/issues/new" % type(e).__name__, + server=self.srv.id, to=self.msgs[0].to_response)) + for res in self.responses: to_server = None if isinstance(res, str): From d74a9067c0b528c57eae18c5b0f7b241440ee061 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Thu, 17 Sep 2015 23:06:14 +0200 Subject: [PATCH 353/674] [bonneannee] Command for the nextyear doesn't require argument anymore --- modules/bonneannee.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/bonneannee.py b/modules/bonneannee.py index a00efe0..296bd94 100644 --- a/modules/bonneannee.py +++ b/modules/bonneannee.py @@ -43,9 +43,9 @@ def load(context): @hook("cmd_hook", "newyear") -@hook("cmd_hook", str(yrn), yrn) -def cmd_newyear(msg, yr): - return Response(countdown_format(datetime(yr, 1, 1, 0, 0, 1, 0, +@hook("cmd_hook", str(yrn)) +def cmd_newyear(msg): + return Response(countdown_format(datetime(yrn, 1, 1, 0, 0, 1, 0, timezone.utc), "Il reste %s avant la nouvelle année.", "Nous faisons déjà la fête depuis %s !"), From 8018369800051a889bbaacb65be8ffcab98ba7c4 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 18 Sep 2015 00:23:42 +0100 Subject: [PATCH 354/674] Add global tracking hook --- modules/suivi.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/modules/suivi.py b/modules/suivi.py index 8d4271b..32c39a3 100644 --- a/modules/suivi.py +++ b/modules/suivi.py @@ -77,6 +77,31 @@ def get_laposte_info(laposte_id): return (poste_type.lower(), poste_id.strip(), poste_status.lower(), poste_location, poste_date) +@hook("cmd_hook", "track") +def get_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 Privé: \x02%s\x0F : \x02%s\x0F." % (msg.args[0], info), msg.channel) + + 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) + + 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): From 35ba5c03c9204c94d001faba07f509e77596c120 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Fri, 18 Sep 2015 00:25:14 +0200 Subject: [PATCH 355/674] [more] Allow method chaining --- modules/more.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/modules/more.py b/modules/more.py index 2aeed91..c5b5e49 100644 --- a/modules/more.py +++ b/modules/more.py @@ -30,6 +30,7 @@ logger = logging.getLogger("nemubot.response") class Response: + def __init__(self, message=None, channel=None, nick=None, server=None, nomore="No more message", title=None, more="(suite) ", count=None, shown_first_count=-1, line_treat=None): @@ -48,6 +49,7 @@ class Response: self.nick = nick self.count = count + @property def receivers(self): if self.channel is None: @@ -83,6 +85,8 @@ class Response: self.rawtitle.append(rawtitle) self.rawtitle.pop() self.rawtitle.append(title) + return self + def append_content(self, message): if message is not None and len(message) > 0: @@ -92,11 +96,14 @@ class Response: else: self.messages[len(self.messages)-1] += message self.alone = self.alone and len(self.messages) <= 1 + return self + @property def empty(self): return len(self.messages) <= 0 + @property def title(self): if isinstance(self.rawtitle, list): @@ -104,6 +111,7 @@ class Response: else: return self.rawtitle + @property def text(self): if len(self.messages) < 1: @@ -115,6 +123,7 @@ class Response: else: return msg + def pop(self): self.messages.pop(0) self.elt = 0 @@ -123,9 +132,11 @@ class Response: if len(self.rawtitle) <= 0: self.rawtitle = None + def accept(self, visitor): visitor.visit(self.next_response()) + def next_response(self, maxlen=440): if self.nick: return DirectAsk(self.nick, @@ -135,6 +146,7 @@ class Response: return Text(self.get_message(maxlen), server=None, to=self.receivers) + def __str__(self): ret = [] if len(self.messages): From ecd94576915f979300b37f63afd4331e235aeed1 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sat, 19 Sep 2015 14:52:18 +0200 Subject: [PATCH 356/674] Help: display on the right place, not always to private conversation --- nemubot/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nemubot/bot.py b/nemubot/bot.py index d5510a7..94425c4 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -81,7 +81,7 @@ class Bot(threading.Thread): def _help_msg(msg): """Parse and response to help messages""" from more import Response - res = Response(channel=msg.frm) + res = Response(channel=msg.to_response) if len(msg.args) >= 1: if msg.args[0] in self.modules: if len(msg.args) >= 2: From 3c51b1f03b1b1f7a9672aaa191ef0fc099c2e59e Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sun, 20 Sep 2015 15:09:18 +0200 Subject: [PATCH 357/674] Add assertion on class initialization --- nemubot/hooks/abstract.py | 2 ++ nemubot/hooks/message.py | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/nemubot/hooks/abstract.py b/nemubot/hooks/abstract.py index b0101e4..7e9aa72 100644 --- a/nemubot/hooks/abstract.py +++ b/nemubot/hooks/abstract.py @@ -23,6 +23,8 @@ def call_game(call, *args, **kargs): **kargs -- named arguments """ + assert callable(call) + l = list() d = kargs diff --git a/nemubot/hooks/message.py b/nemubot/hooks/message.py index 0844358..52d9138 100644 --- a/nemubot/hooks/message.py +++ b/nemubot/hooks/message.py @@ -29,7 +29,11 @@ class Message(Abstract): Abstract.__init__(self, call=call, **kargs) - self.name = name + 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 + + self.name = str(name) if name is not None else None self.regexp = regexp self.server = server self.channels = channels From be776405e36e230a18821e293e0087b7bd540938 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Mon, 21 Sep 2015 02:23:15 +0200 Subject: [PATCH 358/674] Dusting URL stacking modules: fixing error when using channel list as response --- modules/reddit.py | 7 ++++--- modules/youtube-title.py | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/modules/reddit.py b/modules/reddit.py index 63bb6eb..53791ff 100644 --- a/modules/reddit.py +++ b/modules/reddit.py @@ -82,9 +82,10 @@ def parseresponse(msg): try: urls = re.findall("www.reddit.com(/\w/\w+/?)", msg.text) for url in urls: - if msg.channel not in LAST_SUBS: - LAST_SUBS[msg.channel] = list() - LAST_SUBS[msg.channel].append(url) + for recv in msg.receivers: + if recv not in LAST_SUBS: + LAST_SUBS[recv] = list() + LAST_SUBS[recv].append(url) except: pass diff --git a/modules/youtube-title.py b/modules/youtube-title.py index 4842d1d..c7bbd9f 100644 --- a/modules/youtube-title.py +++ b/modules/youtube-title.py @@ -89,7 +89,8 @@ def parseresponse(msg): if o.scheme != "": if o.netloc == "" and len(o.path) < 10: continue - if msg.channel not in LAST_URLS: - LAST_URLS[msg.channel] = list() - LAST_URLS[msg.channel].append(url) + for recv in msg.receivers: + if recv not in LAST_URLS: + LAST_URLS[recv] = list() + LAST_URLS[recv].append(url) return msg From 4f7d89a3a13252952f4f5d8ee631fa76acfb250b Mon Sep 17 00:00:00 2001 From: nemunaire Date: Tue, 22 Sep 2015 19:15:46 +0200 Subject: [PATCH 359/674] [ycc] Dusting module, now named tinyurl --- modules/tinyurl.py | 119 +++++++++++++++++++++++++++++++++++++++++++++ modules/ycc.py | 96 ------------------------------------ 2 files changed, 119 insertions(+), 96 deletions(-) create mode 100644 modules/tinyurl.py delete mode 100644 modules/ycc.py diff --git a/modules/tinyurl.py b/modules/tinyurl.py new file mode 100644 index 0000000..8ef5e04 --- /dev/null +++ b/modules/tinyurl.py @@ -0,0 +1,119 @@ +"""URL reducer module""" + +# PYTHON STUFFS ####################################################### + +import re +from urllib.parse import urlparse +from urllib.parse import quote + +from nemubot.exception import IRCException +from nemubot.hooks import hook +from nemubot.message import Text +from nemubot.tools import web + + +# MODULE VARIABLES #################################################### + +PROVIDERS = { + "tinyurl": "http://tinyurl.com/api-create.php?url=", + "ycc": "http://ycc.fr/redirection/create/", +} +DEFAULT_PROVIDER = "ycc" + + +# LOADING ############################################################# + +def load(context): + global DEFAULT_PROVIDER + + if "provider" in context.config: + if context.config["provider"] == "custom": + PROVIDERS["custom"] = context.config["provider_url"] + DEFAULT_PROVIDER = context.config["provider"] + + +# MODULE CORE ######################################################### + +def reduce(url): + """Ask YCC website to reduce given URL + + Argument: + url -- the URL to reduce + """ + + snd_url = PROVIDERS[DEFAULT_PROVIDER] + quote(url, "/:%@&=?") + return web.getURLContent(snd_url) + + +def gen_response(res, msg, srv): + if res is None: + raise IRCException("bad URL : %s" % srv) + else: + return Text("URL for %s: %s" % (srv, res), server=None, + to=msg.to_response) + + +## URL stack + +LAST_URLS = dict() + + +@hook("msg_default") +def parselisten(msg): + parseresponse(msg) + return None + + +@hook("all_post") +def parseresponse(msg): + global LAST_URLS + try: + urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?[^ ]+)", msg.text) + for url in urls: + o = urlparse(url) + if o.scheme != "": + if o.netloc == "ycc.fr" or o.netloc == "tinyurl.com" or ( + o.netloc == "" and len(o.path) < 10): + continue + for recv in msg.receivers: + if recv not in LAST_URLS: + LAST_URLS[recv] = list() + LAST_URLS[recv].append(url) + except: + pass + return msg + + +# MODULE INTERFACE #################################################### + +@hook("cmd_hook", "tinyurl", + help="Reduce any given URL", + help_usage={None: "Reduce the last URL said on the channel", + "URL [URL ...]": "Reduce the given URL(s)"}) +def cmd_reduceurl(msg): + minify = list() + + if not len(msg.args): + global LAST_URLS + 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.") + + if len(msg.args) > 4: + raise IRCException("I cannot reduce as much URL at once.") + else: + minify += msg.args + + res = list() + for url in minify: + o = urlparse(url, "http") + if o.scheme != "": + minief_url = reduce(url) + if o.netloc == "": + res.append(gen_response(minief_url, msg, o.scheme)) + else: + res.append(gen_response(minief_url, msg, o.netloc)) + else: + res.append(gen_response(None, msg, url)) + return res diff --git a/modules/ycc.py b/modules/ycc.py deleted file mode 100644 index afe415b..0000000 --- a/modules/ycc.py +++ /dev/null @@ -1,96 +0,0 @@ -# coding=utf-8 - -"""URL reducer module""" - -import re -from urllib.parse import urlparse -from urllib.parse import quote - -from nemubot.exception import IRCException -from nemubot.hooks import hook -from nemubot.message import Text -from nemubot.tools import web - -nemubotversion = 3.4 - - -def help_full(): - return ("!ycc []: with an argument, reduce the given thanks to " - "ycc.fr; without argument, reduce the last URL said on the current" - " channel.") - -LAST_URLS = dict() - - -def reduce(url): - """Ask YCC website to reduce given URL - - Argument: - url -- the URL to reduce - """ - - snd_url = "http://ycc.fr/redirection/create/" + quote(url, "/:%@&=?") - return web.getURLContent(snd_url) - - -def gen_response(res, msg, srv): - if res is None: - raise IRCException("mauvaise URL : %s" % srv) - else: - return Text("URL pour %s : %s" % (srv, res), server=None, - to=msg.to_response) - - -@hook("cmd_hook", "ycc") -def cmd_ycc(msg): - minify = list() - - if not len(msg.args): - global LAST_URLS - if msg.channel in LAST_URLS and len(LAST_URLS[msg.channel]) > 0: - minify.append(LAST_URLS[msg.channel].pop()) - else: - raise IRCException("je n'ai pas d'autre URL à réduire.") - - if len(msg.args) > 4: - raise IRCException("je ne peux pas réduire autant d'URL d'un seul coup.") - else: - minify += msg.args - - res = list() - for url in minify: - o = urlparse(url, "http") - if o.scheme != "": - minief_url = reduce(url) - if o.netloc == "": - res.append(gen_response(minief_url, msg, o.scheme)) - else: - res.append(gen_response(minief_url, msg, o.netloc)) - else: - res.append(gen_response(None, msg, url)) - return res - - -@hook("msg_default") -def parselisten(msg): - parseresponse(msg) - return None - - -@hook("all_post") -def parseresponse(msg): - global LAST_URLS - try: - urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?[^ ]+)", msg.text) - for url in urls: - o = urlparse(url) - if o.scheme != "": - if o.netloc == "ycc.fr" or (o.netloc == "" and - len(o.path) < 10): - continue - if msg.channel not in LAST_URLS: - LAST_URLS[msg.channel] = list() - LAST_URLS[msg.channel].append(url) - except: - pass - return msg From 4cb8b0f1a6dd5da67414cce50d4ef80f061c9d6f Mon Sep 17 00:00:00 2001 From: nemunaire Date: Wed, 23 Sep 2015 07:50:16 +0200 Subject: [PATCH 360/674] Improve help On hook declaration, we can now add a help and/or a help_usage argument to provide a simple way to the user to be informed. For example: ```python @hook("cmd_hook", "news", help_usage={"URL": "Display the latests news from a given URL"}) def cmd_news(msg): [...] ``` will be displayed on !help !news as: > Usage for command !news from module news: !news URL: Display the latests news from a given URL Or for module commands help: ```python @hook("cmd_hook", "news", help="display latests news") def cmd_news(msg): [...] ``` will be displayed on !help mymodule (assuming this hook is in the module named mymodule) as: > Available commands for module news: news: display latests news Obviously, both `help` and `help_usage` can be present. If `help_usage` doesn't exist, help on usage will display the content of help. --- nemubot/bot.py | 23 +++++++++++++++-------- nemubot/hooks/message.py | 13 ++++++++++++- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/nemubot/bot.py b/nemubot/bot.py index 94425c4..15abba4 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -84,21 +84,28 @@ class Bot(threading.Thread): res = Response(channel=msg.to_response) if len(msg.args) >= 1: if msg.args[0] in self.modules: - if len(msg.args) >= 2: - if hasattr(self.modules[msg.args[0]], "HELP_cmd"): - return self.modules[msg.args[0]].HELP_cmd(msg.args[1]) - else: - res.append_message("No help for command %s in module %s" % (msg.args[1], msg.args[0])) - elif hasattr(self.modules[msg.args[0]], "help_full"): + 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("No help for module %s" % 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] == "!": + for module in self.modules: + for (s, h) in self.modules[module].__nemubot_context__.hooks: + if s == "in_Command" 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)) + 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]) else: - res.append_message("No module named %s" % msg.args[0]) + res.append_message("Sorry, there is no module named %s" % msg.args[0]) else: res.append_message("Pour me demander quelque chose, commencez " "votre message par mon nom ; je réagis " diff --git a/nemubot/hooks/message.py b/nemubot/hooks/message.py index 52d9138..5f092ad 100644 --- a/nemubot/hooks/message.py +++ b/nemubot/hooks/message.py @@ -25,18 +25,29 @@ class Message(Abstract): """Class storing hook information, specialized for a generic Message""" def __init__(self, call, name=None, regexp=None, channels=list(), - server=None, **kargs): + server=None, help=None, help_usage=dict(), **kargs): Abstract.__init__(self, call=call, **kargs) 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 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 + + + 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 "", + ": %s" % self.help if self.help is not None else "" + ) def match(self, msg, server=None): From 7a1ad6430c63c671d99aaa5aaadf0233fe392005 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Thu, 24 Sep 2015 06:09:59 +0200 Subject: [PATCH 361/674] [whois] update module * change intra-bocal URL to fix CertificateError * fix 23.tf trombi URL * add a catch when trying trombi URL --- modules/whois.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/modules/whois.py b/modules/whois.py index 03091a4..32c13ea 100644 --- a/modules/whois.py +++ b/modules/whois.py @@ -49,11 +49,14 @@ 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.net/trombi/%s.jpg", "http://pub.23.tf/p/%s/%%s.jpg" % self.gid ]: + 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 ]: url = url % self.login - _, status, _, _ = headers(url) - if status == 200: - return url + try: + _, status, _, _ = headers(url) + if status == 200: + return url + except: + logger.exception("On URL %s", url) return None From bbf5acafbb03aa15bbe97424eaa1c1f7fa2fa015 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Fri, 25 Sep 2015 07:32:46 +0200 Subject: [PATCH 362/674] [mediawiki] fix OpenSearch: can have empty description --- modules/mediawiki.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/modules/mediawiki.py b/modules/mediawiki.py index 08d2a19..630afdb 100644 --- a/modules/mediawiki.py +++ b/modules/mediawiki.py @@ -70,7 +70,7 @@ def opensearch(site, term, ssl=False): 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(), + itm.getNode("Description").getContent() if itm.hasNode("Description") else "", itm.getNode("Url").getContent()) @@ -163,14 +163,16 @@ def mediawiki_response(site, term, receivers): line_treat=lambda line: irc_format(parse_wikitext(site, line, ns)), channel=receivers) except: - # Try looking at opensearch - os = [x for x, _, _ in opensearch(site, terms[0])] - # Fallback to global search - 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, - title="Article not found, would you mean") + pass + + # Try looking at opensearch + os = [x for x, _, _ in opensearch(site, terms[0])] + # Fallback to global search + 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, + title="Article not found, would you mean") @hook("cmd_hook", "mediawiki") From 471feca8fb7d290dc086edb87292920c0112180f Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sat, 26 Sep 2015 11:53:03 +0200 Subject: [PATCH 363/674] [networking.atom] use Datetime to store internal dates and can get an ordered list of elements --- modules/networking/atom.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/modules/networking/atom.py b/modules/networking/atom.py index 88505e5..5315aba 100644 --- a/modules/networking/atom.py +++ b/modules/networking/atom.py @@ -1,6 +1,7 @@ #!/usr/bin/python3 # coding=utf-8 +import datetime import time from xml.dom.minidom import parse from xml.dom.minidom import parseString @@ -23,6 +24,7 @@ class AtomEntry: except: print(node.getElementsByTagName("updated")[0].firstChild.nodeValue[:10]) self.updated = time.localtime() + self.updated = datetime.datetime(*self.updated[:6]) if len(node.getElementsByTagName("summary")) > 0 and node.getElementsByTagName("summary")[0].firstChild is not None: self.summary = node.getElementsByTagName("summary")[0].firstChild.nodeValue else: @@ -40,6 +42,9 @@ class AtomEntry: else: self.link2 = None + def __repr__(self): + return "" % (self.title, self.updated) + class Atom: @@ -69,6 +74,9 @@ class Atom: differ.append(other.entries[k]) return differ + def get_ordered_entries(self): + entries = self.entries.values() + return sorted(entries, key=lambda e: e.updated, reverse=True) if __name__ == "__main__": content1 = "" From a4f4bb799c77cd8009f6393387016988915ce288 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sun, 27 Sep 2015 15:13:18 +0200 Subject: [PATCH 364/674] Extract atom from networking module to core --- modules/networking/watchWebsite.py | 8 +- .../atom.py => nemubot/tools/feed.py | 106 ++++++++++++------ 2 files changed, 75 insertions(+), 39 deletions(-) rename modules/networking/atom.py => nemubot/tools/feed.py (50%) diff --git a/modules/networking/watchWebsite.py b/modules/networking/watchWebsite.py index d37a19b..0c1f8d3 100644 --- a/modules/networking/watchWebsite.py +++ b/modules/networking/watchWebsite.py @@ -15,7 +15,6 @@ nemubotversion = 3.4 from more import Response -from .atom import Atom from . import page DATAS = None @@ -154,17 +153,18 @@ def alert_change(content, site): return if site["type"] == "atom": + from nemubot.tools.feed import Feed if site["_lastpage"] is None: if site["lastcontent"] is None or site["lastcontent"] == "": site["lastcontent"] = content - site["_lastpage"] = Atom(site["lastcontent"]) + site["_lastpage"] = Feed(site["lastcontent"]) try: - page = Atom(content) + page = Feed(content) except: print("An error occurs during Atom parsing. Restart event...") start_watching(site) return - diff = site["_lastpage"].diff(page) + diff = site["_lastpage"] & page if len(diff) > 0: site["_lastpage"] = page diff.reverse() diff --git a/modules/networking/atom.py b/nemubot/tools/feed.py similarity index 50% rename from modules/networking/atom.py rename to nemubot/tools/feed.py index 5315aba..5dde748 100644 --- a/modules/networking/atom.py +++ b/nemubot/tools/feed.py @@ -45,52 +45,88 @@ class AtomEntry: def __repr__(self): return "" % (self.title, self.updated) + def __cmp__(self, other): + return not (self.id == other.id) -class Atom: + +class RSSEntry: + + def __init__(self, node): + self.id = node.getElementsByTagName("guid")[0].firstChild.nodeValue + if node.getElementsByTagName("title")[0].firstChild is not None: + self.title = node.getElementsByTagName("title")[0].firstChild.nodeValue + else: + self.title = "" + + self.pubDate = node.getElementsByTagName("pubDate")[0].firstChild.nodeValue + + if len(node.getElementsByTagName("description")) > 0 and node.getElementsByTagName("description")[0].firstChild is not None: + self.summary = node.getElementsByTagName("description")[0].firstChild.nodeValue + else: + self.summary = None + if len(node.getElementsByTagName("link")) > 0: + self.link = node.getElementsByTagName("link")[0].getAttribute("href") + else: + self.link = None + + def __repr__(self): + return "" % (self.title, self.pubDate) + + def __cmp__(self, other): + return not (self.id == other.id) + + +class Feed: def __init__(self, string): - self.raw = string self.feed = parseString(string).documentElement + self.id = None + self.title = None + self.updated = None + self.entries = list() + + if self.feed.tagName == "rss": + self._parse_rss_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") + + + def _parse_atom_feed(self): self.id = self.feed.getElementsByTagName("id")[0].firstChild.nodeValue self.title = self.feed.getElementsByTagName("title")[0].firstChild.nodeValue - self.updated = None - self.entries = dict() for item in self.feed.getElementsByTagName("entry"): - entry = AtomEntry(item) - self.entries[entry.id] = entry - if self.updated is None or self.updated < entry.updated: + self._add_entry(AtomEntry(item)) + + + def _parse_rss_feed(self): + self.title = self.feed.getElementsByTagName("title")[0].firstChild.nodeValue + + for item in self.feed.getElementsByTagName("item"): + self._add_entry(RSSEntry(item)) + + + def _add_entry(self, entry): + if entry is not None: + self.entries.append(entry) + if hasattr(entry, "updated") and (self.updated is None or self.updated < entry.updated): self.updated = entry.updated - def __str__(self): - return self.raw - def diff(self, other): - differ = list() - for k in other.entries.keys(): - if self.updated is None and k not in self.entries: - self.updated = other.entries[k].updated - if k not in self.entries and other.entries[k].updated >= self.updated: - differ.append(other.entries[k]) - return differ + def __and__(self, b): + ret = [] - def get_ordered_entries(self): - entries = self.entries.values() - return sorted(entries, key=lambda e: e.updated, reverse=True) + for e in self.entries: + if e not in b.entries: + ret.append(e) -if __name__ == "__main__": - content1 = "" - with open("rss.php.1", "r") as f: - for line in f: - content1 += line - content2 = "" - with open("rss.php", "r") as f: - for line in f: - content2 += line - a = Atom(content1) - print(a.updated) - b = Atom(content2) - print(b.updated) + for e in b.entries: + if e not in self.entries: + ret.append(e) - diff = a.diff(b) - print(diff) + # TODO: Sort by date + + return ret From ee1910806cbfd13eae6417bdf40ad107685f2347 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Mon, 28 Sep 2015 06:53:59 +0200 Subject: [PATCH 365/674] [news] Introduce new module News: it fetchs atom feed from a website and display it --- modules/news.py | 54 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 modules/news.py diff --git a/modules/news.py b/modules/news.py new file mode 100644 index 0000000..862ae85 --- /dev/null +++ b/modules/news.py @@ -0,0 +1,54 @@ +"""Display latests news from a website""" + +# PYTHON STUFFS ####################################################### + +import datetime +import re +from urllib.parse import urljoin + +from bs4 import BeautifulSoup + +from nemubot.exception import IRCException +from nemubot.hooks import hook +from nemubot.tools import web + +from more import Response +from nemubot.tools.feed import Feed, AtomEntry + + +# HELP ################################################################ + +def help_full(): + return "Display the latests news from a given URL: !news URL" + + +# MODULE CORE ######################################################### + +def find_rss_links(url): + soup = BeautifulSoup(web.getURLContent(url)) + for rss in soup.find_all('link', attrs={"type": re.compile("^application/atom")}): + yield urljoin(url, rss["href"]) + +def get_last_news(url): + feed = Feed(web.getURLContent(url)) + return feed.entries + + +# MODULE INTERFACE #################################################### + +@hook("cmd_hook", "news") +def cmd_news(msg): + if not len(msg.args): + raise IRCException("Indicate the URL to visit.") + + url = " ".join(msg.args) + 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) + 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 3cfbfd96b0af9e9f76569edf3d74a1d0a6c67e31 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Wed, 6 May 2015 13:29:26 +0200 Subject: [PATCH 366/674] Let main thread manage consumer threads --- nemubot/bot.py | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/nemubot/bot.py b/nemubot/bot.py index 15abba4..259d368 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -181,6 +181,16 @@ class Bot(threading.Thread): except: logger.exception("Uncatched exception on server read") + # 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() + + # Events methods @@ -313,25 +323,12 @@ 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)) - self._launch_consumers() self._update_event_timer() # Consumers methods - def _launch_consumers(self): - """Launch new consumer threads if necessary""" - - while self.cnsr_queue.qsize() > self.cnsr_thrd_size: - # Next launch if two more items in queue - self.cnsr_thrd_size += 2 - - c = Consumer(self) - self.cnsr_thrd.append(c) - c.start() - - def add_server(self, srv, autoconnect=True): """Add a new server to the context @@ -441,9 +438,6 @@ class Bot(threading.Thread): self.cnsr_queue.put_nowait(MessageConsumer(srv, msg)) - # Launch a new thread if necessary - self._launch_consumers() - def quit(self): """Save and unload modules and disconnect servers""" From dda78df9d2c8d45f1b0088aaab07bce27104394d Mon Sep 17 00:00:00 2001 From: nemunaire Date: Thu, 7 May 2015 06:39:34 +0200 Subject: [PATCH 367/674] Add new action queue, synchronized with main thread for prompt like actions (conf loading, exit, ...) --- nemubot/__main__.py | 3 +-- nemubot/bot.py | 14 +++++++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 5d0ef6a..d1d8f93 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -104,8 +104,7 @@ def main(): # Load requested configuration files for path in args.files: if os.path.isfile(path): - from nemubot.tools.config import load_file - load_file(path, context) + context.sync_queue.put_nowait(["loadconf", path]) else: logger.error("%s is not a readable file", path) diff --git a/nemubot/bot.py b/nemubot/bot.py index 259d368..41f619b 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -125,11 +125,13 @@ class Bot(threading.Thread): return res self.hooks.add_hook(nemubot.hooks.Message(_help_msg, "help"), "in", "Command") - # Messages to be treated from queue import Queue + # Messages to be treated 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): @@ -190,6 +192,16 @@ class Bot(threading.Thread): 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:]: + from nemubot.tools.config import load_file + load_file(path, self) + self.sync_queue.task_done() + # Events methods From ae7526dd96558c0b04db8ba8f587fa6358bdddfe Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sun, 10 May 2015 04:12:30 +0200 Subject: [PATCH 368/674] Fix double exception when invalid file descriptor found in select --- nemubot/bot.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nemubot/bot.py b/nemubot/bot.py index 41f619b..6e6f27c 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -149,17 +149,17 @@ class Bot(threading.Thread): 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: " + 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: " + 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: " + x) + logger.error("Found invalid object in _xlist: " + str(x)) fnd_smth = True if not fnd_smth: logger.exception("Can't continue, sorry") From f66ed07496902b8ad73ba55996142ef6fdd4f1ec Mon Sep 17 00:00:00 2001 From: nemunaire Date: Mon, 11 May 2015 07:41:04 +0200 Subject: [PATCH 369/674] Lock select lists to avoid invalid states (particularly on closing) --- nemubot/bot.py | 6 +++++- nemubot/consumer.py | 2 +- nemubot/server/__init__.py | 3 +++ nemubot/server/abstract.py | 5 ++++- nemubot/server/socket.py | 5 +++++ 5 files changed, 18 insertions(+), 3 deletions(-) diff --git a/nemubot/bot.py b/nemubot/bot.py index 6e6f27c..d2e7c76 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -136,10 +136,11 @@ class Bot(threading.Thread): def run(self): from select import select - from nemubot.server import _rlist, _wlist, _xlist + from nemubot.server import _lock, _rlist, _wlist, _xlist self.stop = False while not self.stop: + _lock.acquire() try: rl, wl, xl = select(_rlist, _wlist, _xlist, 0.1) except: @@ -164,6 +165,7 @@ class Bot(threading.Thread): if not fnd_smth: logger.exception("Can't continue, sorry") self.quit() + _lock.release() continue for x in xl: @@ -183,6 +185,8 @@ class Bot(threading.Thread): except: logger.exception("Uncatched exception on server read") + _lock.release() + # Launch new consumer threads if necessary while self.cnsr_queue.qsize() > self.cnsr_thrd_size: # Next launch if two more items in queue diff --git a/nemubot/consumer.py b/nemubot/consumer.py index 74b875e..b032c24 100644 --- a/nemubot/consumer.py +++ b/nemubot/consumer.py @@ -231,7 +231,7 @@ class Consumer(threading.Thread): def run(self): try: while not self.stop: - stm = self.context.cnsr_queue.get(True, 20) + stm = self.context.cnsr_queue.get(True, 10) stm.run(self.context) self.context.cnsr_queue.task_done() diff --git a/nemubot/server/__init__.py b/nemubot/server/__init__.py index a171d8f..5289697 100644 --- a/nemubot/server/__init__.py +++ b/nemubot/server/__init__.py @@ -16,6 +16,9 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import threading + +_lock = threading.Lock() # Lists for select _rlist = [] diff --git a/nemubot/server/abstract.py b/nemubot/server/abstract.py index dd71ff6..ebfab7e 100644 --- a/nemubot/server/abstract.py +++ b/nemubot/server/abstract.py @@ -20,7 +20,7 @@ import io import logging import queue -from nemubot.server import _rlist, _wlist, _xlist +from nemubot.server import _lock, _rlist, _wlist, _xlist # Extends from IOBase in order to be compatible with select function class AbstractServer(io.IOBase): @@ -71,6 +71,7 @@ class AbstractServer(io.IOBase): """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) + _lock.acquire() if not hasattr(self, "_close") or self._close(): if self in _rlist: _rlist.remove(self) @@ -78,7 +79,9 @@ class AbstractServer(io.IOBase): _wlist.remove(self) if self in _xlist: _xlist.remove(self) + _lock.release() return True + _lock.release() return False diff --git a/nemubot/server/socket.py b/nemubot/server/socket.py index 54a1703..fdd3be9 100644 --- a/nemubot/server/socket.py +++ b/nemubot/server/socket.py @@ -73,14 +73,19 @@ class SocketServer(AbstractServer): def _close(self): import socket + from nemubot.server import _lock + _lock.release() self._sending_queue.join() + _lock.acquire() if self.connected: try: self.socket.shutdown(socket.SHUT_RDWR) self.socket.close() except socket.error: pass + self.socket = None + return True From b66d7d30ed3667e9291024fb9eeed8d2092ec435 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Wed, 2 Sep 2015 07:12:51 +0200 Subject: [PATCH 370/674] Accelerate shutdown --- nemubot/consumer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nemubot/consumer.py b/nemubot/consumer.py index b032c24..535f41c 100644 --- a/nemubot/consumer.py +++ b/nemubot/consumer.py @@ -231,7 +231,7 @@ class Consumer(threading.Thread): def run(self): try: while not self.stop: - stm = self.context.cnsr_queue.get(True, 10) + stm = self.context.cnsr_queue.get(True, 1) stm.run(self.context) self.context.cnsr_queue.task_done() From 283b0d006e9570c981c37be982a202349cce8736 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Tue, 29 Sep 2015 00:54:05 +0200 Subject: [PATCH 371/674] Add a new builtin: !echo --- nemubot/bot.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/nemubot/bot.py b/nemubot/bot.py index d2e7c76..af4d0ce 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -78,6 +78,11 @@ class Bot(threading.Thread): return msg.respond("pong") self.hooks.add_hook(nemubot.hooks.Message(in_ping), "in", "DirectAsk") + def in_echo(msg): + from nemubot.message import Text + return Text(msg.nick + ": " + " ".join(msg.args), to=msg.to_response) + self.hooks.add_hook(nemubot.hooks.Message(in_echo, "echo"), "in", "Command") + def _help_msg(msg): """Parse and response to help messages""" from more import Response From 59aff52ce1f4ddda8328c7d2f178a7b6c0e148d0 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Wed, 30 Sep 2015 22:04:42 +0200 Subject: [PATCH 372/674] Change the behaviour of send_response in module --- nemubot/modulecontext.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/nemubot/modulecontext.py b/nemubot/modulecontext.py index 23ca9e4..ba3a5a1 100644 --- a/nemubot/modulecontext.py +++ b/nemubot/modulecontext.py @@ -83,11 +83,10 @@ class ModuleContext: def send_response(server, res): if server in context.servers: - r = res.next_response() - if r.server is not None: - return context.servers[r.server].send_response(r) + if res.server is not None: + return context.servers[res.server].send_response(res) else: - return context.servers[server].send_response(r) + return context.servers[server].send_response(res) else: module.logger.error("Try to send a message to the unknown server: %s", server) return False From ff605756ffd50d51b35ace2ae2664f515476e87f Mon Sep 17 00:00:00 2001 From: nemunaire Date: Thu, 1 Oct 2015 00:27:45 +0200 Subject: [PATCH 373/674] [news] Add support for RSS feeds and catch ExpatError when trying to parse a bad URL --- modules/news.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/modules/news.py b/modules/news.py index 862ae85..2b4d68e 100644 --- a/modules/news.py +++ b/modules/news.py @@ -26,12 +26,16 @@ def help_full(): def find_rss_links(url): soup = BeautifulSoup(web.getURLContent(url)) - for rss in soup.find_all('link', attrs={"type": re.compile("^application/atom")}): + for rss in soup.find_all('link', attrs={"type": re.compile("^application/(atom|rss)")}): yield urljoin(url, rss["href"]) def get_last_news(url): - feed = Feed(web.getURLContent(url)) - return feed.entries + from xml.parsers.expat import ExpatError + try: + feed = Feed(web.getURLContent(url)) + return feed.entries + except ExpatError: + return [] # MODULE INTERFACE #################################################### From 080ab9a626008fabb4a344ce19a7898699d95575 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Fri, 2 Oct 2015 05:23:58 +0200 Subject: [PATCH 374/674] Fix bad event behaviour: if an event ends in less than 6 seconds, it was executed in the event creator thread (blocking it until the event end) --- nemubot/bot.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/nemubot/bot.py b/nemubot/bot.py index af4d0ce..f349011 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -322,18 +322,14 @@ class Bot(threading.Thread): if self.event_timer is not None: self.event_timer.cancel() - if len(self.events) > 0: + if len(self.events): logger.debug("Update timer: next event in %d seconds", self.events[0].time_left.seconds) - if datetime.now(timezone.utc) + timedelta(seconds=5) >= self.events[0].current: - import time - while datetime.now(timezone.utc) < self.events[0].current: - time.sleep(0.6) - self._end_event_timer() - else: - self.event_timer = threading.Timer( - self.events[0].time_left.seconds + 1, self._end_event_timer) - self.event_timer.start() + 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) + self.event_timer.start() + else: logger.debug("Update timer: no timer left") From 7970fca93a07f962abaa0a0c0cc0854ec6f9864c Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sat, 5 Sep 2015 10:14:10 +0200 Subject: [PATCH 375/674] Use with section for locking threadsafe region (instead of raw calls to acquire/release) --- nemubot/bot.py | 82 +++++++++++++++++++------------------- nemubot/server/abstract.py | 20 +++++----- 2 files changed, 49 insertions(+), 53 deletions(-) diff --git a/nemubot/bot.py b/nemubot/bot.py index f349011..777b931 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -145,52 +145,50 @@ class Bot(threading.Thread): self.stop = False while not self.stop: - _lock.acquire() - 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() - _lock.release() - continue + 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 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(): + for x in xl: try: - self.receive_message(r, i) + x.exception() except: - logger.exception("Uncatched exception on server read") + logger.exception("Uncatched exception on server exception") + for w in wl: + try: + w.write_select() + except: + logger.exception("Uncatched exception on server write") + for r in rl: + for i in r.read(): + try: + self.receive_message(r, i) + except: + logger.exception("Uncatched exception on server read") - _lock.release() # Launch new consumer threads if necessary while self.cnsr_queue.qsize() > self.cnsr_thrd_size: diff --git a/nemubot/server/abstract.py b/nemubot/server/abstract.py index ebfab7e..99d10d5 100644 --- a/nemubot/server/abstract.py +++ b/nemubot/server/abstract.py @@ -71,17 +71,15 @@ class AbstractServer(io.IOBase): """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) - _lock.acquire() - 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) - _lock.release() - return True - _lock.release() + 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 From c812fd8c16b9ae7532a55c79f4d778a64ae77f2d Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sat, 3 Oct 2015 15:32:36 +0200 Subject: [PATCH 376/674] As the char ':' is not valid in URL, don't expect it --- modules/tinyurl.py | 2 +- modules/youtube-title.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/tinyurl.py b/modules/tinyurl.py index 8ef5e04..731737a 100644 --- a/modules/tinyurl.py +++ b/modules/tinyurl.py @@ -68,7 +68,7 @@ def parselisten(msg): def parseresponse(msg): global LAST_URLS try: - urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?[^ ]+)", msg.text) + urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?[^ :]+)", msg.text) for url in urls: o = urlparse(url) if o.scheme != "": diff --git a/modules/youtube-title.py b/modules/youtube-title.py index c7bbd9f..93240b0 100644 --- a/modules/youtube-title.py +++ b/modules/youtube-title.py @@ -83,7 +83,7 @@ def parselisten(msg): def parseresponse(msg): global LAST_URLS if hasattr(msg, "text"): - urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?[^ ]+)", msg.text) + urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?[^ :]+)", msg.text) for url in urls: o = urlparse(url) if o.scheme != "": From 57ba0d5db9b5e3769e68ea45585577b9a32098a8 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sun, 4 Oct 2015 13:12:05 +0200 Subject: [PATCH 377/674] [networking] Fix traceurl: trace all URL even if an error occurs --- modules/networking/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/modules/networking/__init__.py b/modules/networking/__init__.py index 188dbb5..39009f1 100644 --- a/modules/networking/__init__.py +++ b/modules/networking/__init__.py @@ -87,11 +87,14 @@ def cmd_w3m(msg): @hook("cmd_hook", "traceurl") def cmd_traceurl(msg): - if 1 < len(msg.args) < 5: + if 0 < len(msg.args) < 5: res = list() for url in msg.args: - trace = page.traceURL(url) - res.append(Response(trace, channel=msg.channel, title="TraceURL")) + try: + trace = page.traceURL(url) + res.append(Response(trace, channel=msg.channel, title="TraceURL")) + except: + pass return res else: raise IRCException("Indicate an URL to trace!") From 461c62f59688030808a7b61898c0a5fd2a38b467 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 6 Oct 2015 23:23:42 +0100 Subject: [PATCH 378/674] Fix alias ranges Args are now consumed when in ranges, and ranges with 1 bound now work --- modules/alias.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/modules/alias.py b/modules/alias.py index 00ae770..07e39c7 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -130,18 +130,21 @@ def replace_variables(cnt, msg=None): rv = re.match("([0-9]+)(:([0-9]*))?", res) if rv is not None: varI = int(rv.group(1)) - 1 - print(varI, len(msg.args)) if varI > len(msg.args): cnt = cnt.replace("${%s}" % res, "", 1) elif rv.group(2) is not None: - if rv.group(3) is not None: + if rv.group(3) is not None and len(rv.group(3)): varJ = int(rv.group(3)) - 1 cnt = cnt.replace("${%s}" % res, " ".join(msg.args[varI:varJ]), 1) + for v in range(varI, varJ): + unsetCnt.append(v) else: cnt = cnt.replace("${%s}" % res, " ".join(msg.args[varI:]), 1) + for v in range(varI, len(msg.args)): + unsetCnt.append(v) else: cnt = cnt.replace("${%s}" % res, msg.args[varI], 1) - unsetCnt.append(varI) + unsetCnt.append(varI) else: cnt = cnt.replace("${%s}" % res, get_variable(res), 1) for u in sorted(unsetCnt, reverse=True): From e1310516fa5cbc936c5dba46c1cb20172ef055f0 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 7 Oct 2015 00:14:37 +0100 Subject: [PATCH 379/674] [alias] Fix argument consumption, allow multiple usage of same var --- modules/alias.py | 57 ++++++++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/modules/alias.py b/modules/alias.py index 07e39c7..4908c5a 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -114,7 +114,7 @@ def set_variable(name, value, creator): context.save() -def replace_variables(cnt, msg=None): +def replace_variables(cnts, msg=None): """Replace variables contained in the content Arguments: @@ -122,34 +122,39 @@ def replace_variables(cnt, msg=None): msg -- optional message where pick some variables """ - if isinstance(cnt, list): - return [replace_variables(c, msg) for c in cnt] - unsetCnt = list() - for res in re.findall("\\$\{(?P[a-zA-Z0-9:]+)\}", cnt): - rv = re.match("([0-9]+)(:([0-9]*))?", res) - if rv is not None: - varI = int(rv.group(1)) - 1 - if varI > len(msg.args): - cnt = cnt.replace("${%s}" % res, "", 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 - cnt = cnt.replace("${%s}" % res, " ".join(msg.args[varI:varJ]), 1) - for v in range(varI, varJ): - unsetCnt.append(v) + if not isinstance(cnts, list): + cnts = list(cnts) + resultCnt = list() + + for cnt in cnts: + for res in re.findall("\\$\{(?P[a-zA-Z0-9:]+)\}", cnt): + rv = re.match("([0-9]+)(:([0-9]*))?", res) + if rv is not None: + varI = int(rv.group(1)) - 1 + if varI > len(msg.args): + cnt = cnt.replace("${%s}" % res, "", 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 + cnt = cnt.replace("${%s}" % res, " ".join(msg.args[varI:varJ]), 1) + for v in range(varI, varJ): + unsetCnt.append(v) + else: + cnt = cnt.replace("${%s}" % res, " ".join(msg.args[varI:]), 1) + for v in range(varI, len(msg.args)): + unsetCnt.append(v) else: - cnt = cnt.replace("${%s}" % res, " ".join(msg.args[varI:]), 1) - for v in range(varI, len(msg.args)): - unsetCnt.append(v) + cnt = cnt.replace("${%s}" % res, msg.args[varI], 1) + unsetCnt.append(varI) else: - cnt = cnt.replace("${%s}" % res, msg.args[varI], 1) - unsetCnt.append(varI) - else: - cnt = cnt.replace("${%s}" % res, get_variable(res), 1) - for u in sorted(unsetCnt, reverse=True): - msg.args.pop(u) - return cnt + cnt = cnt.replace("${%s}" % res, get_variable(res), 1) + resultCnt.append(cnt) + + for u in sorted(set(unsetCnt), reverse=True): + k = msg.args.pop(u) + + return resultCnt # MODULE INTERFACE #################################################### From a6a10b78d1670e4be27b9493e59505524e0f0c2d Mon Sep 17 00:00:00 2001 From: nemunaire Date: Mon, 5 Oct 2015 23:54:38 +0200 Subject: [PATCH 380/674] Fill help in some modules --- modules/alias.py | 60 +++++++++++++++++++++++------------------- modules/birthday.py | 35 +++++++++++++++--------- modules/bonneannee.py | 22 +++++++++++----- modules/books.py | 30 ++++++++++++++++----- modules/conjugaison.py | 57 ++++++++++++++++++++------------------- modules/ctfs.py | 43 ++++++++++++++++++------------ 6 files changed, 150 insertions(+), 97 deletions(-) diff --git a/modules/alias.py b/modules/alias.py index 4908c5a..35f9f83 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -16,11 +16,6 @@ from nemubot.tools.xmlparser.node import ModuleState from more import Response -# HELP ################################################################ - -def help_full(): - return "Pour créer un alias, adressez-vous à moi en disant quelque chose comme : \"nouvel alias XX : YY\", où YY sera la commande équivalente à XX. Vous pouvez ajouter des variables comme ${1}, ${2}, ... correspondant aux éventuels arguments.\nDe l'aide supplémentaire existe pour les commandes !alias, !listalias, !unalias, !set et !listvars" - # LOADING ############################################################# def load(context): @@ -161,38 +156,47 @@ def replace_variables(cnts, msg=None): ## Variables management -@hook("cmd_hook", "listvars") +@hook("cmd_hook", "listvars", + help="list defined variables for substitution in input commands", + help_usage={ + None: "List all known variables", + "USER": "List variables created by USER"}) def cmd_listvars(msg): if len(msg.args): res = list() for user in msg.args: als = [v["alias"] for v in list_variables(user)] if len(als) > 0: - res.append("Variables créées par %s : %s" % (user, ", ".join(als))) + res.append("%s's variables: %s" % (user, ", ".join(als))) else: - res.append("%s n'a pas encore créé de variable" % user) + res.append("%s didn't create variable yet." % user) return Response(" ; ".join(res), channel=msg.channel) elif len(context.data.getNode("variables").index): - return Response("Variables connues : %s." % + return Response("Known variables: %s." % ", ".join(list_variables()), channel=msg.channel) else: - return Response("No variable are currently stored.", channel=msg.channel) + return Response("There is currently no variable stored.", channel=msg.channel) -@hook("cmd_hook", "set") +@hook("cmd_hook", "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): if len(msg.args) < 2: - raise IRCException("!set prend au minimum deux arguments : " - "le nom de la variable et sa valeur.") + raise IRCException("!set take two args: the key and the value.") set_variable(msg.args[0], " ".join(msg.args[1:]), msg.nick) - return Response("Variable \$%s définie avec succès." % msg.args[0], + return Response("Variable \$%s successfully defined." % msg.args[0], channel=msg.channel) ## Alias management -@hook("cmd_hook", "listalias") +@hook("cmd_hook", "listalias", + help="List registered aliases", + help_usage={ + None: "List all registered aliases", + "USER": "List all aliases created by USER"}) def cmd_listalias(msg): aliases = [a for a in list_alias(None)] + [a for a in list_alias(msg.channel)] if len(aliases): @@ -202,35 +206,37 @@ def cmd_listalias(msg): return Response("There is no alias currently.", channel=msg.channel) -@hook("cmd_hook", "alias") +@hook("cmd_hook", "alias", + help="Display the replacement command for a given alias") def cmd_alias(msg): if not len(msg.args): - raise IRCException("!alias prend en argument l'alias à étendre.") + raise IRCException("!alias takes as argument an alias to extend.") res = list() for alias in msg.args: if alias[0] == "!": alias = alias[1:] if alias in context.data.getNode("aliases").index: - res.append("!%s correspond à %s" % (alias, context.data.getNode("aliases").index[alias]["origin"])) + res.append("!%s correspond to %s" % (alias, context.data.getNode("aliases").index[alias]["origin"])) else: - res.append("!%s n'est pas un alias" % alias) + res.append("!%s is not an alias" % alias) return Response(res, channel=msg.channel, nick=msg.nick) -@hook("cmd_hook", "unalias") +@hook("cmd_hook", "unalias", + help="Remove a previously created alias") def cmd_unalias(msg): if not len(msg.args): - raise IRCException("Quel alias voulez-vous supprimer ?") + raise IRCException("Which alias would you want to remove?") res = list() for alias in msg.args: if alias[0] == "!" and len(alias) > 1: alias = alias[1:] if alias in context.data.getNode("aliases").index: context.data.getNode("aliases").delChild(context.data.getNode("aliases").index[alias]) - res.append(Response("%s a bien été supprimé" % alias, + res.append(Response("%s doesn't exist anymore." % alias, channel=msg.channel)) else: - res.append(Response("%s n'est pas un alias" % alias, + res.append(Response("%s is not an alias" % alias, channel=msg.channel)) return res @@ -260,16 +266,16 @@ def treat_alias(msg): @hook("ask_default") def parseask(msg): - if re.match(".*(set|cr[ée]{2}|nouvel(le)?) alias.*", msg.text) is not None: - result = re.match(".*alias !?([^ ]+) ?(pour|=|:) ?(.+)$", msg.text) + 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("cet alias est déjà défini.") + raise IRCException("this alias is already defined.") else: create_alias(result.group(1), result.group(3), channel=msg.channel, creator=msg.nick) - res = Response("Nouvel alias %s défini avec succès." % + res = Response("New alias %s successfully registered." % result.group(1), channel=msg.channel) return res return None diff --git a/modules/birthday.py b/modules/birthday.py index 372567f..34d2c28 100644 --- a/modules/birthday.py +++ b/modules/birthday.py @@ -1,7 +1,7 @@ -# coding=utf-8 - """People birthdays and ages""" +# PYTHON STUFFS ####################################################### + import re import sys from datetime import date, datetime @@ -13,21 +13,16 @@ from nemubot.tools.countdown import countdown_format from nemubot.tools.date import extractDate from nemubot.tools.xmlparser.node import ModuleState -nemubotversion = 3.4 - from more import Response +# LOADING ############################################################# + def load(context): context.data.setIndex("name", "birthday") -def help_full(): - return ("!anniv /who/: gives the remaining time before the anniversary of " - "/who/\n!age /who/: gives the age of /who/\nIf /who/ is not given," - " gives the remaining time before your anniversary.\n\n To set you" - "r birthday, say it to nemubot :)") - +# MODULE CORE ######################################################### def findName(msg): if (not len(msg.args) or msg.args[0].lower() == "moi" or @@ -47,7 +42,16 @@ def findName(msg): return (matches, name) -@hook("cmd_hook", "anniv") +# MODULE INTERFACE #################################################### + +## Commands + +@hook("cmd_hook", "anniv", + help="gives the remaining time before the anniversary of known people", + help_usage={ + None: "Calculate the time remaining before your birthday", + "WHO": "Calculate the time remaining before WHO's birthday", + }) def cmd_anniv(msg): (matches, name) = findName(msg) if len(matches) == 1: @@ -76,7 +80,12 @@ def cmd_anniv(msg): msg.channel, msg.nick) -@hook("cmd_hook", "age") +@hook("cmd_hook", "age", + help="Calculate age of known people", + help_usage={ + None: "Calculate your age", + "WHO": "Calculate the age of WHO" + }) def cmd_age(msg): (matches, name) = findName(msg) if len(matches) == 1: @@ -93,6 +102,8 @@ def cmd_age(msg): return True +## Input parsing + @hook("ask_default") 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) diff --git a/modules/bonneannee.py b/modules/bonneannee.py index 296bd94..18ba637 100644 --- a/modules/bonneannee.py +++ b/modules/bonneannee.py @@ -1,7 +1,7 @@ -# coding=utf-8 - """Wishes Happy New Year when the time comes""" +# PYTHON STUFFS ####################################################### + from datetime import datetime, timezone from nemubot import context @@ -9,14 +9,17 @@ from nemubot.event import ModuleEvent from nemubot.hooks import hook from nemubot.tools.countdown import countdown_format -nemubotversion = 4.0 - from more import Response + +# GLOBALS ############################################################# + yr = datetime.now(timezone.utc).year yrn = datetime.now(timezone.utc).year + 1 +# LOADING ############################################################# + def load(context): if not context.config or not context.config.hasNode("sayon"): print("You can append in your configuration some balise to " @@ -42,8 +45,12 @@ def load(context): call=bonneannee)) -@hook("cmd_hook", "newyear") -@hook("cmd_hook", str(yrn)) +# MODULE INTERFACE #################################################### + +@hook("cmd_hook", "newyear", + help="Display the remaining time before the next new year") +@hook("cmd_hook", 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, timezone.utc), @@ -52,7 +59,8 @@ def cmd_newyear(msg): channel=msg.channel) -@hook("cmd_rgxp", data=yrn, regexp="^[0-9]{4}$") +@hook("cmd_rgxp", 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 c8c6817..260267e 100644 --- a/modules/books.py +++ b/modules/books.py @@ -1,7 +1,7 @@ -# coding=utf-8 - """Looking for books""" +# PYTHON STUFFS ####################################################### + import urllib 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 ############################################################# + def load(context): if not context.config or not context.config.getAttribute("goodreadskey"): raise ImportError("You need a Goodreads API key in order to use this " @@ -22,6 +22,8 @@ def load(context): "Get one at https://www.goodreads.com/api/keys") +# MODULE CORE ######################################################### + def get_book(title): """Retrieve a book from its title""" response = web.getXML("https://www.goodreads.com/book/title.xml?key=%s&title=%s" % @@ -54,7 +56,13 @@ def search_author(name): return None -@hook("cmd_hook", "book") +# MODULE INTERFACE #################################################### + +@hook("cmd_hook", "book", + help="Get information about a book from its title", + help_usage={ + "TITLE": "Get information about a book titled TITLE" + }) def cmd_book(msg): if not len(msg.args): raise IRCException("please give me a title to search") @@ -69,7 +77,11 @@ def cmd_book(msg): return res -@hook("cmd_hook", "search_books") +@hook("cmd_hook", "search_books", + help="Search book's title", + help_usage={ + "APPROX_TITLE": "Search for a book approximately titled APPROX_TITLE" + }) def cmd_books(msg): if not len(msg.args): raise IRCException("please give me a title to search") @@ -85,7 +97,11 @@ def cmd_books(msg): return res -@hook("cmd_hook", "author_books") +@hook("cmd_hook", "author_books", + help="Looking for books writen by a given author", + help_usage={ + "AUTHOR": "Looking for books writen by AUTHOR" + }) def cmd_author(msg): if not len(msg.args): raise IRCException("please give me an author to search") diff --git a/modules/conjugaison.py b/modules/conjugaison.py index 7ef1758..fdde315 100644 --- a/modules/conjugaison.py +++ b/modules/conjugaison.py @@ -1,7 +1,7 @@ -# coding=utf-8 - """Find french conjugaison""" +# PYTHON STUFFS ####################################################### + from collections import defaultdict import re from urllib.parse import quote @@ -11,10 +11,11 @@ from nemubot.hooks import hook from nemubot.tools import web from nemubot.tools.web import striphtml -nemubotversion = 3.4 - from more import Response + +# GLOBALS ############################################################# + s = [('present', '0'), ('présent', '0'), ('pr', '0'), ('passé simple', '12'), ('passe simple', '12'), ('ps', '12'), ('passé antérieur', '112'), ('passe anterieur', '112'), ('pa', '112'), @@ -32,29 +33,7 @@ for k, v in s: d[k].append(v) -def help_full(): - return ("!conjugaison : give the conjugaison for in " - ".") - - -@hook("cmd_hook", "conjugaison") -def cmd_conjug(msg): - if len(msg.args) < 2: - raise IRCException("donne moi un temps et un verbe, et je te donnerai " - "sa conjugaison!") - - tens = ' '.join(msg.args[:-1]) - - verb = msg.args[-1] - - conjug = get_conjug(verb, tens) - - if len(conjug) > 0: - return Response(conjug, channel=msg.channel, - title="Conjugaison de %s" % verb) - else: - raise IRCException("aucune conjugaison de '%s' n'a été trouvé" % verb) - +# MODULE CORE ######################################################### def get_conjug(verb, stringTens): url = ("http://leconjugueur.lefigaro.fr/conjugaison/verbe/%s.html" % @@ -89,3 +68,27 @@ def compute_line(line, stringTens): .replace("", "\x02") .replace("", "\x0F"))) return res + + +# MODULE INTERFACE #################################################### + +@hook("cmd_hook", "conjugaison", + help_usage={ + "TENS VERB": "give the conjugaison for VERB in TENS." + }) +def cmd_conjug(msg): + if len(msg.args) < 2: + raise IRCException("donne moi un temps et un verbe, et je te donnerai " + "sa conjugaison!") + + tens = ' '.join(msg.args[:-1]) + + verb = msg.args[-1] + + conjug = get_conjug(verb, tens) + + if len(conjug) > 0: + return Response(conjug, channel=msg.channel, + title="Conjugaison de %s" % verb) + else: + raise IRCException("aucune conjugaison de '%s' n'a été trouvé" % verb) diff --git a/modules/ctfs.py b/modules/ctfs.py index 819bfb4..3e02ae9 100644 --- a/modules/ctfs.py +++ b/modules/ctfs.py @@ -1,25 +1,34 @@ +"""List upcoming CTFs""" + +# PYTHON STUFFS ####################################################### + from bs4 import BeautifulSoup from nemubot.hooks import hook -from nemubot.tools.web import getURLContent +from nemubot.tools.web import getURLContent, striphtml from more import Response -"""List upcoming CTFs""" -nemubotversion = 4.0 +# GLOBALS ############################################################# -@hook("cmd_hook", "ctfs") +URL = 'https://ctftime.org/event/list/upcoming' + + +# MODULE INTERFACE #################################################### + +@hook("cmd_hook", "ctfs", + help="Display the upcoming CTFs") def get_info_yt(msg): - soup = BeautifulSoup(getURLContent('https://ctftime.org/event/list/upcoming')) - res = Response(channel=msg.channel, nomore="No more upcoming CTF") - 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([x.text.replace("\n", " ").strip() for x in n])) - except: - import sys - import traceback - exc_type, exc_value, _ = sys.exc_info() - sys.stderr.write(traceback.format_exception_only(exc_type, exc_value)[0]) - return res + soup = BeautifulSoup(getURLContent(URL)) + + res = Response(channel=msg.channel, nomore="No more upcoming CTF") + + 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 + return res From ff2911dbd37a6ceebfc5e351ab9c8995598e2b69 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Tue, 6 Oct 2015 18:36:32 +0200 Subject: [PATCH 381/674] Add a subtreat method in modulecontext This feature allows module to call the message treatment process on a crafted message --- nemubot/modulecontext.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/nemubot/modulecontext.py b/nemubot/modulecontext.py index ba3a5a1..479b11c 100644 --- a/nemubot/modulecontext.py +++ b/nemubot/modulecontext.py @@ -76,6 +76,8 @@ class ModuleContext: yield i else: yield res + 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): @@ -104,6 +106,8 @@ class ModuleContext: def call_hook(store, msg): # TODO: what can we do here? return None + def subtreat(msg): + return None def add_event(evt, eid=None): return context.add_event(evt, eid, module_src=module) def del_event(evt): @@ -122,6 +126,7 @@ class ModuleContext: self.save = save self.send_response = send_response self.call_hook = call_hook + self.subtreat = subtreat def unload(self): From 684806baaf0538a6a43fda9bf2fec36026f5a222 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Wed, 7 Oct 2015 07:50:19 +0200 Subject: [PATCH 382/674] Help command: skip discovery of command without name --- nemubot/bot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nemubot/bot.py b/nemubot/bot.py index 777b931..345b298 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -100,9 +100,9 @@ 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.is_matching(msg.args[0][1:]): + 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 c55e66dd704e01aaef09d3a8feea76b43609e4c4 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 8 Oct 2015 02:47:42 +0100 Subject: [PATCH 383/674] [tools/web] Add header param to getContentUrl() Add the possibility to specify headers when querying websites. --- nemubot/tools/web.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/nemubot/tools/web.py b/nemubot/tools/web.py index 8955d5d..b1d146a 100644 --- a/nemubot/tools/web.py +++ b/nemubot/tools/web.py @@ -60,7 +60,7 @@ def getPassword(url): # Get real pages -def getURLContent(url, body=None, timeout=7): +def getURLContent(url, body=None, timeout=7, header=None): """Return page content corresponding to URL or None if any error occurs Arguments: @@ -97,19 +97,24 @@ def getURLContent(url, body=None, timeout=7): else: raise IRCException("Invalid URL") + from nemubot import __version__ + if header is None: + header = {"User-agent": "Nemubot v%s" % __version__} + elif "User-agent" not in header: + header["User-agent"] = "Nemubot v%s" % __version__ + import socket try: - from nemubot import __version__ if o.query != '': conn.request("GET" if body is None else "POST", o.path + "?" + o.query, body, - {"User-agent": "Nemubot v%s" % __version__}) + header) else: conn.request("GET" if body is None else "POST", o.path, body, - {"User-agent": "Nemubot v%s" % __version__}) + header) except OSError as e: raise IRCException(e.strerror) From ece42ffe47e985dbdd5ae7bd4aaa2b77548733c2 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 9 Oct 2015 01:47:42 +0100 Subject: [PATCH 384/674] [urlshortner] Add framalink support --- modules/{tinyurl.py => framalink.py} | 31 +++++++++++++++++++--------- 1 file changed, 21 insertions(+), 10 deletions(-) rename modules/{tinyurl.py => framalink.py} (77%) diff --git a/modules/tinyurl.py b/modules/framalink.py similarity index 77% rename from modules/tinyurl.py rename to modules/framalink.py index 731737a..d18f78d 100644 --- a/modules/tinyurl.py +++ b/modules/framalink.py @@ -3,6 +3,7 @@ # PYTHON STUFFS ####################################################### import re +import json from urllib.parse import urlparse from urllib.parse import quote @@ -12,13 +13,26 @@ from nemubot.message import Text from nemubot.tools import web +# MODULE FUCNTIONS #################################################### + +def default_reducer(url, data): + snd_url = url + quote(data, "/:%@&=?") + return web.getURLContent(snd_url) + +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'] + # MODULE VARIABLES #################################################### PROVIDERS = { - "tinyurl": "http://tinyurl.com/api-create.php?url=", - "ycc": "http://ycc.fr/redirection/create/", + "tinyurl": (default_reducer, "http://tinyurl.com/api-create.php?url="), + "ycc": (default_reducer, "http://ycc.fr/redirection/create/"), + "framalink": (framalink_reducer, "https://frama.link/a?format=json") } -DEFAULT_PROVIDER = "ycc" +DEFAULT_PROVIDER = "framalink" # LOADING ############################################################# @@ -35,15 +49,12 @@ def load(context): # MODULE CORE ######################################################### def reduce(url): - """Ask YCC website to reduce given URL + """Ask the url shortner website to reduce given URL Argument: url -- the URL to reduce """ - - snd_url = PROVIDERS[DEFAULT_PROVIDER] + quote(url, "/:%@&=?") - return web.getURLContent(snd_url) - + return PROVIDERS[DEFAULT_PROVIDER][0](PROVIDERS[DEFAULT_PROVIDER][1], url) def gen_response(res, msg, srv): if res is None: @@ -72,7 +83,7 @@ def parseresponse(msg): for url in urls: o = urlparse(url) if o.scheme != "": - if o.netloc == "ycc.fr" or o.netloc == "tinyurl.com" or ( + if o.netloc == "ycc.fr" or o.netloc == "tinyurl.com" or o.netloc == "frama.link" or ( o.netloc == "" and len(o.path) < 10): continue for recv in msg.receivers: @@ -86,7 +97,7 @@ def parseresponse(msg): # MODULE INTERFACE #################################################### -@hook("cmd_hook", "tinyurl", +@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)"}) From db709745049fa4410855d09e6f182219a57570f3 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Thu, 8 Oct 2015 23:59:24 +0200 Subject: [PATCH 385/674] [alias] Fix old issues --- modules/alias.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/alias.py b/modules/alias.py index 35f9f83..22afb95 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -165,7 +165,7 @@ def cmd_listvars(msg): if len(msg.args): res = list() for user in msg.args: - als = [v["alias"] for v in list_variables(user)] + als = [v["name"] for v in list_variables(user)] if len(als) > 0: res.append("%s's variables: %s" % (user, ", ".join(als))) else: @@ -186,7 +186,7 @@ def cmd_set(msg): if len(msg.args) < 2: raise IRCException("!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], + return Response("Variable $%s successfully defined." % msg.args[0], channel=msg.channel) From fd8567c60c93da24c7b35c073efbdb44735963da Mon Sep 17 00:00:00 2001 From: nemunaire Date: Fri, 9 Oct 2015 00:24:44 +0200 Subject: [PATCH 386/674] Fix module unload and reload --- nemubot/bot.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/nemubot/bot.py b/nemubot/bot.py index 345b298..5e8b832 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -19,6 +19,7 @@ from datetime import datetime, timedelta, timezone import logging import threading +import sys from nemubot import __version__ from nemubot.consumer import Consumer, EventConsumer, MessageConsumer @@ -372,12 +373,9 @@ class Bot(threading.Thread): """ if name in self.modules: - import imp self.unload_module(name) - tt = __import__(name) - imp.reload(tt) - else: - __import__(name) + + __import__(name) def add_module(self, module): @@ -433,12 +431,24 @@ class Bot(threading.Thread): """Unload a module""" if name in self.modules: self.modules[name].print("Unloading module %s" % name) + + # Call the user defined unload method if hasattr(self.modules[name], "unload"): self.modules[name].unload(self) self.modules[name].__nemubot_context__.unload() - # Remove from the dict + + # Remove from the nemubot dict del self.modules[name] + + # Remove from the Python dict + del sys.modules[name] + for mod in [i for i in sys.modules]: + if mod[:len(name) + 1] == name + ".": + logger.debug("Module '%s' also removed from system modules list.", mod) + del sys.modules[mod] + logger.info("Module `%s' successfully unloaded.", name) + return True return False From 04dcf07fb24fd0fa3f1ad6b87d504f43a8424368 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sat, 10 Oct 2015 15:55:51 +0200 Subject: [PATCH 387/674] tools/web: use standard unescape instead of custom function when available --- nemubot/tools/web.py | 50 +++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/nemubot/tools/web.py b/nemubot/tools/web.py index b1d146a..35e9e7b 100644 --- a/nemubot/tools/web.py +++ b/nemubot/tools/web.py @@ -192,20 +192,6 @@ def getJSON(url, timeout=15): # Other utils -def htmlentitydecode(s): - """Decode htmlentities - - Argument: - s -- The string to decode - """ - - import re - from html.entities import name2codepoint - - return re.sub('&(%s);' % '|'.join(name2codepoint), - lambda m: chr(name2codepoint[m.group(1)]), s) - - def striphtml(data): """Remove HTML tags from text @@ -213,15 +199,35 @@ def striphtml(data): data -- the string to strip """ - if data is None or (not isinstance(data, str) and not isinstance(data, buffer)): + if not isinstance(data, str) and not isinstance(data, buffer): return data + try: + from html import unescape + except ImportError: + def _replace_charref(s): + s = s.group(1) + + if s[0] == '#': + if s[1] in 'xX': + return chr(int(s[2:], 16)) + else: + return chr(int(s[2:])) + else: + from html.entities import name2codepoint + return chr(name2codepoint[s]) + + # unescape exists from Python 3.4 + def unescape(s): + if '&' not in s: + return s + + import re + + return re.sub('&([^;]+);', _replace_charref, s) + + import re - p = re.compile(r'<.*?>') - r, _ = re.subn(r' +', ' ', htmlentitydecode(p.sub('', data) - .replace("(", "/(") - .replace(")", ")/") - .replace("'", "´") - .replace(""", "\"")) - .replace('\n', ' ')) + r, _ = re.subn(r' +', ' ', + unescape(re.sub(r'<.*?>', '', data)).replace('\n', ' ')) return r From 7102e0800089d165a5905d4fcf33e6364fe64114 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sat, 10 Oct 2015 23:11:50 +0200 Subject: [PATCH 388/674] tools/feed: hardened parser --- nemubot/tools/feed.py | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/nemubot/tools/feed.py b/nemubot/tools/feed.py index 5dde748..0e1f313 100644 --- a/nemubot/tools/feed.py +++ b/nemubot/tools/feed.py @@ -11,11 +11,16 @@ from xml.dom.minidom import getDOMImplementation class AtomEntry: def __init__(self, node): - self.id = node.getElementsByTagName("id")[0].firstChild.nodeValue - if node.getElementsByTagName("title")[0].firstChild is not None: + if len(node.getElementsByTagName("id")) > 0 and node.getElementsByTagName("id")[0].firstChild is not None: + self.id = node.getElementsByTagName("id")[0].firstChild.nodeValue + else: + self.id = None + + if len(node.getElementsByTagName("title")) > 0 and node.getElementsByTagName("title")[0].firstChild is not None: self.title = node.getElementsByTagName("title")[0].firstChild.nodeValue else: self.title = "" + try: self.updated = time.strptime(node.getElementsByTagName("updated")[0].firstChild.nodeValue[:19], "%Y-%m-%dT%H:%M:%S") except: @@ -25,26 +30,32 @@ class AtomEntry: print(node.getElementsByTagName("updated")[0].firstChild.nodeValue[:10]) self.updated = time.localtime() self.updated = datetime.datetime(*self.updated[:6]) + if len(node.getElementsByTagName("summary")) > 0 and node.getElementsByTagName("summary")[0].firstChild is not None: self.summary = node.getElementsByTagName("summary")[0].firstChild.nodeValue else: self.summary = None - if len(node.getElementsByTagName("link")) > 0: + + if len(node.getElementsByTagName("link")) > 0 and node.getElementsByTagName("link")[0].hasAttribute("href"): self.link = node.getElementsByTagName("link")[0].getAttribute("href") else: self.link = None - if len(node.getElementsByTagName("category")) >= 1: + + if len(node.getElementsByTagName("category")) >= 1 and node.getElementsByTagName("category")[0].hasAttribute("term"): self.category = node.getElementsByTagName("category")[0].getAttribute("term") else: self.category = None - if len(node.getElementsByTagName("link")) > 1: + + if len(node.getElementsByTagName("link")) > 1 and node.getElementsByTagName("link")[1].hasAttribute("href"): self.link2 = node.getElementsByTagName("link")[1].getAttribute("href") else: self.link2 = None + def __repr__(self): return "" % (self.title, self.updated) + def __cmp__(self, other): return not (self.id == other.id) @@ -52,26 +63,36 @@ class AtomEntry: class RSSEntry: def __init__(self, node): - self.id = node.getElementsByTagName("guid")[0].firstChild.nodeValue - if node.getElementsByTagName("title")[0].firstChild is not None: + if len(node.getElementsByTagName("guid")) > 0 and node.getElementsByTagName("guid")[0].firstChild is not None: + self.id = node.getElementsByTagName("guid")[0].firstChild.nodeValue + else: + self.id = None + + if len(node.getElementsByTagName("title")) > 0 and node.getElementsByTagName("title")[0].firstChild is not None: self.title = node.getElementsByTagName("title")[0].firstChild.nodeValue else: self.title = "" - self.pubDate = node.getElementsByTagName("pubDate")[0].firstChild.nodeValue + if len(node.getElementsByTagName("pubDate")) > 0 and node.getElementsByTagName("pubDate")[0].firstChild is not None: + self.pubDate = node.getElementsByTagName("pubDate")[0].firstChild.nodeValue + else: + self.pubDate = "" if len(node.getElementsByTagName("description")) > 0 and node.getElementsByTagName("description")[0].firstChild is not None: self.summary = node.getElementsByTagName("description")[0].firstChild.nodeValue else: self.summary = None - if len(node.getElementsByTagName("link")) > 0: + + if len(node.getElementsByTagName("link")) > 0 and node.getElementsByTagName("link")[0].hasAttribute("href"): self.link = node.getElementsByTagName("link")[0].getAttribute("href") else: self.link = None + def __repr__(self): return "" % (self.title, self.pubDate) + def __cmp__(self, other): return not (self.id == other.id) From 20105e7d988e88f76f781066fb80858a0739df63 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sun, 11 Oct 2015 17:04:21 +0200 Subject: [PATCH 389/674] tools/web: add a URL normalizer function --- modules/networking/isup.py | 2 -- modules/networking/w3c.py | 4 +--- nemubot/tools/web.py | 25 ++++++++++++++----------- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/modules/networking/isup.py b/modules/networking/isup.py index 3b68a7f..a666db1 100644 --- a/modules/networking/isup.py +++ b/modules/networking/isup.py @@ -10,8 +10,6 @@ def isup(url): """ o = urllib.parse.urlparse(url, "http") - if o.netloc == "": - o = urllib.parse.urlparse("http://" + url) if o.netloc != "": isup = getJSON("http://isitup.org/%s.json" % o.netloc) if isup is not None and "status_code" in isup and isup["status_code"] == 1: diff --git a/modules/networking/w3c.py b/modules/networking/w3c.py index d9de178..c8331c6 100644 --- a/modules/networking/w3c.py +++ b/modules/networking/w3c.py @@ -13,9 +13,7 @@ def validator(url): o = urllib.parse.urlparse(url, "http") if o.netloc == "": - o = urllib.parse.urlparse("http://" + url) - if o.netloc == "": - raise IRCException("Indiquer une URL valide !") + raise IRCException("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__}) diff --git a/nemubot/tools/web.py b/nemubot/tools/web.py index 35e9e7b..a730a9e 100644 --- a/nemubot/tools/web.py +++ b/nemubot/tools/web.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from urllib.parse import urlparse +from urllib.parse import urlparse, urlsplit, urlunsplit from nemubot.exception import IRCException @@ -24,38 +24,43 @@ from nemubot.exception import IRCException def isURL(url): """Return True if the URL can be parsed""" o = urlparse(url) - return o.scheme == "" and o.netloc == "" and o.path == "" + return o.netloc == "" and o.path == "" + + +def getNormalizedURL(url): + """Return a normalized form for the given URL""" + return urlunsplit(urlsplit(url, "http")) def getScheme(url): """Return the protocol of a given URL""" - o = urlparse(url) + o = urlparse(url, "http") return o.scheme def getHost(url): """Return the domain of a given URL""" - return urlparse(url).hostname + return urlparse(url, "http").hostname def getPort(url): """Return the port of a given URL""" - return urlparse(url).port + return urlparse(url, "http").port def getPath(url): """Return the page request of a given URL""" - return urlparse(url).path + return urlparse(url, "http").path def getUser(url): """Return the page request of a given URL""" - return urlparse(url).username + return urlparse(url, "http").username def getPassword(url): """Return the page request of a given URL""" - return urlparse(url).password + return urlparse(url, "http").password # Get real pages @@ -69,9 +74,7 @@ def getURLContent(url, body=None, timeout=7, header=None): timeout -- maximum number of seconds to wait before returning an exception """ - o = urlparse(url) - if o.netloc == "": - o = urlparse("http://" + url) + o = urlparse(url, "http") import http.client From eb95480c8f90ee3a34978e71bfbf1674901339d1 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sun, 11 Oct 2015 17:08:30 +0200 Subject: [PATCH 390/674] [news] normalize URL before performing a join --- modules/news.py | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/news.py b/modules/news.py index 2b4d68e..7aa323f 100644 --- a/modules/news.py +++ b/modules/news.py @@ -25,6 +25,7 @@ def help_full(): # MODULE CORE ######################################################### def find_rss_links(url): + url = web.getNormalizedURL(url) soup = BeautifulSoup(web.getURLContent(url)) for rss in soup.find_all('link', attrs={"type": re.compile("^application/(atom|rss)")}): yield urljoin(url, rss["href"]) From 2cd8f70cdc344c9f7b05e002d12f48c6f0e5036c Mon Sep 17 00:00:00 2001 From: nemunaire Date: Mon, 12 Oct 2015 06:28:52 +0200 Subject: [PATCH 391/674] [networking] Dusting module --- modules/networking/__init__.py | 116 ++++++++++++++++++++------------- modules/networking/page.py | 2 +- modules/networking/whois.py | 4 +- 3 files changed, 73 insertions(+), 49 deletions(-) diff --git a/modules/networking/__init__.py b/modules/networking/__init__.py index 39009f1..9688830 100644 --- a/modules/networking/__init__.py +++ b/modules/networking/__init__.py @@ -1,16 +1,13 @@ -# coding=utf-8 - """Various network tools (w3m, w3c validator, curl, traceurl, ...)""" +# PYTHON STUFFS ####################################################### + import logging import re from nemubot.exception import IRCException from nemubot.hooks import hook -logger = logging.getLogger("nemubot.module.networking") -nemubotversion = 3.4 - from more import Response from . import isup @@ -19,6 +16,11 @@ from . import w3c from . import watchWebsite from . import whois +logger = logging.getLogger("nemubot.module.networking") + + +# LOADING ############################################################# + def load(context): for mod in [isup, page, w3c, watchWebsite, whois]: mod.add_event = context.add_event @@ -34,11 +36,11 @@ def load(context): logger.exception("Unable to load netwhois module") -def help_full(): - return "!traceurl /url/: Follow redirections from /url/." +# MODULE INTERFACE #################################################### - -@hook("cmd_hook", "title") +@hook("cmd_hook", "title", + help="Retrieve webpage's title", + 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.") @@ -52,7 +54,9 @@ def cmd_title(msg): return Response("%s: %s" % (url, res.group(1).replace("\n", " ")), channel=msg.channel) -@hook("cmd_hook", "curly") +@hook("cmd_hook", "curly", + help="Retrieve webpage's headers", + 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.") @@ -63,7 +67,9 @@ 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("cmd_hook", "curl", + help="Retrieve webpage's body", + 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.") @@ -74,48 +80,55 @@ def cmd_curl(msg): return res -@hook("cmd_hook", "w3m") +@hook("cmd_hook", "w3m", + help="Retrieve and format webpage's content", + help_usage={"URL": "Display and format HTTP content of the given URL"}) def cmd_w3m(msg): - if len(msg.args): - res = Response(channel=msg.channel) - for line in page.render(" ".join(msg.args)).split("\n"): - res.append_message(line) - return res - else: + if not len(msg.args): raise IRCException("Indicate the URL to visit.") + res = Response(channel=msg.channel) + for line in page.render(" ".join(msg.args)).split("\n"): + res.append_message(line) + return res -@hook("cmd_hook", "traceurl") +@hook("cmd_hook", "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): - if 0 < len(msg.args) < 5: - res = list() - for url in msg.args: - try: - trace = page.traceURL(url) - res.append(Response(trace, channel=msg.channel, title="TraceURL")) - except: - pass - return res - else: + if not len(msg.args): raise IRCException("Indicate an URL to trace!") + res = list() + for url in msg.args[:4]: + try: + trace = page.traceURL(url) + res.append(Response(trace, channel=msg.channel, title="TraceURL")) + except: + pass + return res -@hook("cmd_hook", "isup") + +@hook("cmd_hook", "isup", + help="Check if a website is up", + help_usage={"DOMAIN": "Check if a DOMAIN is up"}) def cmd_isup(msg): - if 1 < len(msg.args) < 5: - res = list() - for url in msg.args: - rep = isup.isup(url) - if rep: - res.append(Response("%s is up (response time: %ss)" % (url, rep), channel=msg.channel)) - else: - res.append(Response("%s is down" % (url), channel=msg.channel)) - return res - else: - return Response("Indicate an URL to check!", channel=msg.channel) + if not len(msg.args): + raise IRCException("Indicate an domain name to check!") + + res = list() + for url in msg.args[:4]: + rep = isup.isup(url) + if rep: + res.append(Response("%s is up (response time: %ss)" % (url, rep), channel=msg.channel)) + else: + res.append(Response("%s is down" % (url), channel=msg.channel)) + return res -@hook("cmd_hook", "w3c") +@hook("cmd_hook", "w3c", + help="Perform a w3c HTML validator check", + 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!") @@ -136,8 +149,12 @@ def cmd_w3c(msg): -@hook("cmd_hook", "watch", data="diff") -@hook("cmd_hook", "updown", data="updown") +@hook("cmd_hook", "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", + 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"): if not len(msg.args): raise IRCException("indicate an URL to watch!") @@ -145,7 +162,9 @@ 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("cmd_hook", "listwatch", + help="List URL watched for the channel", + help_usage={None: "List URL watched for the channel"}) def cmd_listwatch(msg): wl = watchWebsite.watchedon(msg.channel) if len(wl): @@ -154,9 +173,12 @@ 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("cmd_hook", "unwatch", + help="Unwatch a previously watched URL", + help_usage={"URL": "Unwatch the given URL"}) def cmd_unwatch(msg): if not len(msg.args): raise IRCException("which URL should I stop watching?") - return watchWebsite.del_site(msg.args[0], msg.frm, msg.channel, msg.frm_owner) + 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 ba1c626..67ec8c7 100644 --- a/modules/networking/page.py +++ b/modules/networking/page.py @@ -66,7 +66,7 @@ def fetch(url, onNone=_onNoneDefault): if req is not None: return req else: - if onNone is not None: + if callable(onNone): return onNone() else: return None diff --git a/modules/networking/whois.py b/modules/networking/whois.py index 781dfdc..469194d 100644 --- a/modules/networking/whois.py +++ b/modules/networking/whois.py @@ -21,7 +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")) + 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"})) def extractdate(str): From aa4050f6cd537fc33fb7c2786ff0de39df24d116 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Mon, 12 Oct 2015 06:52:44 +0200 Subject: [PATCH 392/674] [framalink] some refactor --- modules/framalink.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/modules/framalink.py b/modules/framalink.py index d18f78d..a556563 100644 --- a/modules/framalink.py +++ b/modules/framalink.py @@ -34,6 +34,7 @@ PROVIDERS = { } DEFAULT_PROVIDER = "framalink" +PROVIDERS_NETLOC = [urlparse(url, "http").netloc for f, url in PROVIDERS.values()] # LOADING ############################################################# @@ -81,15 +82,16 @@ def parseresponse(msg): try: urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?[^ :]+)", msg.text) for url in urls: - o = urlparse(url) - if o.scheme != "": - if o.netloc == "ycc.fr" or o.netloc == "tinyurl.com" or o.netloc == "frama.link" or ( - o.netloc == "" and len(o.path) < 10): - continue - for recv in msg.receivers: - if recv not in LAST_URLS: - LAST_URLS[recv] = list() - LAST_URLS[recv].append(url) + o = urlparse(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.receivers: + if recv not in LAST_URLS: + LAST_URLS[recv] = list() + LAST_URLS[recv].append(url) except: pass return msg @@ -119,12 +121,9 @@ def cmd_reduceurl(msg): res = list() for url in minify: o = urlparse(url, "http") - if o.scheme != "": - minief_url = reduce(url) - if o.netloc == "": - res.append(gen_response(minief_url, msg, o.scheme)) - else: - res.append(gen_response(minief_url, msg, o.netloc)) + minief_url = reduce(url) + if o.netloc == "": + res.append(gen_response(minief_url, msg, o.scheme)) else: - res.append(gen_response(None, msg, url)) + res.append(gen_response(minief_url, msg, o.netloc)) return res From 68e357d037b0d9125a702d33e450d9105d52d75c Mon Sep 17 00:00:00 2001 From: nemunaire Date: Tue, 13 Oct 2015 07:14:47 +0200 Subject: [PATCH 393/674] Initialize an empty module configuration if it has any (sentinel value) --- nemubot/modulecontext.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nemubot/modulecontext.py b/nemubot/modulecontext.py index 479b11c..cee68c7 100644 --- a/nemubot/modulecontext.py +++ b/nemubot/modulecontext.py @@ -48,7 +48,8 @@ class ModuleContext: module_name in context.modules_configuration): self.config = context.modules_configuration[module_name] else: - self.config = None + from nemubot.tools.xmlparser.node import ModuleState + self.config = ModuleState("module") self.hooks = list() self.events = list() From 76ec0d26b4281d5a897fc03ec53197ac4ca4119a Mon Sep 17 00:00:00 2001 From: nemunaire Date: Tue, 13 Oct 2015 07:49:53 +0200 Subject: [PATCH 394/674] Modules: avoid unhandled exception in all_post --- modules/framalink.py | 4 +--- modules/reddit.py | 4 +--- modules/youtube-title.py | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/modules/framalink.py b/modules/framalink.py index a556563..8b17cd8 100644 --- a/modules/framalink.py +++ b/modules/framalink.py @@ -79,7 +79,7 @@ def parselisten(msg): @hook("all_post") def parseresponse(msg): global LAST_URLS - try: + if hasattr(msg, "text") and msg.text: urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?[^ :]+)", msg.text) for url in urls: o = urlparse(url, "http") @@ -92,8 +92,6 @@ def parseresponse(msg): if recv not in LAST_URLS: LAST_URLS[recv] = list() LAST_URLS[recv].append(url) - except: - pass return msg diff --git a/modules/reddit.py b/modules/reddit.py index 53791ff..4c376d3 100644 --- a/modules/reddit.py +++ b/modules/reddit.py @@ -79,14 +79,12 @@ def parselisten(msg): def parseresponse(msg): global LAST_SUBS - try: + 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: if recv not in LAST_SUBS: LAST_SUBS[recv] = list() LAST_SUBS[recv].append(url) - except: - pass return msg diff --git a/modules/youtube-title.py b/modules/youtube-title.py index 93240b0..d4061b1 100644 --- a/modules/youtube-title.py +++ b/modules/youtube-title.py @@ -82,7 +82,7 @@ def parselisten(msg): @hook("all_post") def parseresponse(msg): global LAST_URLS - if hasattr(msg, "text"): + if hasattr(msg, "text") and msg.text: urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?[^ :]+)", msg.text) for url in urls: o = urlparse(url) From 55e6550cb141c24345b1fdf0bf1d9bbe910caa89 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Wed, 14 Oct 2015 00:17:02 +0200 Subject: [PATCH 395/674] tools/web: factorize getNormalizedURL --- modules/framalink.py | 6 +++--- modules/networking/isup.py | 4 ++-- modules/networking/page.py | 2 +- modules/networking/w3c.py | 3 ++- modules/networking/watchWebsite.py | 8 ++++---- modules/youtube-title.py | 4 ++-- nemubot/tools/web.py | 20 ++++++++++++-------- 7 files changed, 26 insertions(+), 21 deletions(-) diff --git a/modules/framalink.py b/modules/framalink.py index 8b17cd8..3ed1214 100644 --- a/modules/framalink.py +++ b/modules/framalink.py @@ -34,7 +34,7 @@ PROVIDERS = { } DEFAULT_PROVIDER = "framalink" -PROVIDERS_NETLOC = [urlparse(url, "http").netloc for f, url in PROVIDERS.values()] +PROVIDERS_NETLOC = [urlparse(web.getNormalizedURL(url), "http").netloc for f, url in PROVIDERS.values()] # LOADING ############################################################# @@ -82,7 +82,7 @@ def parseresponse(msg): if hasattr(msg, "text") and msg.text: urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?[^ :]+)", msg.text) for url in urls: - o = urlparse(url, "http") + 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: @@ -118,7 +118,7 @@ def cmd_reduceurl(msg): res = list() for url in minify: - o = urlparse(url, "http") + o = urlparse(web.getNormalizedURL(url), "http") minief_url = reduce(url) if o.netloc == "": res.append(gen_response(minief_url, msg, o.scheme)) diff --git a/modules/networking/isup.py b/modules/networking/isup.py index a666db1..c518900 100644 --- a/modules/networking/isup.py +++ b/modules/networking/isup.py @@ -1,6 +1,6 @@ import urllib -from nemubot.tools.web import getJSON +from nemubot.tools.web import getNormalizedURL, getJSON def isup(url): """Determine if the given URL is up or not @@ -9,7 +9,7 @@ def isup(url): url -- the URL to check """ - o = urllib.parse.urlparse(url, "http") + o = urllib.parse.urlparse(getNormalizedURL(url), "http") if o.netloc != "": isup = getJSON("http://isitup.org/%s.json" % o.netloc) if isup is not None and "status_code" in isup and isup["status_code"] == 1: diff --git a/modules/networking/page.py b/modules/networking/page.py index 67ec8c7..6179e34 100644 --- a/modules/networking/page.py +++ b/modules/networking/page.py @@ -21,7 +21,7 @@ def headers(url): url -- the page URL to get header """ - o = urllib.parse.urlparse(url, "http") + o = urllib.parse.urlparse(web.getNormalizedURL(url), "http") if o.netloc == "": raise IRCException("invalid URL") if o.scheme == "http": diff --git a/modules/networking/w3c.py b/modules/networking/w3c.py index c8331c6..3d920ef 100644 --- a/modules/networking/w3c.py +++ b/modules/networking/w3c.py @@ -3,6 +3,7 @@ import urllib from nemubot import __version__ from nemubot.exception import IRCException +from nemubot.tools.web import getNormalizedURL def validator(url): """Run the w3c validator on the given URL @@ -11,7 +12,7 @@ def validator(url): url -- the URL to validate """ - o = urllib.parse.urlparse(url, "http") + o = urllib.parse.urlparse(getNormalizedURL(url), "http") if o.netloc == "": raise IRCException("Indicate a valid URL!") diff --git a/modules/networking/watchWebsite.py b/modules/networking/watchWebsite.py index 0c1f8d3..41ea7d3 100644 --- a/modules/networking/watchWebsite.py +++ b/modules/networking/watchWebsite.py @@ -8,10 +8,10 @@ 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 logger = logging.getLogger("nemubot.module.networking.watchWebsite") -nemubotversion = 3.4 from more import Response @@ -56,7 +56,7 @@ def del_site(url, nick, channel, frm_owner): url -- URL to unwatch """ - o = urlparse(url, "http") + o = urlparse(getNormalizedURL(url), "http") if o.scheme != "" and url in DATAS.index: site = DATAS.index[url] for a in site.getNodes("alert"): @@ -80,7 +80,7 @@ def add_site(url, nick, channel, server, diffType="diff"): url -- URL to watch """ - o = urlparse(url, "http") + o = urlparse(getNormalizedURL(url), "http") if o.netloc == "": raise IRCException("sorry, I can't watch this URL :(") @@ -210,7 +210,7 @@ def start_watching(site, offset=0): offset -- offset time to delay the launch of the first check """ - o = urlparse(site["url"], "http") + o = urlparse(getNormalizedURL(site["url"]), "http") #print_debug("Add %s event for site: %s" % (site["type"], o.netloc)) try: diff --git a/modules/youtube-title.py b/modules/youtube-title.py index d4061b1..4bf115c 100644 --- a/modules/youtube-title.py +++ b/modules/youtube-title.py @@ -3,7 +3,7 @@ import re, json, subprocess from nemubot.exception import IRCException from nemubot.hooks import hook -from nemubot.tools.web import getURLContent +from nemubot.tools.web import _getNormalizedURL, getURLContent from more import Response """Get information of youtube videos""" @@ -85,7 +85,7 @@ def parseresponse(msg): if hasattr(msg, "text") and msg.text: urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?[^ :]+)", msg.text) for url in urls: - o = urlparse(url) + o = urlparse(_getNormalizedURL(url)) if o.scheme != "": if o.netloc == "" and len(o.path) < 10: continue diff --git a/nemubot/tools/web.py b/nemubot/tools/web.py index a730a9e..938e02d 100644 --- a/nemubot/tools/web.py +++ b/nemubot/tools/web.py @@ -23,13 +23,17 @@ from nemubot.exception import IRCException def isURL(url): """Return True if the URL can be parsed""" - o = urlparse(url) + o = urlparse(_getNormalizedURL(url)) return o.netloc == "" and o.path == "" +def _getNormalizedURL(url): + """Return a light normalized form for the given URL""" + return url if "//" in url else "//" + url + def getNormalizedURL(url): """Return a normalized form for the given URL""" - return urlunsplit(urlsplit(url, "http")) + return urlunsplit(urlsplit(_getNormalizedURL(url), "http")) def getScheme(url): @@ -40,27 +44,27 @@ def getScheme(url): def getHost(url): """Return the domain of a given URL""" - return urlparse(url, "http").hostname + return urlparse(_getNormalizedURL(url), "http").hostname def getPort(url): """Return the port of a given URL""" - return urlparse(url, "http").port + return urlparse(_getNormalizedURL(url), "http").port def getPath(url): """Return the page request of a given URL""" - return urlparse(url, "http").path + return urlparse(_getNormalizedURL(url), "http").path def getUser(url): """Return the page request of a given URL""" - return urlparse(url, "http").username + return urlparse(_getNormalizedURL(url), "http").username def getPassword(url): """Return the page request of a given URL""" - return urlparse(url, "http").password + return urlparse(_getNormalizedURL(url), "http").password # Get real pages @@ -74,7 +78,7 @@ def getURLContent(url, body=None, timeout=7, header=None): timeout -- maximum number of seconds to wait before returning an exception """ - o = urlparse(url, "http") + o = urlparse(_getNormalizedURL(url), "http") import http.client From fd99deed1d2dfa63c96e578bc1271bbe17bd9237 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Thu, 15 Oct 2015 19:26:09 +0200 Subject: [PATCH 396/674] tools/web: colon char in URL precedes optional // --- 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 938e02d..d195daf 100644 --- a/nemubot/tools/web.py +++ b/nemubot/tools/web.py @@ -29,7 +29,7 @@ def isURL(url): def _getNormalizedURL(url): """Return a light normalized form for the given URL""" - return url if "//" in url else "//" + url + return url if "//" in url or ":" in url else "//" + url def getNormalizedURL(url): """Return a normalized form for the given URL""" From ffc8fe40c3a78854e5da32dee3c7a3863dfb9732 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Fri, 16 Oct 2015 07:10:20 +0200 Subject: [PATCH 397/674] Add a build system --- .travis.yml | 9 ++++++ requirements.txt | 0 setup.py | 82 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+) create mode 100644 .travis.yml create mode 100644 requirements.txt create mode 100755 setup.py diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..2d5698a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: python +python: + - 3.3 + - 3.4 + - 3.5 + - nightly +install: pip install -r requirements.txt +script: pip install . +sudo: false diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..37f4aef --- /dev/null +++ b/setup.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 + +import os +import re +from glob import glob +try: + from setuptools import setup +except ImportError: + from distutils.core import setup + +with open(os.path.join(os.path.dirname(__file__), + 'nemubot', + '__init__.py')) as f: + version = re.search("__version__ = '([^']+)'", f.read()).group(1) + +with open('requirements.txt', 'r') as f: + requires = [x.strip() for x in f if x.strip()] + +#with open('test-requirements.txt', 'r') as f: +# test_requires = [x.strip() for x in f if x.strip()] + +dirs = os.listdir("./modules/") +data_files = [] +for i in dirs: + data_files.append(("nemubot/modules", glob('./modules/' + i + '/*'))) + +setup( + name = "nemubot", + version = version, + description = "An extremely modulable IRC bot, built around XML configuration files!", + long_description = open('README.md').read(), + + author = 'nemunaire', + author_email = 'nemunaire@nemunai.re', + + url = 'https://github.com/nemunaire/nemubot', + license = 'AGPLv3', + + classifiers = [ + 'Development Status :: 2 - Pre-Alpha', + + 'Environment :: Console', + + 'Topic :: Communications :: Chat :: Internet Relay Chat', + 'Intended Audience :: Information Technology', + + 'License :: OSI Approved :: GNU Affero General Public License v3', + + 'Operating System :: POSIX', + + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + ], + + keywords = 'bot irc', + + provides = ['nemubot'], + + install_requires = requires, + + packages=[ + 'nemubot', + 'nemubot.datastore', + 'nemubot.event', + 'nemubot.hooks', + 'nemubot.message', + 'nemubot.message.printer', + 'nemubot.prompt', + 'nemubot.server', + 'nemubot.server.message', + 'nemubot.tools', + 'nemubot.tools.xmlparser', + ], + + scripts=[ + 'bin/nemubot', +# 'bin/module_tester', + ], + +# data_files=data_files, +) From 2f1f573af2f8e49f0fae46859eef1dc2eeaa4f80 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sat, 17 Oct 2015 17:00:33 +0200 Subject: [PATCH 398/674] Fix abstract name of to_server_string function --- nemubot/server/message/abstract.py | 2 +- nemubot/server/test_IRC.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/nemubot/server/message/abstract.py b/nemubot/server/message/abstract.py index 35d7105..03e10cd 100644 --- a/nemubot/server/message/abstract.py +++ b/nemubot/server/message/abstract.py @@ -28,7 +28,7 @@ class Abstract: raise NotImplemented - def to_server_message(self): + def to_server_string(self, **kwargs): """Pretty print the message to close to original input string """ diff --git a/nemubot/server/test_IRC.py b/nemubot/server/test_IRC.py index 3553d6b..552a1d3 100644 --- a/nemubot/server/test_IRC.py +++ b/nemubot/server/test_IRC.py @@ -23,13 +23,13 @@ class TestIRCMessage(unittest.TestCase): def test_prettyprint(self): - bst1 = self.msg.to_server_message(False) + bst1 = self.msg.to_server_string(False) msg2 = IRC.IRCMessage(bst1.encode()) - bst2 = msg2.to_server_message(False) + bst2 = msg2.to_server_string(False) msg3 = IRC.IRCMessage(bst2.encode()) - bst3 = msg3.to_server_message(False) + bst3 = msg3.to_server_string(False) self.assertEqual(bst2, bst3) From c0ce0ca263d05750545746c5cd3340819d0945f2 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sat, 17 Oct 2015 17:10:21 +0200 Subject: [PATCH 399/674] Server Factory: Handle URL arguments without value --- nemubot/server/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nemubot/server/__init__.py b/nemubot/server/__init__.py index 5289697..700a198 100644 --- a/nemubot/server/__init__.py +++ b/nemubot/server/__init__.py @@ -46,7 +46,10 @@ def factory(uri): queries = o.query.split("&") for q in queries: - key, val = tuple(q.split("=", 1)) + if "=" in q: + key, val = tuple(q.split("=", 1)) + else: + key, val = q, "" if key == "msg": args["on_connect"] = [ "PRIVMSG %s :%s" % (target, unquote(val)) ] elif key == "key": From 3004c01db46de532b7c819a48a8ccaa71b69ce85 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sun, 18 Oct 2015 10:47:13 +0200 Subject: [PATCH 400/674] tools/web: set timeout to 7 secs --- 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 d195daf..4cec48a 100644 --- a/nemubot/tools/web.py +++ b/nemubot/tools/web.py @@ -164,7 +164,7 @@ def getURLContent(url, body=None, timeout=7, header=None): (res.status, http.client.responses[res.status])) -def getXML(url, timeout=15): +def getXML(url, timeout=7): """Get content page and return XML parsed content Arguments: @@ -180,7 +180,7 @@ def getXML(url, timeout=15): return parse_string(cnt.encode()) -def getJSON(url, timeout=15): +def getJSON(url, timeout=7): """Get content page and return JSON content Arguments: From dbca402fe7b717b73a23661aa9bf9a1176076d27 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sun, 18 Oct 2015 11:39:44 +0200 Subject: [PATCH 401/674] [alias] Use title in response --- modules/alias.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/alias.py b/modules/alias.py index 22afb95..8d67000 100644 --- a/modules/alias.py +++ b/modules/alias.py @@ -172,9 +172,9 @@ def cmd_listvars(msg): res.append("%s didn't create variable yet." % user) return Response(" ; ".join(res), channel=msg.channel) elif len(context.data.getNode("variables").index): - return Response("Known variables: %s." % - ", ".join(list_variables()), - channel=msg.channel) + return Response(list_variables(), + channel=msg.channel, + title="Known variables") else: return Response("There is currently no variable stored.", channel=msg.channel) @@ -202,7 +202,7 @@ def cmd_listalias(msg): if len(aliases): return Response([a["alias"] for a in aliases], channel=msg.channel, - title="Known aliases:") + title="Known aliases") return Response("There is no alias currently.", channel=msg.channel) From 981f6cc66caa393bd8c7e8ef2623c0090fda846c Mon Sep 17 00:00:00 2001 From: nemunaire Date: Mon, 19 Oct 2015 07:15:07 +0200 Subject: [PATCH 402/674] Run core tests in CI --- .travis.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2d5698a..d109d2a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,8 @@ python: - 3.4 - 3.5 - nightly -install: pip install -r requirements.txt -script: pip install . +install: + - pip install -r requirements.txt + - pip install . +script: nosetests -w nemubot sudo: false From a1e7a7cff8ee6d15dac1dee67a5ea5bd932252de Mon Sep 17 00:00:00 2001 From: nemunaire Date: Tue, 20 Oct 2015 08:25:34 +0200 Subject: [PATCH 403/674] Add test for socket printer --- nemubot/message/printer/test_socket.py | 86 ++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 nemubot/message/printer/test_socket.py diff --git a/nemubot/message/printer/test_socket.py b/nemubot/message/printer/test_socket.py new file mode 100644 index 0000000..50da862 --- /dev/null +++ b/nemubot/message/printer/test_socket.py @@ -0,0 +1,86 @@ +# 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 unittest + +from nemubot.message import Command, DirectAsk, Text +from nemubot.message.printer.socket import Socket as SocketVisitor + +class TestSocketPrinter(unittest.TestCase): + + + def setUp(self): + self.msgs = [ + # Texts + ( + Text(message="TEXT", + ), + "TEXT" + ), + ( + Text(message="TEXT TEXT2", + ), + "TEXT TEXT2" + ), + ( + Text(message="TEXT @ARG=1 TEXT2", + ), + "TEXT @ARG=1 TEXT2" + ), + + + # DirectAsk + ( + DirectAsk(message="TEXT", + designated="someone", + to=["#somechannel"] + ), + "someone: TEXT" + ), + ( + # Private message to someone + DirectAsk(message="TEXT", + designated="someone", + to=["someone"] + ), + "TEXT" + ), + + + # Commands + ( + Command(cmd="COMMAND", + ), + "!COMMAND" + ), + ( + Command(cmd="COMMAND", + args=["TEXT"], + ), + "!COMMAND TEXT" + ), + ] + + + def test_printer(self): + for msg, pp in self.msgs: + sv = SocketVisitor() + msg.accept(sv) + self.assertEqual(sv.pp, pp) + + +if __name__ == '__main__': + unittest.main() From 39b7ecdaa44c06eefc0cfbba5829cc940b45672f Mon Sep 17 00:00:00 2001 From: nemunaire Date: Tue, 20 Oct 2015 08:34:07 +0200 Subject: [PATCH 404/674] Add keyworded arguments in command received --- nemubot/message/command.py | 3 ++- nemubot/message/printer/socket.py | 8 +++++--- nemubot/message/printer/test_socket.py | 26 ++++++++++++++++++++++++++ nemubot/server/message/IRC.py | 20 +++++++++++++++++++- 4 files changed, 52 insertions(+), 5 deletions(-) diff --git a/nemubot/message/command.py b/nemubot/message/command.py index 842b19f..895d16e 100644 --- a/nemubot/message/command.py +++ b/nemubot/message/command.py @@ -21,11 +21,12 @@ class Command(Abstract): """This class represents a specialized TextMessage""" - def __init__(self, cmd, args=None, *nargs, **kargs): + def __init__(self, cmd, args=None, kwargs=None, *nargs, **kargs): Abstract.__init__(self, *nargs, **kargs) self.cmd = cmd self.args = args if args is not None else list() + self.kwargs = kwargs if kwargs is not None else dict() def __str__(self): return self.cmd + " @" + ",@".join(self.args) diff --git a/nemubot/message/printer/socket.py b/nemubot/message/printer/socket.py index dfa0115..2df7d5e 100644 --- a/nemubot/message/printer/socket.py +++ b/nemubot/message/printer/socket.py @@ -51,9 +51,11 @@ class Socket(AbstractVisitor): def visit_Command(self, msg): - res = Text("!%s%s%s" % (msg.cmd, - " " if len(msg.args) else "", - " ".join(msg.args)), + res = Text("!%s%s%s%s%s" % (msg.cmd, + " " if len(msg.kwargs) else "", + " ".join(["@%s=%s" % (k, msg.kwargs[k]) if msg.kwargs[k] is not None else "@%s" % k for k in msg.kwargs]), + " " if len(msg.args) else "", + " ".join(msg.args)), server=msg.server, date=msg.date, to=msg.to, frm=msg.frm) res.accept(self) diff --git a/nemubot/message/printer/test_socket.py b/nemubot/message/printer/test_socket.py index 50da862..aa8d833 100644 --- a/nemubot/message/printer/test_socket.py +++ b/nemubot/message/printer/test_socket.py @@ -72,6 +72,32 @@ class TestSocketPrinter(unittest.TestCase): ), "!COMMAND TEXT" ), + ( + Command(cmd="COMMAND", + kwargs={"KEY1": "VALUE"}, + ), + "!COMMAND @KEY1=VALUE" + ), + ( + Command(cmd="COMMAND", + args=["TEXT"], + kwargs={"KEY1": "VALUE"}, + ), + "!COMMAND @KEY1=VALUE TEXT" + ), + ( + Command(cmd="COMMAND", + kwargs={"KEY2": None}, + ), + "!COMMAND @KEY2" + ), + ( + Command(cmd="COMMAND", + args=["TEXT"], + kwargs={"KEY2": None}, + ), + "!COMMAND @KEY2 TEXT" + ), ] diff --git a/nemubot/server/message/IRC.py b/nemubot/server/message/IRC.py index 43dcf49..6249716 100644 --- a/nemubot/server/message/IRC.py +++ b/nemubot/server/message/IRC.py @@ -177,7 +177,25 @@ class IRC(Abstract): except ValueError: args = text.split(' ') - return message.Command(cmd=args[0], args=args[1:], **common_args) + # Extract explicit named arguments: @key=value or just @key + kwargs = {} + for i in range(len(args) - 1, 0, -1): + arg = args[i] + if len(arg) > 2: + if arg[0:1] == '\\@': + args[i] = 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) + + 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: From aee8545e65b912b5ef4872d0d106f926445cfbf4 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sun, 12 Jul 2015 18:00:09 +0200 Subject: [PATCH 405/674] Fix exception if no owner defined --- nemubot/consumer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nemubot/consumer.py b/nemubot/consumer.py index 535f41c..5ba1299 100644 --- a/nemubot/consumer.py +++ b/nemubot/consumer.py @@ -47,7 +47,7 @@ class MessageConsumer: msg.server = self.srv.id if hasattr(msg, "frm_owner"): - msg.frm_owner = (self.srv.owner == msg.frm) + msg.frm_owner = (not hasattr(self.srv, "owner") or self.srv.owner == msg.frm) return msg From e925c47961b01f4fb6fa44c8d99df647f5c19a6f Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sun, 19 Jul 2015 14:04:01 +0200 Subject: [PATCH 406/674] New function requires_version if module want to restrict to some version for compatibility --- nemubot/__init__.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/nemubot/__init__.py b/nemubot/__init__.py index 444886a..16173c9 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -22,6 +22,24 @@ __author__ = 'nemunaire' from nemubot.modulecontext import ModuleContext context = ModuleContext(None, None) + +def requires_version(min=None, max=None): + """Raise ImportError if the current version is not in the given range + + Keyword arguments: + min -- minimal compatible version + max -- last compatible version + """ + + from distutils.version import LooseVersion + if min is not None and LooseVersion(__version__) < LooseVersion(str(min)): + raise ImportError("Requires version above %s, " + "but this is nemubot v%s." % (str(min), __version__)) + if max is not None and LooseVersion(__version__) > LooseVersion(str(max)): + raise ImportError("Requires version under %s, " + "but this is nemubot v%s." % (str(max), __version__)) + + def reload(): """Reload code of all Python modules used by nemubot """ From 5e95f50fb6a2b440353faeeef5506d681094ce0b Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sun, 17 May 2015 11:11:15 +0200 Subject: [PATCH 407/674] Expand argument paths --- nemubot/__main__.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/nemubot/__main__.py b/nemubot/__main__.py index d1d8f93..24556c3 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -55,6 +55,11 @@ def main(): print(nemubot.__version__) sys.exit(0) + # Resolve relatives paths + args.data_path = os.path.abspath(os.path.expanduser(args.data_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)] + # Setup loggin interface import logging logger = logging.getLogger("nemubot") @@ -79,16 +84,16 @@ def main(): modules_paths = list() for path in args.modules_path: if os.path.isdir(path): - modules_paths.append( - os.path.realpath(os.path.abspath(path))) + modules_paths.append(path) else: logger.error("%s is not a directory", path) # Create bot context from nemubot import datastore from nemubot.bot import Bot - context = Bot(modules_paths=modules_paths, data_store=datastore.XML(args.data_path), - verbosity=args.verbose) + context = Bot(modules_paths=modules_paths, + data_store=datastore.XML(args.data_path), + verbosity=args.verbose) if args.no_connect: context.noautoconnect = True From c94d9743dd293cf452eac27fbe9fd4a37400a7bf Mon Sep 17 00:00:00 2001 From: nemunaire Date: Wed, 13 May 2015 00:23:37 +0200 Subject: [PATCH 408/674] Add --logfile option --- nemubot/__main__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/nemubot/__main__.py b/nemubot/__main__.py index 24556c3..f455607 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("-l", "--logfile", default="./nemubot.log", + help="Path to store logs") + parser.add_argument("-m", "--module", nargs='*', help="load given modules") @@ -57,6 +60,7 @@ def main(): # Resolve relatives paths args.data_path = os.path.abspath(os.path.expanduser(args.data_path)) + 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)] @@ -75,7 +79,7 @@ def main(): ch.setLevel(logging.INFO) logger.addHandler(ch) - fh = logging.FileHandler('./nemubot.log') + fh = logging.FileHandler(args.logfile) fh.setFormatter(formatter) fh.setLevel(logging.DEBUG) logger.addHandler(fh) From 6c244cffa0ca7b1cd14160fb42e91f1d17df44c9 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Tue, 19 May 2015 12:32:23 +0200 Subject: [PATCH 409/674] Server: add a socket listener, able to accept client on Unix or TCP socket --- nemubot/server/socket.py | 67 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/nemubot/server/socket.py b/nemubot/server/socket.py index fdd3be9..1c680ee 100644 --- a/nemubot/server/socket.py +++ b/nemubot/server/socket.py @@ -117,3 +117,70 @@ class SocketServer(AbstractServer): for line in temp: yield line + + +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) + 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 fileno(self): + return self.socket.fileno() if self.socket else None + + + @property + def connected(self): + """Indicator of the connection aliveness""" + return self.socket is not None + + + def _open(self): + import os + import socket + + self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + if self.sock_location is not None: + 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.bind((self.host, self.port)) + self.socket.listen(5) + + return True + + + def _close(self): + import os + import socket + + try: + self.socket.shutdown(socket.SHUT_RDWR) + self.socket.close() + if self.sock_location is not None: + os.remove(self.sock_location) + except socket.error: + pass + + # Read + + def read(self): + if not self.connected: + return [] + + conn, addr = self.socket.accept() + self.nb_son += 1 + ss = SocketServer(id=self.id + "#" + str(self.nb_son), socket=conn) + self.new_server_cb(ss) + + return [] From f9ee1074032e17007e05aa9a5385d6977b6c5465 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Thu, 21 May 2015 10:13:16 +0200 Subject: [PATCH 410/674] SocketServer: able to connect to Unix socket --- nemubot/server/socket.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/nemubot/server/socket.py b/nemubot/server/socket.py index 1c680ee..052579b 100644 --- a/nemubot/server/socket.py +++ b/nemubot/server/socket.py @@ -24,13 +24,18 @@ class SocketServer(AbstractServer): """Concrete implementation of a socket connexion (can be wrapped with TLS)""" - def __init__(self, host, port, ssl=False): + 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) - self.host = host - self.port = int(port) + if sock_location is not None: + self.filename = sock_location + elif host is not None: + self.host = host + self.port = int(port) self.ssl = ssl - self.socket = None + self.socket = socket self.readbuffer = b'' self.printer = SocketPrinter @@ -51,9 +56,17 @@ class SocketServer(AbstractServer): import os import socket + if self.connected: + return True + try: - self.socket = socket.create_connection((self.host, self.port)) - self.logger.info("Connected to %s:%d", self.host, self.port) + 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) except socket.error as e: self.socket = None self.logger.critical("Unable to connect to %s:%d: %s", @@ -109,7 +122,7 @@ class SocketServer(AbstractServer): def read(self): if not self.connected: - return + return [] raw = self.socket.recv(1024) temp = (self.readbuffer + raw).split(b'\r\n') From f9f54989fe6671e752b9852d54888eb158484a58 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Thu, 14 May 2015 11:12:13 +0200 Subject: [PATCH 411/674] Fix logger level filtering --- nemubot/__main__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/nemubot/__main__.py b/nemubot/__main__.py index f455607..992c3ad 100644 --- a/nemubot/__main__.py +++ b/nemubot/__main__.py @@ -67,21 +67,19 @@ def main(): # Setup loggin interface import logging logger = logging.getLogger("nemubot") + logger.setLevel(logging.DEBUG) formatter = logging.Formatter( '%(asctime)s %(name)s %(levelname)s %(message)s') ch = logging.StreamHandler() ch.setFormatter(formatter) - if args.verbose > 1: - ch.setLevel(logging.DEBUG) - else: + if args.verbose < 2: ch.setLevel(logging.INFO) logger.addHandler(ch) fh = logging.FileHandler(args.logfile) fh.setFormatter(formatter) - fh.setLevel(logging.DEBUG) logger.addHandler(fh) # Add modules dir paths From a4fd04c310b6887e54775f4b78ac2fb3d91d95b9 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Thu, 16 Jul 2015 20:38:04 +0200 Subject: [PATCH 412/674] Remove print unhandled in daemon mode --- nemubot/bot.py | 3 ++- nemubot/hooks/manager.py | 2 -- nemubot/importer.py | 3 --- nemubot/modulecontext.py | 3 +-- nemubot/server/IRC.py | 2 +- nemubot/tools/config.py | 8 ++++---- 6 files changed, 8 insertions(+), 13 deletions(-) diff --git a/nemubot/bot.py b/nemubot/bot.py index 5e8b832..6cbab75 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -393,9 +393,10 @@ class Bot(threading.Thread): # Overwrite print built-in def prnt(*args): - print("[%s]" % module_name, *args) if hasattr(module, "logger"): module.logger.info(" ".join([str(s) for s in args])) + else: + logger.info("[%s] %s", module_name, " ".join([str(s) for s in args])) module.print = prnt # Create module context diff --git a/nemubot/hooks/manager.py b/nemubot/hooks/manager.py index ddc99c5..200091e 100644 --- a/nemubot/hooks/manager.py +++ b/nemubot/hooks/manager.py @@ -40,7 +40,6 @@ class HooksManager: if trigger not in self.hooks: self.hooks[trigger] = list() - #print("ADD hook: %s => %s" % (trigger, hook)) self.hooks[trigger].append(hook) @@ -86,7 +85,6 @@ class HooksManager: if trigger.find(key) == 0: res += self.hooks[key] - #print("GET hooks: %s => %d" % (trigger, len(res))) return res diff --git a/nemubot/importer.py b/nemubot/importer.py index 4d4b6a7..6769ea9 100644 --- a/nemubot/importer.py +++ b/nemubot/importer.py @@ -31,11 +31,9 @@ class ModuleFinder(Finder): self.add_module = add_module def find_module(self, fullname, path=None): - # print ("looking for", fullname, "in", path) # Search only for new nemubot modules (packages init) if path is None: for mpath in self.modules_paths: - # print ("looking for", fullname, "in", mpath) if os.path.isfile(os.path.join(mpath, fullname + ".py")): return ModuleLoader(self.add_module, fullname, os.path.join(mpath, fullname + ".py")) @@ -44,7 +42,6 @@ class ModuleFinder(Finder): os.path.join( os.path.join(mpath, fullname), "__init__.py")) - # print ("not found") return None diff --git a/nemubot/modulecontext.py b/nemubot/modulecontext.py index cee68c7..be7842b 100644 --- a/nemubot/modulecontext.py +++ b/nemubot/modulecontext.py @@ -26,7 +26,6 @@ def convert_legacy_store(old): elif old == "all_pre": return "pre" else: - print("UNKNOWN store:", old) return old @@ -115,7 +114,7 @@ class ModuleContext: return context.del_event(evt, module_src=module) def send_response(server, res): - print(res) + module.logger.info("Send response: %s", res) def save(): context.datastore.save(module_name, self.data) diff --git a/nemubot/server/IRC.py b/nemubot/server/IRC.py index 5c26d21..672d7af 100644 --- a/nemubot/server/IRC.py +++ b/nemubot/server/IRC.py @@ -96,7 +96,7 @@ class IRC(SocketServer): import nemubot - self.ctcp_capabilities["ACTION"] = lambda msg, cmds: print ("ACTION receive: %s" % cmds) + 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__ diff --git a/nemubot/tools/config.py b/nemubot/tools/config.py index 608da56..479b96f 100644 --- a/nemubot/tools/config.py +++ b/nemubot/tools/config.py @@ -109,9 +109,9 @@ def load_file(filename, context): # Add the server in the context if context.add_server(srv, get_boolean(server, "autoconnect")): - print("Server '%s' successfully added." % srv.id) + logger.info("Server '%s' successfully added." % srv.id) else: - print("Can't add server '%s'." % srv.id) + logger.error("Can't add server '%s'." % srv.id) # Load module and their configuration for mod in config.getNodes("module"): @@ -130,8 +130,8 @@ def load_file(filename, context): # Other formats else: - print (" Can't load `%s'; this is not a valid nemubot " - "configuration file." % filename) + logger.error("Can't load `%s'; this is not a valid nemubot " + "configuration file." % filename) # Unexisting file, assume a name was passed, import the module! else: From 4af108265b3d53226779c38c1ddf6ccd6dbb6570 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Thu, 3 Sep 2015 01:26:13 +0200 Subject: [PATCH 413/674] Split and rewrite message treatment from consumers --- nemubot/consumer.py | 189 +++++++++---------------------------------- nemubot/treatment.py | 131 ++++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+), 150 deletions(-) create mode 100644 nemubot/treatment.py diff --git a/nemubot/consumer.py b/nemubot/consumer.py index 5ba1299..c764a69 100644 --- a/nemubot/consumer.py +++ b/nemubot/consumer.py @@ -29,176 +29,62 @@ class MessageConsumer: def __init__(self, srv, msg): self.srv = srv - self.orig = msg - self.msgs = [ ] - self.responses = None - - - def first_treat(self, msg): - """Qualify a new message/response - - Argument: - msg -- The Message or Response to qualify - """ - - # Define the source server if not already done - 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) - - return msg - - - def pre_treat(self, hm): - """Modify input Messages - - Arguments: - hm -- Hooks manager - """ - - new_msg = list() - new_msg += self.msgs - self.msgs = list() - - while len(new_msg) > 0: - msg = new_msg.pop(0) - for h in hm.get_hooks("pre", type(msg).__name__): - if h.match(msg, server=self.srv): - res = h.run(msg) - if isinstance(res, list): - for i in range(len(res)): - if res[i] == msg: - res.pop(i) - break - new_msg += res - elif res is not None and res != msg: - new_msg.append(res) - msg = None - break - elif res is None or res is False: - msg = None - break - if msg is not None: - self.msgs.append(msg) - - - def in_treat(self, hm): - """Treat Messages and store responses - - Arguments: - hm -- Hooks manager - """ - - self.responses = list() - for msg in self.msgs: - for h in hm.get_hooks("in", type(msg).__name__): - if h.match(msg, server=self.srv): - res = h.run(msg) - if isinstance(res, list): - self.responses += res - elif res is not None: - self.responses.append(res) - - - def post_treat(self, hm): - """Modify output Messages - - Arguments: - hm -- Hooks manager - """ - - new_msg = list() - new_msg += self.responses - self.responses = list() - - while len(new_msg) > 0: - ff = new_msg.pop(0) - if isinstance(ff, str): - self.responses.append(ff) - continue - msg = self.first_treat(ff) - for h in hm.get_hooks("post"): - if h.match(msg, server=self.srv): - res = h.run(msg) - if isinstance(res, list): - for i in range(len(res)): - if isinstance(res[i], str): - self.responses.append(res.pop(i)) - break - msg = None - new_msg += res - break - elif res is not None and res != msg: - new_msg.append(res) - msg = None - break - elif res is None or res is False: - msg = None - break - else: - msg = res - if msg is not None: - self.responses.append(msg) def run(self, context): """Create, parse and treat the message""" + + from nemubot.bot import Bot + assert isinstance(context, Bot) + + msgs = [] + + # Parse the message try: for msg in self.srv.parse(self.orig): - self.msgs.append(msg) + msgs.append(msg) except: logger.exception("Error occurred during the processing of the %s: " "%s", type(self.msgs[0]).__name__, self.msgs[0]) - if len(self.msgs) <= 0: + if len(msgs) <= 0: return - try: - for msg in self.msgs: - self.first_treat(msg) + # 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) - # Run pre-treatment: from Message to [ Message ] - self.pre_treat(context.hooks) + # Treat the message + from nemubot.treatment import MessageTreater + mt = MessageTreater(context.hooks) # Should be in context, this is static + for msg in msgs: + for res in mt.treat_msg(msg): + # Identify the destination + to_server = None + if isinstance(res, str): + to_server = self.srv + 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] - # Run in-treatment: from Message to [ Response ] - if len(self.msgs) > 0: - self.in_treat(context.hooks) + if to_server is None: + logger.error("The server defined in this response doesn't " + "exist: %s", res.server) + continue - # Run post-treatment: from Response to [ Response ] - if self.responses is not None and len(self.responses) > 0: - self.post_treat(context.hooks) - except BaseException as e: - logger.exception("Error occurred during the processing of the %s: " - "%s", type(self.msgs[0]).__name__, self.msgs[0]) - - from nemubot.message import Text - self.responses.append(Text("Sorry, an error occured (%s). Feel free to open a new issue at https://github.com/nemunaire/nemubot/issues/new" % type(e).__name__, - server=self.srv.id, to=self.msgs[0].to_response)) - - for res in self.responses: - to_server = None - if isinstance(res, str): - to_server = self.srv - 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] - - if to_server is None: - logger.error("The server defined in this response doesn't " - "exist: %s", res.server) - continue - - # Sent the message only if treat_post authorize it - to_server.send_response(res) + # Sent the message only if treat_post authorize it + to_server.send_response(res) class EventConsumer: + """Store a event before treating""" + def __init__(self, evt, timeout=20): self.evt = evt self.timeout = timeout @@ -222,12 +108,15 @@ class EventConsumer: class Consumer(threading.Thread): + """Dequeue and exec requested action""" + def __init__(self, context): self.context = context self.stop = False threading.Thread.__init__(self) + def run(self): try: while not self.stop: diff --git a/nemubot/treatment.py b/nemubot/treatment.py new file mode 100644 index 0000000..0ea535b --- /dev/null +++ b/nemubot/treatment.py @@ -0,0 +1,131 @@ +# 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 logging + +logger = logging.getLogger("nemubot.treatment") + + +class MessageTreater: + + """Treat a message""" + + def __init__(self, hm): + self.hm = hm # Pointer to the HookManager + + + def treat_msg(self, msg): + """Treat a given message + + Arguments: + msg -- the message to treat + """ + + try: + # 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) + + m = next(msg_gen, None) + except BaseException as e: + logger.exception("Error occurred during the processing of the %s: " + "%s", type(msg).__name__, msg) + + from nemubot.message import Text + yield from self._post_treat(Text("Sorry, an error occured (%s). Feel free to open a new issue at https://github.com/nemunaire/nemubot/issues/new" % type(e).__name__, + to=msg.to_response)) + + + + def _pre_treat(self, msg): + """Modify input Messages + + Arguments: + msg -- message to treat + """ + + for h in self.hm.get_hooks("pre", type(msg).__name__): + if h.match(msg): + res = h.run(msg) + + 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 + else: + yield msg + + + def _in_treat(self, msg): + """Treats Messages and returns Responses + + Arguments: + msg -- message to treat + """ + + for h in self.hm.get_hooks("in", type(msg).__name__): + if h.match(msg): + res = h.run(msg) + + 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 + + yield res + + + def _post_treat(self, msg): + """Modify output Messages + + Arguments: + msg -- response to treat + """ + + for h in self.hm.get_hooks("post"): + if h.match(msg): + res = h.run(msg) + + 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 + + else: + yield msg From 60f7c6eea702dbe49908af9698ec496cd0db563c Mon Sep 17 00:00:00 2001 From: nemunaire Date: Fri, 4 Sep 2015 22:47:02 +0200 Subject: [PATCH 414/674] Place MessageTreater in context --- nemubot/bot.py | 12 ++++++------ nemubot/consumer.py | 4 +--- nemubot/modulecontext.py | 6 +++--- nemubot/treatment.py | 5 +++-- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/nemubot/bot.py b/nemubot/bot.py index 6cbab75..db2f653 100644 --- a/nemubot/bot.py +++ b/nemubot/bot.py @@ -25,7 +25,6 @@ from nemubot import __version__ from nemubot.consumer import Consumer, EventConsumer, MessageConsumer from nemubot import datastore import nemubot.hooks -from nemubot.modulecontext import ModuleContext logger = logging.getLogger("nemubot") @@ -70,19 +69,19 @@ class Bot(threading.Thread): self.event_timer = None # Own hooks - from nemubot.hooks.manager import HooksManager - self.hooks = HooksManager() + from nemubot.treatment import MessageTreater + self.treater = MessageTreater() 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.hooks.add_hook(nemubot.hooks.Message(in_ping), "in", "DirectAsk") + self.treater.hm.add_hook(nemubot.hooks.Message(in_ping), "in", "DirectAsk") def in_echo(msg): from nemubot.message import Text return Text(msg.nick + ": " + " ".join(msg.args), to=msg.to_response) - self.hooks.add_hook(nemubot.hooks.Message(in_echo, "echo"), "in", "Command") + self.treater.hm.add_hook(nemubot.hooks.Message(in_echo, "echo"), "in", "Command") def _help_msg(msg): """Parse and response to help messages""" @@ -129,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.hooks.add_hook(nemubot.hooks.Message(_help_msg, "help"), "in", "Command") + self.treater.hm.add_hook(nemubot.hooks.Message(_help_msg, "help"), "in", "Command") from queue import Queue # Messages to be treated @@ -400,6 +399,7 @@ class Bot(threading.Thread): module.print = prnt # Create module context + from nemubot.modulecontext import ModuleContext module.__nemubot_context__ = ModuleContext(self, module) if not hasattr(module, "logger"): diff --git a/nemubot/consumer.py b/nemubot/consumer.py index c764a69..9c9d90d 100644 --- a/nemubot/consumer.py +++ b/nemubot/consumer.py @@ -58,10 +58,8 @@ class MessageConsumer: msg.frm_owner = (not hasattr(self.srv, "owner") or self.srv.owner == msg.frm) # Treat the message - from nemubot.treatment import MessageTreater - mt = MessageTreater(context.hooks) # Should be in context, this is static for msg in msgs: - for res in mt.treat_msg(msg): + for res in context.treater.treat_msg(msg): # Identify the destination to_server = None if isinstance(res, str): diff --git a/nemubot/modulecontext.py b/nemubot/modulecontext.py index be7842b..5b47278 100644 --- a/nemubot/modulecontext.py +++ b/nemubot/modulecontext.py @@ -62,13 +62,13 @@ class ModuleContext: def add_hook(store, hook): store = convert_legacy_store(store) self.hooks.append((store, hook)) - return context.hooks.add_hook(hook, store) + 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.hooks.del_hook(hook, store) + return context.treater.hm.del_hook(hook, store) def call_hook(store, msg): - for h in context.hooks.get_hooks(store): + for h in context.treater.hm.get_hooks(store): if h.match(msg): res = h.run(msg) if isinstance(res, list): diff --git a/nemubot/treatment.py b/nemubot/treatment.py index 0ea535b..8bbdabb 100644 --- a/nemubot/treatment.py +++ b/nemubot/treatment.py @@ -23,8 +23,9 @@ class MessageTreater: """Treat a message""" - def __init__(self, hm): - self.hm = hm # Pointer to the HookManager + def __init__(self): + from nemubot.hooks.manager import HooksManager + self.hm = HooksManager() def treat_msg(self, msg): From 7400957ac2edf0723ad9fd90cb79e393d972ae0c Mon Sep 17 00:00:00 2001 From: nemunaire Date: Wed, 21 Oct 2015 00:21:35 +0200 Subject: [PATCH 415/674] Bump version 4.0.dev3 --- nemubot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nemubot/__init__.py b/nemubot/__init__.py index 16173c9..84403e0 100644 --- a/nemubot/__init__.py +++ b/nemubot/__init__.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -__version__ = '4.0.dev2' +__version__ = '4.0.dev3' __author__ = 'nemunaire' from nemubot.modulecontext import ModuleContext From 089823d8843efddc6ac65ddf7030378e974f8bf3 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Wed, 14 Oct 2015 06:47:01 +0200 Subject: [PATCH 416/674] [rnd] New command to choice between cmd --- modules/rnd.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/modules/rnd.py b/modules/rnd.py index 6ecf9eb..84c5693 100644 --- a/modules/rnd.py +++ b/modules/rnd.py @@ -1,17 +1,20 @@ -# coding=utf-8 - """Help to make choice""" -import random +# PYTHON STUFFS ####################################################### +import random +import shlex + +from nemubot import context from nemubot.exception import IRCException from nemubot.hooks import hook - -nemubotversion = 3.4 +from nemubot.message import Command from more import Response +# MODULE INTERFACE #################################################### + @hook("cmd_hook", "choice") def cmd_choice(msg): if not len(msg.args): @@ -20,3 +23,17 @@ def cmd_choice(msg): return Response(random.choice(msg.args), channel=msg.channel, nick=msg.nick) + + +@hook("cmd_hook", "choicecmd") +def cmd_choice(msg): + if not len(msg.args): + raise IRCException("indicate some command to pick!") + + 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))] From 56c43179f31e70d2d4e531a197215448f1772134 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Thu, 22 Oct 2015 00:04:40 +0200 Subject: [PATCH 417/674] 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 418/674] 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 419/674] [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 420/674] [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 421/674] [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 422/674] 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 423/674] 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 424/674] 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 425/674] 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 426/674] 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 427/674] [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 428/674] [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 429/674] [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 430/674] [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 431/674] [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 432/674] 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 433/674] [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 434/674] 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 435/674] 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 436/674] 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 437/674] 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 438/674] [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 439/674] 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 440/674] [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 441/674] 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 442/674] 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 443/674] 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 444/674] 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 445/674] 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 446/674] [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 447/674] [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 448/674] [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 449/674] [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 450/674] [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 451/674] [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 452/674] [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 453/674] 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 454/674] 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 455/674] [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 456/674] 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 457/674] [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 458/674] [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 459/674] 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 460/674] 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 461/674] 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 462/674] 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 463/674] 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 464/674] 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 465/674] [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 466/674] 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 467/674] [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 468/674] [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 469/674] 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 470/674] 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 471/674] 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 472/674] [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 473/674] [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 474/674] [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 475/674] [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 476/674] [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 477/674] [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 478/674] [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 479/674] [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 480/674] [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 481/674] [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 482/674] [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 483/674] 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 484/674] [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 485/674] [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 486/674] [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 487/674] [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 488/674] 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 489/674] [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 490/674] 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 491/674] 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 492/674] 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 493/674] [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 494/674] 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 495/674] 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 496/674] 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 497/674] [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 498/674] [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 499/674] [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 500/674] 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 501/674] 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 502/674] [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 503/674] [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 504/674] [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 505/674] 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 506/674] 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 507/674] 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 508/674] 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 509/674] 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 510/674] 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 511/674] 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 512/674] 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 513/674] 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 514/674] 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 515/674] 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 516/674] 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 517/674] 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 518/674] 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 519/674] 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 520/674] 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 521/674] 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 522/674] 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 523/674] 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 524/674] 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 525/674] 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 526/674] 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 527/674] [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 528/674] 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 529/674] 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 530/674] 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 531/674] 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 532/674] 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 533/674] 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 534/674] 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 535/674] 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 536/674] 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 537/674] 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 538/674] 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 539/674] 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 540/674] 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 541/674] [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 542/674] 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 543/674] 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 544/674] 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 545/674] 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 546/674] 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 547/674] 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 548/674] 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 549/674] 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 550/674] 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 551/674] 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 552/674] 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 553/674] 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 554/674] 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 555/674] 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 556/674] 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 557/674] 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 558/674] 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 559/674] 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 560/674] 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 561/674] 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 562/674] 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 563/674] 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 564/674] 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 565/674] 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 566/674] 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 567/674] 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 568/674] 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 569/674] 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 570/674] 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 571/674] 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 572/674] 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 573/674] 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 574/674] 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 575/674] 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 576/674] 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 577/674] 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 578/674] 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 579/674] 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 580/674] 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 581/674] 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 582/674] 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 583/674] 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 584/674] 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 585/674] 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 586/674] 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 587/674] 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 588/674] 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 589/674] 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 590/674] 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 591/674] 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 592/674] 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 593/674] 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 594/674] 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 595/674] 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 596/674] 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 597/674] 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 598/674] 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 599/674] 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 600/674] [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 601/674] [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 602/674] [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 603/674] [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 604/674] 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 605/674] 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 606/674] [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 607/674] 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 608/674] 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 609/674] 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 610/674] 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 611/674] 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 612/674] 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 613/674] 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 614/674] 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 615/674] 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 616/674] 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 617/674] 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 618/674] 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 619/674] 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 620/674] 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 621/674] 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 622/674] 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 623/674] 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 624/674] 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 625/674] 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 626/674] 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 627/674] 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 628/674] 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 629/674] 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 630/674] 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 631/674] 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 632/674] 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 633/674] 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 634/674] 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 635/674] 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 636/674] 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 637/674] 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 638/674] 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 639/674] 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 640/674] 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 641/674] 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 642/674] 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 643/674] 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 644/674] 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 645/674] 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 646/674] 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 647/674] 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 648/674] 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 649/674] 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 650/674] 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 651/674] 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 652/674] 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 653/674] 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 654/674] 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 655/674] 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 656/674] 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 657/674] 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 658/674] 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 659/674] 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 660/674] 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 661/674] 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 662/674] 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 663/674] 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 664/674] 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 665/674] 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 666/674] 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 667/674] 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 668/674] 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 669/674] 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 670/674] 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 671/674] 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 672/674] 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 673/674] 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 674/674] 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)