Compare commits

..

1 commit

Author SHA1 Message Date
3f2ae59b9f WIP on Epita Cristal module 2014-04-08 15:26:42 +02:00
194 changed files with 7534 additions and 13174 deletions

View file

@ -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

1
.gitignore vendored
View file

@ -1,6 +1,5 @@
*#
*~
*.log
TAGS
*.py[cod]
__pycache__

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "modules/nextstop/external"]
path = modules/nextstop/external
url = git://github.com/nbr23/NextStop.git

View file

@ -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

241
DCC.py Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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)

View file

@ -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" ]

290
IRCServer.py Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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)

View file

@ -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

View file

@ -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 <http://www.gnu.org/licenses/>.
import sys
from nemubot.__main__ import main
if __name__ == "__main__":
main()

641
bot.py Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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)

View file

@ -1,23 +1,13 @@
<nemubotconfig nick="nemubot" realname="nemubot clone" owner="someone">
<server uri="irc://irc.rezosup.org:6667" autoconnect="true" caps="znc.in/server-time-iso">
<nemubotconfig nick="nemubot" realname="nemubot speaker" owner="someone">
<server server="irc.freenode.org" port="6667" password="secret" autoconnect="true">
<channel name="#nemutest" />
</server>
<!--
<server host="ircs://my_host.local:6667" password="secret" autoconnect="true">
<channel name="#nemutest" />
</server>
-->
<!--
<module name="wolframalpha" apikey="YOUR-APIKEY" />
-->
<module name="cmd_server" />
<module name="alias" />
<module name="ycc" />
<module name="events" />
<load path="modules/birthday.xml" />
<load path="modules/ycc.xml" />
<load path="modules/qcm.xml" />
<load path="modules/soutenance.xml" />
<load path="modules/velib.xml" />
<load path="modules/whereis.xml" />
<load path="modules/watchWebsite.xml" />
<load path="modules/events.xml" />
</nemubotconfig>

102
channel.py Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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)

143
consumer.py Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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

43
credits.py Normal file
View file

@ -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))

0
datas/datas Normal file
View file

118
event.py Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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)

220
hooks.py Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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)

264
importer.py Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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)

294
message.py Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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

View file

@ -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

156
modules/alias/__init__.py Normal file
View file

@ -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

View file

