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 TAGS
*.py[cod] *.py[cod]
__pycache__ __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! An extremely modulable IRC bot, built around XML configuration files!
## Documentation
Requirements Have a look to the wiki at https://github.com/nemunaire/nemubot/wiki
------------
*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.

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"> <nemubotconfig nick="nemubot" realname="nemubot speaker" owner="someone">
<server server="irc.freenode.org" port="6667" password="secret" autoconnect="true">
<server uri="irc://irc.rezosup.org:6667" autoconnect="true" caps="znc.in/server-time-iso">
<channel name="#nemutest" /> <channel name="#nemutest" />
</server> </server>
<load path="modules/birthday.xml" />
<!-- <load path="modules/ycc.xml" />
<server host="ircs://my_host.local:6667" password="secret" autoconnect="true"> <load path="modules/qcm.xml" />
<channel name="#nemutest" /> <load path="modules/soutenance.xml" />
</server> <load path="modules/velib.xml" />
--> <load path="modules/whereis.xml" />
<load path="modules/watchWebsite.xml" />
<!-- <load path="modules/events.xml" />
<module name="wolframalpha" apikey="YOUR-APIKEY" />
-->
<module name="cmd_server" />
<module name="alias" />
<module name="ycc" />
<module name="events" />
</nemubotconfig> </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,134 +1,111 @@
"""People birthdays and ages""" # coding=utf-8
# PYTHON STUFFS #######################################################
import re import re
import sys import sys
from datetime import date, datetime from datetime import datetime
from datetime import date
from nemubot import context from xmlparser.node import ModuleState
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 nemubot.module.more import Response nemubotversion = 3.3
# LOADING #############################################################
def load(context): 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): def findName(msg):
if (not len(msg.args) or msg.args[0].lower() == "moi" or if len(msg.cmds) < 2 or msg.cmds[1].lower() == "moi" or msg.cmds[1].lower() == "me":
msg.args[0].lower() == "me"): name = msg.nick.lower()
name = msg.frm.lower()
else: else:
name = msg.args[0].lower() name = msg.cmds[1].lower()
matches = [] matches = []
if name in context.data.index: if name in DATAS.index:
matches.append(name) matches.append(name)
else: else:
for k in context.data.index.keys(): for k in DATAS.index.keys ():
if k.find(name) == 0: if k.find (name) == 0:
matches.append(k) matches.append (k)
return (matches, name) 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): def cmd_anniv(msg):
(matches, name) = findName(msg) (matches, name) = findName(msg)
if len(matches) == 1: if len(matches) == 1:
name = matches[0] 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) tyd = datetime(date.today().year, tyd.month, tyd.day)
if (tyd.day == datetime.today().day and if (tyd.day == datetime.today().day and
tyd.month == datetime.today().month): tyd.month == datetime.today().month):
return Response(countdown_format( return Response(msg.sender, msg.countdown_format(
context.data.index[name].getDate("born"), "", DATAS.index[name].getDate("born"), "",
"C'est aujourd'hui l'anniversaire de %s !" "C'est aujourd'hui l'anniversaire de %s !"
" Il a %s. Joyeux anniversaire :)" % (name, "%s")), " Il a %s. Joyeux anniversaire :)" % (name, "%s")),
msg.channel) msg.channel)
else: else:
if tyd < datetime.today(): if tyd < datetime.today():
tyd = datetime(date.today().year + 1, tyd.month, tyd.day) 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", "Il reste %s avant l'anniversaire de %s !" % ("%s",
name), ""), name), ""),
msg.channel) msg.channel)
else: 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, " 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): def cmd_age(msg):
(matches, name) = findName(msg) (matches, name) = findName(msg)
if len(matches) == 1: if len(matches) == 1:
name = matches[0] 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 va naître dans %s." % (name, "%s"),
"%s a %s." % (name, "%s")), "%s a %s." % (name, "%s")),
msg.channel) msg.channel)
else: else:
return Response("désolé, je ne connais pas l'âge de %s." return Response(msg.sender, "désolé, je ne connais pas l'âge de %s."
" Quand est-il né ?" % name, msg.channel, msg.frm) " Quand est-il né ?" % name, msg.channel, msg.nick)
return True return True
## Input parsing
@hook.ask()
def parseask(msg): 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) msgl = msg.content.lower ()
if res is not None: if re.match("^.*(date de naissance|birthday|geburtstag|née? |nee? le|born on).*$", msgl) is not None:
try: try:
extDate = extractDate(msg.message) extDate = msg.extractDate()
if extDate is None or extDate.year > datetime.now().year: 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.channel,
msg.frm) msg.nick)
else: else:
nick = res.group(1) if msg.nick.lower() in DATAS.index:
if nick == "my" or nick == "I" or nick == "i" or nick == "je" or nick == "mon" or nick == "ma": DATAS.index[msg.nick.lower()]["born"] = extDate
nick = msg.frm
if nick.lower() in context.data.index:
context.data.index[nick.lower()]["born"] = extDate
else: else:
ms = ModuleState("birthday") ms = ModuleState("birthday")
ms.setAttribute("name", nick.lower()) ms.setAttribute("name", msg.nick.lower())
ms.setAttribute("born", extDate) ms.setAttribute("born", extDate)
context.data.addChild(ms) DATAS.addChild(ms)
context.save() save()
return Response("ok, c'est noté, %s est né le %s" return Response(msg.sender,
% (nick, extDate.strftime("%A %d %B %Y à %H:%M")), "ok, c'est noté, ta date de naissance est le %s"
% extDate.strftime("%A %d %B %Y à %H:%M"),
msg.channel, msg.channel,
msg.frm) msg.nick)
except: 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 nemubotversion = 3.3
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 #############################################################
def load(context): def load(context):
if not context.config or not context.config.hasNode("sayon"): yr = datetime.today().year
print("You can append in your configuration some balise to " yrn = datetime.today().year + 1
"automaticaly wish an happy new year on some channels like:\n"
"<sayon hostid=\"nemubot@irc.freenode.net:6667\" "
"channel=\"#nemutest\" />")
def bonneannee(): d = datetime(yrn, 1, 1, 0, 0, 0) - datetime.now()
txt = "Bonne année %d !" % yrn # d = datetime(yr, 12, 31, 19, 34, 0) - datetime.now()
print(txt) add_event(ModuleEvent(intervalle=0, offset=d.total_seconds(), call=bonneannee))
if context.config and context.config.hasNode("sayon"):
for sayon in context.config.getNodes("sayon"):
if "hostid" not in sayon or "channel" not in sayon:
print("Error: missing hostif or channel")
continue
srv = sayon["hostid"]
chan = sayon["channel"]
context.send_response(srv, Response(txt, chan))
d = datetime(yrn, 1, 1, 0, 0, 0, 0, from hooks import Hook
timezone.utc) - datetime.now(timezone.utc) add_hook("cmd_rgxp", Hook(cmd_timetoyear, data=yrn, regexp="^[0-9]{4}$"))
context.add_event(ModuleEvent(interval=0, offset=d.total_seconds(), add_hook("cmd_hook", Hook(cmd_newyear, str(yrn), yrn))
call=bonneannee)) add_hook("cmd_hook", Hook(cmd_newyear, "ny", yrn))
add_hook("cmd_hook", Hook(cmd_newyear, "newyear", yrn))
add_hook("cmd_hook", Hook(cmd_newyear, "new-year", yrn))
add_hook("cmd_hook", Hook(cmd_newyear, "new year", yrn))
def bonneannee():
txt = "Bonne année %d !" % datetime.today().year
print (txt)
send_response("localhost:2771", Response(None, txt, "#epitagueule"))
send_response("localhost:2771", Response(None, txt, "#yaka"))
send_response("localhost:2771", Response(None, txt, "#epita2014"))
send_response("localhost:2771", Response(None, txt, "#ykar"))
send_response("localhost:2771", Response(None, txt, "#ordissimo"))
send_response("localhost:2771", Response(None, txt, "#42sh"))
send_response("localhost:2771", Response(None, txt, "#nemubot"))
# MODULE INTERFACE #################################################### def cmd_newyear(msg, yr):
return Response(msg.sender,
@hook.command("newyear", msg.countdown_format(datetime(yr, 1, 1, 0, 0, 1),
help="Display the remaining time before the next new year") "Il reste %s avant la nouvelle année.",
@hook.command(str(yrn), "Nous faisons déjà la fête depuis %s !"),
help="Display the remaining time before %d" % yrn)
def cmd_newyear(msg):
return Response(countdown_format(datetime(yrn, 1, 1, 0, 0, 1, 0,
timezone.utc),
"Il reste %s avant la nouvelle année.",
"Nous faisons déjà la fête depuis %s !"),
channel=msg.channel) 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): def cmd_timetoyear(msg, cur):
yr = int(msg.cmd) yr = int(msg.cmds[0])
if yr == cur: if yr == cur:
return None return None
return Response(countdown_format(datetime(yr, 1, 1, 0, 0, 1, 0, return Response(msg.sender,
timezone.utc), msg.countdown_format(datetime(yr, 1, 1, 0, 0, 1),
"Il reste %s avant %d." % ("%s", yr), "Il reste %s avant %d." % ("%s", yr),
"Le premier janvier %d est passé " "Le premier janvier %d est passé depuis %s !" % (yr, "%s")),
"depuis %s !" % (yr, "%s")),
channel=msg.channel) 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