diff --git a/.drone.yml b/.drone.yml deleted file mode 100644 index dccc156..0000000 --- a/.drone.yml +++ /dev/null @@ -1,26 +0,0 @@ ---- -kind: pipeline -type: docker -name: default-arm64 - -platform: - os: linux - arch: arm64 - -steps: - - name: build - image: python:3.11-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/.gitignore b/.gitignore index 6e6afac..50aca48 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ *# *~ -*.log TAGS *.py[cod] __pycache__ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..23cf4a0 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "modules/nextstop/external"] + path = modules/nextstop/external + url = git://github.com/nbr23/NextStop.git diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 8efd20f..0000000 --- a/.travis.yml +++ /dev/null @@ -1,12 +0,0 @@ -language: python -python: - - 3.4 - - 3.5 - - 3.6 - - 3.7 - - nightly -install: - - pip install -r requirements.txt - - pip install . -script: nosetests -w nemubot -sudo: false diff --git a/DCC.py b/DCC.py new file mode 100644 index 0000000..5dc46ea --- /dev/null +++ b/DCC.py @@ -0,0 +1,241 @@ +# -*- 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 . + +import imp +import os +import re +import socket +import sys +import time +import threading +import traceback + +import message +import server + +#Store all used ports +PORTS = list() + +class DCC(server.Server): + def __init__(self, srv, dest, socket=None): + server.Server.__init__(self) + + 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: + print ("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)) + print ('Accepted user from', host, port, "for", 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 + print ('Listen on', self.port, "for", 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() + print ('Connected by', 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: + print("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] + + print ("Closing connection with", 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/Dockerfile b/Dockerfile deleted file mode 100644 index b830622..0000000 --- a/Dockerfile +++ /dev/null @@ -1,21 +0,0 @@ -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 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 - -VOLUME /var/lib/nemubot - -COPY . /usr/src/app/ - -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 diff --git a/IRCServer.py b/IRCServer.py new file mode 100644 index 0000000..f354330 --- /dev/null +++ b/IRCServer.py @@ -0,0 +1,290 @@ +# -*- 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 . + +import errno +import os +import socket +import threading +import traceback + +from channel import Channel +from DCC import DCC +from hooks import Hook +import message +import server +import xmlparser + +class IRCServer(server.Server): + """Class to interact with an IRC server""" + + def __init__(self, node, nick, owner, realname): + """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 + + """ + server.Server.__init__(self) + + self.node = node + + self.nick = nick + self.owner = owner + self.realname = realname + + # 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.channel is not None: + if msg.channel in self.channels: + self.channels[msg.channel].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) + + 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()) + return True + else: + 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 + try: + 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.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: + print ("Unable to connect to %s:%d" % (self.host, self.port)) + return + self.connected = True + print ("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() + print ("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: + 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 ()) + + 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) diff --git a/README.md b/README.md index 6977c9f..e021df2 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,7 @@ -nemubot -======= +# *nemubot* An extremely modulable IRC bot, built around XML configuration files! +## Documentation -Requirements ------------- - -*nemubot* requires at least Python 3.3 to work. - -Some modules (like `cve`, `nextstop` or `laposte`) require the -[BeautifulSoup module](https://www.crummy.com/software/BeautifulSoup/), -but the core and framework has no dependency. - - -Installation ------------- - -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. +Have a look to the wiki at https://github.com/nemunaire/nemubot/wiki diff --git a/bin/nemubot b/bin/nemubot deleted file mode 100755 index c248802..0000000 --- a/bin/nemubot +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python3 - -# 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 . - -import sys - -from nemubot.__main__ import main - -if __name__ == "__main__": - main() diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..87bd1ea --- /dev/null +++ b/bot.py @@ -0,0 +1,641 @@ +# -*- 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 datetime import datetime +from datetime import timedelta +from queue import Queue +import threading +import time +import re + +import consumer +import event +import hooks +from networkbot import NetworkBot +from IRCServer import IRCServer +from DCC import DCC +import response + +ID_letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + +class Bot: + def __init__(self, ip, realname, mp=list()): + # Bot general informations + self.version = 3.3 + self.version_txt = "3.3-dev" + + # Save various informations + self.ip = ip + self.realname = realname + self.ctcp_capabilities = dict() + self.init_ctcp_capabilities() + + # Keep global context: servers and modules + self.servers = dict() + self.modules = dict() + + # Context paths + self.modules_path = mp + self.datas_path = './datas/' + + # Events + self.events = list() + self.event_timer = None + + # Own hooks + self.hooks = hooks.MessagesHook(self, self) + + # Other known bots, making a bots network + self.network = dict() + self.hooks_cache = dict() + + # Messages to be treated + self.cnsr_queue = Queue() + self.cnsr_thrd = list() + self.cnsr_thrd_size = -1 + + self.hooks.add_hook("irc_hook", + hooks.Hook(self.treat_prvmsg, "PRIVMSG"), + self) + + + def init_ctcp_capabilities(self): + """Reset existing CTCP capabilities to default one""" + self.ctcp_capabilities["ACTION"] = lambda 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( + msg.sender, "NEMUBOT %f" % self.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) + self.ctcp_capabilities["VERSION"] = lambda srv, msg: _ctcp_response( + msg.sender, "VERSION nemubot v%s" % self.version_txt) + + 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: + print ("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 + + # Add the event in place + t = evt.current + i = -1 + 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: + return None + + if module_src is not None: + module_src.REGISTERED_EVENTS.append(evt.id) + + return evt.id + + def del_event(self, id, module_src=None): + """Find and remove an event from list""" + if len(self.events) > 0 and id == self.events[0].id: + self.events.remove(self.events[0]) + self.update_timer() + if module_src is not None: + module_src.REGISTERED_EVENTS.remove(evt.id) + return True + + for evt in self.events: + if evt.id == id: + self.events.remove(evt) + + if module_src is not None: + module_src.REGISTERED_EVENTS.remove(evt.id) + return True + return False + + def update_timer(self): + """Relaunch 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: + #print ("Update timer, next in", self.events[0].time_left.seconds, + # "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() + else: + 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") + + 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 addServer(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 + if srv.autoconnect: + srv.launch(self.receive_message) + return True + else: + return False + + + def add_module(self, module): + """Add a module to the context, if already exists, unload the + old one before""" + # Check if the module already exists + for mod in self.modules.keys(): + if self.modules[mod].name == module.name: + self.unload_module(self.modules[mod].name) + break + + self.modules[module.name] = module + return True + + + 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, verb=False): + """Unload a module""" + if name in self.modules: + print (name) + self.modules[name].save() + if hasattr(self.modules[name], "unload"): + self.modules[name].unload(self) + # Remove registered hooks + for (s, h) in self.modules[name].REGISTERED_HOOKS: + self.hooks.del_hook(s, h) + # Remove registered events + for e in self.modules[name].REGISTERED_EVENTS: + self.del_event(e) + # Remove from the dict + del self.modules[name] + 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)) + + # Launch a new thread if necessary + self.update_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, verb=False): + """Save and unload modules and disconnect servers""" + if self.event_timer is not None: + if verb: print ("Stop the event timer...") + self.event_timer.cancel() + + if verb: print ("Save and unload all modules...") + k = list(self.modules.keys()) + for mod in k: + self.unload_module(mod, verb) + + if verb: print ("Close all servers connection...") + k = list(self.servers.keys()) + for srv in k: + self.servers[srv].disconnect() + +# 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): + """Remove from store the hook if it has been executed given time""" + if hook.times == 0: + if isinstance(store, dict): + store[hook.name].remove(hook) + if len(store) == 0: + del store[hook.name] + elif isinstance(store, list): + store.remove(hook) + + def treat_pre(self, msg, srv): + """Treat a message before all other treatment""" + 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 + if re.match("^ *(m[' ]?entends?[ -]+tu|h?ear me|do you copy|ping)", + msg.content, re.I) is not None: + return response.Response(msg.sender, message="pong", + channel=msg.channel, nick=msg.nick) + + # Ask hooks + else: + return self.treat_ask(msg, srv) + + def treat_prvmsg(self, msg, srv): + # First, treat CTCP + if msg.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") + + # 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 msg.content[0] == '`' and msg.nick == srv.owner: + #TODO: owner commands + pass + + elif msg.content[0] == '!' and len(msg.content) > 1: + # 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] == "more": + if msg.channel == 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] + + elif msg.cmds[0] == "dcc": + print("dcctest for", msg.sender) + srv.send_dcc("Hello %s!" % msg.nick, msg.sender) + elif msg.cmds[0] == "pvdcctest": + print("dcctest") + return Response(msg.sender, message="Test DCC") + elif msg.cmds[0] == "dccsendtest": + print("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 (res is None or len(res) <= 0) and msg.private: + return self.treat_prvmsg_ask(msg, srv) + 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.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.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.channel, server=srv) and (msg.private or lvl == 0 or bot.nick not in srv.channels[msg.channel].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.content in ask_hook: + hks, lvl, store, bot = ask_hook[msg.content] + 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 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.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 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.content in msg_hook: + hks, lvl, store, bot = msg_hook[msg.content] + 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 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.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 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) + + +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].help_tiny ()) for im in modules if hasattr(modules[im], "help_tiny")]) + return res + +def hotswap(bak): + return Bot(bak.servers, bak.modules, bak.modules_path) + +def reload(): + import imp + + import channel + imp.reload(channel) + + import consumer + imp.reload(consumer) + + import DCC + imp.reload(DCC) + + import event + imp.reload(event) + + import hooks + imp.reload(hooks) + + import importer + imp.reload(importer) + + import message + imp.reload(message) + + import prompt.builtins + imp.reload(prompt.builtins) + + import server + imp.reload(server) + + import xmlparser + imp.reload(xmlparser) + import xmlparser.node + imp.reload(xmlparser.node) diff --git a/bot_sample.xml b/bot_sample.xml index ed1a41f..8b19830 100644 --- a/bot_sample.xml +++ b/bot_sample.xml @@ -1,23 +1,13 @@ - - - + + - - - - - - - - - - - + + + + + + + + diff --git a/channel.py b/channel.py new file mode 100644 index 0000000..6a67d76 --- /dev/null +++ b/channel.py @@ -0,0 +1,102 @@ +# 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 . + +class Channel: + def __init__(self, name, password=None): + self.name = name + self.password = password + self.people = dict() + self.topic = "" + + def treat(self, cmd, msg): + if cmd == "353": + self.parse353(msg) + elif cmd == "332": + self.parse332(msg) + elif cmd == "MODE": + self.mode(msg) + elif cmd == "JOIN": + self.join(msg.nick) + elif cmd == "NICK": + self.nick(msg.nick, msg.content) + elif cmd == "PART" or cmd == "QUIT": + self.part(msg.nick) + elif cmd == "TOPIC": + self.topic = self.content + + def join(self, nick, level = 0): + """Someone join the channel""" + #print ("%s arrive sur %s" % (nick, self.name)) + self.people[nick] = level + + def chtopic(self, newtopic): + """Send command to change the topic""" + self.srv.send_msg(self.name, newtopic, "TOPIC") + self.topic = newtopic + + 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)) + lvl = self.people[oldnick] + del self.people[oldnick] + self.people[newnick] = lvl + + def part(self, nick): + """Someone leave the channel""" + if nick in self.people: + #print ("%s vient de quitter %s" % (nick, self.name)) + del self.people[nick] + + def mode(self, msg): + if msg.content[0] == "-k": + self.password = "" + elif msg.content[0] == "+k": + if len(msg.content) > 1: + self.password = ' '.join(msg.content[1:])[1:] + else: + self.password = msg.content[1] + elif msg.content[0] == "+o": + self.people[msg.nick] |= 4 + elif msg.content[0] == "-o": + self.people[msg.nick] &= ~4 + elif msg.content[0] == "+h": + self.people[msg.nick] |= 2 + elif msg.content[0] == "-h": + self.people[msg.nick] &= ~2 + elif msg.content[0] == "+v": + self.people[msg.nick] |= 1 + elif msg.content[0] == "-v": + self.people[msg.nick] &= ~1 + + def parse332(self, msg): + self.topic = msg.content + + def parse353(self, msg): + for p in msg.content: + p = p.decode() + if p[0] == "@": + level = 4 + elif p[0] == "%": + level = 2 + elif p[0] == "+": + level = 1 + else: + self.join(p, 0) + continue + self.join(p[1:], level) diff --git a/consumer.py b/consumer.py new file mode 100644 index 0000000..a443dca --- /dev/null +++ b/consumer.py @@ -0,0 +1,143 @@ +# -*- 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 . + +import queue +import re +import threading +import traceback +import sys + +import bot +from DCC import DCC +from message import Message +import response +import server + +class MessageConsumer: + """Store a message before treating""" + def __init__(self, srv, raw, time, prvt, data): + self.srv = srv + self.raw = raw + self.time = time + self.prvt = prvt + self.data = data + + + 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): + # All messages + context.treat_pre(msg, self.srv) + + return context.treat_irc(msg, self.srv) + + 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) + + 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)): + print ("\033[1;35mWarning:\033[0m the server defined in this " + "response doesn't exist: %s" % (res.server)) + res.server = None + if res.server is None: + res.server = self.srv + + # Sent the message only if treat_post authorize it + 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: + print ("\033[1;35mWarning:\033[0m 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) + res = self.treat_in(context, msg) + except: + print ("\033[1;31mERROR:\033[0m 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) + return + + # Send message + self.treat_out(context, res) + + # Inform that the message has been treated + self.srv.msg_treated(self.data) + + + +class EventConsumer: + """Store a event before treating""" + def __init__(self, evt, timeout=20): + self.evt = evt + self.timeout = timeout + + + def run(self, context): + try: + self.evt.launch_check() + except: + print ("\033[1;31mError:\033[0m during event end") + exc_type, exc_value, exc_traceback = sys.exc_info() + traceback.print_exception(exc_type, exc_value, + exc_traceback) + if self.evt.next is not None: + context.add_event(self.evt, self.evt.id) + + + +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: + stm = self.context.cnsr_queue.get(True, 20) + stm.run(self.context) + + except queue.Empty: + pass + finally: + self.context.cnsr_thrd_size -= 2 diff --git a/credits.py b/credits.py new file mode 100644 index 0000000..fc0978e --- /dev/null +++ b/credits.py @@ -0,0 +1,43 @@ +# 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/datas/datas b/datas/datas new file mode 100644 index 0000000..e69de29 diff --git a/event.py b/event.py new file mode 100644 index 0000000..89b10f3 --- /dev/null +++ b/event.py @@ -0,0 +1,118 @@ +# -*- 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 datetime import datetime +from datetime import 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): + # What have we to check? + self.func = func + self.func_data = func_data + + # How detect a change? + self.check = check + 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) + 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 + self.call = call + if call_data is not None: + self.call_data = call_data + else: + self.call_data = func_data + + # How many times do this event? + self.times = times + + + @property + def current(self): + """Return the date of the near check""" + if self.times != 0: + if self.end is None: + self.end = datetime.now() + self.offset + self.intervalle + 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: + return self.current + elif self.end < datetime.now(): + self.end += self.intervalle + 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 + + def launch_check(self): + if self.func is None: + d = self.func_data + elif self.func_data is None: + d = self.func() + elif isinstance(self.func_data, dict): + d = self.func(**self.func_data) + else: + d = self.func(self.func_data) + #print ("do test with", d, self.cmp_data) + + if self.check is None: + if self.cmp_data is None: + r = True + else: + r = d != self.cmp_data + elif self.cmp_data is None: + r = self.check(d) + elif isinstance(self.cmp_data, dict): + r = self.check(d, **self.cmp_data) + else: + r = self.check(d, self.cmp_data) + + if r: + self.times -= 1 + if self.call_data is None: + if d is None: + self.call() + else: + self.call(d) + elif isinstance(self.call_data, dict): + self.call(d, **self.call_data) + else: + self.call(d, self.call_data) diff --git a/hooks.py b/hooks.py new file mode 100644 index 0000000..ea70bc5 --- /dev/null +++ b/hooks.py @@ -0,0 +1,220 @@ +# -*- 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 . + +import re + +from response import Response + +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""" + 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") + + if store in self.context.hooks_cache: + del self.context.hooks_cache[store] + + if not hasattr(self, store): + print ("\033[1;35mWarning:\033[0m unrecognized hook 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: + print ("\033[1;32mWarning:\033[0m unrecognized hook store type") + 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): + print ("Warning: 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): + 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 + + 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 + + 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) diff --git a/importer.py b/importer.py new file mode 100644 index 0000000..7f9ed62 --- /dev/null +++ b/importer.py @@ -0,0 +1,264 @@ +# -*- 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 importlib.abc import Finder +from importlib.abc import SourceLoader +import imp +import os +import sys + +import event +from hooks import Hook +import response +import xmlparser + +class ModuleFinder(Finder): + def __init__(self, context, prompt): + self.context = context + self.prompt = prompt + + 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_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")): + return ModuleLoader(self.context, self.prompt, + fullname, mpath, None) + #print ("not found") + return None + + +class ModuleLoader(SourceLoader): + def __init__(self, context, prompt, fullname, path, config_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"] + else: + self.config = None + + if os.path.isfile(path + fullname + ".py"): + self.source_path = 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" + self.package = True + self.mpath = path + self.name + "/" + else: + raise ImportError + + 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) + + # 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) + # Check module version + if module.nemubotversion != self.context.version: + raise ImportError("Module `%s' is not compatible with this " + "version." % self.name) + + # Set module common functions and datas + module.__LOADED__ = True + + # 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) + + 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) + else: + 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 + module.Response = response.Response + + # 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: + print ("\033[1;31mERROR:\033[0m 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) + + print (" Module `%s' successfully loaded." % module.name) + else: + raise ImportError("An error occurs while importing `%s'." + % module.name) + return module + + +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' " + "command hook." % (module.name, cmd["call"], cmd["name"])) + +def register_hooks(module, context, prompt): + """Register all available hooks""" + 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", Hook(module.parseanswer), module) + if hasattr(module, "parseask"): + 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_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) + else: + print("\033[1;35mWarning:\033[0m Try to send a message to the unknown server: %s" % server) diff --git a/message.py b/message.py new file mode 100644 index 0000000..d14a16d --- /dev/null +++ b/message.py @@ -0,0 +1,294 @@ +# -*- 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 datetime import datetime +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 + 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:]) + 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: + #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:]) + self.decode() + if self.cmd == 'PRIVMSG': + self.parse_content() + self.private = private + + 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) + except ValueError: + self.cmds = self.content.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): + """Decode the content string usign a specific encoding""" + if isinstance(self.content, bytes): + try: + self.content = self.content.decode() + except UnicodeDecodeError: + #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 # +# # +############################## + + 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 () + 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) + + 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: + 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/alias.py b/modules/alias.py deleted file mode 100644 index c432a85..0000000 --- a/modules/alias.py +++ /dev/null @@ -1,277 +0,0 @@ -"""Create alias of commands""" - -# PYTHON STUFFS ####################################################### - -import re -from datetime import datetime, timezone - -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 nemubot.module.more import Response - - -# LOADING ############################################################# - -def load(context): - """Load this module""" - 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") - - -# 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): - """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 msg is not None and (name == "chan" or name == "channel"): - return msg.channel - elif name == "date": - return datetime.now(timezone.utc).strftime("%c") - elif name in context.data.getNode("variables").index: - return context.data.getNode("variables").index[name]["value"] - else: - return None - - -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() - - -def replace_variables(cnts, msg): - """Replace variables contained in the content - - Arguments: - cnt -- content where search variables - msg -- Message where pick some variables - """ - - unsetCnt = list() - if not isinstance(cnts, list): - cnts = list(cnts) - resultCnt = list() - - for cnt in cnts: - 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, 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 - 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) - else: - 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) - - return resultCnt - - -# MODULE INTERFACE #################################################### - -## Variables management - -@hook.command("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["name"] for v in list_variables(user)] - if len(als) > 0: - res.append("%s's variables: %s" % (user, ", ".join(als))) - else: - 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(list_variables(), - channel=msg.channel, - title="Known variables") - else: - return Response("There is currently no variable stored.", channel=msg.channel) - - -@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): - 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.frm) - return Response("Variable $%s successfully defined." % msg.args[0], - channel=msg.channel) - - -## Alias management - -@hook.command("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): - 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.command("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.") - - 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]) - - 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.frm) - - elif len(msg.args) > 1: - create_alias(alias.cmd, - " ".join(msg.args[1:]), - channel=msg.channel, - creator=msg.frm) - return Response("New alias %s successfully registered." % alias.cmd, - channel=msg.channel) - - 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", - help="Remove a previously created alias") -def cmd_unalias(msg): - if not len(msg.args): - raise IMException("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 doesn't exist anymore." % alias, - channel=msg.channel)) - else: - res.append(Response("%s is not an alias" % alias, - channel=msg.channel)) - return res - - -## Alias replacement - -@hook.add(["pre","Command"]) -def treat_alias(msg): - 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): - 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 not isinstance(rpl_msg, Command) or msg.cmd != rpl_msg.cmd: - return rpl_msg - - return msg diff --git a/modules/alias/__init__.py b/modules/alias/__init__.py new file mode 100644 index 0000000..6904f19 --- /dev/null +++ b/modules/alias/__init__.py @@ -0,0 +1,156 @@ +# coding=utf-8 + +import re +import sys +from datetime import datetime + +nemubotversion = 3.3 + +def load(context): + """Load this module""" + from hooks import Hook + 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")) + DATAS.getNode("aliases").setIndex("alias") + if not DATAS.hasNode("variables"): + DATAS.addChild(ModuleState("variables")) + 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" + +def set_variable(name, value): + var = ModuleState("variable") + var["name"] = name + var["value"] = value + DATAS.getNode("variables").addChild(var) + +def get_variable(name, msg=None): + if name == "sender": + return msg.sender + elif name == "nick": + return msg.nick + 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)) + elif name in DATAS.getNode("variables").index: + return DATAS.getNode("variables").index[name]["value"] + else: + return "" + +def cmd_set(msg): + if len (msg.cmds) > 2: + set_variable(msg.cmds[1], " ".join(msg.cmds[2:])) + 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_alias(msg): + 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(msg.sender, "!%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, + channel=msg.channel)) + return res + else: + return Response(msg.sender, "!alias prend en argument l'alias à étendre.", + channel=msg.channel) + +def cmd_unalias(msg): + if len (msg.cmds) > 1: + res = list() + 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.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)) + else: + res.append(Response(msg.sender, "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)) + return res + else: + return Response(msg.sender, "!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) + return " ".join(cnt) + + +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 True + +def treat_alias(msg, hooks_cache): + if msg.cmd == "PRIVMSG" 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() + return True + return False + + +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 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: + 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)) + save() + return res + return False diff --git a/modules/birthday.py b/modules/birthday.py index e1406d4..74013e0 100644 --- a/modules/birthday.py +++ b/modules/birthday.py @@ -1,134 +1,111 @@ -"""People birthdays and ages""" - -# PYTHON STUFFS ####################################################### +# coding=utf-8 import re import sys -from datetime import date, datetime +from datetime import datetime +from datetime import date -from nemubot import context -from nemubot.exception import IMException -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 +from xmlparser.node import ModuleState -from nemubot.module.more import Response - - -# LOADING ############################################################# +nemubotversion = 3.3 def load(context): - context.data.setIndex("name", "birthday") + global DATAS + DATAS.setIndex("name", "birthday") -# MODULE CORE ######################################################### +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 :)" + def findName(msg): - if (not len(msg.args) or msg.args[0].lower() == "moi" or - msg.args[0].lower() == "me"): - name = msg.frm.lower() + if len(msg.cmds) < 2 or msg.cmds[1].lower() == "moi" or msg.cmds[1].lower() == "me": + name = msg.nick.lower() else: - name = msg.args[0].lower() + name = msg.cmds[1].lower() matches = [] - if name in context.data.index: + if name in DATAS.index: matches.append(name) else: - for k in context.data.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) -# MODULE INTERFACE #################################################### - -## Commands - -@hook.command("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: name = matches[0] - tyd = context.data.index[name].getDate("born") + tyd = DATAS.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( - context.data.index[name].getDate("born"), "", - "C'est aujourd'hui l'anniversaire de %s !" - " Il a %s. Joyeux anniversaire :)" % (name, "%s")), + return Response(msg.sender, msg.countdown_format( + 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(): tyd = datetime(date.today().year + 1, tyd.month, tyd.day) - return Response(countdown_format(tyd, + return Response(msg.sender, msg.countdown_format(tyd, "Il reste %s avant l'anniversaire de %s !" % ("%s", name), ""), msg.channel) else: - return Response("désolé, je ne connais pas la date d'anniversaire" + return Response(msg.sender, "désolé, je ne connais pas la date d'anniversaire" " de %s. Quand est-il né ?" % name, - msg.channel, msg.frm) + msg.channel, msg.nick) - -@hook.command("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: name = matches[0] - d = context.data.index[name].getDate("born") + d = DATAS.index[name].getDate("born") - return Response(countdown_format(d, - "%s va naître dans %s." % (name, "%s"), - "%s a %s." % (name, "%s")), + return Response(msg.sender, msg.countdown_format(d, + "%s va naître dans %s." % (name, "%s"), + "%s a %s." % (name, "%s")), msg.channel) else: - return Response("désolé, je ne connais pas l'âge de %s." - " Quand est-il né ?" % name, msg.channel, msg.frm) + return Response(msg.sender, "désolé, je ne connais pas l'âge de %s." + " Quand est-il né ?" % name, msg.channel, msg.nick) return True - -## Input parsing - -@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.message, re.I) - if res is not None: + msgl = msg.content.lower () + if re.match("^.*(date de naissance|birthday|geburtstag|née? |nee? le|born on).*$", msgl) is not None: try: - extDate = extractDate(msg.message) + extDate = msg.extractDate() if extDate is None or extDate.year > datetime.now().year: - return Response("la date de naissance ne paraît pas valide...", + return Response(msg.sender, + "ta date de naissance ne paraît pas valide...", msg.channel, - msg.frm) + msg.nick) 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.frm - if nick.lower() in context.data.index: - context.data.index[nick.lower()]["born"] = extDate + if msg.nick.lower() in DATAS.index: + DATAS.index[msg.nick.lower()]["born"] = extDate else: ms = ModuleState("birthday") - ms.setAttribute("name", nick.lower()) + ms.setAttribute("name", msg.nick.lower()) ms.setAttribute("born", extDate) - 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")), + 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"), msg.channel, - msg.frm) + msg.nick) except: - raise IMException("la date de naissance ne paraît pas valide.") + return Response(msg.sender, "ta date de naissance ne paraît pas valide...", + msg.channel, msg.nick) diff --git a/modules/birthday.xml b/modules/birthday.xml new file mode 100644 index 0000000..e03a15b --- /dev/null +++ b/modules/birthday.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/modules/bonneannee.py b/modules/bonneannee.py index 1829bce..9c65b2d 100644 --- a/modules/bonneannee.py +++ b/modules/bonneannee.py @@ -1,74 +1,51 @@ -"""Wishes Happy New Year when the time comes""" +# coding=utf-8 -# PYTHON STUFFS ####################################################### +from datetime import datetime -from datetime import datetime, timezone - -from nemubot.event import ModuleEvent -from nemubot.hooks import hook -from nemubot.tools.countdown import countdown_format - -from nemubot.module.more import Response - - -# GLOBALS ############################################################# - -yr = datetime.now(timezone.utc).year -yrn = datetime.now(timezone.utc).year + 1 - - -# LOADING ############################################################# +nemubotversion = 3.3 def load(context): - 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" - "") + yr = datetime.today().year + yrn = datetime.today().year + 1 - def bonneannee(): - txt = "Bonne année %d !" % yrn - print(txt) - 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"] - context.send_response(srv, Response(txt, chan)) + 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)) - d = datetime(yrn, 1, 1, 0, 0, 0, 0, - timezone.utc) - datetime.now(timezone.utc) - context.add_event(ModuleEvent(interval=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) + 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, "#ordissimo")) + send_response("localhost:2771", Response(None, txt, "#42sh")) + send_response("localhost:2771", Response(None, txt, "#nemubot")) -# MODULE INTERFACE #################################################### - -@hook.command("newyear", - help="Display the remaining time before the next new year") -@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, - timezone.utc), - "Il reste %s avant la nouvelle année.", - "Nous faisons déjà la fête depuis %s !"), +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 !"), channel=msg.channel) - -@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) + yr = int(msg.cmds[0]) 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(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")), channel=msg.channel) diff --git a/modules/books.py b/modules/books.py deleted file mode 100644 index 5ab404b..0000000 --- a/modules/books.py +++ /dev/null @@ -1,115 +0,0 @@ -"""Looking for books""" - -# PYTHON STUFFS ####################################################### - -import urllib - -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 - - -# LOADING ############################################################# - -def load(context): - 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" - "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" % - (context.config["goodreadskey"], urllib.parse.quote(title))) - if response is not None and len(response.getElementsByTagName("book")): - return response.getElementsByTagName("book")[0] - else: - return None - - -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 len(response.getElementsByTagName("search")): - return response.getElementsByTagName("search")[0].getElementsByTagName("results")[0].getElementsByTagName("work") - else: - return [] - - -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 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.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 - - -# MODULE INTERFACE #################################################### - -@hook.command("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 IMException("please give me a title to search") - - book = get_book(" ".join(msg.args)) - if book is None: - 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, - web.striphtml(book.getElementsByTagName("description")[0].firstChild.nodeValue if book.getElementsByTagName("description")[0].firstChild else ""))) - return res - - -@hook.command("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 IMException("please give me a title to search") - - title = " ".join(msg.args) - res = Response(channel=msg.channel, - title="%s" % (title), - count=" (%d more books)") - - for book in search_books(title): - 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 - - -@hook.command("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 IMException("please give me an author to search") - - name = " ".join(msg.args) - ath = search_author(name) - if ath is None: - 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/cat.py b/modules/cat.py deleted file mode 100644 index 5eb3e19..0000000 --- a/modules/cat.py +++ /dev/null @@ -1,55 +0,0 @@ -"""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 nemubot.module.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 diff --git a/modules/chronos.py b/modules/chronos.py new file mode 100644 index 0000000..261cb09 --- /dev/null +++ b/modules/chronos.py @@ -0,0 +1,96 @@ +# 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 new file mode 100644 index 0000000..2dcf2b6 --- /dev/null +++ b/modules/chronos.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/modules/cmd_server.py b/modules/cmd_server.py new file mode 100644 index 0000000..3624c26 --- /dev/null +++ b/modules/cmd_server.py @@ -0,0 +1,201 @@ +# -*- 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 networkbot import NetworkBot + +nemubotversion = 3.3 +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) + else: + return (None, toks) + +def close(data, toks, context, prompt): + """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].disconnect() + del context.servers[s] + else: + print ("close: server `%s' not found." % s) + elif prompt.selectedServer is not None: + prompt.selectedServer.disconnect() + del prompt.servers[selectedServer.id] + prompt.selectedServer = None + return + +def connect(data, toks, context, prompt): + """Make the connexion to a server""" + if len(toks) > 1: + for s in toks[1:]: + if s in context.servers: + context.servers[s].launch(context.receive_message) + else: + print ("connect: server `%s' not found." % s) + + elif prompt.selectedServer is not None: + prompt.selectedServer.launch(context.receive_message) + else: + print (" Please SELECT a server or give its name in argument.") + +def disconnect(data, toks, context, prompt): + """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].disconnect(): + 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(): + print ("disconnect: server `%s' already disconnected." + % prompt.selectedServer.id) + else: + print (" Please SELECT a server or give its name in argument.") + +def discover(data, toks, context, prompt): + """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.") + +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() + else: + print (" Please SELECT a server or give its name in argument.") + +def join(data, toks, context, prompt): + """Join or leave a channel""" + rd = 1 + if len(toks) <= rd: + print ("%s: not enough arguments." % toks[0]) + 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 ("%s: not enough arguments." % toks[0]) + return + + if toks[0] == "join": + if len(toks) > rd + 1: + srv.join(toks[rd], toks[rd + 1]) + else: + srv.join(toks[rd]) + elif toks[0] == "leave" or toks[0] == "part": + srv.leave(toks[rd]) + return + +def save_mod(data, toks, context, prompt): + """Force save module data""" + if len(toks) < 2: + print ("save: not enough arguments.") + return + + for mod in toks[1:]: + if mod in context.modules: + context.modules[mod].save() + print ("save: module `%s´ saved successfully" % mod) + else: + print ("save: no module named `%s´" % mod) + return + +def send(data, toks, context, prompt): + """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 + + #Check the server is connected + if not srv.connected: + print ("send: server `%s' not connected." % srv.id) + return + + 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: + print ("send: not enough arguments.") + return + + srv.send_msg_final(chan, toks[rd]) + return "done" + +def zap(data, toks, context, prompt): + """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.") diff --git a/modules/cmd_server.xml b/modules/cmd_server.xml new file mode 100644 index 0000000..e37c1e4 --- /dev/null +++ b/modules/cmd_server.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/modules/conjugaison.py b/modules/conjugaison.py deleted file mode 100644 index c953da3..0000000 --- a/modules/conjugaison.py +++ /dev/null @@ -1,94 +0,0 @@ -"""Find french conjugaison""" - -# PYTHON STUFFS ####################################################### - -from collections import defaultdict -import re -from urllib.parse import quote - -from nemubot.exception import IMException -from nemubot.hooks import hook -from nemubot.tools import web -from nemubot.tools.web import striphtml - -from nemubot.module.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'), - ('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) - - -# MODULE CORE ######################################################### - -def get_conjug(verb, stringTens): - url = ("https://leconjugueur.lefigaro.fr/conjugaison/verbe/%s.html" % - quote(verb.encode("ISO-8859-1"))) - 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) - return list() - - -def compute_line(line, stringTens): - try: - idTemps = d[stringTens] - except: - raise IMException("le temps demandé n'existe pas") - - if len(idTemps) == 0: - raise IMException("le temps demandé n'existe pas") - - index = line.index('
([^/]*/b>)", newLine): - res.append(striphtml(elt.group(1) - .replace("", "\x02") - .replace("", "\x0F"))) - return res - - -# MODULE INTERFACE #################################################### - -@hook.command("conjugaison", - help_usage={ - "TENS VERB": "give the conjugaison for VERB in TENS." - }) -def cmd_conjug(msg): - if len(msg.args) < 2: - raise IMException("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 IMException("aucune conjugaison de '%s' n'a été trouvé" % verb) diff --git a/modules/cristal.py b/modules/cristal.py new file mode 100644 index 0000000..fb674ea --- /dev/null +++ b/modules/cristal.py @@ -0,0 +1,64 @@ +# coding=utf-8 + +from tools import web + +nemubotversion = 3.3 + +def help_tiny (): + """Line inserted in the response to the command !help""" + return "Gets information about Cristal missions" + +def help_full (): + return "!cristal [id|name] : gives information about id Cristal mission." + + +def get_all_missions(): + print (web.getContent(CONF.getNode("server")["url"])) + response = web.getXML(CONF.getNode("server")["url"]) + print (CONF.getNode("server")["url"]) + if response is not None: + return response.getNodes("mission") + else: + return None + +def get_mission(id=None, name=None, people=None): + missions = get_all_missions() + if missions is not None: + for m in missions.childs: + if id is not None and m.getFirstNode("id").getContent() == id: + return m + elif (name is not None or name in m.getFirstNode("title").getContent()) and (people is not None or people in m.getFirstNode("contact").getContent()): + return m + return None + +def cmd_cristal(msg): + if len(msg.cmds) > 1: + srch = msg.cmds[1] + else: + srch = "" + + res = Response(msg.sender, channel=msg.channel, nomore="Je n'ai pas d'autre mission à afficher") + + try: + id=int(srch) + name="" + except: + id=None + name=srch + + missions = get_all_missions() + if missions is not None: + print (missions) + for m in missions: + print (m) + idm = m.getFirstNode("id").getContent() + crs = m.getFirstNode("title").getContent() + contact = m.getFirstNode("contact").getDate() + updated = m.getFirstNode("updated").getDate() + content = m.getFirstNode("content").getContent() + + res.append_message(msg, crs + " ; contacter : " + contact + " : " + content) + else: + res.append_message("Aucune mission n'a été trouvée") + + return res diff --git a/modules/cristal.xml b/modules/cristal.xml new file mode 100644 index 0000000..3e83d90 --- /dev/null +++ b/modules/cristal.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/modules/ctfs.py b/modules/ctfs.py deleted file mode 100644 index ac27c4a..0000000 --- a/modules/ctfs.py +++ /dev/null @@ -1,32 +0,0 @@ -"""List upcoming CTFs""" - -# 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://ctftime.org/event/list/upcoming' - - -# MODULE INTERFACE #################################################### - -@hook.command("ctfs", - help="Display the upcoming CTFs") -def get_info_yt(msg): - 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) == 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 diff --git a/modules/cve.py b/modules/cve.py deleted file mode 100644 index 18d9898..0000000 --- a/modules/cve.py +++ /dev/null @@ -1,99 +0,0 @@ -"""Read CVE in your IM client""" - -# PYTHON STUFFS ####################################################### - -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 - -from nemubot.module.more import Response - -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", - - "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)) - - vuln = {} - - for vd in VULN_DATAS: - r = soup.body.find(attrs={"data-testid": VULN_DATAS[vd]}) - if r: - vuln[vd] = r.text.strip() - - return vuln - - -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 #################################################### - -@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) - - for cve_id in msg.args: - if cve_id[:3].lower() != 'cve': - cve_id = 'cve-' + cve_id - - cve = get_cve(cve_id) - 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}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}), last modified on \x02{last_modified}\x0F. {description}".format(alert=alert, metrics=metrics, **cve), title=cve_id) - - return res diff --git a/modules/ddg.py b/modules/ddg.py deleted file mode 100644 index 089409b..0000000 --- a/modules/ddg.py +++ /dev/null @@ -1,138 +0,0 @@ -"""Search around DuckDuckGo search engine""" - -# PYTHON STUFFS ####################################################### - -from urllib.parse import quote - -from nemubot.exception import IMException -from nemubot.hooks import hook -from nemubot.tools import web - -from nemubot.module.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 IMException("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 None - 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.command("define") -def define(msg): - if not len(msg.args): - raise IMException("Indicate a term to define") - - s = do_search(msg.args) - - if not s.definition: - raise IMException("no definition found for '%s'." % " ".join(msg.args)) - - return Response(s.definition, channel=msg.channel) - -@hook.command("search") -def search(msg): - if not len(msg.args): - raise IMException("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([r for r 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 new file mode 100644 index 0000000..77aee50 --- /dev/null +++ b/modules/ddg/DDGSearch.py @@ -0,0 +1,68 @@ +# coding=utf-8 + +from urllib.parse import quote +from urllib.request import urlopen + +import xmlparser +from tools import web + +class DDGSearch: + def __init__(self, terms): + self.terms = terms + + raw = urlopen("https://api.duckduckgo.com/?q=%s&format=xml" % quote(terms), timeout=10) + self.ddgres = xmlparser.parse_string(raw.read()) + + @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/WFASearch.py b/modules/ddg/WFASearch.py new file mode 100644 index 0000000..b91fa2c --- /dev/null +++ b/modules/ddg/WFASearch.py @@ -0,0 +1,71 @@ +# coding=utf-8 + +from urllib.parse import quote +from urllib.request import urlopen + +import xmlparser + +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 = xmlparser.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\nRegister at " + "http://products.wolframalpha.com/api/") + self.wfares = None + + @property + def success(self): + try: + return self.wfares["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": + return "An error occurs during computation: " + self.wfares.getNode("error").getNode("msg").getContent() + elif self.wfares.hasNode("didyoumeans"): + start = "Did you mean: " + tag = "didyoumean" + end = "?" + elif self.wfares.hasNode("tips"): + start = "Tips: " + tag = "tip" + end = "" + elif self.wfares.hasNode("relatedexamples"): + start = "Related examples: " + tag = "relatedexample" + end = "" + elif self.wfares.hasNode("futuretopic"): + return self.wfares.getNode("futuretopic")["msg"] + else: + return "An error occurs during computation" + proposal = list() + for dym in self.wfares.getNode(tag + "s").getNodes(tag): + if tag == "tip": + proposal.append(dym["text"]) + elif tag == "relatedexample": + proposal.append(dym["desc"]) + else: + proposal.append(dym.getContent()) + 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 diff --git a/modules/ddg/Wikipedia.py b/modules/ddg/Wikipedia.py new file mode 100644 index 0000000..314af38 --- /dev/null +++ b/modules/ddg/Wikipedia.py @@ -0,0 +1,56 @@ +# 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 = 0 + + 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"(|]*/>|]*>[^>]*|]*>[^>]*|\{\{[^{}]*\}\}|\[\[([^\[\]]*\[\[[^\]\[]*\]\])+[^\[\]]*\]\]|\{\{([^{}]*\{\{[^{}]*\}\}[^{}]*)+\}\}|\{\{([^{}]*\{\{([^{}]*\{\{[^{}]*\}\}[^{}]*)+\}\}[^{}]*)+\}\}|\[\[[^\]|]+(\|[^\]\|]+)*\]\])|#\* ''" + "\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 new file mode 100644 index 0000000..ff50274 --- /dev/null +++ b/modules/ddg/__init__.py @@ -0,0 +1,129 @@ +# coding=utf-8 + +import imp + +nemubotversion = 3.3 + +from . import DDGSearch +from . import WFASearch +from . import Wikipedia + +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")) + +def reload(): + imp.reload(DDGSearch) + imp.reload(WFASearch) + imp.reload(Wikipedia) + + +def define(msg): + if len(msg.cmds) <= 1: + return Response(msg.sender, + "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.append_message(s.definition) + + return res + + +def search(msg): + if len(msg.cmds) <= 1: + return Response(msg.sender, + "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", + 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) + + return res + + +def calculate(msg): + if len(msg.cmds) <= 1: + return Response(msg.sender, + "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") + 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) + + +def wiki(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 + 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) + + 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: + tout.remove(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/dig.py b/modules/dig.py deleted file mode 100644 index bec0a87..0000000 --- a/modules/dig.py +++ /dev/null @@ -1,94 +0,0 @@ -"""DNS resolver""" - -# PYTHON STUFFS ####################################################### - -import ipaddress -import socket - -import dns.exception -import dns.name -import dns.rdataclass -import dns.rdatatype -import dns.resolver - -from nemubot.exception import IMException -from nemubot.hooks import hook - -from nemubot.module.more import Response - - -# MODULE INTERFACE #################################################### - -@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.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") - - 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(".")] - - 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("%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 diff --git a/modules/disas.py b/modules/disas.py deleted file mode 100644 index cb80ef3..0000000 --- a/modules/disas.py +++ /dev/null @@ -1,89 +0,0 @@ -"""The Ultimate Disassembler Module""" - -# PYTHON STUFFS ####################################################### - -import capstone - -from nemubot.exception import IMException -from nemubot.hooks import hook - -from nemubot.module.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 diff --git a/modules/events.py b/modules/events.py deleted file mode 100644 index acac196..0000000 --- a/modules/events.py +++ /dev/null @@ -1,296 +0,0 @@ -"""Create countdowns and reminders""" - -import calendar -from datetime import datetime, timedelta, timezone -from functools import partial -import re - -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.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.keys()) if hasattr(context, "datas") else "") + "\n!eventslist: gets list of timer\n!start /something/: launch a timer" - - -def load(context): - context.set_knodes({ - "dict": DictNode, - "event": Event, - }) - - 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(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() - - -@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) - 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) - - -@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) - return Response(countdown_format(ndate, - "Il reste %s avant le week-end, courage ;)", - "Youhou, on est en week-end depuis %s."), - channel=msg.channel) - - -@hook.command("start") -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: - raise IMException("%s existe déjà." % msg.args[0]) - - 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]) - 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 - 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: - evt.end = datetime(yea, int(result2.group(3)), int(result2.group(2)), hou, minu, sec, timezone.utc) - elif result2 is not None: - 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: - evt.end = datetime(now.year, now.month, now.day, hou, minu, sec, timezone.utc) - else: - evt.end = datetime(now.year, now.month, now.day + 1, hou, minu, sec, timezone.utc) - except: - 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: - evt.end = msg.date - for (t, g) in result1: - if g is None or g == "" or g == "m" or g == "M": - evt.end += timedelta(minutes=int(t)) - elif g == "h" or g == "H": - evt.end += timedelta(hours=int(t)) - elif g == "d" or g == "D" or g == "j" or g == "J": - evt.end += timedelta(days=int(t)) - elif g == "w" or g == "W": - evt.end += timedelta(days=int(t)*7) - elif g == "y" or g == "Y" or g == "a" or g == "A": - evt.end += timedelta(days=int(t)*365) - 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() - - 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"), - 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")), - channel=msg.channel) - - -@hook.command("end") -@hook.command("forceend") -def end_countdown(msg): - if len(msg.args) < 1: - raise IMException("quel événement terminer ?") - - 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[msg.args[0]].creator)) - else: - return Response("%s n'est pas un compteur connu."% (msg.args[0]), channel=msg.channel, nick=msg.frm) - - -@hook.command("eventslist") -def liste(msg): - """!eventslist: gets list of timer""" - if len(msg.args): - res = Response(channel=msg.channel) - for user in msg.args: - cmptr = [k for k in context.data if context.data[k].creator == user] - if len(cmptr) > 0: - res.append_message(cmptr, title="Events created by %s" % user) - else: - res.append_message("%s doesn't have any counting events" % user) - return res - else: - 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) -def parseanswer(msg): - 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.frm - - 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[msg.cmd].start))) - else: - res.append_message(countdown_format(context.data[msg.cmd].start, context.data[msg.cmd]["msg_before"], context.data[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(match=lambda msg: RGXP_ask.match(msg.message)) -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: - 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) - if texts is not None and texts.group(3) is not None: - extDate = extractDate(msg.message) - 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 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.frm - 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.frm - 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.") diff --git a/modules/events.xml b/modules/events.xml new file mode 100644 index 0000000..a96794d --- /dev/null +++ b/modules/events.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/modules/events/__init__.py b/modules/events/__init__.py new file mode 100644 index 0000000..c331157 --- /dev/null +++ b/modules/events/__init__.py @@ -0,0 +1,238 @@ +# 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, vacs, " + (", ".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 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: + + 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])) + +def end_countdown(msg): + 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) + 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])) + +def liste(msg): + msg.send_snd ("Compteurs connus : %s." % ", ".join(DATAS.index.keys())) + +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à.") diff --git a/modules/freetarifs.py b/modules/freetarifs.py deleted file mode 100644 index 49ad8a6..0000000 --- a/modules/freetarifs.py +++ /dev/null @@ -1,64 +0,0 @@ -"""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 nemubot.module.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 diff --git a/modules/github.py b/modules/github.py deleted file mode 100644 index 5f9a7d9..0000000 --- a/modules/github.py +++ /dev/null @@ -1,231 +0,0 @@ -"""Repositories, users or issues on GitHub""" - -# PYTHON STUFFS ####################################################### - -import re -from urllib.parse import quote - -from nemubot.exception import IMException -from nemubot.hooks import hook -from nemubot.tools import web - -from nemubot.module.more import Response - - -# MODULE CORE ######################################################### - -def info_repos(repo): - return web.getJSON("https://api.github.com/search/repositories?q=%s" % - quote(repo)) - - -def info_user(username): - 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)) - - 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"]: - fullname = rp["items"][0]["full_name"] - else: - fullname = repo - - 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): - rp = info_repos(repo) - if rp["items"]: - fullname = rp["items"][0]["full_name"] - else: - fullname = repo - - 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)) - - -# 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") - - repos = info_repos(" ".join(msg.args)) - - 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"])) - - return res - - -@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") - - res = Response(channel=msg.channel, nomore="No more user") - - user = info_user(" ".join(msg.args)) - - 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 IMException("User not found") - - 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={ - "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") - - issue = None - - li = re.match("^#?([0-9]+)$", msg.args[0]) - ri = re.match("^#?([0-9]+)$", msg.args[-1]) - if li is not None: - issue = li.group(1) - del msg.args[0] - elif ri is not None: - issue = ri.group(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) - - issues = info_issue(repo, issue) - - if issues is None: - 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" % - (issue["state"][0].upper(), - issue["state"][1:], - issue["number"], - issue["title"], - issue["user"]["login"], - issue["created_at"], - issue["body"].replace("\n", " "))) - return res - - -@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") - - commit = None - 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.args) - - count = " (%d more commits)" if commit is None else None - res = Response(channel=msg.channel, nomore="No more commit", count=count) - - commits = info_commit(repo, commit) - - if commits is None: - raise IMException("Repository or commit not found") - - 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 diff --git a/modules/grep.py b/modules/grep.py deleted file mode 100644 index fde8ecb..0000000 --- a/modules/grep.py +++ /dev/null @@ -1,85 +0,0 @@ -"""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 nemubot.module.more import Response - - -# MODULE CORE ######################################################### - -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 - msg -- The original message - icase -- like the --ignore-case parameter of grep - only -- like the --only-matching parameter of grep - """ - - fltr = re.compile(fltr, re.I if icase else 0) - - 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): - for j in range(len(r.messages[i]) - 1, -1, -1): - 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): - 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): - res = fltr.match(r.message) - if res: - if only: - r.message = res.group(1) if fltr.groups else res.group(0) - 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"}, - keywords={ - "nocase": "Perform case-insensitive matching", - "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") - - only = "only" in msg.kwargs - - 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, - only=only) if m is not None] - - if len(l) <= 0: - raise IMException("Pattern not found in output") - - return l diff --git a/modules/imdb.py b/modules/imdb.py deleted file mode 100644 index 7a42935..0000000 --- a/modules/imdb.py +++ /dev/null @@ -1,115 +0,0 @@ -"""Show many information about a movie or serie""" - -# PYTHON STUFFS ####################################################### - -import re -import urllib.parse - -from bs4 import BeautifulSoup - -from nemubot.exception import IMException -from nemubot.hooks import hook -from nemubot.tools import web - -from nemubot.module.more import Response - - -# MODULE CORE ######################################################### - -def get_movie_by_id(imdbid): - """Returns the information about the matching movie""" - - url = "http://www.imdb.com/title/" + urllib.parse.quote(imdbid) - soup = BeautifulSoup(web.getURLContent(url)) - - 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("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() 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", - "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"})]), - } - - -def find_movies(title, year=None): - """Find existing movies matching a approximate title""" - - title = title.lower() - - # Built URL - 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, remove_callback=True) - - 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] - - -# 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): - if not len(msg.args): - raise IMException("precise a movie/serie title!") - - title = ' '.join(msg.args) - - if re.match("^tt[0-9]{7}$", title) is not None: - data = get_movie_by_id(imdbid=title) - else: - rm = re.match(r"^(.+)\s\(([0-9]{4})\)$", title) - if rm is not None: - data = find_movies(rm.group(1), year=rm.group(2)) - else: - 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("%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'])) - - return res - - -@hook.command("imdbs", - help="Search a movie/serie by title", - help_usage={ - "TITLE": "Search a movie/serie by TITLE", - }) -def cmd_search(msg): - if not len(msg.args): - raise IMException("precise a movie/serie title!") - - data = find_movies(' '.join(msg.args)) - - movies = list() - 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) diff --git a/modules/jsonbot.py b/modules/jsonbot.py deleted file mode 100644 index 3126dc1..0000000 --- a/modules/jsonbot.py +++ /dev/null @@ -1,58 +0,0 @@ -from nemubot.hooks import hook -from nemubot.exception import IMException -from nemubot.tools import web -from nemubot.module.more import Response -import json - -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 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.command("json") -def get_json_info(msg): - if not len(msg.args): - 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 IMException("Please specify a valid url.") - json_data = json.loads(request_data) - - if len(msg.args) == 1: - raise IMException("Please specify the keys to return (%s)" % ", ".join(getJsonKeys(json_data))) - - 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/man.py b/modules/man.py index f60e0cf..00edc8e 100644 --- a/modules/man.py +++ b/modules/man.py @@ -1,78 +1,66 @@ -"""Read manual pages on IRC""" - -# PYTHON STUFFS ####################################################### +# coding=utf-8 import subprocess import re import os -from nemubot.hooks import hook +nemubotversion = 3.3 -from nemubot.module.more import Response +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 (): + """Line inserted in the response to the command !help""" + return "Read man on IRC" -# GLOBALS ############################################################# +def help_full (): + return "!man [0-9] /what/: gives informations about /what/." RGXP_s = re.compile(b'\x1b\\[[0-9]+m') - -# 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 - if len(msg.args) == 1: - args.append(msg.args[0]) - elif len(msg.args) >= 2: + if len(msg.cmds) == 2: + args.append(msg.cmds[1]) + elif len(msg.cmds) >= 3: try: - num = int(msg.args[0]) + num = int(msg.cmds[1]) args.append("%d" % num) - args.append(msg.args[1]) + args.append(msg.cmds[2]) except ValueError: - args.append(msg.args[0]) + args.append(msg.cmds[1]) os.unsetenv("LANG") - res = Response(channel=msg.channel) - with subprocess.Popen(args, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) as proc: + 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"): (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("There is no entry %s in section %d." % - (msg.args[0], num)) + res.append_message("Il n'y a pas d'entrée %s dans la section %d du manuel." % (msg.cmds[1], num)) else: - res.append_message("There is no man page for %s." % msg.args[0]) + res.append_message("Il n'y a pas de page de manuel pour %s." % msg.cmds[1]) return res - -@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)] + args = ["whatis", " ".join(msg.cmds[1:])] - res = Response(channel=msg.channel) - with subprocess.Popen(args, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) as proc: + 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"): (line, n) = RGXP_s.subn(b'', line) res.append_message(" ".join(line.decode().split())) if len(res.messages) <= 0: - res.append_message("There is no man page for %s." % msg.args[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)) + else: + res.append_message("Il n'y a pas de page de manuel pour %s." % msg.cmds[1]) return res diff --git a/modules/mapquest.py b/modules/mapquest.py deleted file mode 100644 index f328e1d..0000000 --- a/modules/mapquest.py +++ /dev/null @@ -1,68 +0,0 @@ -"""Transform name location to GPS coordinates""" - -# PYTHON STUFFS ####################################################### - -import re -from urllib.parse import quote - -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://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 " - "module. Add it to the module configuration file:\n" - "\nRegister at https://developer.mapquest.com/") - global URL_API - URL_API = URL_API % context.config["apikey"].replace("%", "%%") - - -# MODULE CORE ######################################################### - -def geocode(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"]: - yield loc - - -def where(loc): - return re.sub(" +", " ", - "{street} {adminArea5} {adminArea4} {adminArea3} " - "{adminArea1}".format(**loc)).strip() - - -# 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") - - 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 (%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 deleted file mode 100644 index be608ca..0000000 --- a/modules/mediawiki.py +++ /dev/null @@ -1,249 +0,0 @@ -# coding=utf-8 - -"""Use MediaWiki API to get pages""" - -import re -import urllib.parse - -from nemubot.exception import IMException -from nemubot.hooks import hook -from nemubot.tools import web - -nemubotversion = 3.4 - -from nemubot.module.more import Response - - -# MEDIAWIKI REQUESTS ################################################## - -def get_namespaces(site, ssl=False, path="/w/api.php"): - # Built URL - 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) - - 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, path="/w/api.php"): - # Built URL - 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) - - for k in data["query"]["pages"]: - try: - return data["query"]["pages"][k]["revisions"][0]["*"] - except: - raise IMException("article not found") - - -def get_unwikitextified(site, wikitext, ssl=False, path="/w/api.php"): - # Built URL - 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) - - return data["expandtemplates"]["*"] - - -## Search - -def opensearch(site, term, ssl=False, path="/w/api.php"): - # Built URL - 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) - - 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, path="/w/api.php"): - # Built URL - 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) - - 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("", "\x03\x02").replace("", "\x03\x02")), - web.striphtml(itm["snippet"].replace("", "\x03\x02").replace("", "\x03\x02"))) - - -# 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) - - # 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==+)\s*(.*?)\s*(?P=title))\n+", r"\1", cnt) - - # Strip HTML comments - cnt = re.sub(r"<!--.*?-->", "", cnt, flags=re.DOTALL) - - # Strip ref - cnt = re.sub(r"<ref.*?/ref>", "", cnt, flags=re.DOTALL) - return cnt - - -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, **kwargs), 1) - - # Strip [[...]] - 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) - - 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 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) - - 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 raw - - -# NEMUBOT ############################################################# - -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(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 - - # Try looking at opensearch - 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], **kwargs) if x is not None and x != ""] - return Response(os, - channel=to, - title="Article not found, would you mean") - - -@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): - 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.kwargs) - - -@hook.command("mediawiki_search", - 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): - 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:]), **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: - raise IMException("indicate a lang and a term to search") - - return mediawiki_response(msg.args[0] + ".wikipedia.org", - " ".join(msg.args[1:]), - msg.to_response) diff --git a/modules/networking.py b/modules/networking.py new file mode 100644 index 0000000..d6431e0 --- /dev/null +++ b/modules/networking.py @@ -0,0 +1,119 @@ +# coding=utf-8 + +import http.client +import json +import socket +from urllib.parse import quote +from urllib.parse import urlparse +from urllib.request import urlopen + +from tools import web + +nemubotversion = 3.3 + +def load(context): + from hooks import Hook + add_hook("cmd_hook", Hook(cmd_traceurl, "traceurl")) + add_hook("cmd_hook", Hook(cmd_isup, "isup")) + add_hook("cmd_hook", Hook(cmd_curl, "curl")) + + +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/." + +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.decode().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.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) + +def cmd_traceurl(msg): + if 1 < len(msg.cmds) < 6: + res = list() + for url in msg.cmds[1:]: + trace = traceURL(url) + 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) + +def cmd_isup(msg): + if 1 < len(msg.cmds) < 6: + res = list() + for url in msg.cmds[1:]: + o = urlparse(url, "http") + if o.netloc == "": + o = urlparse("http://" + url) + if o.netloc != "": + raw = urlopen("http://isitup.org/" + o.netloc + ".json", 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)) + else: + res.append(Response(msg.sender, "%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)) + return res + else: + return Response(msg.sender, "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 = 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 diff --git a/modules/networking/__init__.py b/modules/networking/__init__.py deleted file mode 100644 index 3b939ab..0000000 --- a/modules/networking/__init__.py +++ /dev/null @@ -1,184 +0,0 @@ -"""Various network tools (w3m, w3c validator, curl, traceurl, ...)""" - -# PYTHON STUFFS ####################################################### - -import logging -import re - -from nemubot.exception import IMException -from nemubot.hooks import hook - -from nemubot.module.more import Response - -from . import isup -from . import page -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 - mod.del_event = context.del_event - mod.save = context.save - mod.print = print - mod.send_response = context.send_response - page.load(context.config, context.add_hook) - watchWebsite.load(context.data) - try: - whois.load(context.config, context.add_hook) - except ImportError: - logger.exception("Unable to load netwhois module") - - -# MODULE INTERFACE #################################################### - -@hook.command("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 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 IMException("The page %s has no title" % url) - else: - return Response("%s: %s" % (url, res.group(1).replace("\n", " ")), channel=msg.channel) - - -@hook.command("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 IMException("Indicate the URL to visit.") - - 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) - - -@hook.command("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 IMException("Indicate the URL to visit.") - - res = Response(channel=msg.channel) - for m in page.fetch(" ".join(msg.args)).split("\n"): - res.append_message(m) - return res - - -@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): - if not len(msg.args): - 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) - return res - - -@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): - if not len(msg.args): - raise IMException("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.command("isup", - help="Check if a website is up", - help_usage={"DOMAIN": "Check if a DOMAIN is up"}) -def cmd_isup(msg): - if not len(msg.args): - raise IMException("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.command("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 IMException("Indicate an URL to validate!") - - headers, validator = w3c.validator(msg.args[0]) - - 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 - - - -@hook.command("watch", data="diff", - help="Alert on webpage change", - help_usage={"URL": "Watch the given URL and alert when it changes"}) -@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"): - if not len(msg.args): - raise IMException("indicate an URL to watch!") - - return watchWebsite.add_site(msg.args[0], msg.frm, msg.channel, msg.server, diffType) - - -@hook.command("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): - 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.command("unwatch", - help="Unwatch a previously watched URL", - help_usage={"URL": "Unwatch the given URL"}) -def cmd_unwatch(msg): - if not len(msg.args): - 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/isup.py b/modules/networking/isup.py deleted file mode 100644 index 99e2664..0000000 --- a/modules/networking/isup.py +++ /dev/null @@ -1,18 +0,0 @@ -import urllib - -from nemubot.tools.web import getNormalizedURL, getJSON - -def isup(url): - """Determine if the given URL is up or not - - Argument: - url -- the URL to check - """ - - o = urllib.parse.urlparse(getNormalizedURL(url), "http") - if 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"] - - return None diff --git a/modules/networking/page.py b/modules/networking/page.py deleted file mode 100644 index 689944b..0000000 --- a/modules/networking/page.py +++ /dev/null @@ -1,131 +0,0 @@ -import http.client -import socket -import subprocess -import tempfile -import urllib - -from nemubot import __version__ -from nemubot.exception import IMException -from nemubot.tools import web - - -def load(CONF, add_hook): - # TODO: 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(web.getNormalizedURL(url), "http") - if o.netloc == "": - raise IMException("invalid URL") - if o.scheme == "http": - conn = http.client.HTTPConnection(o.hostname, port=o.port, timeout=5) - else: - conn = http.client.HTTPSConnection(o.hostname, port=o.port, timeout=5) - try: - conn.request("HEAD", o.path, None, {"User-agent": - "Nemubot v%s" % __version__}) - except ConnectionError as e: - raise IMException(e.strerror) - except socket.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 IMException("an unexpected error occurs") - - try: - res = conn.getresponse() - except http.client.BadStatusLine: - raise IMException("An error occurs") - finally: - conn.close() - - return (res.version, res.status, res.reason, res.getheaders()) - - -def _onNoneDefault(): - raise IMException("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 callable(onNone): - return onNone() - else: - return None - except ConnectionError as e: - raise IMException(e.strerror) - except socket.timeout: - raise IMException("The request timeout when trying to access the page") - except socket.error as e: - raise IMException(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 - - Argument: - url -- the URL to render - """ - - return _render(fetch(url, onNone)) - - -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, _, heads = headers(url) - - if status == http.client.FOUND or status == http.client.MOVED_PERMANENTLY or status == http.client.SEE_OTHER: - for h, c in heads: - 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 deleted file mode 100644 index 3c8084f..0000000 --- a/modules/networking/w3c.py +++ /dev/null @@ -1,32 +0,0 @@ -import json -import urllib - -from nemubot import __version__ -from nemubot.exception import IMException -from nemubot.tools.web import getNormalizedURL - -def validator(url): - """Run the w3c validator on the given URL - - Argument: - url -- the URL to validate - """ - - o = urllib.parse.urlparse(getNormalizedURL(url), "http") - if o.netloc == "": - raise IMException("Indicate a valid URL!") - - try: - 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)) - - 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 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 deleted file mode 100644 index d6b806f..0000000 --- a/modules/networking/watchWebsite.py +++ /dev/null @@ -1,223 +0,0 @@ -"""Alert on changes on websites""" - -from functools import partial -import logging -from random import randint -import urllib.parse -from urllib.parse import urlparse - -from nemubot.event import ModuleEvent -from nemubot.exception import IMException -from nemubot.tools.web import getNormalizedURL -from nemubot.tools.xmlparser.node import ModuleState - -logger = logging.getLogger("nemubot.module.networking.watchWebsite") - -from nemubot.module.more import Response - -from . import page - -DATAS = None - - -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"): - start_watching(site, randint(-30, 30)) - else: - print("No alert defined for this site: " + site["url"]) - #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 - - Argument: - url -- URL to unwatch - """ - - o = urlparse(getNormalizedURL(url), "http") - if o.scheme != "" and url in DATAS.index: - site = DATAS.index[url] - for a in site.getNodes("alert"): - if a["channel"] == channel: -# if not (nick == a["nick"] or frm_owner): -# raise IMException("you cannot unwatch this URL.") - site.delChild(a) - if not site.hasNode("alert"): - del_event(site["_evt_id"]) - DATAS.delChild(site) - save() - return Response("I don't watch this URL anymore.", - channel=channel, nick=nick) - raise IMException("I didn't watch this URL!") - - -def add_site(url, nick, channel, server, diffType="diff"): - """Add a site to watching list - - Argument: - url -- URL to watch - """ - - o = urlparse(getNormalizedURL(url), "http") - if o.netloc == "": - raise IMException("sorry, I can't watch this URL :(") - - alert = ModuleState("alert") - alert["nick"] = nick - alert["server"] = server - alert["channel"] = channel - alert["message"] = "{url} just changed!" - - 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) - - save() - return Response(channel=channel, nick=nick, - 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"])) - - -def alert_change(content, site): - """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 - - if (content is not None) != site.getBool("lastcontent"): - format_response(site, link=site["url"]) - site["lastcontent"] = content is not None - start_watching(site) - return - - if content is None: - start_watching(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"] = Feed(site["lastcontent"]) - try: - page = Feed(content) - except: - print("An error occurs during Atom parsing. Restart event...") - start_watching(site) - return - diff = site["_lastpage"] & page - if len(diff) > 0: - site["_lastpage"] = page - diff.reverse() - for d in diff: - site.setIndex("term", "category") - categories = site.index - - if len(categories) > 0: - if d.category is None or d.category not in categories: - format_response(site, link=d.link, categ=categories[""]["part"], title=d.title) - else: - format_response(site, link=d.link, categ=categories[d.category]["part"], title=d.title) - else: - 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 - - else: # Just looking for any changes - format_response(site, link=site["url"], content=content) - site["lastcontent"] = content - start_watching(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 - - Argument: - site -- DATAS structure representing a site to watch - - Keyword argument: - offset -- offset time to delay the launch of the first check - """ - - #o = urlparse(getNormalizedURL(site["url"]), "http") - #print("Add %s event for site: %s" % (site["type"], o.netloc)) - - try: - 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/networking/whois.py b/modules/networking/whois.py deleted file mode 100644 index 999dc01..0000000 --- a/modules/networking/whois.py +++ /dev/null @@ -1,136 +0,0 @@ -# PYTHON STUFFS ####################################################### - -import datetime -import urllib - -from nemubot.exception import IMException -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 = "https://www.whoisxmlapi.com/whoisserver/WhoisService?da=2&domainName=%%s&outputFormat=json&userName=%s&password=%s" - - -# LOADING ############################################################# - -def load(CONF, add_hook): - 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 " - "the !netwhois feature. Add it to the module " - "configuration file:\n\nRegister at " - "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"])) - - import nemubot.hooks - 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 ######################################################### - -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"] - - 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 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): - 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 IMException(js["ErrorMessage"]["msg"]) - - whois = js["WhoisRecord"] - - 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") diff --git a/modules/news.py b/modules/news.py deleted file mode 100644 index c4c967a..0000000 --- a/modules/news.py +++ /dev/null @@ -1,61 +0,0 @@ -"""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 IMException -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 - - -# HELP ################################################################ - -def help_full(): - return "Display the latests news from a given URL: !news URL" - - -# 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"]) - -def get_last_news(url): - from xml.parsers.expat import ExpatError - try: - feed = Feed(web.getURLContent(url)) - return feed.entries - except ExpatError: - return [] - - -# MODULE INTERFACE #################################################### - -@hook.command("news") -def cmd_news(msg): - if not len(msg.args): - raise IMException("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, 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 diff --git a/modules/nextstop.xml b/modules/nextstop.xml new file mode 100644 index 0000000..d34e8ae --- /dev/null +++ b/modules/nextstop.xml @@ -0,0 +1,4 @@ + + + + diff --git a/modules/nextstop/__init__.py b/modules/nextstop/__init__.py new file mode 100644 index 0000000..71816a8 --- /dev/null +++ b/modules/nextstop/__init__.py @@ -0,0 +1,50 @@ +# coding=utf-8 + +import http.client +import re +from xml.dom.minidom import parseString + +from .external.src import ratp + +nemubotversion = 3.3 + +def load(context): + global DATAS + 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." + + +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(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" % + (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) + 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) + +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]) + 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) + return False diff --git a/modules/nextstop/external b/modules/nextstop/external new file mode 160000 index 0000000..e5675c6 --- /dev/null +++ b/modules/nextstop/external @@ -0,0 +1 @@ +Subproject commit e5675c631665dfbdaba55a0be66708a07d157408 diff --git a/modules/nntp.py b/modules/nntp.py deleted file mode 100644 index 7fdceb4..0000000 --- a/modules/nntp.py +++ /dev/null @@ -1,229 +0,0 @@ -"""The NNTP module""" - -# 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 -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), policy=email.policy.SMTPUTF8) - - -servers_lastcheck = dict() -servers_lastseen = 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() - - if idx not in servers_lastseen: - servers_lastseen[idx] = [] - - with NNTP(**fill) as srv: - response, servers_lastcheck[idx] = srv.date() - - 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: - 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): - 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=partial(_ticker, **args), interval=42)) - -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(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="*", **server): - _newevt(to_server=to_server, to_channel=to_channel, group=group, 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) diff --git a/modules/openai.py b/modules/openai.py deleted file mode 100644 index b9b6e21..0000000 --- a/modules/openai.py +++ /dev/null @@ -1,87 +0,0 @@ -"""Perform requests to openai""" - -# PYTHON STUFFS ####################################################### - -from openai import OpenAI - -from nemubot import context -from nemubot.hooks import hook -from nemubot.tools import web - -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") - 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.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( - 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) diff --git a/modules/openroute.py b/modules/openroute.py deleted file mode 100644 index c280dec..0000000 --- a/modules/openroute.py +++ /dev/null @@ -1,158 +0,0 @@ -"""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 nemubot.module.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" - "\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 diff --git a/modules/pkgs.py b/modules/pkgs.py deleted file mode 100644 index 386946f..0000000 --- a/modules/pkgs.py +++ /dev/null @@ -1,68 +0,0 @@ -"""Get information about common software""" - -# PYTHON STUFFS ####################################################### - -import portage - -from nemubot import context -from nemubot.exception import IMException -from nemubot.hooks import hook - -from nemubot.module.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 diff --git a/modules/qcm.xml b/modules/qcm.xml new file mode 100644 index 0000000..05a7076 --- /dev/null +++ b/modules/qcm.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/modules/qcm/Course.py b/modules/qcm/Course.py new file mode 100644 index 0000000..9cddf1a --- /dev/null +++ b/modules/qcm/Course.py @@ -0,0 +1,31 @@ +# 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 new file mode 100644 index 0000000..6895680 --- /dev/null +++ b/modules/qcm/Question.py @@ -0,0 +1,93 @@ +# 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 new file mode 100644 index 0000000..48ed23f --- /dev/null +++ b/modules/qcm/QuestionFile.py @@ -0,0 +1,16 @@ +# 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 new file mode 100644 index 0000000..11ab46b --- /dev/null +++ b/modules/qcm/Session.py @@ -0,0 +1,67 @@ +# 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 new file mode 100644 index 0000000..5f18831 --- /dev/null +++ b/modules/qcm/User.py @@ -0,0 +1,27 @@ +# 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 new file mode 100644 index 0000000..b8b01df --- /dev/null +++ b/modules/qcm/__init__.py @@ -0,0 +1,197 @@ +# 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 [] []" + +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 new file mode 100644 index 0000000..a81ac5d --- /dev/null +++ b/modules/qd/DelayedTuple.py @@ -0,0 +1,32 @@ +# 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 new file mode 100644 index 0000000..7449489 --- /dev/null +++ b/modules/qd/GameUpdater.py @@ -0,0 +1,60 @@ +# 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 new file mode 100644 index 0000000..41b2eff --- /dev/null +++ b/modules/qd/QDWrapper.py @@ -0,0 +1,20 @@ +# 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 new file mode 100644 index 0000000..52c5692 --- /dev/null +++ b/modules/qd/Score.py @@ -0,0 +1,126 @@ +# 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 new file mode 100644 index 0000000..871512b --- /dev/null +++ b/modules/qd/__init__.py @@ -0,0 +1,224 @@ +# 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/ratp.py b/modules/ratp.py deleted file mode 100644 index 06f5f1d..0000000 --- a/modules/ratp.py +++ /dev/null @@ -1,74 +0,0 @@ -"""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 nemubot.module.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/reddit.py b/modules/reddit.py deleted file mode 100644 index d4def85..0000000 --- a/modules/reddit.py +++ /dev/null @@ -1,97 +0,0 @@ -# coding=utf-8 - -"""Get information about subreddit""" - -import re - -from nemubot.exception import IMException -from nemubot.hooks import hook -from nemubot.tools import web - -nemubotversion = 3.4 - -from nemubot.module.more import Response - - -def help_full(): - return "!subreddit /subreddit/: Display information on the subreddit." - -LAST_SUBS = dict() - - -@hook.command("subreddit") -def cmd_subreddit(msg): - global LAST_SUBS - 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 IMException("Which subreddit? Need inspiration? " - "type !horny or !bored") - else: - subs = msg.args - - all_res = list() - 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) - else: - where = "r" - - sbr = web.getJSON("https://www.reddit.com/%s/%s/about.json" % - (where, sub.group(2))) - - if sbr is None: - raise IMException("subreddit not found") - - 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"]) - if sbr["data"]["public_description"] != "": - 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)) - else: - all_res.append(Response("%s is not a valid subreddit" % osub, - channel=msg.channel, nick=msg.frm)) - - return all_res - - -@hook.message() -def parselisten(msg): - 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() -def parseresponse(msg): - global LAST_SUBS - - 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: - if recv not in LAST_SUBS: - LAST_SUBS[recv] = list() - LAST_SUBS[recv].append(url) - - return msg diff --git a/modules/repology.py b/modules/repology.py deleted file mode 100644 index 8dbc6da..0000000 --- a/modules/repology.py +++ /dev/null @@ -1,94 +0,0 @@ -# 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 diff --git a/modules/rnd.py b/modules/rnd.py index d1c6fe7..198983c 100644 --- a/modules/rnd.py +++ b/modules/rnd.py @@ -1,54 +1,12 @@ -"""Help to make choice""" - -# PYTHON STUFFS ####################################################### +# coding=utf-8 import random -import shlex -from nemubot import context -from nemubot.exception import IMException -from nemubot.hooks import hook +nemubotversion = 3.3 -from nemubot.module.more import Response +def load(context): + from hooks import Hook + add_hook("cmd_hook", Hook(cmd_choice, "choice")) - -# MODULE INTERFACE #################################################### - -@hook.command("choice") def cmd_choice(msg): - if not len(msg.args): - raise IMException("indicate some terms to pick!") - - return Response(random.choice(msg.args), - channel=msg.channel, - nick=msg.frm) - - -@hook.command("choicecmd") -def cmd_choicecmd(msg): - if not len(msg.args): - raise IMException("indicate some command to pick!") - - choice = shlex.split(random.choice(msg.args)) - - 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 + return Response(msg.sender, random.choice(msg.cmds[1:]), channel=msg.channel) diff --git a/modules/sap.py b/modules/sap.py deleted file mode 100644 index 0b9017f..0000000 --- a/modules/sap.py +++ /dev/null @@ -1,43 +0,0 @@ -# coding=utf-8 - -"""Find information about an SAP transaction codes""" - -import urllib.parse -import urllib.request -from bs4 import BeautifulSoup - -from nemubot.exception import IMException -from nemubot.hooks import hook -from nemubot.tools import web - -nemubotversion = 4.0 - -from nemubot.module.more import Response - - -def help_full(): - return "Retrieve SAP transaction codes and details using tcodes or keywords: !tcode " - - -@hook.command("tcode") -def cmd_tcode(msg): - if not len(msg.args): - raise IMException("indicate a transaction code or " - "a keyword to search!") - - url = ("https://www.tcodesearch.com/tcodes/search?q=%s" % - urllib.parse.quote(msg.args[0])) - - page = web.getURLContent(url) - soup = BeautifulSoup(page) - - res = Response(channel=msg.channel, - nomore="No more transaction code", - count=" (%d more tcodes)") - - - 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 diff --git a/modules/shodan.py b/modules/shodan.py deleted file mode 100644 index 9c158c6..0000000 --- a/modules/shodan.py +++ /dev/null @@ -1,104 +0,0 @@ -"""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 nemubot.module.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" - "\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 diff --git a/modules/sleepytime.py b/modules/sleepytime.py index f7fb626..b53a2e5 100644 --- a/modules/sleepytime.py +++ b/modules/sleepytime.py @@ -1,50 +1,52 @@ # coding=utf-8 -"""as http://sleepyti.me/, give you the best time to go to bed""" - import re import imp -from datetime import datetime, timedelta, timezone +from datetime import datetime +from datetime import timedelta -from nemubot.hooks import hook +nemubotversion = 3.3 -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" -from nemubot.module.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" + +def load(context): + from hooks import Hook + add_hook("cmd_hook", Hook(cmd_sleep, "sleeptime")) + add_hook("cmd_hook", Hook(cmd_sleep, "sleepytime")) -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") - - -@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: + 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.args[0]) - f = [datetime(datetime.now(timezone.utc).year, - datetime.now(timezone.utc).month, - datetime.now(timezone.utc).day, + 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, 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(6): - f.append(f[i] - timedelta(hours=1, minutes=30)) + 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("You should try to fall asleep at one of the following" - " times: %s" % ', '.join(g), channel=msg.channel) + return Response(msg.sender, + "You should try to fall asleep at one of the following" + " times: %s" % ', '.join(g), msg.channel) # Just get awake times else: - f = [datetime.now(timezone.utc) + timedelta(minutes=15)] + f = [datetime.now() + timedelta(minutes=15)] g = list() - for i in range(6): - f.append(f[i] + timedelta(hours=1, minutes=30)) + 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("If you head to bed right now, you should try to wake" + 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), channel=msg.channel) + ', '.join(g), msg.channel) diff --git a/modules/smmry.py b/modules/smmry.py deleted file mode 100644 index b1fe72c..0000000 --- a/modules/smmry.py +++ /dev/null @@ -1,116 +0,0 @@ -"""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 -from nemubot.module.urlreducer import LAST_URLS - - -# GLOBALS ############################################################# - -URL_API = "https://api.smmry.com/?SM_API_KEY=%s" - - -# 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" - "\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": "" - }, - 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", - "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): - 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.") - - 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" - 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) - - if web.isURL(" ".join(msg.args)): - 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))): - 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, 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 "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: - res.append_message(smmry["sm_api_content"]) - - return res diff --git a/modules/sms.py b/modules/sms.py deleted file mode 100644 index 57ab3ae..0000000 --- a/modules/sms.py +++ /dev/null @@ -1,153 +0,0 @@ -# coding=utf-8 - -"""Send SMS using SMS API (currently only Free Mobile)""" - -import re -import socket -import time -import urllib.error -import urllib.request -import urllib.parse - -from nemubot import context -from nemubot.exception import IMException -from nemubot.hooks import hook -from nemubot.tools.xmlparser.node import ModuleState - -nemubotversion = 3.4 - -from nemubot.module.more import Response - -def load(context): - context.data.setIndex("name", "phone") - -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 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 - - -def send_sms_to_list(msg, frm, dests, content, cur_epoch): - fails = list() - for u in dests: - context.data.index[u]["lastuse"] = cur_epoch - 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) ) - - if len(fails) > 0: - 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.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[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) - -@hook.ask() -def parseask(msg): - 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") - - 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.frm) - - 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.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.frm) diff --git a/modules/soutenance.xml b/modules/soutenance.xml new file mode 100644 index 0000000..957423b --- /dev/null +++ b/modules/soutenance.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/modules/soutenance/Delayed.py b/modules/soutenance/Delayed.py new file mode 100644 index 0000000..8cf47c5 --- /dev/null +++ b/modules/soutenance/Delayed.py @@ -0,0 +1,13 @@ +# 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 new file mode 100644 index 0000000..63833b7 --- /dev/null +++ b/modules/soutenance/SiteSoutenances.py @@ -0,0 +1,179 @@ +# 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("", line) is not None: + save = False + elif re.match("", line) is not None: + save = True + last = Soutenance() + self.souts.append(last) + elif save: + result = re.match("]+>(.*)", 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 new file mode 100644 index 0000000..e2a0882 --- /dev/null +++ b/modules/soutenance/Soutenance.py @@ -0,0 +1,11 @@ +# 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 new file mode 100644 index 0000000..61b3aa6 --- /dev/null +++ b/modules/soutenance/__init__.py @@ -0,0 +1,48 @@ +# 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 : gives the date of the next defense of /who/.\n!soutenances : 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/speak.py b/modules/speak.py deleted file mode 100644 index c08b2bd..0000000 --- a/modules/speak.py +++ /dev/null @@ -1,133 +0,0 @@ -# coding=utf-8 - -from datetime import timedelta -from queue import Queue -import re -import subprocess -from threading import Thread - -from nemubot.hooks import hook -from nemubot.message import Text -from nemubot.message.visitor import AbstractVisitor - -nemubotversion = 3.4 - -queue = Queue() -spk_th = None -last = None - -SMILEY = list() -CORRECTIONS = list() - -def load(context): - 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 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)) - - -class Speaker(Thread): - - def run(self): - global queue, spk_th - while not queue.empty(): - sentence = queue.get_nowait() - lang = "fr" - 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_Text(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 = 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 = 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 = 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) - - -@hook("in") -def treat_for_speak(msg): - if not msg.frm_owner: - append_message(msg) - -def append_message(msg): - global last, spk_th - - 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) - queue.put_nowait(vprnt.pp) - last = msg - - if spk_th is None: - spk_th = Speaker() - spk_th.start() diff --git a/modules/spell/__init__.py b/modules/spell/__init__.py index da16a80..918831b 100644 --- a/modules/spell/__init__.py +++ b/modules/spell/__init__.py @@ -1,97 +1,89 @@ -"""Check words spelling""" +# coding=utf-8 -# PYTHON STUFFS ####################################################### - -from nemubot import context -from nemubot.exception import IMException -from nemubot.hooks import hook -from nemubot.tools.xmlparser.node import ModuleState +import re +from urllib.parse import quote from .pyaspell import Aspell from .pyaspell import AspellError -from nemubot.module.more import Response +nemubotversion = 3.3 +def help_tiny (): + return "Check words spelling" -# LOADING ############################################################# +def help_full (): + return "!spell [] : give the correct spelling of in ." def load(context): - context.data.setIndex("name", "score") + 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")) -# MODULE CORE ######################################################### +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) + + lang = "fr" + strRes = list() + for word in msg.cmds[1:]: + if len(word) <= 2 and len(msg.cmds) > 2: + lang = word + else: + try: + r = check_spell(word, lang) + except AspellError: + return Response(msg.sender, "Je n'ai pas le dictionnaire `%s' :(" % lang, msg.channel) + 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(msg.sender, strRes, channel=msg.channel) def add_score(nick, t): - if nick not in context.data.index: + global DATAS + if nick not in DATAS.index: st = ModuleState("score") st["name"] = nick - context.data.addChild(st) + DATAS.addChild(st) - if context.data.index[nick].hasAttribute(t): - context.data.index[nick][t] = context.data.index[nick].getInt(t) + 1 + if DATAS.index[nick].hasAttribute(t): + DATAS.index[nick][t] = DATAS.index[nick].getInt(t) + 1 else: - context.data.index[nick][t] = 1 - context.save() + DATAS.index[nick][t] = 1 + save() - -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.frm, "correct") - res.append_message("l'orthographe de `%s' est correcte" % word) - - elif len(r) > 0: - add_score(msg.frm, "bad") - res.append_message(r, title="suggestions pour `%s'" % word) - - else: - add_score(msg.frm, "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): + global DATAS res = list() unknown = list() - if not len(msg.args): - 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)) - else: - unknown.append(cmd) + 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)) + else: + unknown.append(cmd) + else: + return Response(msg.sender, "De qui veux-tu voir les scores ?", channel=msg.channel, nick=msg.nick) if len(unknown) > 0: - res.append(Response("%s inconnus" % ", ".join(unknown), channel=msg.channel)) + res.append(Response(msg.sender, "%s inconnus" % ", ".join(unknown), channel=msg.channel)) return res + +def check_spell(word, lang='fr'): + a = Aspell([("lang", lang), ("lang", "fr")]) + if a.check(word.encode("iso-8859-15")): + ret = True + else: + ret = a.suggest(word.encode("iso-8859-15")) + a.close() + return ret diff --git a/modules/suivi.py b/modules/suivi.py deleted file mode 100644 index a54b722..0000000 --- a/modules/suivi.py +++ /dev/null @@ -1,332 +0,0 @@ -"""Postal tracking module""" - -# PYTHON STUFF ############################################ - -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, getURLHeaders, getJSON -from nemubot.module.more import Response - - -# POSTAGE SERVICE PARSERS ############################################ - -def get_tnt_info(track_id): - values = [] - 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: - 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): - colissimo_data = getURLContent("https://www.laposte.fr/particulier/outils/suivre-vos-envois?code=%s" % colissimo_id) - soup = BeautifulSoup(colissimo_data) - - 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): - data = urllib.parse.urlencode({'listeNumeros': track_id}) - 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) - - 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 = getURLContent(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_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})) - - 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] - - 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}) - - shipment = laposte_data["shipment"] - return (shipment["product"], shipment["idShip"], shipment["event"][0]["label"], shipment["event"][0]["date"]) - - -def get_postnl_info(postnl_id): - data = urllib.parse.urlencode({'barcodes': postnl_id}) - postnl_baseurl = "http://www.postnl.post/details/" - - postnl_data = getURLContent(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) - - -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(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() - 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({ - "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"]) 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] - - -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): - 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 mis à jour le \x02{date}\x0f{place}.' - .format(trackid=tracknum, status=status, - date=re.sub(r'\s+', ' ', date), place=placestr)) - - -def handle_laposte(tracknum): - info = get_laposte_info(tracknum) - if info: - 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): - 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_usps(tracknum): - info = get_usps_info(tracknum) - if info: - notif, last_date, last_status, last_location = info - 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: - 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)) - - -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)) - - -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, - 'colissimo': handle_colissimo, - 'chronopost': handle_chronopost, - 'coliprive': handle_coliprive, - 'tnt': handle_tnt, - 'fedex': handle_fedex, - 'dhl': handle_dhl, - 'usps': handle_usps, - 'ups': handle_ups, -} - - -# HOOKS ############################################################## - -@hook.command("track", - 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.") - - res = Response(channel=msg.channel, count=" (%d suivis supplémentaires)") - - if 'tracker' in msg.kwargs: - if msg.kwargs['tracker'] in TRACKING_HANDLERS: - trackers = { - msg.kwargs['tracker']: TRACKING_HANDLERS[msg.kwargs['tracker']] - } - else: - raise IMException("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(): - 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 diff --git a/modules/syno.py b/modules/syno.py index 78f0b7d..047fe03 100644 --- a/modules/syno.py +++ b/modules/syno.py @@ -1,117 +1,61 @@ -"""Find synonyms""" - -# PYTHON STUFFS ####################################################### +# coding=utf-8 import re +import traceback +import sys from urllib.parse import quote -from nemubot.exception import IMException -from nemubot.hooks import hook -from nemubot.tools import web +from tools import web -from nemubot.module.more import Response +nemubotversion = 3.3 +def help_tiny (): + return "Find french synonyms" -# LOADING ############################################################# +def help_full (): + return "!syno : give a list of synonyms for ." def load(context): - global lang_binding - - if not context.config or not "bighugelabskey" in context.config: - logger.error("You need a NigHugeLabs API key in order to have english " - "theasorus. Add it to the module configuration file:\n" - "\nRegister at https://words.bighugelabs.com/getkey.php") - else: - lang_binding["en"] = lambda word: get_english_synos(context.config["bighugelabskey"], word) + from hooks import Hook + add_hook("cmd_hook", Hook(cmd_syno, "syno")) + add_hook("cmd_hook", Hook(cmd_syno, "synonyme")) -# MODULE CORE ######################################################### +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) -def get_french_synos(word): - url = "https://crisco.unicaen.fr/des/synonymes/" + quote(word) + 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 get_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() - if page is not None: - for line in page.split("\n"): - - if line.find("!-- Fin liste des antonymes --") > 0: - for elt in re.finditer(">([^<>]+)", line): - anton.append(elt.group(1)) - - elif line.find("!--Fin liste des synonymes--") > 0: - for elt in re.finditer(">([^<>]+)", line): - synos.append(elt.group(1)) - - elif re.match("[ \t]*]*>.*[ \t]*.*", line) is not None: + synos = list() + for line in page.decode().split("\n"): + if re.match("[ \t]*]*>.*[ \t]*.*", line) is not None: for elt in re.finditer(">&[^;]+;([^&]*)&[^;]+;<", line): - best.append(elt.group(1)) - - return (best, synos, anton) - - -def get_english_synos(key, word): - 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() - - 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 } - - -# 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 ?") - - lang = msg.kwargs["lang"] if "lang" in msg.kwargs else "fr" - word = ' '.join(msg.args) - - try: - best, synos, anton = lang_binding[lang](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 IMException("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 IMException("Aucun antonyme de %s n'a été trouvé" % word) - + synos.append(elt.group(1)) + return synos else: - raise IMException("WHAT?!") + return None diff --git a/modules/tpb.py b/modules/tpb.py deleted file mode 100644 index a752324..0000000 --- a/modules/tpb.py +++ /dev/null @@ -1,40 +0,0 @@ -from datetime import datetime -import urllib - -from nemubot.exception import IMException -from nemubot.hooks import hook -from nemubot.tools import human -from nemubot.tools.web import getJSON - -nemubotversion = 4.0 - -from nemubot.module.more import Response - -URL_TPBAPI = None - -def load(context): - if not context.config or "url" not in context.config: - raise ImportError("You need a TPB API in order to use the !tpb feature" - ". Add it to the module configuration file:\n\nSample " - "API: " - "https://gist.github.com/colona/07a925f183cfb47d5f20") - global URL_TPBAPI - URL_TPBAPI = context.config["url"] - -@hook.command("tpb") -def cmd_tpb(msg): - if not len(msg.args): - raise IMException("indicate an item to search!") - - torrents = getJSON(URL_TPBAPI + urllib.parse.quote(" ".join(msg.args))) - - res = Response(channel=msg.channel, nomore="No more torrents", count=" (%d more torrents)") - - if torrents: - 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}; 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 diff --git a/modules/translate.py b/modules/translate.py index 906ba93..60838f0 100644 --- a/modules/translate.py +++ b/modules/translate.py @@ -1,111 +1,97 @@ -"""Translation module""" - -# PYTHON STUFFS ####################################################### +# coding=utf-8 +import http.client +import re +import socket +import json from urllib.parse import quote -from nemubot.exception import IMException -from nemubot.hooks import hook -from nemubot.tools import web +nemubotversion = 3.3 -from nemubot.module.more import Response - - -# GLOBALS ############################################################# +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" - - -# 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 " - "this module. Add it to the module configuration " - "file:\n\nRegister at http://" - "www.wordreference.com/docs/APIregistration.aspx") - global URL - URL = URL % context.config["wrapikey"] + 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")) -# MODULE CORE ######################################################### - -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]))) - 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.command("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"] + global LANG + startWord = 1 + if msg.cmds[startWord] in LANG: + langTo = msg.cmds[startWord] + startWord += 1 else: - langTo = "fr" if langFrom == "en" else "en" + 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" - 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, 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) - 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 + +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): + print ("You need a WordReference API key in order to use this module." + " Add it to the module configuration file:\n\nRegister at " + "http://www.wordreference.com/docs/APIregistration.aspx") + return (http.client.INTERNAL_SERVER_ERROR, None) + + res = conn.getresponse() + data = res.read() + + conn.close() + return (res.status, data) diff --git a/modules/urbandict.py b/modules/urbandict.py deleted file mode 100644 index b561e89..0000000 --- a/modules/urbandict.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Search definition from urbandictionnary""" - -# PYTHON STUFFS ####################################################### - -from urllib.parse import quote - -from nemubot.exception import IMException -from nemubot.hooks import hook -from nemubot.tools import web - -from nemubot.module.more import Response - -# MODULE CORE ######################################################### - -def search(terms): - return web.getJSON( - "https://api.urbandictionary.com/v0/define?term=%s" - % quote(' '.join(terms))) - - -# MODULE INTERFACE #################################################### - -@hook.command("urbandictionnary") -def udsearch(msg): - if not len(msg.args): - raise IMException("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 diff --git a/modules/urlreducer.py b/modules/urlreducer.py deleted file mode 100644 index 86f4d42..0000000 --- a/modules/urlreducer.py +++ /dev/null @@ -1,173 +0,0 @@ -"""URL reducer module""" - -# PYTHON STUFFS ####################################################### - -import re -import json -from urllib.parse import urlparse -from urllib.parse import quote - -from nemubot.exception import IMException -from nemubot.hooks import hook -from nemubot.message import Text -from nemubot.tools import web - - -# MODULE FUNCTIONS #################################################### - -def default_reducer(url, data): - snd_url = url + quote(data, "/:%@&=?") - return web.getURLContent(snd_url) - - -def ycc_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), - header={"Content-Type": "application/x-www-form-urlencoded"})) - if 'short' in json_data: - return json_data['short'] - elif 'msg' in json_data: - raise IMException("Error: %s" % json_data['msg']) - else: - IMException("An error occured while shortening %s." % data) - -# MODULE VARIABLES #################################################### - -PROVIDERS = { - "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"), -} -DEFAULT_PROVIDER = "framalink" - -PROVIDERS_NETLOC = [urlparse(web.getNormalizedURL(url), "http").netloc for f, url in PROVIDERS.values()] - -# 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_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 - - Argument: - url -- the URL to reduce - """ - if provider is None: - provider = DEFAULT_PROVIDER - return PROVIDERS[provider][0](PROVIDERS[provider][1], url) - - -def gen_response(res, msg, srv): - if res is None: - raise IMException("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.message() -def parselisten(msg): - 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() -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) - 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) - return msg - - -# MODULE INTERFACE #################################################### - -@hook.command("framalink", - 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() - - 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 IMException("I have no more URL to reduce.") - - if len(msg.args) > 4: - raise IMException("I cannot reduce that many URLs at once.") - 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, provider) - if o.netloc == "": - res.append(gen_response(minief_url, msg, o.scheme)) - else: - res.append(gen_response(minief_url, msg, o.netloc)) - return res diff --git a/modules/velib.py b/modules/velib.py index 71c472c..8385476 100644 --- a/modules/velib.py +++ b/modules/velib.py @@ -1,53 +1,51 @@ -"""Gets information about velib stations""" - -# PYTHON STUFFS ####################################################### +# coding=utf-8 import re -from nemubot import context -from nemubot.exception import IMException -from nemubot.hooks import hook -from nemubot.tools import web +from tools import web -from nemubot.module.more import Response - - -# LOADING ############################################################# - -URL_API = None # http://www.velib.paris.fr/service/stationdetails/paris/%s +nemubotversion = 3.3 def load(context): - global URL_API - if not context.config or "url" not in context.config: - raise ImportError("Please provide url attribute in the module configuration") - URL_API = context.config["url"] - context.data.setIndex("name", "station") + global DATAS + DATAS.setIndex("name", "station") # evt = ModuleEvent(station_available, "42706", # (lambda a, b: a != b), None, 60, # 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/." -# MODULE CORE ######################################################### def station_status(station): """Gets available and free status of a given station""" - response = web.getXML(URL_API % station) + response = web.getXML(CONF.getNode("server")["url"] + station) if response is not None: - available = int(response.getElementsByTagName("available")[0].firstChild.nodeValue) - free = int(response.getElementsByTagName("free")[0].firstChild.nodeValue) + 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 return (available, free) 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) @@ -58,30 +56,33 @@ 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" - " disponibles." % (station, available, free), - channel=msg.channel) - raise IMException("station %s inconnue." % station) + 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) - -# MODULE INTERFACE #################################################### - -@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" - }) def ask_stations(msg): - if len(msg.args) > 4: - raise IMException("demande-moi moins de stations à la fois.") - elif not len(msg.args): - raise IMException("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 IMException("numéro de station invalide.") + """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) + elif len(msg.cmds) > 1: + 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: + return print_station_status(msg, DATAS.index[station]["number"]) + else: + return Response(msg.sender, + "numéro de station invalide.", + msg.channel, nick=msg.nick) + else: + return Response(msg.sender, + "Pour quelle station ?", + msg.channel, nick=msg.nick) diff --git a/modules/virtualradar.py b/modules/virtualradar.py deleted file mode 100644 index 2c87e79..0000000 --- a/modules/virtualradar.py +++ /dev/null @@ -1,100 +0,0 @@ -"""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 nemubot.module.more import Response -from nemubot.module import mapquest - -# GLOBALS ############################################################# - -URL_API = "https://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.frm, - 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 diff --git a/modules/watchWebsite.xml b/modules/watchWebsite.xml new file mode 100644 index 0000000..7a116e9 --- /dev/null +++ b/modules/watchWebsite.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/modules/watchWebsite/__init__.py b/modules/watchWebsite/__init__.py new file mode 100644 index 0000000..1f69158 --- /dev/null +++ b/modules/watchWebsite/__init__.py @@ -0,0 +1,181 @@ +# coding=utf-8 + +from datetime import datetime +from datetime import timedelta +import http.client +import hashlib +import re +import socket +import sys +import urllib.parse +from urllib.parse import urlparse +from urllib.request import urlopen + +from .atom import Atom + +nemubotversion = 3.3 + +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." + +def load(context): + """Register watched website""" + DATAS.setIndex("url", "watch") + for site in DATAS.getNodes("watch"): + if site.hasNode("alert"): + start_watching(site) + else: + 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) + return raw.read().decode() + except: + return None + +def start_watching(site): + 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"], + intervalle=site.getInt("time"), + call=alert_change, call_data=site) + site["_evt_id"] = add_event(evt) + + +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) + + url = msg.cmds[1] + + o = urlparse(url, "http") + if o.scheme != "" and url in DATAS.index: + site = DATAS.index[url] + 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) + +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) + + 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 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) + else: + return Response(msg.sender, "je ne peux pas surveiller cette URL", + channel=msg.channel, nick=msg.nick) + + save() + 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'): + 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), + channel=a["channel"], server=a["server"])) + +def alert_change(content, site): + """Alert when a change is detected""" + if site["type"] == "updown": + if site["lastcontent"] is None: + site["lastcontent"] = content is not None + + if (content is not None) != site.getBool("lastcontent"): + format_response(site, link=site["url"]) + site["lastcontent"] = content is not None + start_watching(site) + return + + if content is None: + start_watching(site) + return + + if site["type"] == "atom": + if site["_lastpage"] is None: + if site["lastcontent"] is None or site["lastcontent"] == "": + site["lastcontent"] = content + site["_lastpage"] = Atom(site["lastcontent"]) + try: + page = Atom(content) + except: + print ("An error occurs during Atom parsing. Restart event...") + start_watching(site) + return + diff = site["_lastpage"].diff(page) + if len(diff) > 0: + site["_lastpage"] = page + diff.reverse() + for d in diff: + site.setIndex("term", "category") + categories = site.index + + if len(categories) > 0: + if d.category is None or d.category not in categories: + format_response(site, link=d.link, categ=categories[""]["part"], title=d.title) + else: + format_response(site, link=d.link, categ=categories[d.category]["part"], title=d.title) + else: + 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 + + else: # Just looking for any changes + format_response(site, link=site["url"]) + site["lastcontent"] = content + start_watching(site) + save() diff --git a/modules/watchWebsite/atom.py b/modules/watchWebsite/atom.py new file mode 100755 index 0000000..30272e0 --- /dev/null +++ b/modules/watchWebsite/atom.py @@ -0,0 +1,84 @@ +#!/usr/bin/python3 +# coding=utf-8 + +import time +from xml.dom.minidom import parse +from xml.dom.minidom import parseString +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: + 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: + 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 () + 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") + else: + self.link = None + 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") + else: + self.link2 = None + +class Atom: + def __init__ (self, string): + self.raw = string + 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 () + 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.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 + + +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) + + diff = a.diff (b) + print (diff) diff --git a/modules/weather.py b/modules/weather.py deleted file mode 100644 index 9b36470..0000000 --- a/modules/weather.py +++ /dev/null @@ -1,261 +0,0 @@ -# coding=utf-8 - -"""The weather module. Powered by Dark Sky """ - -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 - -from nemubot.module import mapquest - -nemubotversion = 4.0 - -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 " - "module. Add it to the module configuration file:\n" - "\n" - "Register at https://developer.forecast.io/") - context.data.setIndex("name", "city") - global URL_DSAPI - URL_DSAPI = URL_DSAPI % context.config["darkskyapikey"] - - -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, 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"): - 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.args) > 0: - - # catch dans X[jh]$ - 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() - - if len(msg.args) == 2: - coords = msg.args - else: - coords = msg.args[0].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 city in context.data.index: - coords = list() - coords.append(context.data.index[city]["lat"]) - coords.append(context.data.index[city]["long"]) - return city, coords, specific - - else: - 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 IMException("Je ne sais pas où se trouve %s." % city) - - else: - raise IMException("indique-moi un nom de ville ou des coordonnées.") - - -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 - if wth is None or "darksky-unavailable" in wth["flags"]: - 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 - - -@hook.command("coordinates") -def cmd_coordinates(msg): - if len(msg.args) < 1: - raise IMException("indique-moi un nom de ville.") - - j = msg.args[0].lower() - if j not in context.data.index: - 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) - - -@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: 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 "ca") - - res = Response(channel=msg.channel, nomore="No more weather alert", count=" (%d more alerts)") - - if "alerts" in wth: - for alert in wth["alerts"]: - 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 - - -@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: 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 "ca") - - res = Response(channel=msg.channel, nomore="No more weather information") - - if "alerts" in wth: - alert_msgs = list() - for alert in wth["alerts"]: - 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: - 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] - 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, 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"], wth["flags"])) - - 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]: - res.append_message("\x03\x02At %sh:\x03\x02 %s" % (format_timestamp(int(hour["time"]), wth["timezone"], wth["offset"], '%H'), - 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, wth["flags"]))) - - 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) - - -@hook.ask() -def parseask(msg): - res = gps_ask.match(msg.message) - 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 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) - context.data.addChild(ms) - context.save() - return Response("ok, j'ai bien noté les coordonnées de %s" % res.group("city"), - msg.channel, msg.frm) diff --git a/modules/whereis.xml b/modules/whereis.xml new file mode 100644 index 0000000..90b2c2f --- /dev/null +++ b/modules/whereis.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/whereis/Delayed.py b/modules/whereis/Delayed.py new file mode 100644 index 0000000..45826f4 --- /dev/null +++ b/modules/whereis/Delayed.py @@ -0,0 +1,5 @@ +# coding=utf-8 + +class Delayed: + def __init__(self): + self.names = dict() diff --git a/modules/whereis/UpdatedStorage.py b/modules/whereis/UpdatedStorage.py new file mode 100644 index 0000000..de09848 --- /dev/null +++ b/modules/whereis/UpdatedStorage.py @@ -0,0 +1,57 @@ +# 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 new file mode 100644 index 0000000..d4b48b4 --- /dev/null +++ b/modules/whereis/User.py @@ -0,0 +1,35 @@ +# 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 new file mode 100644 index 0000000..57ebb73 --- /dev/null +++ b/modules/whereis/__init__.py @@ -0,0 +1,206 @@ +# 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 : gives the position of /who/.\n!whereare [ ...]: gives the position of these .\n!peoplein : gives the number of people in this /sm/.\n!ip : gets the IP adress of /who/.\n!whoison : gives the name or the number (if > 15) of people at this /location/.\n!whoisin : 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/whois.py b/modules/whois.py deleted file mode 100644 index 1a5f598..0000000 --- a/modules/whois.py +++ /dev/null @@ -1,167 +0,0 @@ -# coding=utf-8 - -import json -import re - -from nemubot import context -from nemubot.exception import IMException -from nemubot.hooks import hook -from nemubot.tools.xmlparser.node import ModuleState - -nemubotversion = 3.4 - -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/?limit=10000' > 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 - - if not context.data.hasNode("aliases"): - context.data.addChild(ModuleState("aliases")) - context.data.getNode("aliases").setIndex("from", "alias") - - 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") - -class Login: - - 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] - 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 = 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"): - try: - return self.home.split("/")[2].replace("_", " ") - except: - return self.gid - - def get_photo(self): - 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) - if status == 200: - return url - except: - logger.exception("On URL %s", url) - return None - - -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["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) - - login_ = login + (":" if not search else "") - lsize = len(login_) - - 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: - raise IMException("Provide a name") - - def format_response(t): - srch, l = t - if type(l) is Login: - pic = l.get_photo() - 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: - 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 login_lookup(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") -def cmd_nicks(msg): - if len(msg.args) < 1: - raise IMException("Provide a login") - nick = login_lookup(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() -def parseask(msg): - 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) - if nick == "my" or nick == "I" or nick == "i" or nick == "je" or nick == "mon" or nick == "ma": - nick = msg.frm - 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.frm) diff --git a/modules/wolframalpha.py b/modules/wolframalpha.py deleted file mode 100644 index fc83815..0000000 --- a/modules/wolframalpha.py +++ /dev/null @@ -1,118 +0,0 @@ -"""Performing search and calculation""" - -# PYTHON STUFFS ####################################################### - -from urllib.parse import quote -import re - -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 - - -# LOADING ############################################################# - -URL_API = "https://api.wolframalpha.com/v2/query?input=%%s&format=plaintext&appid=%s" - -def load(context): - global URL_API - 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\n" - "Register at https://products.wolframalpha.com/api/") - URL_API = URL_API % quote(context.config["apikey"]).replace("%", "%%") - - -# MODULE CORE ######################################################### - -class WFAResults: - - def __init__(self, terms): - self.wfares = web.getXML(URL_API % quote(terms), - timeout=12) - - - @property - def success(self): - try: - 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.documentElement.hasAttribute("error") and self.wfares.documentElement.getAttribute("error") == "true": - return ("An error occurs during computation: " + - self.wfares.getElementsByTagName("error")[0].getElementsByTagName("msg")[0].firstChild.nodeValue) - elif len(self.wfares.getElementsByTagName("didyoumeans")): - start = "Did you mean: " - tag = "didyoumean" - end = "?" - elif len(self.wfares.getElementsByTagName("tips")): - start = "Tips: " - tag = "tip" - end = "" - elif len(self.wfares.getElementsByTagName("relatedexamples")): - start = "Related examples: " - tag = "relatedexample" - end = "" - 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.getElementsByTagName(tag): - if tag == "tip": - proposal.append(dym.getAttribute("text")) - elif tag == "relatedexample": - proposal.append(dym.getAttribute("desc")) - else: - proposal.append(dym.firstChild.nodeValue) - - return start + ', '.join(proposal) + end - - - @property - 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"))) - - -# MODULE INTERFACE #################################################### - -@hook.command("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 IMException("Indicate a calcul to compute") - - s = WFAResults(' '.join(msg.args)) - - if not s.success: - raise IMException(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 diff --git a/modules/worldcup.py b/modules/worldcup.py deleted file mode 100644 index e72f1ac..0000000 --- a/modules/worldcup.py +++ /dev/null @@ -1,216 +0,0 @@ -# coding=utf-8 - -"""The 2014,2018 football worldcup module""" - -from datetime import datetime, timezone -from functools import partial -import json -import re -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 - -nemubotversion = 3.4 - -from nemubot.module.more import Response - -API_URL="http://worldcup.sfg.io/%s" - -def load(context): - 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 (): - return "!worldcup: do something." - - -def start_watch(msg): - w = ModuleState("watch") - w["server"] = msg.server - w["channel"] = msg.channel - w["proprio"] = msg.frm - w["start"] = datetime.now(timezone.utc) - context.data.addChild(w) - context.save() - raise IMException("This channel is now watching world cup events!") - -@hook.command("watch_worldcup") -def cmd_watch(msg): - - # Get current state - node = None - for n in context.data.getChilds(): - if n["server"] == msg.server and n["channel"] == msg.channel: - node = n - break - - if len(msg.args): - if msg.args[0] == "stop" and node is not None: - context.data.delChild(node) - context.save() - 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 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 IMException("This channel will not anymore receives world cup events.") - -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=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): - 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 context.data.getChilds(): - context.send_response(n["server"], Response(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 "%s minute : %s %s (%s)" % (e["time"], detail_event(e["type_of_event"]), e["player"], e["team"]["code"]) - -def prettify(match): - 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["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["fifa_id"], matchdate.strftime("%A %d à %H:%M")) - else: - 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"]) - - 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["fifa_id"]) == matchid: - return [ m ] - -def get_matches(url): - try: - raw = urlopen(url) - except: - raise IMException("requête invalide") - matches = json.loads(raw.read().decode()) - - for match in matches: - if is_valid(match): - yield match - -@hook.command("worldcup") -def cmd_worldcup(msg): - res = Response(channel=msg.channel, nomore="No more match to display", count=" (%d more matches)") - - url = None - if len(msg.args) == 1: - if msg.args[0] == "today" or msg.args[0] == "aujourd'hui": - url = "matches/today?by_date=ASC" - elif msg.args[0] == "tomorrow" or msg.args[0] == "demain": - url = "matches/tomorrow?by_date=ASC" - elif msg.args[0] == "all" or msg.args[0] == "tout" or msg.args[0] == "tous": - url = "matches/" - 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.args[0]) - else: - 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" - 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 diff --git a/modules/ycc.py b/modules/ycc.py new file mode 100644 index 0000000..7180ba2 --- /dev/null +++ b/modules/ycc.py @@ -0,0 +1,74 @@ +# coding=utf-8 + +import re +from urllib.parse import urlparse +from urllib.parse import quote +from urllib.request import urlopen + +nemubotversion = 3.3 + +def help_tiny (): + """Line inserted in the response to the command !help""" + return "Gets YCC urls" + +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." + +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): + if res is None: + return Response(msg.sender, "La situation est embarassante, il semblerait que YCC soit down :(", msg.channel) + 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) + +def cmd_ycc(msg): + 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()) + else: + return Response(msg.sender, "Je n'ai pas d'autre URL à réduire.", msg.channel) + + 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)) + 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) + +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()) + return False + +def parseresponse(res): + parselisten(res) + return True diff --git a/modules/youtube-title.py b/modules/youtube-title.py deleted file mode 100644 index 41b613a..0000000 --- a/modules/youtube-title.py +++ /dev/null @@ -1,96 +0,0 @@ -from urllib.parse import urlparse -import re, json, subprocess - -from nemubot.exception import IMException -from nemubot.hooks import hook -from nemubot.tools.web import _getNormalizedURL, getURLContent -from nemubot.module.more import Response - -"""Get information of youtube videos""" - -nemubotversion = 3.4 - -def help_full(): - return "!yt []: 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() - cmd.extend(links) - res = [] - with subprocess.Popen(cmd, stdout=subprocess.PIPE) as p: - if p.wait() > 0: - raise IMException("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 IMException("No video information to retrieve about this. Sorry!") - return res - -LAST_URLS = dict() - - -@hook.command("yt") -def get_info_yt(msg): - links = list() - - 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 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) - - data = _get_ytdl(links) - res = Response(channel=msg.channel) - for msg in data: - res.append_message(msg) - return res - - -@hook.message() -def parselisten(msg): - parseresponse(msg) - return None - - -@hook.post() -def parseresponse(msg): - global LAST_URLS - 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)) - if o.scheme != "": - if o.netloc == "" and len(o.path) < 10: - continue - for recv in msg.to: - if recv not in LAST_URLS: - LAST_URLS[recv] = list() - LAST_URLS[recv].append(url) - return msg diff --git a/modules/youtube.py b/modules/youtube.py new file mode 100644 index 0000000..f28ef77 --- /dev/null +++ b/modules/youtube.py @@ -0,0 +1,51 @@ +# 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] diff --git a/nemubot.py b/nemubot.py new file mode 100755 index 0000000..5948304 --- /dev/null +++ b/nemubot.py @@ -0,0 +1,76 @@ +#!/usr/bin/python3 +# -*- 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 . + +import sys +import os +import imp +import traceback + +import bot +import prompt +from prompt.builtins import load_file +import importer + +if __name__ == "__main__": + # Create bot context + context = bot.Bot(0, "FIXME") + + # Load the prompt + prmpt = prompt.Prompt() + + # Register the hook for futur import + import sys + 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) + + print ("Nemubot v%s ready, my PID is %i!" % (context.version_txt, + os.getpid())) + while prmpt.run(context): + try: + # Reload context + imp.reload(bot) + context = bot.hotswap(context) + # Reload prompt + imp.reload(prompt) + prmpt = prompt.hotswap(prmpt) + # Reload all other modules + bot.reload() + 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]) + + print ("\nWaiting for other threads shuts down...") + sys.exit(0) diff --git a/nemubot/__init__.py b/nemubot/__init__.py deleted file mode 100644 index 62807c6..0000000 --- a/nemubot/__init__.py +++ /dev/null @@ -1,148 +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 . - -__version__ = '4.0.dev3' -__author__ = 'nemunaire' - -from nemubot.modulecontext import _ModuleContext - -context = _ModuleContext() - - -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 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) - try: - sock.connect(socketfile) - except socket.error as e: - sys.stderr.write(str(e)) - sys.stderr.write("\n") - return 1 - - 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: - for fd, flag in mypoll.poll(): - if flag & (select.POLLERR | select.POLLHUP | select.POLLNVAL): - sock.close() - print("Connection closed.") - return 1 - - 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 == "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 fd == sock.fileno(): - sys.stdout.write(sock.recv(2048).decode()) - - except KeyboardInterrupt: - pass - except: - return 1 - finally: - sock.close() - return 0 - - -def daemonize(socketfile=None): - """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()) diff --git a/nemubot/__main__.py b/nemubot/__main__.py deleted file mode 100644 index 7070639..0000000 --- a/nemubot/__main__.py +++ /dev/null @@ -1,279 +0,0 @@ -# Nemubot is a smart and modulable IM bot. -# 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 -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -def main(): - import os - import signal - 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("-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") - - 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") - - 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) - - # 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)) 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)] - - # 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") - logger.setLevel(logging.DEBUG) - - formatter = logging.Formatter( - '%(asctime)s %(name)s %(levelname)s %(message)s') - - 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) - 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(args.pidfile, args.socketfile)) - - # Add modules dir paths - modules_paths = list() - for path in args.modules_path: - if os.path.isdir(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), - debug=args.verbose > 0) - - if args.no_connect: - context.noautoconnect = True - - # Register the hook for futur import - from nemubot.importer import ModuleFinder - module_finder = ModuleFinder(context.modules_paths, context.add_module) - sys.meta_path.append(module_finder) - - # Load requested configuration files - for path in args.files: - 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: - # Add the server in the context - 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 Exception as e: - logger.error("Unable to connect to '%s': %s", srv.name, e) - continue - break - - # Load module and their configuration - for mod in config.modules: - context.modules_configuration[mod.name] = mod - if mod.autoload: - try: - __import__("nemubot.module." + 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: - __import__("nemubot.module." + 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(args.socketfile) - - # Signals handling - def sigtermhandler(signum, frame): - """On SIGTERM and SIGINT, quit nicely""" - context.quit() - signal.signal(signal.SIGINT, sigtermhandler) - signal.signal(signal.SIGTERM, sigtermhandler) - - def sighuphandler(signum, frame): - """On SIGHUP, perform a deep reload""" - nonlocal context - - logger.debug("SIGHUP receive, iniate reload procedure...") - - # Reload configuration file - for path in args.files: - if os.path.isfile(path): - sync_act("loadconf", path) - signal.signal(signal.SIGHUP, sighuphandler) - - def sigusr1handler(signum, frame): - """On SIGHUSR1, display stacktraces""" - import threading, traceback - for threadId, stack in sys._current_frames().items(): - 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) - - # 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 - while oldcontext != context: - oldcontext = context - context.start() - context.join() - - # Wait for consumers - logger.info("Waiting for other threads shuts down...") - if args.debug: - sigusr1handler(0, None) - 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 deleted file mode 100644 index 2b6e15c..0000000 --- a/nemubot/bot.py +++ /dev/null @@ -1,548 +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 . - -from datetime import datetime, timezone -import logging -from multiprocessing import JoinableQueue -import threading -import select -import sys -import weakref - -from nemubot import __version__ -from nemubot.consumer import Consumer, EventConsumer, MessageConsumer -from nemubot import datastore -import nemubot.hooks - -logger = logging.getLogger("nemubot") - -sync_queue = JoinableQueue() - -def sync_act(*args): - sync_queue.put(list(args)) - - -class Bot(threading.Thread): - - """Class containing the bot context and ensuring key goals""" - - def __init__(self, ip="127.0.0.1", modules_paths=list(), - 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 - debug -- enable debug - """ - - super().__init__(name="Nemubot main") - - 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.debug = debug - self.stop = True - - # External IP for accessing this bot - import ipaddress - self.ip = ipaddress.ip_address(ip) - - # Context paths - self.modules_paths = modules_paths - self.datastore = data_store - self.datastore.open() - - # Keep global context: servers and modules - self._poll = select.poll() - self.servers = dict() - self.modules = dict() - self.modules_configuration = dict() - - # Events - self.events = list() - self.event_timer = None - - # Own hooks - from nemubot.treatment import MessageTreater - self.treater = MessageTreater() - - import re - def in_ping(msg): - return msg.respond("pong") - self.treater.hm.add_hook(nemubot.hooks.Message(in_ping, - match=lambda msg: re.match("^ *(m[' ]?entends?[ -]+tu|h?ear me|do you copy|ping)", - msg.message, re.I)), - "in", "DirectAsk") - - def in_echo(msg): - from nemubot.message import Text - return Text(msg.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): - """Parse and response to help messages""" - from nemubot.module.more import Response - res = Response(channel=msg.to_response) - if len(msg.args) >= 1: - 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[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:])): - if h.help_usage: - lp = ["\x03\x02%s%s\x03\x02: %s" % (msg.args[0], (" " + k if k is not None else ""), h.help_usage[k]) for k in h.help_usage] - jp = h.keywords.help() - return res.append_message(lp + ([". Moreover, you can provides some optional parameters: "] + jp if len(jp) else []), title="Usage for command %s" % msg.args[0]) - elif h.help: - return res.append_message("Command %s: %s" % (msg.args[0], h.help)) - else: - return res.append_message("Sorry, there is currently no help for the command %s. Feel free to make a pull request at https://github.com/nemunaire/nemubot/compare" % msg.args[0]) - res.append_message("Sorry, there is no command %s" % msg.args[0]) - else: - res.append_message("Sorry, there is no module named %s" % msg.args[0]) - else: - res.append_message("Pour me demander quelque chose, commencez " - "votre message par mon nom ; je réagis " - "également à certaine commandes commençant par" - " !. Pour plus d'informations, envoyez le " - "message \"!more\".") - res.append_message("Mon code source est libre, publié sous " - "licence AGPL (http://www.gnu.org/licenses/). " - "Vous pouvez le consulter, le dupliquer, " - "envoyer des rapports de bogues ou bien " - "contribuer au projet sur GitHub : " - "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", - 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") - - import os - from queue import Queue - # 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): - self.datastore.close() - - - 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) - - - 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 - if fd != sync_queue._reader.fileno() and fd in self.servers: - srv = self.servers[fd] - - if flag & (select.POLLERR | select.POLLHUP | select.POLLNVAL): - try: - srv.exception(flag) - except: - logger.exception("Uncatched exception on server exception") - - if srv.fileno() > 0: - if flag & (select.POLLOUT): - try: - srv.async_write() - except: - logger.exception("Uncatched exception on server write") - - if flag & (select.POLLIN | select.POLLPRI): - try: - for i in srv.async_read(): - self.receive_message(srv, i) - except: - logger.exception("Uncatched exception on server read") - - else: - del self.servers[fd] - - - # Always check the sync queue - while not sync_queue.empty(): - args = sync_queue.get() - action = args.pop(0) - - logger.debug("Executing sync_queue action %s%s", action, args) - - 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": - try: - self._poll.unregister(int(args[1])) - except KeyError: - pass - except: - logger.exception("Unhandled excpetion during action:") - - elif action == "exit": - self.quit() - - elif action == "launch_consumer": - pass # This is treated after the loop - - sync_queue.task_done() - - - # 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") - - - - # 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 quiet useless... - try: - evt.id = str(uuid.UUID(eid)) - except ValueError: - evt.id = eid - - # TODO: mutex here plz - - # Add the event in its place - t = evt.current - i = 0 # sentinel - for i in range(0, len(self.events)): - if self.events[i].current > t: - break - self.events.insert(i, evt) - - if i == 0 and not self.stop: - # 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, evt.id)) - evt.module_src = module_src - - logger.info("New event registered in %d position: %s", i, t) - return evt.id - - - def del_event(self, evt, module_src=None): - """Find and remove an event from list - - Return: - True if the event has been found and removed, False else - - Argument: - evt -- The ModuleEvent object to remove or just the event identifier - - Keyword arguments: - module_src -- The module to which the event is attached to (ignored if evt is a ModuleEvent) - """ - - logger.info("Removing event: %s from %s", evt, module_src) - - from nemubot.event import ModuleEvent - if type(evt) is ModuleEvent: - id = evt.id - module_src = evt.module_src - else: - id = evt - - if len(self.events) > 0 and id == self.events[0].id: - 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() - 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, 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): - 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() - - 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)) - sync_act("launch_consumer") - - self._update_event_timer() - - - # Consumers methods - - def add_server(self, srv, autoconnect=True): - """Add a new server to the context - - Arguments: - srv -- a concrete AbstractServer instance - autoconnect -- connect after add? - """ - - fileno = srv.fileno() - if fileno not in self.servers: - self.servers[fileno] = srv - self.servers[srv.name] = srv - if autoconnect and not hasattr(self, "noautoconnect"): - srv.connect() - return True - - else: - return False - - - # Modules methods - - def import_module(self, name): - """Load a module - - Argument: - name -- name of the module to load - """ - - if name in self.modules: - self.unload_module(name) - - __import__(name) - - - def add_module(self, module): - """Add a module to the context, if already exists, unload the - old one before""" - module_name = module.__spec__.name if hasattr(module, "__spec__") else module.__name__ - - # Check if the module already exists - if module_name in self.modules: - self.unload_module(module_name) - - # Overwrite print built-in - def prnt(*args): - if hasattr(module, "logger"): - module.logger.info(" ".join([str(s) for s in args])) - else: - logger.info("[%s] %s", module_name, " ".join([str(s) for s in args])) - module.print = prnt - - # Create module context - from nemubot.modulecontext import _ModuleContext, ModuleContext - module.__nemubot_context__ = ModuleContext(self, module) - - if not hasattr(module, "logger"): - module.logger = logging.getLogger("nemubot.module." + module_name) - - # Replace imported context by real one - for attr in module.__dict__: - if attr != "__nemubot_context__" and type(module.__dict__[attr]) == _ModuleContext: - module.__dict__[attr] = module.__nemubot_context__ - - # Register decorated functions - import nemubot.hooks - for s, h in nemubot.hooks.hook.last_registered: - module.__nemubot_context__.add_hook(h, *s if isinstance(s, list) else s) - nemubot.hooks.hook.last_registered = [] - - # Launch the module - if hasattr(module, "load"): - try: - module.load(module.__nemubot_context__) - except: - module.__nemubot_context__.unload() - raise - - # Save a reference to the module - self.modules[module_name] = weakref.ref(module) - logger.info("Module '%s' successfully loaded.", module_name) - - - def unload_module(self, name): - """Unload a module""" - 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(module, "unload"): - module.unload(self) - module.__nemubot_context__.unload() - - # Remove from the nemubot dict - del self.modules[name] - - # Remove from the Python dict - del sys.modules[name] - for mod in [i for i in sys.modules]: - if mod[:len(name) + 1] == name + ".": - logger.debug("Module '%s' also removed from system modules list.", mod) - del sys.modules[mod] - - logger.info("Module `%s' successfully unloaded.", name) - - return True - return False - - - def receive_message(self, srv, msg): - """Queued the message for treatment - - Arguments: - srv -- The server where the message comes from - msg -- The message not parsed, as simple as possible - """ - - self.cnsr_queue.put_nowait(MessageConsumer(srv, msg)) - - - def quit(self): - """Save and unload modules and disconnect servers""" - - if self.event_timer is not None: - logger.info("Stop the event timer...") - self.event_timer.cancel() - - logger.info("Save and unload all modules...") - for mod in [m for m in self.modules.keys()]: - 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") - with self.cnsr_lock: - k = list(self.cnsr_thrd) - for cnsr in k: - cnsr.stop = True - - if self.stop is False or sync_queue is not None: - self.stop = True - sync_act("end") - sync_queue.join() - - - # Treatment - - def check_rest_times(self, store, hook): - """Remove from store the hook if it has been executed given time""" - if hook.times == 0: - if isinstance(store, dict): - store[hook.name].remove(hook) - if len(store) == 0: - del store[hook.name] - elif isinstance(store, list): - store.remove(hook) diff --git a/nemubot/channel.py b/nemubot/channel.py deleted file mode 100644 index 835c22f..0000000 --- a/nemubot/channel.py +++ /dev/null @@ -1,162 +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 . - -import logging - - -class Channel: - - """A chat room""" - - def __init__(self, name, password=None, encoding=None): - """Initialize the channel - - Arguments: - name -- the channel name - password -- the optional password use to join it - encoding -- the optional encoding of the channel - """ - - self.name = name - self.password = password - self.encoding = encoding - self.people = dict() - self.topic = "" - 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": - self.parse332(msg) - elif cmd == "MODE": - self.mode(msg) - elif cmd == "JOIN": - self.join(msg.frm) - elif cmd == "NICK": - self.nick(msg.frm, msg.text) - elif cmd == "PART" or cmd == "QUIT": - self.part(msg.frm) - elif cmd == "TOPIC": - self.topic = self.text - - 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 - - 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 - - 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] - del self.people[oldnick] - self.people[newnick] = lvl - - def part(self, nick): - """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": - if len(msg.text) > 1: - self.password = ' '.join(msg.text[1:])[1:] - else: - self.password = msg.text[1] - elif msg.text[0] == "+o": - self.people[msg.frm] |= 4 - elif msg.text[0] == "-o": - self.people[msg.frm] &= ~4 - elif msg.text[0] == "+h": - self.people[msg.frm] |= 2 - elif msg.text[0] == "-h": - self.people[msg.frm] &= ~2 - elif msg.text[0] == "+v": - self.people[msg.frm] |= 1 - elif msg.text[0] == "-v": - self.people[msg.frm] &= ~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] == "@": - level = 4 - elif p[0] == "%": - level = 2 - elif p[0] == "+": - level = 1 - else: - self.join(p, 0) - continue - self.join(p[1:], level) diff --git a/nemubot/config/__init__.py b/nemubot/config/__init__.py deleted file mode 100644 index 6bbc1b2..0000000 --- a/nemubot/config/__init__.py +++ /dev/null @@ -1,26 +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 . - -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 diff --git a/nemubot/config/include.py b/nemubot/config/include.py deleted file mode 100644 index 408c09a..0000000 --- a/nemubot/config/include.py +++ /dev/null @@ -1,20 +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 . - -class Include: - - def __init__(self, path): - self.path = path diff --git a/nemubot/config/module.py b/nemubot/config/module.py deleted file mode 100644 index ab51971..0000000 --- a/nemubot/config/module.py +++ /dev/null @@ -1,26 +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 . - -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 deleted file mode 100644 index 992cd8e..0000000 --- a/nemubot/config/nemubot.py +++ /dev/null @@ -1,46 +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 . - -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 deleted file mode 100644 index 17bfaee..0000000 --- a/nemubot/config/server.py +++ /dev/null @@ -1,45 +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 . - -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, trynb=0): - 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, trynb=trynb, **self.args) diff --git a/nemubot/consumer.py b/nemubot/consumer.py deleted file mode 100644 index a9a4146..0000000 --- a/nemubot/consumer.py +++ /dev/null @@ -1,129 +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 . - -import logging -import queue -import threading - -logger = logging.getLogger("nemubot.consumer") - - -class MessageConsumer: - - """Store a message before treating""" - - def __init__(self, srv, msg): - self.srv = srv - self.orig = msg - - - def run(self, context): - """Create, parse and treat the message""" - - from nemubot.bot import Bot - assert isinstance(context, Bot) - - msgs = [] - - # Parse message - try: - for msg in self.srv.parse(self.orig): - msgs.append(msg) - except: - logger.exception("Error occurred during the processing of the %s: " - "%s", type(self.orig).__name__, self.orig) - - # Treat message - for msg in msgs: - for res in context.treater.treat_msg(msg): - # Identify destination - 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.fileno() - elif res.server in context.servers: - to_server = context.servers[res.server] - else: - to_server = 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 message - to_server.send_response(res) - - -class EventConsumer: - - """Store a event before treating""" - - def __init__(self, evt, timeout=20): - self.evt = evt - self.timeout = timeout - - - def run(self, context): - try: - 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, eid=self.evt.id) - - # Or remove reference of this event - elif (hasattr(self.evt, "module_src") and - self.evt.module_src is not None): - self.evt.module_src.__nemubot_context__.events.remove((self.evt, self.evt.id)) - - - -class Consumer(threading.Thread): - - """Dequeue and exec requested action""" - - def __init__(self, context): - self.context = context - self.stop = False - super().__init__(name="Nemubot consumer", daemon=True) - - - def run(self): - try: - while not self.stop: - try: - stm = self.context.cnsr_queue.get(True, 1) - except queue.Empty: - break - - 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: - with self.context.cnsr_lock: - self.context.cnsr_thrd.remove(self) diff --git a/nemubot/datastore/__init__.py b/nemubot/datastore/__init__.py deleted file mode 100644 index 3e38ad2..0000000 --- a/nemubot/datastore/__init__.py +++ /dev/null @@ -1,18 +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 . - -from nemubot.datastore.abstract import Abstract -from nemubot.datastore.xml import XML diff --git a/nemubot/datastore/abstract.py b/nemubot/datastore/abstract.py deleted file mode 100644 index aeaecc6..0000000 --- a/nemubot/datastore/abstract.py +++ /dev/null @@ -1,69 +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 . - -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 - - def close(self): - return - - 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): - """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 deleted file mode 100644 index aa6cbd0..0000000 --- a/nemubot/datastore/xml.py +++ /dev/null @@ -1,171 +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 . - -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): - - """A concrete implementation of a data store that relies on XML files""" - - 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""" - - if not os.path.isdir(self.basedir): - os.mkdir(self.basedir) - - 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) - - self.lock_file.truncate() - self.lock_file.write(str(os.getpid())) - self.lock_file.flush() - - return True - - def close(self): - """Release a locked path""" - - 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 - - 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, 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): - try: - 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 = _true_load(path) - - logger.warn("Restoring from backup: %s", path) - - return cnt - except xml.parsers.expat.ExpatError: - continue - - # Default case: initialize a new empty datastore - return super().load(module, knodes) - - 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 - - Argument: - module -- the module name of data to load - data -- the new data to save - """ - - path = self._get_data_file_path(module) - - if self.rotate: - self._rotate(path) - - if data is None: - return - - 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/event/__init__.py b/nemubot/event/__init__.py deleted file mode 100644 index 49c6902..0000000 --- a/nemubot/event/__init__.py +++ /dev/null @@ -1,104 +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 . - -from datetime import datetime, timedelta, timezone - - -class ModuleEvent: - - """Representation of a event initiated by a bot module""" - - 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 - func -- Function called to check - 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) - """ - - # What have we to check? - self.func = func - - # How detect a change? - self.cmp = cmp - - # What should we call when? - self.call = call - - # Store times - 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? - self.times = times - - @property - def current(self): - """Return the date of the near check""" - if self.times != 0: - if self._end is None: - self._end = datetime.now(timezone.utc) + self.offset + self.interval - return self._end - return None - - @property - def next(self): - """Return the date of the next check""" - if self.times != 0: - if self._end is None: - return self.current - elif self._end < datetime.now(timezone.utc): - self._end += self.interval - return self._end - return None - - @property - def time_left(self): - """Return the time left before/after the near check""" - if self.current is not None: - return self.current - datetime.now(timezone.utc) - return timedelta.max - - def check(self): - """Run a check and realized the event if this is time""" - - # Get new data - if self.func is not None: - d_new = self.func() - else: - d_new = None - - # then compare with current data - 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.func is not None: - self.call(d_new) - else: - self.call() diff --git a/nemubot/exception/__init__.py b/nemubot/exception/__init__.py deleted file mode 100644 index 84464a0..0000000 --- a/nemubot/exception/__init__.py +++ /dev/null @@ -1,34 +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 . - -class IMException(Exception): - - - def __init__(self, message, personnal=True): - 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.args, - server=msg.server, to=msg.to_response) - - else: - from nemubot.message import Text - return Text(*self.args, - server=msg.server, to=msg.to_response) diff --git a/nemubot/exception/keyword.py b/nemubot/exception/keyword.py deleted file mode 100644 index 6e3c07f..0000000 --- a/nemubot/exception/keyword.py +++ /dev/null @@ -1,23 +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 . - -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 deleted file mode 100644 index 9024494..0000000 --- a/nemubot/hooks/__init__.py +++ /dev/null @@ -1,51 +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 . - -from nemubot.hooks.abstract import Abstract -from nemubot.hooks.command import Command -from nemubot.hooks.message import Message - - -class hook: - - last_registered = [] - - - 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) diff --git a/nemubot/hooks/abstract.py b/nemubot/hooks/abstract.py deleted file mode 100644 index ffe79fb..0000000 --- a/nemubot/hooks/abstract.py +++ /dev/null @@ -1,138 +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 . - -import types - -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 - """ - - assert callable(call) - - 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, channels=None, servers=None, mtimes=-1, - end_call=None, check=None, match=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 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 - - 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 - - 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 __str__(self): - return "" - - - def can_write(self, receivers=list(), server=None): - return True - - - def check(self, data1): - return self.mod_check(data1) if self.mod_check is not None else True - - - def match(self, data1): - return self.mod_match(data1) if self.mod_match is not None else True - - - def run(self, data1, *args): - """Run the hook""" - - from nemubot.exception import IMException - self.times -= 1 - - ret = None - - 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) - - if isinstance(ret, list): - for r in ret: - yield ret - elif ret is not None: - yield ret diff --git a/nemubot/hooks/command.py b/nemubot/hooks/command.py deleted file mode 100644 index 863d672..0000000 --- a/nemubot/hooks/command.py +++ /dev/null @@ -1,67 +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 . - -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 -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)) and - Abstract.match(self, msg) - ) diff --git a/nemubot/hooks/keywords/__init__.py b/nemubot/hooks/keywords/__init__.py deleted file mode 100644 index 598b04f..0000000 --- a/nemubot/hooks/keywords/__init__.py +++ /dev/null @@ -1,47 +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 . - -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) - - -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 diff --git a/nemubot/hooks/keywords/abstract.py b/nemubot/hooks/keywords/abstract.py deleted file mode 100644 index a990cf3..0000000 --- a/nemubot/hooks/keywords/abstract.py +++ /dev/null @@ -1,35 +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 . - -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 deleted file mode 100644 index c2d3f2e..0000000 --- a/nemubot/hooks/keywords/dict.py +++ /dev/null @@ -1,59 +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 . - -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 ((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: - 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/manager.py b/nemubot/hooks/manager.py deleted file mode 100644 index 6a57d2a..0000000 --- a/nemubot/hooks/manager.py +++ /dev/null @@ -1,134 +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 . - -import logging - - -class HooksManager: - - """Class to manage hooks""" - - 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): - """Add a hook to the manager - - Argument: - hook -- a Hook instance - triggers -- string that trigger the hook - """ - - assert hook is not None, hook - - h = self._access(*triggers) - - h["__end__"].append(hook) - - self.logger.debug("New hook successfully added in %s: %s", - "/".join(triggers), hook) - - - def del_hooks(self, *triggers, hook=None): - """Remove the given hook from the manager - - Argument: - triggers -- trigger string to remove - - Keyword argument: - hook -- a Hook instance to remove from the trigger string - """ - - assert hook is not None or len(triggers) - - 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 = dict() - - - def get_hooks(self, *triggers): - """Returns list of trigger hooks that match the given trigger string - - Argument: - triggers -- the trigger string - """ - - for n in range(len(triggers) + 1): - i = self._access(*triggers[:n]) - for h in i["__end__"]: - yield h - - - def get_reverse_hooks(self, *triggers, exclude_first=False): - """Returns list of triggered hooks that are bellow or at the same level - - Argument: - triggers -- the trigger string - - Keyword arguments: - exclude_first -- start reporting hook at the next level - """ - - 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 deleted file mode 100755 index a0f38d7..0000000 --- a/nemubot/hooks/manager_test.py +++ /dev/null @@ -1,115 +0,0 @@ -#!/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/hooks/message.py b/nemubot/hooks/message.py deleted file mode 100644 index ee07600..0000000 --- a/nemubot/hooks/message.py +++ /dev/null @@ -1,49 +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 . - -import re - -from nemubot.hooks.abstract import Abstract -import nemubot.message - - -class Message(Abstract): - - """Class storing hook information, specialized for a generic Message""" - - def __init__(self, call, regexp=None, help=None, **kwargs): - super().__init__(call=call, **kwargs) - - assert regexp is None or type(regexp) is str, regexp - - self.regexp = regexp - self.help = help - - - def __str__(self): - # 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 super().check(msg) - - - def match(self, msg): - if not isinstance(msg, nemubot.message.text.Text): - return False - else: - return (self.regexp is None or re.match(self.regexp, msg.message)) and super().match(msg) diff --git a/nemubot/importer.py b/nemubot/importer.py deleted file mode 100644 index 674ab40..0000000 --- a/nemubot/importer.py +++ /dev/null @@ -1,69 +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 . - -from importlib.abc import Finder -from importlib.machinery import SourceFileLoader -import logging -import os - -logger = logging.getLogger("nemubot.importer") - - -class ModuleFinder(Finder): - - def __init__(self, modules_paths, add_module): - self.modules_paths = modules_paths - self.add_module = add_module - - def find_module(self, fullname, path=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, module_name + ".py")): - return ModuleLoader(self.add_module, fullname, - 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, module_name), - "__init__.py")) - return None - - -class ModuleLoader(SourceFileLoader): - - def __init__(self, add_module, fullname, path): - self.add_module = add_module - SourceFileLoader.__init__(self, fullname, path) - - - def _load(self, module, name): - # Add the module to the global modules list - self.add_module(module) - 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().exec_module(module) - self._load(module, module.__spec__.name) - - - # Python 3.3 - def load_module(self, fullname): - module = super().load_module(fullname) - return self._load(module, module.__name__) diff --git a/nemubot/message/__init__.py b/nemubot/message/__init__.py deleted file mode 100644 index 4d69dbb..0000000 --- a/nemubot/message/__init__.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 . - -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 diff --git a/nemubot/message/abstract.py b/nemubot/message/abstract.py deleted file mode 100644 index 3af0511..0000000 --- a/nemubot/message/abstract.py +++ /dev/null @@ -1,83 +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 . - -from datetime import datetime, timezone - - -class Abstract: - - """This class represents an abstract message""" - - def __init__(self, server=None, date=None, to=None, to_response=None, frm=None, frm_owner=False): - """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 = frm_owner - - - @property - def to_response(self): - if self._to_response is not None: - return self._to_response - else: - return self.to - - - @property - def channel(self): - # TODO: this is for legacy modules - if self.to_response is not None and len(self.to_response) > 0: - return self.to_response[0] - else: - return None - - 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, - "frm_owner": self.frm_owner, - } - - 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 deleted file mode 100644 index ca87e4c..0000000 --- a/nemubot/message/command.py +++ /dev/null @@ -1,39 +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 . - -from nemubot.message.abstract import Abstract - - -class Command(Abstract): - - """This class represents a specialized TextMessage""" - - def __init__(self, cmd, args=None, kwargs=None, *nargs, **kargs): - super().__init__(*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) - - -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 deleted file mode 100644 index 3b1fabb..0000000 --- a/nemubot/message/directask.py +++ /dev/null @@ -1,39 +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 . - -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 - """ - - super().__init__(*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/IRCLib.py b/nemubot/message/printer/IRCLib.py deleted file mode 100644 index abd1f2f..0000000 --- a/nemubot/message/printer/IRCLib.py +++ /dev/null @@ -1,67 +0,0 @@ -# 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 . - -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/message/printer/Matrix.py b/nemubot/message/printer/Matrix.py deleted file mode 100644 index ad1b99e..0000000 --- a/nemubot/message/printer/Matrix.py +++ /dev/null @@ -1,69 +0,0 @@ -# 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 . - -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/message/printer/__init__.py b/nemubot/message/printer/__init__.py deleted file mode 100644 index e0fbeef..0000000 --- a/nemubot/message/printer/__init__.py +++ /dev/null @@ -1,15 +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 . diff --git a/nemubot/message/printer/socket.py b/nemubot/message/printer/socket.py deleted file mode 100644 index 6884c88..0000000 --- a/nemubot/message/printer/socket.py +++ /dev/null @@ -1,68 +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 . - -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) == 0 or 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%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) - - - 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/message/printer/test_socket.py b/nemubot/message/printer/test_socket.py deleted file mode 100644 index 41f74b0..0000000 --- a/nemubot/message/printer/test_socket.py +++ /dev/null @@ -1,112 +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 . - -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" - ), - ( - 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" - ), - ] - - - 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() diff --git a/nemubot/message/response.py b/nemubot/message/response.py deleted file mode 100644 index f9353ad..0000000 --- a/nemubot/message/response.py +++ /dev/null @@ -1,29 +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 . - -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) diff --git a/nemubot/message/text.py b/nemubot/message/text.py deleted file mode 100644 index f691a04..0000000 --- a/nemubot/message/text.py +++ /dev/null @@ -1,41 +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 . - -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 - """ - - super().__init__(*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/message/visitor.py b/nemubot/message/visitor.py deleted file mode 100644 index 454633a..0000000 --- a/nemubot/message/visitor.py +++ /dev/null @@ -1,24 +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 . - - -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/nemubot/module/__init__.py b/nemubot/module/__init__.py deleted file mode 100644 index 33f0e41..0000000 --- a/nemubot/module/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# -# 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/nemubot/module/more.py b/nemubot/module/more.py deleted file mode 100644 index 206d97a..0000000 --- a/nemubot/module/more.py +++ /dev/null @@ -1,299 +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 . - -"""Progressive display of very long messages""" - -# PYTHON STUFFS ####################################################### - -import logging - -from nemubot.message import Text, DirectAsk -from nemubot.hooks import hook - -logger = logging.getLogger("nemubot.response") - - -# MODULE CORE ######################################################### - -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): - self.nomore = nomore - self.more = more - self.line_treat = line_treat - self.rawtitle = title - self.server = server - self.messages = list() - 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.channel = channel - self.nick = nick - self.count = count - - - @property - def to(self): - if self.channel is None: - if self.nick is not None: - return [self.nick] - return list() - elif isinstance(self.channel, list): - return self.channel - else: - return [self.channel] - - - 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) - return self - - - 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 = [message] - self.alone = True - 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): - return self.rawtitle[0] - 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 - if isinstance(self.rawtitle, list): - self.rawtitle.pop(0) - 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.to) - else: - return Text(self.get_message(maxlen), - server=None, to=self.to) - - - 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 - - if self.empty: - if hasattr(self.nomore, '__call__'): - res = self.nomore(self) - if res is None: - return "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.nomore - - if self.line_treat is not None and self.elt == 0: - 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: - if self.elt > 0: - msg += self.title + " " + self.more + ": " - else: - msg += self.title + ": " - - elif self.elt > 0: - msg += "[…]" - if self.messages[0][self.elt - 1] == ' ': - msg += " " - - elts = self.messages[0][self.elt:] - if isinstance(elts, list): - for e in elts: - if len(msg) + len(e) > maxlen - 3: - msg += "[…]" - self.alone = False - return msg - else: - msg += e + ", " - self.elt += 1 - self.pop() - return msg[:len(msg)-2] - - else: - if len(elts.encode()) <= maxlen: - self.pop() - if self.count is not None and not self.alone: - return msg + elts + (self.count % len(self.messages)) - else: - return msg + elts - - else: - words = elts.split(' ') - - 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()) >= maxlen: - msg += "[…]" - self.alone = False - return msg - else: - msg += w + " " - self.elt += len(w) + 1 - self.pop() - return msg - - -SERVERS = dict() - - -# MODULE INTERFACE #################################################### - -@hook.post() -def parseresponse(res): - # TODO: handle inter-bot communication NOMORE - # TODO: check that the response is not the one already saved - if isinstance(res, Response): - if res.server not in SERVERS: - SERVERS[res.server] = dict() - for receiver in res.to: - 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.command("more") -def cmd_more(msg): - """Display next chunck of the message""" - res = list() - if msg.server in SERVERS: - for receiver in msg.to_response: - if receiver in SERVERS[msg.server]: - nw, bk = SERVERS[msg.server][receiver] - if nw is not None and not nw.alone: - bk = nw - SERVERS[msg.server][receiver] = None, bk - if bk is not None: - res.append(bk) - return res - - -@hook.command("next") -def cmd_next(msg): - """Display the next information include in the message""" - res = list() - if msg.server in SERVERS: - for receiver in msg.to_response: - if receiver in SERVERS[msg.server]: - 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() - if bk is not None: - res.append(bk) - return res diff --git a/nemubot/modulecontext.py b/nemubot/modulecontext.py deleted file mode 100644 index 4af3731..0000000 --- a/nemubot/modulecontext.py +++ /dev/null @@ -1,155 +0,0 @@ -# Nemubot is a smart and modulable IM bot. -# 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 -# 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 _ModuleContext: - - def __init__(self, module=None, knodes=None): - self.module = module - - if module is not None: - self.module_name = (module.__spec__.name if hasattr(module, "__spec__") else module.__name__).replace("nemubot.module.", "") - else: - self.module_name = "" - - self.hooks = list() - self.events = list() - self.debug = False - - 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 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 - self.hooks.append((triggers, hook)) - - def del_hook(self, hook, *triggers): - from nemubot.hooks import Abstract as AbstractHook - assert isinstance(hook, AbstractHook), hook - self.hooks.remove((triggers, hook)) - - def subtreat(self, msg): - return None - - def add_event(self, evt, eid=None): - return self.events.append((evt, eid)) - - 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(self, server, res): - self.module.logger.info("Send response: %s", res) - - 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): - if not hasattr(self, "_data"): - self._data = self.load_data() - return self._data - - - def unload(self): - """Perform actions for unloading the module""" - - # Remove registered hooks - for (s, h) in self.hooks: - self.del_hook(h, *s) - - # Remove registered events - for evt, eid 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.debug - - - def load_data(self): - return self.context.datastore.load(self.module_name, self._knodes) - - 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 diff --git a/nemubot/server/IRCLib.py b/nemubot/server/IRCLib.py deleted file mode 100644 index cdd13cf..0000000 --- a/nemubot/server/IRCLib.py +++ /dev/null @@ -1,375 +0,0 @@ -# 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 . - -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/Matrix.py b/nemubot/server/Matrix.py deleted file mode 100644 index ed4b746..0000000 --- a/nemubot/server/Matrix.py +++ /dev/null @@ -1,200 +0,0 @@ -# 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 . - -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 deleted file mode 100644 index db9ad87..0000000 --- a/nemubot/server/__init__.py +++ /dev/null @@ -1,98 +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 . - - -def factory(uri, ssl=False, **init_args): - from urllib.parse import urlparse, unquote, parse_qs - o = urlparse(uri) - - srv = None - - 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 = 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"] = unquote(o.password) - - modifiers = o.path.split(",") - target = unquote(modifiers.pop(0)[1:]) - - # 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"][0])) - - if "key" in params: - if "channels" not in args: - args["channels"] = [] - args["channels"].append((target, params["key"][0])) - - if "pass" in params: - args["password"] = params["pass"][0] - - if "charset" in params: - args["encoding"] = params["charset"][0] - - if "channels" not in args and "isnick" not in modifiers: - args["channels"] = [target] - - args["ssl"] = ssl - - from nemubot.server.IRCLib import IRCLib as IRCServer - srv = IRCServer(**args) - - 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/nemubot/server/abstract.py b/nemubot/server/abstract.py deleted file mode 100644 index 8fbb923..0000000 --- a/nemubot/server/abstract.py +++ /dev/null @@ -1,167 +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 . - -import logging -import queue -import traceback - -from nemubot.bot import sync_act - - -class AbstractServer: - - """An abstract server: handle communication with an IM server""" - - 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._fd = fdClass(**kwargs) - - self._logger = logging.getLogger("nemubot.server." + str(self.name)) - self._readbuffer = b'' - self._sending_queue = queue.Queue() - - - @property - def name(self): - if self._name is not None: - return self._name - else: - return self._fd.fileno() - - - # Open/close - - def connect(self, *args, **kwargs): - """Register the server in _poll""" - - self._logger.info("Opening connection") - - self._fd.connect(*args, **kwargs) - - self._on_connect() - - def _on_connect(self): - sync_act("sckt", "register", self._fd.fileno()) - - - def close(self, *args, **kwargs): - """Unregister the server from _poll""" - - self._logger.info("Closing connection") - - if self._fd.fileno() > 0: - sync_act("sckt", "unregister", self._fd.fileno()) - - self._fd.close(*args, **kwargs) - - - # Writes - - def write(self, message): - """Asynchronymously send a message to the server using send_callback - - Argument: - message -- message to send - """ - - self._sending_queue.put(self.format(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()) - - - def async_write(self): - """Internal function used when the file descriptor is writable""" - - try: - 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() - - except queue.Empty: - pass - - - 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) - - - # 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, flags): - """Exception occurs on fd""" - - self._fd.close() - - # Proxy - - def fileno(self): - return self._fd.fileno() diff --git a/nemubot/server/socket.py b/nemubot/server/socket.py deleted file mode 100644 index bf55bf5..0000000 --- a/nemubot/server/socket.py +++ /dev/null @@ -1,172 +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 . - -import os -import socket - -import nemubot.message as message -from nemubot.message.printer.socket import Socket as SocketPrinter -from nemubot.server.abstract import AbstractServer - - -class _Socket(AbstractServer): - - """Concrete implementation of a socket connection""" - - def __init__(self, printer=SocketPrinter, **kwargs): - """Create a server socket - """ - - super().__init__(**kwargs) - - self.readbuffer = b'' - self.printer = printer - - - # Write - - def _write(self, cnt): - self._fd.sendall(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, bufsize=1024, *args, **kwargs): - return self._fd.recv(bufsize, *args, **kwargs) - - - def parse(self, line): - """Implement a default behaviour for socket""" - import shlex - - line = line.strip().decode() - try: - args = shlex.split(line) - except ValueError: - args = line.split(' ') - - if len(args): - yield message.Command(cmd=args[0], args=args[1:], server=self._fd.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, 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) - - self._bind = bind - - - def connect(self): - 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: - self._fd.bind(self._bind) - - -class UnixSocket: - - def __init__(self, location, **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) - self.connect(self._socket_path) - - -class SocketClient(_Socket): - - def __init__(self, **kwargs): - super().__init__(fdClass=socket.socket, **kwargs) - - -class _Listener: - - def __init__(self, new_server_cb, instanciate=SocketClient, **kwargs): - super().__init__(**kwargs) - - self._instanciate = instanciate - self._new_server_cb = new_server_cb - - - def read(self): - conn, addr = self._fd.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): - - 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._fd.bind(self._socket_path) - self._fd.listen(5) - self._logger.info("Socket ready for accepting new connections") - - self._on_connect() - - - def close(self): - import os - import socket - - try: - self._fd.shutdown(socket.SHUT_RDWR) - except socket.error: - pass - - super().close() - - try: - if self._socket_path is not None: - os.remove(self._socket_path) - except: - pass diff --git a/nemubot/server/threaded.py b/nemubot/server/threaded.py deleted file mode 100644 index eb1ae19..0000000 --- a/nemubot/server/threaded.py +++ /dev/null @@ -1,132 +0,0 @@ -# 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 . - -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) diff --git a/nemubot/tools/__init__.py b/nemubot/tools/__init__.py deleted file mode 100644 index 57f3468..0000000 --- a/nemubot/tools/__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 . diff --git a/nemubot/tools/countdown.py b/nemubot/tools/countdown.py deleted file mode 100644 index afd585f..0000000 --- a/nemubot/tools/countdown.py +++ /dev/null @@ -1,108 +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 . - -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, 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: - 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 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 deleted file mode 100644 index 9e9bbad..0000000 --- a/nemubot/tools/date.py +++ /dev/null @@ -1,83 +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 . - -# Extraction/Format text - -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[0-9]{1,4}) .+? - (?P[0-9]{1,2}|"''' + "|".join(month_binding) + '''") - (?:.+?(?P[0-9]{1,4}))? (?:[^0-9]+ - (?:(?P[0-9]{1,2})[^0-9]*[h':] - (?:[^0-9]*(?P[0-9]{1,2}) - (?:[^0-9]*[m\":][^0-9]*(?P[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 in month_binding: - month = month_binding[month] - - 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: - from datetime import date - 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 - - from datetime import datetime - return datetime(int(year), int(month), int(day), - int(hour), int(minute), int(second)) - else: - return None diff --git a/nemubot/tools/feed.py b/nemubot/tools/feed.py deleted file mode 100644 index 6f8930d..0000000 --- a/nemubot/tools/feed.py +++ /dev/null @@ -1,157 +0,0 @@ -#!/usr/bin/python3 - -import datetime -import time -from xml.dom.minidom import parse -from xml.dom.minidom import parseString -from xml.dom.minidom import getDOMImplementation - - -class AtomEntry: - - def __init__(self, node): - 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: - 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() - 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 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 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 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) - - -class RSSEntry: - - def __init__(self, node): - 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 = "" - - 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: - 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 "" % (self.title, self.pubDate) - - - def __cmp__(self, other): - return not (self.id == other.id) - - -class Feed: - - def __init__(self, string): - self.feed = parseString(string).documentElement - self.id = None - self.title = None - self.updated = None - self.entries = list() - - if self.feed.tagName == "rdf:RDF" or self.feed.tagName == "rss": - 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.") - - - def _parse_atom_feed(self): - self.id = self.feed.getElementsByTagName("id")[0].firstChild.nodeValue - self.title = self.feed.getElementsByTagName("title")[0].firstChild.nodeValue - - for item in self.feed.getElementsByTagName("entry"): - 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 __and__(self, b): - ret = [] - - for e in self.entries: - if e not in b.entries: - ret.append(e) - - for e in b.entries: - if e not in self.entries: - ret.append(e) - - # TODO: Sort by date - - return ret diff --git a/nemubot/tools/human.py b/nemubot/tools/human.py deleted file mode 100644 index a18cde2..0000000 --- a/nemubot/tools/human.py +++ /dev/null @@ -1,67 +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 . - -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) - - 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): - """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)] - - -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 deleted file mode 100644 index 8ebdd49..0000000 --- a/nemubot/tools/test_human.py +++ /dev/null @@ -1,40 +0,0 @@ -import unittest - -from nemubot.tools.human import guess, 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) - 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")) - - 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() diff --git a/nemubot/tools/test_xmlparser.py b/nemubot/tools/test_xmlparser.py deleted file mode 100644 index 0feda73..0000000 --- a/nemubot/tools/test_xmlparser.py +++ /dev/null @@ -1,113 +0,0 @@ -import unittest - -import io -import xml.parsers.expat - -from nemubot.tools.xmlparser import XMLParser - - -class StringNode(): - def __init__(self): - self.string = "" - - 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): - self.option = option - self.mystr = None - - def addChild(self, name, child): - 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): - self.option = option - self.mystrs = list() - - def startElement(self, name, attrs): - if name == "string": - 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): - - 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 - - inputstr = "toto" - p.Parse(inputstr, 1) - - self.assertEqual(mod.root.string, "toto") - self.assertEqual(mod.saveDocument(header=False).getvalue(), inputstr) - - - 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 - - inputstr = 'toto' - 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): - p = xml.parsers.expat.ParserCreate() - mod = XMLParser({"string": StringNode, "test": Test2Node}) - - p.StartElementHandler = mod.startElement - p.CharacterDataHandler = mod.characters - p.EndElementHandler = mod.endElement - - inputstr = '' - 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__': - unittest.main() diff --git a/nemubot/tools/web.py b/nemubot/tools/web.py deleted file mode 100644 index a545b19..0000000 --- a/nemubot/tools/web.py +++ /dev/null @@ -1,274 +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 . - -from urllib.parse import urljoin, urlparse, urlsplit, urlunsplit -import socket - -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 != "" - - -def _getNormalizedURL(url): - """Return a light normalized form for the given URL""" - return url if "//" in url or ":" in url else "//" + url - -def getNormalizedURL(url): - """Return a normalized form for the given URL""" - return urlunsplit(urlsplit(_getNormalizedURL(url), "http")) - - -def getScheme(url): - """Return the protocol of a given URL""" - o = urlparse(url, "http") - return o.scheme - - -def getHost(url): - """Return the domain of a given URL""" - return urlparse(_getNormalizedURL(url), "http").hostname - - -def getPort(url): - """Return the port of a given URL""" - return urlparse(_getNormalizedURL(url), "http").port - - -def getPath(url): - """Return the page request of a given URL""" - return urlparse(_getNormalizedURL(url), "http").path - - -def getUser(url): - """Return the page request of a given URL""" - return urlparse(_getNormalizedURL(url), "http").username - - -def getPassword(url): - """Return the page request of a given URL""" - return urlparse(_getNormalizedURL(url), "http").password - - -# Get real pages - -def _URLConn(cb, url, body=None, timeout=7, header=None, follow_redir=True): - o = urlparse(_getNormalizedURL(url), "http") - - import http.client - - kwargs = { - 'host': o.hostname, - 'port': o.port, - 'timeout': timeout - } - - if o.scheme == "http": - conn = http.client.HTTPConnection(**kwargs) - elif o.scheme == "https": - # 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(**kwargs) - else: - raise IMException("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__ - - 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 != '': - conn.request("GET" if body is None else "POST", - o.path + "?" + o.query, - body, - header) - else: - conn.request("GET" if body is None else "POST", - o.path, - body, - header) - except socket.timeout as e: - raise IMException(e) - except OSError as e: - raise IMException(e.strerror) - - 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") - 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 - - 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 _nextURLContent(res): - size = int(res.getheader("Content-Length", 524288)) - cntype = res.getheader("Content-Type") - - 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) - - # Decode content - charset = "utf-8" - if cntype is not None: - lcharset = res.getheader("Content-Type").split(";") - if len(lcharset) > 1: - for c in lcharset: - 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] - - import http.client - - if res.status == http.client.OK or res.status == http.client.SEE_OTHER: - return data.decode(charset, errors='ignore').strip() - elif decode_error: - return data.decode(charset, errors='ignore').strip() - else: - raise IMException("A HTTP error occurs: %d - %s" % - (res.status, http.client.responses[res.status])) - - return _URLConn(_nextURLContent, url=url, body=body, timeout=timeout, header=header) - - -def getXML(*args, **kwargs): - """Get content page and return XML parsed content - - Arguments: same as getURLContent - """ - - cnt = getURLContent(*args, **kwargs) - if cnt is None: - return None - else: - from xml.dom.minidom import parseString - return parseString(cnt) - - -def getJSON(*args, remove_callback=False, **kwargs): - """Get content page and return JSON content - - Arguments: same as getURLContent - """ - - cnt = getURLContent(*args, **kwargs) - if cnt is None: - return None - else: - import json - if remove_callback: - import re - cnt = re.sub(r"^[^(]+\((.*)\)$", r"\1", cnt) - return json.loads(cnt) - - -# Other utils - -def striphtml(data): - """Remove HTML tags from text - - Argument: - data -- the string to strip - """ - - if not isinstance(data, str) and not isinstance(data, bytes): - 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 - return re.sub(r' +', ' ', - unescape(re.sub(r'<.*?>', '', data)).replace('\n', ' ')) diff --git a/nemubot/tools/xmlparser/__init__.py b/nemubot/tools/xmlparser/__init__.py deleted file mode 100644 index 1bf60a8..0000000 --- a/nemubot/tools/xmlparser/__init__.py +++ /dev/null @@ -1,174 +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 . - -import xml.parsers.expat - -from nemubot.tools.xmlparser import node as module_state - - -class ModuleStatesFile: - - def __init__(self): - self.root = None - self.stack = list() - - def startElement(self, name, attrs): - cur = module_state.ModuleState(name) - - for name in attrs.keys(): - cur.setAttribute(name, attrs[name]) - - self.stack.append(cur) - - 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 - - -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][0] - else: - return None - - - @property - def current(self): - if len(self.stack): - return self.stack[-1][0] - else: - return None - - - def display_stack(self): - return " in ".join([str(type(s).__name__) for s,c 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), self.child)) - self.child = 0 - 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 hasattr(self.current, "endElement"): - self.current.endElement(None) - - if self.child: - self.child -= 1 - - # Don't remove root - elif len(self.stack) > 1: - last, self.child = 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 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() - 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): - p = xml.parsers.expat.ParserCreate() - mod = ModuleStatesFile() - - 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/basic.py b/nemubot/tools/xmlparser/basic.py deleted file mode 100644 index dadff23..0000000 --- a/nemubot/tools/xmlparser/basic.py +++ /dev/null @@ -1,153 +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 . - -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__() - - - 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 - """ - - 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 not None or self._cur is None: - return - - key, cnt = self._cur - if isinstance(cnt, list) and len(cnt) == 1: - self.items[key] = cnt[0] - 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__() - - - 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: - 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() diff --git a/nemubot/tools/xmlparser/genericnode.py b/nemubot/tools/xmlparser/genericnode.py deleted file mode 100644 index 425934c..0000000 --- a/nemubot/tools/xmlparser/genericnode.py +++ /dev/null @@ -1,102 +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 . - -class ParsingNode: - - """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 = [] - - - def characters(self, content): - self.content += content - - - def addChild(self, name, child): - self.children.append(child) - 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 - - - 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 - """ - - 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 diff --git a/nemubot/tools/xmlparser/node.py b/nemubot/tools/xmlparser/node.py deleted file mode 100644 index 7df255e..0000000 --- a/nemubot/tools/xmlparser/node.py +++ /dev/null @@ -1,223 +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 . - -import logging - -logger = logging.getLogger("nemubot.tools.xmlparser.node") - - -class ModuleState: - """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 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 __str__(self): - return self.display() - - def __repr__(self): - return self.display() - - 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 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: - 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 calendar, time - return datetime.utcfromtimestamp(calendar.timegm(time.strptime(source[:19], "%Y-%m-%d %H:%M:%S"))).replace(tzinfo=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""" - from datetime import datetime - 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 - - 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 saveElement(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: - gen.startElement(self.name, attrs) - - for child in self.childs: - child.saveElement(gen) - - gen.endElement(self.name) - except: - logger.exception("Error occured when saving the following " - "XML node: %s with %s", self.name, attrs) diff --git a/nemubot/treatment.py b/nemubot/treatment.py deleted file mode 100644 index ed7cacb..0000000 --- a/nemubot/treatment.py +++ /dev/null @@ -1,161 +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 . - -import logging - -logger = logging.getLogger("nemubot.treatment") - - -class MessageTreater: - - """Treat a message""" - - def __init__(self): - from nemubot.hooks.manager import HooksManager - self.hm = HooksManager() - - - def treat_msg(self, msg): - """Treat a given message - - Arguments: - msg -- the message to treat - """ - - 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: - - 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) - - 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.can_read(msg.to, msg.server) and h.match(msg): - for res in flatify(h.run(msg)): - if 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_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 - """ - - if hasattr(msg, "frm_owner"): - msg.frm_owner = (not hasattr(msg.server, "owner") or msg.server.owner == msg.frm) - - while hook is not None: - 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) - - - def _in_miss(self, msg): - from nemubot.message.command import Command as CommandMessage - from nemubot.message.directask import DirectAsk as DirectAskMessage - - if isinstance(msg, CommandMessage): - from nemubot.hooks import Command as CommandHook - 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 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): - """Modify output Messages - - Arguments: - msg -- response to treat - """ - - for h in self.hm.get_hooks("post", type(msg).__name__): - if h.can_write(msg.to, msg.server) and h.match(msg): - for res in flatify(h.run(msg)): - if 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 - - -def flatify(g): - if hasattr(g, "__iter__"): - for i in g: - yield from flatify(i) - else: - yield g diff --git a/nemuspeak.py b/nemuspeak.py new file mode 100755 index 0000000..9501e17 --- /dev/null +++ b/nemuspeak.py @@ -0,0 +1,188 @@ +#!/usr/bin/python3 +# 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 +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 +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.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': + msg.treat (self.mods) + 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"]) + 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/networkbot.py b/networkbot.py new file mode 100644 index 0000000..756ab3c --- /dev/null +++ b/networkbot.py @@ -0,0 +1,240 @@ +# -*- 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 . + +import json +import random +import shlex +import urllib.parse +import zlib + +from DCC import DCC +import hooks +from response import Response + +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/prompt/__init__.py b/prompt/__init__.py new file mode 100644 index 0000000..62c8dc3 --- /dev/null +++ b/prompt/__init__.py @@ -0,0 +1,105 @@ +# -*- 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 . + +import imp +import os +import shlex +import sys +import traceback + +from . import builtins + +class Prompt: + def __init__(self, hc=dict(), hl=dict()): + self.selectedServer = None + + self.HOOKS_CAPS = hc + self.HOOKS_LIST = hl + + 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 lex_cmd(self, line): + """Return an array of tokens""" + ret = list() + 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() + sys.stderr.write (traceback.format_exception_only( + exc_type, exc_value)[0]) + return ret + + def exec_cmd(self, toks, context): + """Execute the command""" + 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]] + return f(d, toks, context, self) + else: + print ("Unknown command: `%s'" % toks[0]) + return "" + + 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""" + 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") + cmds = self.lex_cmd(line.strip()) + for toks in cmds: + try: + 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) + except KeyboardInterrupt: + print ("") + return ret != "quit" + + +def hotswap(prompt): + return Prompt(prompt.HOOKS_CAPS, prompt.HOOKS_LIST) diff --git a/prompt/builtins.py b/prompt/builtins.py new file mode 100644 index 0000000..512549d --- /dev/null +++ b/prompt/builtins.py @@ -0,0 +1,157 @@ +# -*- 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 . + +import os +import xmlparser + +def end(toks, context, prompt): + """Quit the prompt for reload or exit""" + if toks[0] == "refresh": + return "refresh" + elif toks[0] == "reset": + return "reset" + else: + context.quit() + return "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 ;" % srv) + else: + 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: + 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: + 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" + or config.getName() == "config"): + # Preset each server in this file + for server in config.getNodes("server"): + if context.addServer(server, config["nick"], + config["owner"], config["realname"]): + print (" Server `%s:%s' successfully added." + % (server["server"], server["port"])) + else: + 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) + + # This is a nemubot module configuration file, load the module + elif config.getName() == "nemubotmodule": + __import__(config["name"]) + + # 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: + __import__(filename) + + +def load(toks, context, prompt): + """Load an XML configuration file""" + if len(toks) > 1: + for filename in toks[1:]: + load_file(filename, context) + else: + print ("Not enough arguments. `load' takes a filename.") + return + + +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]) + else: + prompt.selectedServer = None + return + + +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) + else: + print ("Not enough arguments. `unload' takes a module name.") + + +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) + else: + print ("Not enough arguments. `debug' takes a module name.") + + +#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/requirements.txt b/requirements.txt deleted file mode 100644 index e037895..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -irc -matrix-nio diff --git a/response.py b/response.py new file mode 100644 index 0000000..9fda7f8 --- /dev/null +++ b/response.py @@ -0,0 +1,176 @@ +# -*- 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 . + +import traceback +import sys + +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): + self.nomore = nomore + self.more = more + self.rawtitle = title + self.server = server + self.messages = list() + self.alone = True + self.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.channel = channel + self.nick = nick + self.set_sender(sender) + self.count = count + + @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: + 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) + self.sender = None + else: + self.sender = sender + + def append_message(self, message, title=None, shown_first_count=-1): + 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) + if isinstance(self.rawtitle, list): + self.rawtitle.pop(0) + if len(self.rawtitle) <= 0: + self.rawtitle = None + + def get_message(self): + if self.alone and len(self.messages) > 1: + self.alone = False + + if self.empty: + return self.nomore + + 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 msg + else: + 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: + return msg + elts + + else: + words = elts.split(' ') + + if len(words[0]) > 432 - len(msg): + self.elt += 432 - len(msg) + return msg + elts[:self.elt] + "[…]" + + for w in words: + if len(msg) + len(w) > 431: + msg += "[…]" + self.alone = False + return msg + else: + msg += w + " " + self.elt += len(w) + 1 + self.pop() + self.elt = 0 + 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 diff --git a/server.py b/server.py new file mode 100644 index 0000000..e16bd57 --- /dev/null +++ b/server.py @@ -0,0 +1,169 @@ +# -*- 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 . + +import socket +import threading + +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 + + self.moremessages = dict() + + threading.Thread.__init__(self) + + def isDCC(self, to=None): + return to is not None and to in self.dcc_clients + + @property + def ip(self): + """Convert common IP representation to little-endian integer representation""" + sum = 0 + if self.node.hasAttribute("ip"): + ip = self.node["ip"] + else: + #TODO: find the external IP + ip = "0.0.0.0" + for b in ip.split("."): + sum = 256 * sum + int(b) + return sum + + 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 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 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()) + + elif res.channel is not None and res.channel != self.nick: + self.send_msg(res.channel, res.get_message()) + + 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 + + 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) + + 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" % self.nick).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 + 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() diff --git a/setup.py b/setup.py deleted file mode 100755 index 7b5bdcd..0000000 --- a/setup.py +++ /dev/null @@ -1,84 +0,0 @@ -#!/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.config', - 'nemubot.datastore', - 'nemubot.event', - 'nemubot.exception', - 'nemubot.hooks', - 'nemubot.hooks.keywords', - 'nemubot.message', - 'nemubot.message.printer', - 'nemubot.module', - 'nemubot.server', - 'nemubot.tools', - 'nemubot.tools.xmlparser', - ], - - scripts=[ - 'bin/nemubot', -# 'bin/module_tester', - ], - -# data_files=data_files, -) diff --git a/speak_sample.xml b/speak_sample.xml index ee403ac..c1c6f61 100644 --- a/speak_sample.xml +++ b/speak_sample.xml @@ -1,35 +1,27 @@ - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + diff --git a/tools/__init__.py b/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/web.py b/tools/web.py new file mode 100644 index 0000000..b0bf2e3 --- /dev/null +++ b/tools/web.py @@ -0,0 +1,119 @@ +# 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 . + +import http.client +import json +import re +import socket +from urllib.parse import quote +from urllib.parse import urlparse +from urllib.request import urlopen + +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 + + +# Get real pages + +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) + try: + if o.query != '': + 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: + return None + except socket.gaierror: + print (" Unable to receive page %s from %s on %d." + % (o.path, o.netloc, o.port)) + return None + + try: + res = conn.getresponse() + size = int(res.getheader("Content-Length", 200000)) + cntype = res.getheader("Content-Type") + + if size > 200000 or (cntype[:4] != "text" and cntype[:4] != "appl"): + return None + + data = res.read(size) + except http.client.BadStatusLine: + return None + finally: + conn.close() + + if res.status == http.client.OK or res.status == http.client.SEE_OTHER: + return data + elif res.status == http.client.FOUND or res.status == http.client.MOVED_PERMANENTLY: + return getURLContent(res.getheader("Location"), timeout) + else: + return None + +def getXML(url, timeout=15): + """Get content page and return XML parsed content""" + cnt = getURLContent(url, timeout) + if cnt is None: + return None + else: + return xmlparser.parse_string(cnt) + +def getJSON(url, timeout=15): + """Get content page and return JSON content""" + cnt = getURLContent(url, timeout) + if cnt is None: + return None + else: + return json.loads(cnt.decode()) + +# Other utils + +def striphtml(data): + """Remove HTML tags from text""" + p = re.compile(r'<.*?>') + return p.sub('', data).replace("(", "/(").replace(")", ")/").replace(""", "\"") diff --git a/tools/wrapper.py b/tools/wrapper.py new file mode 100644 index 0000000..3f4f5e6 --- /dev/null +++ b/tools/wrapper.py @@ -0,0 +1,66 @@ +# 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 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/__init__.py b/xmlparser/__init__.py new file mode 100644 index 0000000..adfb85b --- /dev/null +++ b/xmlparser/__init__.py @@ -0,0 +1,74 @@ +# -*- 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 . + +import os +import imp +import xml.sax + +from . import node as module_state + +class ModuleStatesFile(xml.sax.ContentHandler): + def startDocument(self): + self.root = None + self.stack = list() + + def startElement(self, name, attrs): + cur = module_state.ModuleState(name) + + for name in attrs.keys(): + cur.setAttribute(name, attrs.getValue(name)) + + self.stack.append(cur) + + 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 parse_file(filename): + parser = xml.sax.make_parser() + mod = ModuleStatesFile() + parser.setContentHandler(mod) + try: + parser.parse(open(filename, "r")) + return mod.root + except IOError: + return module_state.ModuleState("nemubotstate") + except: + 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: + if mod.root is None: + return module_state.ModuleState("nemubotstate") + else: + return mod.root diff --git a/xmlparser/node.py b/xmlparser/node.py new file mode 100644 index 0000000..4aa5d2f --- /dev/null +++ b/xmlparser/node.py @@ -0,0 +1,191 @@ +# coding=utf-8 + +import xml.sax +from datetime import datetime +from datetime import date +import time + +class ModuleState: + """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 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 __str__(self): + return self.display() + + 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 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: + return None + + if isinstance(source, datetime): + return source + else: + try: + return datetime.fromtimestamp(float(source)) + except ValueError: + while True: + try: + return datetime.fromtimestamp(time.mktime( + time.strptime(source[:19], "%Y-%m-%d %H:%M:%S"))) + 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 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 + for child in self.childs: + if (tagname is None or tagname == child.name) and child.hasAttribute(fieldname): + self.index[child[fieldname]] = child + + def __contains__(self, i): + """Return true if i is found in the index""" + return i in self.index + + def hasAttribute(self, name): + """DOM like method""" + return (name in self.attributes) + + def setAttribute(self, name, value): + """DOM like method""" + self.attributes[name] = 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 + + 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 + + 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 + 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) + + gen.startElement(self.name, attrs) + + for child in self.childs: + child.save_node(gen) + + gen.endElement(self.name) + + 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()