@ -1,68 +1,56 @@
"""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():
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"), "",
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)
@ -70,65 +58,54 @@ def cmd_anniv(msg):
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,
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)

5
modules/birthday.xml Normal file
View file

@ -0,0 +1,5 @@
<?xml version="1.0" ?>
<nemubotmodule name="birthday">
<message type="cmd" name="anniv" call="cmd_anniv" />
<message type="cmd" name="age" call="cmd_age" />
</nemubotmodule>

View file

@ -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"
"<sayon hostid=\"nemubot@irc.freenode.net:6667\" "
"channel=\"#nemutest\" />")
yr = datetime.today().year
yrn = datetime.today().year + 1
d = datetime(yrn, 1, 1, 0, 0, 0) - datetime.now()
# d = datetime(yr, 12, 31, 19, 34, 0) - datetime.now()
add_event(ModuleEvent(intervalle=0, offset=d.total_seconds(), call=bonneannee))
from hooks import Hook
add_hook("cmd_rgxp", Hook(cmd_timetoyear, data=yrn, regexp="^[0-9]{4}$"))
add_hook("cmd_hook", Hook(cmd_newyear, str(yrn), yrn))
add_hook("cmd_hook", Hook(cmd_newyear, "ny", yrn))
add_hook("cmd_hook", Hook(cmd_newyear, "newyear", yrn))
add_hook("cmd_hook", Hook(cmd_newyear, "new-year", yrn))
add_hook("cmd_hook", Hook(cmd_newyear, "new year", yrn))
def bonneannee():
txt = "Bonne année %d !" % yrn
txt = "Bonne année %d !" % datetime.today().year
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))
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"))
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))
# 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),
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),
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")),
"Le premier janvier %d est passé depuis %s !" % (yr, "%s")),
channel=msg.channel)

View file

@ -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"
"<module name=\"books\" goodreadskey=\"XXXXXX\" />\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)

View file

@ -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

96
modules/chronos.py Normal file
View file

@ -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

6
modules/chronos.xml Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" ?>
<nemubotmodule name="chronos">
<server url="http://chronos.23.tf/index.php?xml" />
<message type="cmd" name="chronos" call="cmd_chronos" />
<message type="cmd" name="Χρονος" call="cmd_chronos" />
</nemubotmodule>

201
modules/cmd_server.py Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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.")

14
modules/cmd_server.xml Normal file
View file

@ -0,0 +1,14 @@
<?xml version="1.0" ?>
<nemubotmodule name="cmd_server">
<command name="close" call="close" />
<command name="connect" call="connect" />
<command name="discover" call="discover" />
<command name="disconnect" call="disconnect" />
<command name="hotswap" call="hotswap" />
<command name="join" call="join" />
<command name="leave" call="join" />
<command name="part" call="join" />
<command name="save" call="save_mod" />
<command name="send" call="send" />
<command name="zap" call="zap" />
</nemubotmodule>

View file

@ -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('<div class="modeBloc">', 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('<div id="temps' + idTemps[0] + '\"')
endIndex = line[index:].index('<div class=\"conjugBloc\"')
endIndex += index
newLine = line[index:endIndex]
res = list()
for elt in re.finditer("[p|/]>([^/]*/b>)", newLine):
res.append(striphtml(elt.group(1)
.replace("<b>", "\x02")
.replace("</b>", "\x0F")))
return res
# 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)

64
modules/cristal.py Normal file
View file

@ -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

5
modules/cristal.xml Normal file
View file

@ -0,0 +1,5 @@
<?xml version="1.0" ?>
<nemubotmodule name="cristal">
<server url="http://p0m.fr/cristal.php?f=xml" />
<message type="cmd" name="cristal" call="cmd_cristal" />
</nemubotmodule>

View file

@ -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

View file

@ -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

View file

@ -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

68
modules/ddg/DDGSearch.py Normal file
View file

@ -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

71
modules/ddg/WFASearch.py Normal file
View file

@ -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<wfaapi"
" key=\"XXXXXX-XXXXXXXXXX\" />\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

56
modules/ddg/Wikipedia.py Normal file
View file

@ -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"(<!--.*-->|<ref[^>]*/>|<ref[^>]*>[^>]*</ref>|<dfn[^>]*>[^>]*</dfn>|\{\{[^{}]*\}\}|\[\[([^\[\]]*\[\[[^\]\[]*\]\])+[^\[\]]*\]\]|\{\{([^{}]*\{\{[^{}]*\}\}[^{}]*)+\}\}|\{\{([^{}]*\{\{([^{}]*\{\{[^{}]*\}\}[^{}]*)+\}\}[^{}]*)+\}\}|\[\[[^\]|]+(\|[^\]\|]+)*\]\])|#\* ''" + "\n", re.I)
RGXP_l = re.compile(r'\{\{(nobr|lang\|[^|}]+)\|([^}]+)\}\}', re.I)
RGXP_m = re.compile(r'\{\{pron\|([^|}]+)\|[^}]+\}\}', re.I)
RGXP_t = re.compile("==+ *([^=]+) *=+=\n+([^\n])", re.I)
RGXP_q = re.compile(r'\[\[([^\[\]|]+)\|([^\]|]+)]]', re.I)
RGXP_r = re.compile(r'\[\[([^\[\]|]+)\]\]', re.I)
RGXP_s = re.compile(r'\s+')
def striplink(s):
s.replace("{{m}}", "masculin").replace("{{f}}", "feminin").replace("{{n}}", "neutre")
(s, n) = RGXP_m.subn(r"[\1]", s)
(s, n) = RGXP_l.subn(r"\2", s)
(s, n) = RGXP_q.subn(r"\1", s)
(s, n) = RGXP_r.subn(r"\1", s)
(s, n) = RGXP_p.subn('', s)
if s == "": return s
(s, n) = RGXP_t.subn("\x03\x16" + r"\1" + " :\x03\x16 " + r"\2", s)
return s.replace("'''", "\x03\x02").replace("''", "\x03\x1f")

129
modules/ddg/__init__.py Normal file
View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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.")

14
modules/events.xml Normal file
View file

@ -0,0 +1,14 @@
<?xml version="1.0" ?>
<nemubotmodule name="events">
<message type="cmd" name="start" call="start_countdown" />
<message type="cmd" name="end" call="end_countdown" />
<message type="cmd" name="forceend" call="end_countdown" />
<message type="cmd" name="eventlist" call="liste" />
<message type="cmd" name="eventslist" call="liste" />
<message type="cmd" name="eventliste" call="liste" />
<message type="cmd" name="eventsliste" call="liste" />
<message type="cmd" name="gouter" call="cmd_gouter" />
<message type="cmd" name="goûter" call="cmd_gouter" />
<message type="cmd" name="week-end" call="cmd_we" />
<message type="cmd" name="weekend" call="cmd_we" />
</nemubotmodule>

238
modules/events/__init__.py Normal file
View file

@ -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à.")

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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)")

View file

@ -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

View file

@ -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"
"<module name=\"mapquest\" key=\"XXXXXXXXXXXXXXXX\" "
"/>\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

View file

@ -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("<span class='searchmatch'>", "\x03\x02").replace("</span>", "\x03\x02")),
web.striphtml(itm["snippet"].replace("<span class='searchmatch'>", "\x03\x02").replace("</span>", "\x03\x02")))
# PARSING FUNCTIONS ###################################################
def 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<title>==+)\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)

119
modules/networking.py Normal file
View file

@ -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

View file

@ -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>(.*?)</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)

View file

@ -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

View file

@ -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 ("<tools.web> 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

View file

@ -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())

View file

@ -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"])

View file

@ -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<whoisxmlapi username=\"XX\" "
"password=\"XXX\" />\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")

View file

@ -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

4
modules/nextstop.xml Normal file
View file

@ -0,0 +1,4 @@
<?xml version="1.0" ?>
<nemubotmodule name="nextstop">
<message type="cmd" name="ratp" call="ask_ratp" />
</nemubotmodule>

View file

@ -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

@ -0,0 +1 @@
Subproject commit e5675c631665dfbdaba55a0be66708a07d157408

View file

@ -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)

View file

@ -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<module name=\"openai\" "
"apikey=\"XXXXXX-XXXXXXXXXX\" endpoint=\"https://...\" model=\"gpt-4\" />")
kwargs = {
"api_key": context.config["apikey"] or "",
}
if "endpoint" in context.config:
ENDPOINT = context.config["endpoint"]
kwargs["base_url"] = ENDPOINT
CLIENT = OpenAI(**kwargs)
if "model" in context.config:
MODEL = context.config["model"]
# MODULE INTERFACE ####################################################
@hook.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)

View file

@ -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"
"<module name=\"ors\" apikey=\"XXXXXXXXXXXXXXXX\" "
"/>\nRegister at https://developers.openrouteservice.org")
global URL_DIRECTIONS_API
URL_DIRECTIONS_API = URL_DIRECTIONS_API % context.config["apikey"]
global URL_GEOCODE_API
URL_GEOCODE_API = URL_GEOCODE_API % context.config["apikey"]
# MODULE CORE #########################################################
def approx_distance(lng):
if lng > 1111:
return "%f km" % (lng / 1000)
else:
return "%f m" % lng
def approx_duration(sec):
days = int(sec / 86400)
if days > 0:
return "%d days %f hours" % (days, (sec % 86400) / 3600)
hours = int((sec % 86400) / 3600)
if hours > 0:
return "%d hours %f minutes" % (hours, (sec % 3600) / 60)
minutes = (sec % 3600) / 60
if minutes > 0:
return "%d minutes" % minutes
else:
return "%d seconds" % sec
def geocode(query, limit=7):
obj = web.getJSON(URL_GEOCODE_API + urllib.parse.urlencode({
'query': query,
'limit': limit,
}))
for f in obj["features"]:
yield f["geometry"]["coordinates"], f["properties"]
def firstgeocode(query):
for g in geocode(query, limit=1):
return g
def where(loc):
return "{name} {city} {state} {county} {country}".format(**loc)
def directions(coordinates, **kwargs):
kwargs['coordinates'] = '|'.join(coordinates)
print(URL_DIRECTIONS_API + urllib.parse.urlencode(kwargs))
return web.getJSON(URL_DIRECTIONS_API + urllib.parse.urlencode(kwargs), decode_error=True)
# MODULE INTERFACE ####################################################
@hook.command("geocode",
help="Get GPS coordinates of a place",
help_usage={
"PLACE": "Get GPS coordinates of PLACE"
})
def cmd_geocode(msg):
res = Response(channel=msg.channel, nick=msg.frm,
nomore="No more geocode", count=" (%s more geocode)")
for loc in geocode(' '.join(msg.args)):
res.append_message("%s is at %s,%s" % (
where(loc[1]),
loc[0][1], loc[0][0],
))
return res
@hook.command("directions",
help="Get routing instructions",
help_usage={
"POINT1 POINT2 ...": "Get routing instructions to go from POINT1 to the last POINTX via intermediates POINTX"
},
keywords={
"profile=PROF": "One of driving-car, driving-hgv, cycling-regular, cycling-road, cycling-safe, cycling-mountain, cycling-tour, cycling-electric, foot-walking, foot-hiking, wheelchair. Default: foot-walking",
"preference=PREF": "One of fastest, shortest, recommended. Default: recommended",
"lang=LANG": "default: en",
})
def cmd_directions(msg):
drcts = directions(["{0},{1}".format(*firstgeocode(g)[0]) for g in msg.args],
profile=msg.kwargs["profile"] if "profile" in msg.kwargs else "foot-walking",
preference=msg.kwargs["preference"] if "preference" in msg.kwargs else "recommended",
units="m",
language=msg.kwargs["lang"] if "lang" in msg.kwargs else "en",
geometry=False,
instructions=True,
instruction_format="text")
if "error" in drcts and "message" in drcts["error"] and drcts["error"]["message"]:
raise IMException(drcts["error"]["message"])
if "routes" not in drcts or not drcts["routes"]:
raise IMException("No route available for this trip")
myway = drcts["routes"][0]
myway["summary"]["strduration"] = approx_duration(myway["summary"]["duration"])
myway["summary"]["strdistance"] = approx_distance(myway["summary"]["distance"])
res = Response("Trip summary: {strdistance} in approximate {strduration}; elevation +{ascent} m -{descent} m".format(**myway["summary"]), channel=msg.channel, count=" (%d more steps)", nomore="You have arrived!")
def formatSegments(segments):
for segment in segments:
for step in segment["steps"]:
step["strtype"] = waytype[step["type"]]
step["strduration"] = approx_duration(step["duration"])
step["strdistance"] = approx_distance(step["distance"])
yield "{instruction} for {strdistance} on {strtype} (approximate time: {strduration})".format(**step)
if "segments" in myway:
res.append_message([m for m in formatSegments(myway["segments"])])
return res

View file

@ -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

7
modules/qcm.xml Normal file
View file

@ -0,0 +1,7 @@
<?xml version="1.0" ?>
<nemubotmodule name="qcm">
<file name="main" url="/var/www/nemunai.re/bot/htdocs/questions.xml"/>
<file name="courses" url="/var/www/nemunai.re/bot/htdocs/courses.xml"/>
<file name="users" url="/var/www/nemunai.re/bot/htdocs/users.xml"/>
<server url="bot.nemunai.re" />
</nemubotmodule>

31
modules/qcm/Course.py Normal file
View file

@ -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

93
modules/qcm/Question.py Normal file
View file

@ -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

View file

@ -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

67
modules/qcm/Session.py Normal file
View file

@ -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()

27
modules/qcm/User.py Normal file
View file

@ -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

197
modules/qcm/__init__.py Normal file
View file

@ -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 [<nbQuest>] [<theme>]"
from . import Question
from . import Course
from . import Session
def load(context):
CONF.setIndex("name", "file")
def buildSession(msg, categ = None, nbQuest = 10, channel = False):
if Question.QUESTIONS is None:
Question.QUESTIONS = xmlparser.parse_file(CONF.index["main"]["url"])
Question.QUESTIONS.setIndex("xml:id")
Course.COURSES = xmlparser.parse_file(CONF.index["courses"]["url"])
Course.COURSES.setIndex("xml:id")
User.USERS = xmlparser.parse_file(CONF.index["users"]["url"])
User.USERS.setIndex("xml:id")
#Remove no validated questions
keys = list()
for k in Question.QUESTIONS.index.keys():
keys.append(k)
for ques in keys:
if Question.QUESTIONS.index[ques]["validated"] != "1" or Question.QUESTIONS.index[ques]["reported"] == "1":
del Question.QUESTIONS.index[ques]
#Apply filter
QS = list()
if categ is not None and len(categ) > 0:
#Find course id corresponding to categ
courses = list()
for c in Course.COURSES.childs:
if c["code"] in categ:
courses.append(c["xml:id"])
#Keep only questions matching course or branch
for q in Question.QUESTIONS.index.keys():
if (Question.QUESTIONS.index[q]["branch"] is not None and Question.QUESTIONS.index[q]["branch"].find(categ)) or Question.QUESTIONS.index[q]["course"] in courses:
QS.append(q)
else:
for q in Question.QUESTIONS.index.keys():
QS.append(q)
nbQuest = min(nbQuest, len(QS))
if channel:
sess = Session.Session(msg.srv, msg.channel, msg.channel)
else:
sess = Session.Session(msg.srv, msg.channel, msg.sender)
maxQuest = len(QS) - 1
for i in range(0, nbQuest):
while True:
q = QS[random.randint(0, maxQuest)]
if sess.addQuestion(q):
break
if channel:
Session.SESSIONS[msg.channel] = sess
else:
Session.SESSIONS[msg.realname] = sess
def askQuestion(msg, bfr = ""):
return Session.SESSIONS[msg.realname].askNext(bfr)
def parseanswer(msg):
global DATAS
if msg.cmd[0] == "qcm" or msg.cmd[0] == "qcmchan" or msg.cmd[0] == "simulateqcm":
if msg.realname in Session.SESSIONS:
if len(msg.cmd) > 1:
if msg.cmd[1] == "stop" or msg.cmd[1] == "end":
sess = Session.SESSIONS[msg.realname]
if sess.good > 1: goodS = "s"
else: goodS = ""
del Session.SESSIONS[msg.realname]
return Response(msg.sender,
"Fini, tu as donné %d bonne%s réponse%s sur %d questions." % (sess.good, goodS, goodS, sess.current),
msg.channel, nick=msg.nick)
elif msg.cmd[1] == "next" or msg.cmd[1] == "suivant" or msg.cmd[1] == "suivante":
return askQuestion(msg)
return Response(msg.sender, "tu as déjà une session de QCM en cours, finis-la avant d'en commencer une nouvelle.", msg.channel, msg.nick)
elif msg.channel in Session.SESSIONS:
if len(msg.cmd) > 1:
if msg.cmd[1] == "stop" or msg.cmd[1] == "end":
sess = Session.SESSIONS[msg.channel]
if sess.good > 1: goodS = "s"
else: goodS = ""
del Session.SESSIONS[msg.channel]
return Response(msg.sender, "Fini, vous avez donné %d bonne%s réponse%s sur %d questions." % (sess.good, goodS, goodS, sess.current), msg.channel)
elif msg.cmd[1] == "next" or msg.cmd[1] == "suivant" or msg.cmd[1] == "suivante":
Session.SESSIONS[msg.channel].prepareNext(1)
return True
else:
nbQuest = 10
filtre = list()
if len(msg.cmd) > 1:
for cmd in msg.cmd[1:]:
try:
tmp = int(cmd)
nbQuest = tmp
except ValueError:
filtre.append(cmd.upper())
if len(filtre) == 0:
filtre = None
if msg.channel in Session.SESSIONS:
return Response(msg.sender, "Il y a deja une session de QCM sur ce chan.")
else:
buildSession(msg, filtre, nbQuest, msg.cmd[0] == "qcmchan")
if msg.cmd[0] == "qcm":
return askQuestion(msg)
elif msg.cmd[0] == "qcmchan":
return Session.SESSIONS[msg.channel].askNext()
else:
del Session.SESSIONS[msg.realname]
return Response(msg.sender, "QCM de %d questions" % len(Session.SESSIONS[msg.realname].questions), msg.channel)
return True
elif msg.realname in Session.SESSIONS:
if msg.cmd[0] == "info" or msg.cmd[0] == "infoquestion":
return Response(msg.sender, "Cette question a été écrite par %s et validée par %s, le %s" % Session.SESSIONS[msg.realname].question.tupleInfo, msg.channel)
elif msg.cmd[0] == "report" or msg.cmd[0] == "reportquestion":
if len(msg.cmd) == 1:
return Response(msg.sender, "Veuillez indiquer une raison de report", msg.channel)
elif Session.SESSIONS[msg.realname].question.report(' '.join(msg.cmd[1:])):
return Response(msg.sender, "Cette question vient d'être signalée.", msg.channel)
Session.SESSIONS[msg.realname].askNext()
else:
return Response(msg.sender, "Une erreur s'est produite lors du signalement de la question, veuillez recommencer plus tard.", msg.channel)
elif msg.channel in Session.SESSIONS:
if msg.cmd[0] == "info" or msg.cmd[0] == "infoquestion":
return Response(msg.sender, "Cette question a été écrite par %s et validée par %s, le %s" % Session.SESSIONS[msg.channel].question.tupleInfo, msg.channel)
elif msg.cmd[0] == "report" or msg.cmd[0] == "reportquestion":
if len(msg.cmd) == 1:
return Response(msg.sender, "Veuillez indiquer une raison de report", msg.channel)
elif Session.SESSIONS[msg.channel].question.report(' '.join(msg.cmd[1:])):
Session.SESSIONS[msg.channel].prepareNext()
return Response(msg.sender, "Cette question vient d'être signalée.", msg.channel)
else:
return Response(msg.sender, "Une erreur s'est produite lors du signalement de la question, veuillez recommencer plus tard.", msg.channel)
else:
if msg.cmd[0] == "listecours":
if Course.COURSES is None:
return Response(msg.sender, "La liste de cours n'est pas encore construite, lancez un QCM pour la construire.", msg.channel)
else:
res = Response(msg.sender, channel=msg.channel, title="Liste des cours existants : ")
res.append_message([cours["code"] + " (" + cours["name"] + ")" for cours in Course.COURSES.getNodes("course")])
return res
elif msg.cmd[0] == "refreshqcm":
Question.QUESTIONS = None
Course.COURSES = None
User.USERS = None
return True
return False
def parseask(msg):
if msg.realname in Session.SESSIONS:
dest = msg.realname
if Session.SESSIONS[dest].question.isCorrect(msg.content):
Session.SESSIONS[dest].good += 1
Session.SESSIONS[dest].score += Session.SESSIONS[dest].question.getScore(msg.content)
return askQuestion(msg, "correct ; ")
else:
Session.SESSIONS[dest].bad += 1
if Session.SESSIONS[dest].trys == 0:
Session.SESSIONS[dest].trys = 1
return Response(msg.sender, "non, essaie encore :p", msg.channel, msg.nick)
else:
return askQuestion(msg, "non, la bonne reponse était : %s ; " % Session.SESSIONS[dest].question.bestAnswer)
elif msg.channel in Session.SESSIONS:
dest = msg.channel
if Session.SESSIONS[dest].question.isCorrect(msg.content):
Session.SESSIONS[dest].good += 1
Session.SESSIONS[dest].score += Session.SESSIONS[dest].question.getScore(msg.content)
Session.SESSIONS[dest].prepareNext()
return Response(msg.sender, "correct :)", msg.channel, nick=msg.nick)
else:
Session.SESSIONS[dest].bad += 1
return Response(msg.sender, "non, essaie encore :p", msg.channel, nick=msg.nick)
return False

View file

@ -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)

60
modules/qd/GameUpdater.py Normal file
View file

@ -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()

20
modules/qd/QDWrapper.py Normal file
View file

@ -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

126
modules/qd/Score.py Normal file
View file

@ -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())

224
modules/qd/__init__.py Normal file
View file

@ -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

View file

@ -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)")

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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 <transaction code|keywords>"
@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

View file

@ -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"
"<module name=\"shodan\" apikey=\"XXXXXXXXXXXXXXXX\" "
"/>\nRegister at https://account.shodan.io/register")
# MODULE CORE #########################################################
def host_lookup(ip):
url = BASEURL + "host/" + urllib.parse.quote(ip) + "?" + urllib.parse.urlencode({'key': context.config["apikey"]})
return web.getJSON(url)
def search_hosts(query):
url = BASEURL + "host/search?" + urllib.parse.urlencode({'query': query, 'key': context.config["apikey"]})
return web.getJSON(url, max_size=4194304)
def print_ssl(ssl):
return (
"SSL: " +
" ".join([v for v in ssl["versions"] if v[0] != "-"]) +
"; cipher used: " + ssl["cipher"]["name"] +
("; certificate: " + ssl["cert"]["sig_alg"] +
" issued by: " + ssl["cert"]["issuer"]["CN"] +
" expires on: " + str(datetime.strptime(ssl["cert"]["expires"], "%Y%m%d%H%M%SZ")) if "cert" in ssl else "")
)
def print_service(svc):
ip = ipaddress.ip_address(svc["ip_str"])
return ((svc["ip_str"] if ip.version == 4 else "[%s]" % svc["ip_str"]) +
":{port}/{transport} ({module}):" +
(" {os}" if svc["os"] else "") +
(" {product}" if "product" in svc else "") +
(" {version}" if "version" in svc else "") +
(" {info}" if "info" in svc else "") +
(" Vulns: " + ", ".join(svc["opts"]["vulns"]) if "opts" in svc and "vulns" in svc["opts"] else "") +
(" " + print_ssl(svc["ssl"]) if "ssl" in svc else "") +
(" \x03\x1D" + svc["data"].replace("\r\n", "\n").split("\n")[0] + "\x03\x1D" if "data" in svc else "") +
(" " + svc["title"] if "title" in svc else "")
).format(module=svc["_shodan"]["module"], **svc)
# MODULE INTERFACE ####################################################
@hook.command("shodan",
help="Use shodan.io to get information on machines connected to Internet",
help_usage={
"IP": "retrieve information about the given IP (can be v4 or v6)",
"TERM": "retrieve all hosts matching TERM somewhere in their exposed stuff"
})
def shodan(msg):
if not msg.args:
raise IMException("indicate an IP or a term to search!")
terms = " ".join(msg.args)
try:
ip = ipaddress.ip_address(terms)
except ValueError:
ip = None
if ip:
h = host_lookup(terms)
res = Response(channel=msg.channel,
title="%s" % ((h["ip_str"] if ip.version == 4 else "[%s]" % h["ip_str"]) + (" (" + ", ".join(h["hostnames"]) + ")") if h["hostnames"] else ""))
res.append_message("{isp} ({asn}) -> {city} ({country_code}), running {os}. Vulns: {vulns_str}. Open ports: {open_ports}. Last update: {last_update}".format(
open_ports=", ".join(map(lambda a: str(a), h["ports"])), vulns_str=", ".join(h["vulns"]) if "vulns" in h else None, **h).strip())
for d in h["data"]:
res.append_message(print_service(d))
else:
q = search_hosts(terms)
res = Response(channel=msg.channel,
count=" (%%s/%s results)" % q["total"])
for r in q["matches"]:
res.append_message(print_service(r))
return res

View file

@ -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.4
from nemubot.module.more import Response
nemubotversion = 3.3
def help_tiny ():
"""Line inserted in the response to the command !help"""
return "as http://sleepyti.me/, give you the best time to go to bed"
def help_full ():
return ("If you would like to sleep soon, use !sleepytime to know the best"
" time to wake up; use !sleepytime hh:mm if you want to wake up at"
" hh:mm")
return "If you would like to sleep soon, use !sleepytime to know the best time to wake up; use !sleepytime hh:mm if you want to wake up at hh:mm"
def load(context):
from hooks import Hook
add_hook("cmd_hook", Hook(cmd_sleep, "sleeptime"))
add_hook("cmd_hook", Hook(cmd_sleep, "sleepytime"))
@hook.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)))
g = list()
for i in range(6):
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):
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)

View file

@ -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"
"<module name=\"smmry\" apikey=\"XXXXXXXXXXXXXXXX\" "
"/>\nRegister at https://smmry.com/partner")
global URL_API
URL_API = URL_API % context.config["apikey"]
# MODULE INTERFACE ####################################################
@hook.command("smmry",
help="Summarize the following words/command return",
help_usage={
"WORDS/CMD": ""
},
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

View file

@ -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<user>[0-9]{7,})", re.IGNORECASE)
apikey_ask = re.compile(r"(clef|key|password|mot de passe?)\s+(?:est|is)?\s+(?P<key>[a-zA-Z0-9]{10,})", re.IGNORECASE)
@hook.ask()
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)

5
modules/soutenance.xml Normal file
View file

@ -0,0 +1,5 @@
<?xml version="1.0" ?>
<nemubotmodule name="soutenance">
<server ip="www.acu.epita.fr" url="/intra/sout_liste.html" />
<message type="cmd" name="soutenance" call="ask_soutenance" />
</nemubotmodule>

View file

@ -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)

View file

@ -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("</tr>", line) is not None:
save = False
elif re.match("<tr.*>", line) is not None:
save = True
last = Soutenance()
self.souts.append(last)
elif save:
result = re.match("<td[^>]+>(.*)</td>", line)
if last.hour is None:
try:
last.hour = datetime.fromtimestamp(time.mktime(time.strptime(result.group(1), "%Y-%m-%d %H:%M")))
except ValueError:
continue
elif last.rank == 0:
last.rank = int (result.group(1))
elif last.login == None:
last.login = result.group(1)
elif last.state == None:
last.state = result.group(1)
elif last.assistant == None:
last.assistant = result.group(1)
elif last.start == None:
try:
last.start = datetime.fromtimestamp(time.mktime(time.strptime(result.group(1), "%Y-%m-%d %H:%M")))
except ValueError:
last.start = None
elif last.end == None:
try:
last.end = datetime.fromtimestamp(time.mktime(time.strptime(result.group(1), "%Y-%m-%d %H:%M")))
except ValueError:
last.end = None
def gen_response(self, req, msg):
"""Generate a text response on right server and channel"""
return Response(req["sender"], msg, req["channel"], server=req["server"])
def res_next(self, req):
soutenance = self.findLast()
if soutenance is None:
return self.gen_response(req, "Il ne semble pas y avoir de soutenance pour le moment.")
else:
if soutenance.start > soutenance.hour:
avre = "%s de *retard*"%msg.just_countdown(soutenance.start - soutenance.hour, 4)
else:
avre = "%s *d'avance*"%msg.just_countdown(soutenance.hour - soutenance.start, 4)
self.gen_response(req, "Actuellement à la soutenance numéro %d, commencée il y a %s avec %s."%(soutenance.rank, msg.just_countdown(datetime.now () - soutenance.start, 4), avre))
def res_assistants(self, req):
assistants = self.findAssistants()
if len(assistants) > 0:
return self.gen_response(req, "Les %d assistants faisant passer les soutenances sont : %s." % (len(assistants), ', '.join(assistants.keys())))
else:
return self.gen_response(req, "Il ne semble pas y avoir de soutenance pour le moment.")
def res_soutenance(self, req):
name = req["user"]
if name == "acu" or name == "yaka" or name == "acus" or name == "yakas" or name == "assistant" or name == "assistants":
return self.res_assistants(req)
elif name == "next":
return self.res_next(req)
soutenance = self.findClose(name)
if soutenance is None:
return self.gen_response(req, "Pas d'horaire de soutenance pour %s."%name)
else:
if soutenance.state == "En cours":
return self.gen_response(req, "%s est actuellement en soutenance avec %s. Elle était prévue à %s, position %d."%(name, soutenance.assistant, soutenance.hour, soutenance.rank))
elif soutenance.state == "Effectue":
return self.gen_response(req, "%s a passé sa soutenance avec %s. Elle a duré %s."%(name, soutenance.assistant, msg.just_countdown(soutenance.end - soutenance.start, 4)))
elif soutenance.state == "Retard":
return self.gen_response(req, "%s était en retard à sa soutenance de %s."%(name, soutenance.hour))
else:
last = self.findLast()
if last is not None:
if soutenance.hour + (last.start - last.hour) > datetime.now ():
return self.gen_response(req, "Soutenance de %s : %s, position %d ; estimation du passage : dans %s."%(name, soutenance.hour, soutenance.rank, msg.just_countdown((soutenance.hour - datetime.now ()) + (last.start - last.hour))))
else:
return self.gen_response(req, "Soutenance de %s : %s, position %d ; passage imminent."%(name, soutenance.hour, soutenance.rank))
else:
return self.gen_response(req, "Soutenance de %s : %s, position %d."%(name, soutenance.hour, soutenance.rank))
def res_list(self, req):
name = req["user"]
souts = self.findAll(name)
if souts is None:
self.gen_response(req, "Pas de soutenance prévues pour %s."%name)
else:
first = True
for s in souts:
if first:
self.gen_response(req, "Soutenance(s) de %s : - %s (position %d) ;"%(name, s.hour, s.rank))
first = False
else:
self.gen_response(req, " %s - %s (position %d) ;"%(len(name)*' ', s.hour, s.rank))
def run(self):
self.parsePage(self.getPage().decode())
res = list()
for u in self.datas.getNodes("request"):
res.append(self.res_soutenance(u))
return res
def needUpdate(self):
if self.findLast() is not None and datetime.now () - self.updated > timedelta(minutes=2):
return True
elif datetime.now () - self.updated < timedelta(hours=1):
return False
else:
return True
def findAssistants(self):
h = dict()
for s in self.souts:
if s.assistant is not None and s.assistant != "":
h[s.assistant] = (s.start, s.end)
return h
def findLast(self):
close = None
for s in self.souts:
if (s.state != "En attente" and s.start is not None and (close is None or close.rank < s.rank or close.hour.day > s.hour.day)) and (close is None or s.hour - close.hour < timedelta(seconds=2499)):
close = s
return close
def findAll(self, login):
ss = list()
for s in self.souts:
if s.login == login:
ss.append(s)
return ss
def findClose(self, login):
ss = self.findAll(login)
close = None
for s in ss:
if close is not None:
print (close.hour)
print (s.hour)
if close is None or (close.hour < s.hour and close.hour.day >= datetime.datetime().day):
close = s
return close

View file

@ -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

View file

@ -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 <who>: gives the date of the next defense of /who/.\n!soutenances <who>: gives all defense dates of /who/"
def load(context):
global CONF
SiteSoutenances.CONF = CONF
def ask_soutenance(msg):
req = ModuleState("request")
if len(msg.cmds) > 1:
req.setAttribute("user", msg.cmds[1])
else:
req.setAttribute("user", "next")
req.setAttribute("server", msg.server)
req.setAttribute("channel", msg.channel)
req.setAttribute("sender", msg.sender)
#An instance of this module is already running?
if not DATAS.hasAttribute("_running") or DATAS["_running"].needUpdate():
DATAS.addChild(req)
site = SiteSoutenances.SiteSoutenances(DATAS)
DATAS.setAttribute("_running", site)
res = site.run()
for n in DATAS.getNodes("request"):
DATAS.delChild(n)
return res
else:
site = DATAS["_running"]
return site.res_soutenance(req)

View file

@ -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()

View file

@ -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 [<lang>] <word>: give the correct spelling of <word> in <lang=fr>."
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 add_score(nick, t):
if nick not in context.data.index:
st = ModuleState("score")
st["name"] = nick
context.data.addChild(st)
if context.data.index[nick].hasAttribute(t):
context.data.index[nick][t] = context.data.index[nick].getInt(t) + 1
else:
context.data.index[nick][t] = 1
context.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.")
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 = msg.kwargs["lang"] if "lang" in msg.kwargs else "fr"
res = Response(channel=msg.channel)
for word in msg.args:
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:
raise IMException("Je n'ai pas le dictionnaire `%s' :(" % lang)
return Response(msg.sender, "Je n'ai pas le dictionnaire `%s' :(" % lang, msg.channel)
if r == True:
add_score(msg.frm, "correct")
res.append_message("l'orthographe de `%s' est correcte" % word)
add_score(msg.nick, "correct")
strRes.append("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)
add_score(msg.nick, "bad")
strRes.append("suggestions pour `%s' : %s" % (word, ", ".join(r)))
else:
add_score(msg.frm, "bad")
res.append_message("aucune suggestion pour `%s'" % word)
add_score(msg.nick, "bad")
strRes.append("aucune suggestion pour `%s'" % word)
return Response(msg.sender, strRes, channel=msg.channel)
return res
def add_score(nick, t):
global DATAS
if nick not in DATAS.index:
st = ModuleState("score")
st["name"] = nick
DATAS.addChild(st)
if DATAS.index[nick].hasAttribute(t):
DATAS.index[nick][t] = DATAS.index[nick].getInt(t) + 1
else:
DATAS.index[nick][t] = 1
save()
@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))
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

View file

@ -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

View file

@ -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 <word>: give a list of synonyms for <word>."
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"
"<module name=\"syno\" bighugelabskey=\"XXXXXXXXXXXXXXXX\""
" />\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 get_french_synos(word):
url = "https://crisco.unicaen.fr/des/synonymes/" + quote(word)
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(">([^<>]+)</a>", line):
anton.append(elt.group(1))
elif line.find("!--Fin liste des synonymes--") > 0:
for elt in re.finditer(">([^<>]+)</a>", line):
synos.append(elt.group(1))
elif re.match("[ \t]*<tr[^>]*>.*</tr>[ \t]*</table>.*", line) is not None:
for elt in re.finditer(">&[^;]+;([^&]*)&[^;]+;<", line):
best.append(elt.group(1))
return (best, synos, anton)
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)
def cmd_syno(msg):
if 1 < len(msg.cmds) < 6:
for word in msg.cmds[1:]:
try:
best, synos, anton = lang_binding[lang](word)
synos = get_synos(word)
except:
best, synos, anton = (list(), list(), list())
synos = None
exc_type, exc_value, exc_traceback = sys.exc_info()
traceback.print_exception(exc_type, exc_value,
exc_traceback)
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
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:
raise IMException("Aucun synonyme de %s n'a été trouvé" % word)
return Response(msg.sender,
"Aucun synonymes de %s n'a été trouvé" % word,
msg.channel)
return False
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)
def get_synos(word):
url = "http://www.crisco.unicaen.fr/des/synonymes/" + quote(word.encode("ISO-8859-1"))
print_debug (url)
page = web.getURLContent(url)
if page is not None:
synos = list()
for line in page.decode().split("\n"):
if re.match("[ \t]*<tr[^>]*>.*</tr>[ \t]*</table>.*", line) is not None:
for elt in re.finditer(">&[^;]+;([^&]*)&[^;]+;<", line):
synos.append(elt.group(1))
return synos
else:
raise IMException("WHAT?!")
return None

View file

@ -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<module"
"name=\"tpb\" url=\"http://tpbapi.org/\" />\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

View file

@ -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<module name=\"translate\" wrapikey=\"XXXXX\""
" />\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 <lang>."
},
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<wrapi key=\"XXXXX\""
" />\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)

Some files were not shown because too many files have changed in this diff Show more