Compare commits
3 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 26faed014f | |||
| f9970cba42 | |||
| 35ae6b6245 |
191 changed files with 8116 additions and 12612 deletions
26
.drone.yml
26
.drone.yml
|
|
@ -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
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1,7 +1,8 @@
|
|||
*#
|
||||
*~
|
||||
*.log
|
||||
TAGS
|
||||
*.py[cod]
|
||||
__pycache__
|
||||
build/
|
||||
datas/
|
||||
dist/
|
||||
|
|
|
|||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[submodule "modules/nextstop/external"]
|
||||
path = modules/nextstop/external
|
||||
url = git://github.com/nbr23/NextStop.git
|
||||
13
.travis.yml
13
.travis.yml
|
|
@ -1,12 +1,7 @@
|
|||
language: python
|
||||
python:
|
||||
- 3.2
|
||||
- 3.3
|
||||
- 3.4
|
||||
- 3.5
|
||||
- 3.6
|
||||
- 3.7
|
||||
- nightly
|
||||
install:
|
||||
- pip install -r requirements.txt
|
||||
- pip install .
|
||||
script: nosetests -w nemubot
|
||||
sudo: false
|
||||
install: pip install -r requirements.txt
|
||||
script: pip install .
|
||||
|
|
|
|||
21
Dockerfile
21
Dockerfile
|
|
@ -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" ]
|
||||
51
README.md
51
README.md
|
|
@ -1,50 +1,21 @@
|
|||
nemubot
|
||||
=======
|
||||
# nemubot
|
||||
|
||||
An extremely modulable IRC bot, built around XML configuration files!
|
||||
A smart and modulable IM bot!
|
||||
|
||||
## Usage
|
||||
|
||||
Requirements
|
||||
------------
|
||||
TODO
|
||||
|
||||
*nemubot* requires at least Python 3.3 to work.
|
||||
## Documentation
|
||||
|
||||
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.
|
||||
Have a look to the wiki at https://github.com/nemunaire/nemubot/wiki
|
||||
|
||||
## Building
|
||||
|
||||
Installation
|
||||
------------
|
||||
TODO
|
||||
|
||||
Use the `setup.py` file: `python setup.py install`.
|
||||
## License
|
||||
|
||||
### VirtualEnv setup
|
||||
This software is copyright (c) 2014 by nemunaire.
|
||||
|
||||
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.
|
||||
This is free software; you can redistribute it and/or modify it under the same terms as GNU Affero General Public Licence v3.
|
||||
|
|
|
|||
60
bin/nemubot
60
bin/nemubot
|
|
@ -1,7 +1,8 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2016 Mercier Pierre-Olivier
|
||||
# Copyright (C) 2012-2014 nemunaire
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
|
|
@ -17,8 +18,61 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
import os
|
||||
import imp
|
||||
import traceback
|
||||
|
||||
from nemubot.__main__ import main
|
||||
import nemubot
|
||||
from nemubot import prompt
|
||||
from nemubot.prompt.builtins import load_file
|
||||
from nemubot import importer
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
# Create bot context
|
||||
context = nemubot.Bot()
|
||||
|
||||
# Load the prompt
|
||||
prmpt = prompt.Prompt()
|
||||
|
||||
# Register the hook for futur import
|
||||
sys.meta_path.append(importer.ModuleFinder(context, prmpt))
|
||||
|
||||
#Add modules dir path
|
||||
if os.path.isdir("./modules/"):
|
||||
context.add_modules_path(
|
||||
os.path.realpath(os.path.abspath("./modules/")))
|
||||
|
||||
# Parse command line arguments
|
||||
if len(sys.argv) >= 2:
|
||||
for arg in sys.argv[1:]:
|
||||
if os.path.isdir(arg):
|
||||
context.add_modules_path(arg)
|
||||
else:
|
||||
load_file(arg, context)
|
||||
|
||||
print ("Nemubot v%s ready, my PID is %i!" % (nemubot.__version__,
|
||||
os.getpid()))
|
||||
while prmpt.run(context):
|
||||
try:
|
||||
# Reload context
|
||||
imp.reload(bot)
|
||||
context = bot.hotswap(context)
|
||||
# Reload prompt
|
||||
imp.reload(prompt)
|
||||
prmpt = prompt.hotswap(prmpt)
|
||||
# Reload all other modules
|
||||
bot.reload()
|
||||
print ("\033[1;32mContext reloaded\033[0m, now in Nemubot %s" %
|
||||
nemubot.__version__)
|
||||
except:
|
||||
print ("\033[1;31mUnable to reload the prompt due to errors.\033[0"
|
||||
"m Fix them before trying to reload the prompt.")
|
||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||
sys.stderr.write (traceback.format_exception_only(exc_type,
|
||||
exc_value)[0])
|
||||
|
||||
print ("\nWaiting for other threads shuts down...")
|
||||
|
||||
# Indeed, the server socket is waiting for receiving some data
|
||||
|
||||
sys.exit(0)
|
||||
|
|
|
|||
186
bin/nemuspeak
Executable file
186
bin/nemuspeak
Executable file
|
|
@ -0,0 +1,186 @@
|
|||
#!/usr/bin/python3
|
||||
# coding=utf-8
|
||||
|
||||
import sys
|
||||
import signal
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
import _thread
|
||||
|
||||
if len(sys.argv) <= 1:
|
||||
print ("This script takes exactly 1 arg: a XML config file")
|
||||
sys.exit(1)
|
||||
|
||||
def onSignal(signum, frame):
|
||||
print ("\nSIGINT receive, saving states and close")
|
||||
sys.exit (0)
|
||||
signal.signal(signal.SIGINT, onSignal)
|
||||
|
||||
if len(sys.argv) == 3:
|
||||
basedir = sys.argv[2]
|
||||
else:
|
||||
basedir = "./"
|
||||
|
||||
import xmlparser as msf
|
||||
import message
|
||||
import IRCServer
|
||||
|
||||
SMILEY = list()
|
||||
CORRECTIONS = list()
|
||||
g_queue = list()
|
||||
talkEC = 0
|
||||
stopSpk = 0
|
||||
lastmsg = None
|
||||
|
||||
def speak(endstate):
|
||||
global lastmsg, g_queue, talkEC, stopSpk
|
||||
talkEC = 1
|
||||
stopSpk = 0
|
||||
|
||||
if lastmsg is None:
|
||||
lastmsg = message.Message(b":Quelqun!someone@p0m.fr PRIVMSG channel nothing", datetime.now())
|
||||
|
||||
while not stopSpk and len(g_queue) > 0:
|
||||
srv, msg = g_queue.pop(0)
|
||||
lang = "fr"
|
||||
sentence = ""
|
||||
force = 0
|
||||
|
||||
#Skip identic body
|
||||
if msg.content == lastmsg.content:
|
||||
continue
|
||||
|
||||
if force or msg.time - lastmsg.time > timedelta(0, 500):
|
||||
sentence += "A {0} heure {1} : ".format(msg.time.hour, msg.time.minute)
|
||||
force = 1
|
||||
|
||||
if force or msg.channel != lastmsg.channel:
|
||||
if msg.channel == srv.owner:
|
||||
sentence += "En message priver. " #Just to avoid é :p
|
||||
else:
|
||||
sentence += "Sur " + msg.channel + ". "
|
||||
force = 1
|
||||
|
||||
action = 0
|
||||
if msg.content.find("ACTION ") == 1:
|
||||
sentence += msg.nick + " "
|
||||
msg.content = msg.content.replace("ACTION ", "")
|
||||
action = 1
|
||||
for (txt, mood) in SMILEY:
|
||||
if msg.content.find(txt) >= 0:
|
||||
sentence += msg.nick + (" %s : "%mood)
|
||||
msg.content = msg.content.replace(txt, "")
|
||||
action = 1
|
||||
break
|
||||
|
||||
for (bad, good) in CORRECTIONS:
|
||||
if msg.content.find(bad) >= 0:
|
||||
msg.content = (" " + msg.content + " ").replace(bad, good)
|
||||
|
||||
if action == 0 and (force or msg.sender != lastmsg.sender):
|
||||
sentence += msg.nick + " dit : "
|
||||
|
||||
if re.match(".*(https?://)?(www\\.)?ycc.fr/[a-z0-9A-Z]+.*", msg.content) is not None:
|
||||
msg.content = re.sub("(https?://)?(www\\.)?ycc.fr/[a-z0-9A-Z]+", " U.R.L Y.C.C ", msg.content)
|
||||
|
||||
if re.match(".*https?://.*", msg.content) is not None:
|
||||
msg.content = re.sub(r'https?://[^ ]+', " U.R.L ", msg.content)
|
||||
|
||||
if re.match("^ *[^a-zA-Z0-9 ][a-zA-Z]{2}[^a-zA-Z0-9 ]", msg.content) is not None:
|
||||
if sentence != "":
|
||||
intro = subprocess.call(["espeak", "-v", "fr", "--", sentence])
|
||||
#intro.wait()
|
||||
|
||||
lang = msg.content[1:3].lower()
|
||||
sentence = msg.content[4:]
|
||||
else:
|
||||
sentence += msg.content
|
||||
|
||||
spk = subprocess.call(["espeak", "-v", lang, "--", sentence])
|
||||
#spk.wait()
|
||||
|
||||
lastmsg = msg
|
||||
|
||||
if not stopSpk:
|
||||
talkEC = endstate
|
||||
else:
|
||||
talkEC = 1
|
||||
|
||||
|
||||
class Server(IRCServer.IRCServer):
|
||||
def treat_msg(self, line, private = False):
|
||||
global stopSpk, talkEC, g_queue
|
||||
try:
|
||||
msg = message.Message (line, datetime.now(), private)
|
||||
if msg.cmd == 'PING':
|
||||
self.send_pong(msg.content)
|
||||
elif msg.cmd == 'PRIVMSG' and self.accepted_channel(msg.channel):
|
||||
if msg.nick != self.owner:
|
||||
g_queue.append((self, msg))
|
||||
if talkEC == 0:
|
||||
_thread.start_new_thread(speak, (0,))
|
||||
elif msg.content[0] == "`" and len(msg.content) > 1:
|
||||
msg.cmds = msg.cmds[1:]
|
||||
if msg.cmds[0] == "speak":
|
||||
_thread.start_new_thread(speak, (0,))
|
||||
elif msg.cmds[0] == "reset":
|
||||
while len(g_queue) > 0:
|
||||
g_queue.pop()
|
||||
elif msg.cmds[0] == "save":
|
||||
if talkEC == 0:
|
||||
talkEC = 1
|
||||
stopSpk = 1
|
||||
elif msg.cmds[0] == "add":
|
||||
self.channels.append(msg.cmds[1])
|
||||
print (cmd[1] + " added to listened channels")
|
||||
elif msg.cmds[0] == "del":
|
||||
if self.channels.count(msg.cmds[1]) > 0:
|
||||
self.channels.remove(msg.cmds[1])
|
||||
print (msg.cmds[1] + " removed from listened channels")
|
||||
else:
|
||||
print (cmd[1] + " not in listened channels")
|
||||
except:
|
||||
print ("\033[1;31mERROR:\033[0m occurred during the processing of the message: %s" % line)
|
||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||
traceback.print_exception(exc_type, exc_value, exc_traceback)
|
||||
|
||||
|
||||
config = msf.parse_file(sys.argv[1])
|
||||
|
||||
for smiley in config.getNodes("smiley"):
|
||||
if smiley.hasAttribute("txt") and smiley.hasAttribute("mood"):
|
||||
SMILEY.append((smiley.getAttribute("txt"), smiley.getAttribute("mood")))
|
||||
print ("%d smileys loaded"%len(SMILEY))
|
||||
|
||||
for correct in config.getNodes("correction"):
|
||||
if correct.hasAttribute("bad") and correct.hasAttribute("good"):
|
||||
CORRECTIONS.append((" " + (correct.getAttribute("bad") + " "), (" " + correct.getAttribute("good") + " ")))
|
||||
print ("%d corrections loaded"%len(CORRECTIONS))
|
||||
|
||||
for serveur in config.getNodes("server"):
|
||||
srv = Server(serveur, config["nick"], config["owner"], config["realname"], serveur.hasAttribute("ssl"))
|
||||
srv.launch(None)
|
||||
|
||||
def sighup_h(signum, frame):
|
||||
global talkEC, stopSpk
|
||||
sys.stdout.write ("Signal reçu ... ")
|
||||
if os.path.exists("/tmp/isPresent"):
|
||||
_thread.start_new_thread(speak, (0,))
|
||||
print ("Morning!")
|
||||
else:
|
||||
print ("Sleeping!")
|
||||
if talkEC == 0:
|
||||
talkEC = 1
|
||||
stopSpk = 1
|
||||
signal.signal(signal.SIGHUP, sighup_h)
|
||||
|
||||
print ("Nemuspeak ready, waiting for new messages...")
|
||||
prompt=""
|
||||
while prompt != "quit":
|
||||
prompt=sys.stdin.readlines ()
|
||||
|
||||
sys.exit(0)
|
||||
|
|
@ -1,23 +1,27 @@
|
|||
<nemubotconfig nick="nemubot" realname="nemubot clone" owner="someone">
|
||||
|
||||
<server uri="irc://irc.rezosup.org:6667" autoconnect="true" caps="znc.in/server-time-iso">
|
||||
<server server="irc.rezosup.org" port="6667" autoconnect="true">
|
||||
<channel name="#nemutest" />
|
||||
</server>
|
||||
|
||||
<!--
|
||||
<server host="ircs://my_host.local:6667" password="secret" autoconnect="true">
|
||||
<server server="my_host.local" port="6667" password="secret" autoconnect="true" ip="10.69.42.23" ssl="on" allowall="true">
|
||||
<channel name="#nemutest" />
|
||||
</server>
|
||||
-->
|
||||
|
||||
<!--
|
||||
<module name="wolframalpha" apikey="YOUR-APIKEY" />
|
||||
-->
|
||||
<load path="cmd_server" />
|
||||
|
||||
<module name="cmd_server" />
|
||||
|
||||
<module name="alias" />
|
||||
<module name="ycc" />
|
||||
<module name="events" />
|
||||
<load path="alias" />
|
||||
<load path="birthday" />
|
||||
<load path="ycc" />
|
||||
<load path="velib" />
|
||||
<load path="watchWebsite" />
|
||||
<load path="events" />
|
||||
<load path="sleepytime" />
|
||||
<load path="spell" />
|
||||
<load path="syno" />
|
||||
<load path="man" />
|
||||
<load path="reddit" />
|
||||
|
||||
</nemubotconfig>
|
||||
|
|
|
|||
0
datas/datas
Normal file
0
datas/datas
Normal file
241
lib/DCC.py
Normal file
241
lib/DCC.py
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2014 nemunaire
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import imp
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
import threading
|
||||
import traceback
|
||||
|
||||
import nemubot.message
|
||||
from nemubot 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)
|
||||
295
lib/IRCServer.py
Normal file
295
lib/IRCServer.py
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2014 nemunaire
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import errno
|
||||
import os
|
||||
import ssl
|
||||
import socket
|
||||
import threading
|
||||
import traceback
|
||||
|
||||
from nemubot.channel import Channel
|
||||
from nemubot.DCC import DCC
|
||||
from nemubot.hooks import Hook
|
||||
import nemubot.message
|
||||
from nemubot import server
|
||||
import nemubot.xmlparser
|
||||
|
||||
class IRCServer(server.Server):
|
||||
"""Class to interact with an IRC server"""
|
||||
|
||||
def __init__(self, node, nick, owner, realname, ssl=False):
|
||||
"""Initialize an IRC server
|
||||
|
||||
Arguments:
|
||||
node -- server node from XML configuration
|
||||
nick -- nick used by the bot on this server
|
||||
owner -- nick used by the bot owner on this server
|
||||
realname -- string used as realname on this server
|
||||
ssl -- require SSL?
|
||||
"""
|
||||
server.Server.__init__(self)
|
||||
|
||||
self.node = node
|
||||
|
||||
self.nick = nick
|
||||
self.owner = owner
|
||||
self.realname = realname
|
||||
self.ssl = ssl
|
||||
|
||||
# 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
|
||||
if self.ssl:
|
||||
ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
|
||||
self.s = ctx.wrap_socket(self.s)
|
||||
try:
|
||||
self.s.connect((self.host, self.port)) #Connect to server
|
||||
except socket.error as e:
|
||||
self.s = None
|
||||
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)
|
||||
657
lib/__init__.py
Normal file
657
lib/__init__.py
Normal file
|
|
@ -0,0 +1,657 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2014 nemunaire
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from ipaddress import ip_address
|
||||
import logging
|
||||
from queue import Queue
|
||||
import uuid
|
||||
|
||||
__version__ = '4.0.dev0'
|
||||
__author__ = 'nemunaire'
|
||||
|
||||
from nemubot import consumer
|
||||
from nemubot import event
|
||||
from nemubot import hooks
|
||||
from nemubot.networkbot import NetworkBot
|
||||
from nemubot.IRCServer import IRCServer
|
||||
from nemubot.DCC import DCC
|
||||
from nemubot import response
|
||||
|
||||
class Bot:
|
||||
|
||||
"""Class containing the bot context and ensuring key goals"""
|
||||
|
||||
def __init__(self, ip="127.0.0.1", modules_paths=list(), data_path="./datas/"):
|
||||
"""Initialize the bot context
|
||||
|
||||
Keyword arguments:
|
||||
ip -- The external IP of the bot (default: 127.0.0.1)
|
||||
modules_paths -- Paths to all directories where looking for module
|
||||
data_path -- Path to directory where store bot context data
|
||||
"""
|
||||
|
||||
# External IP for accessing this bot
|
||||
self.ip = ip_address(ip)
|
||||
|
||||
# Context paths
|
||||
self.modules_paths = modules_paths
|
||||
self.data_path = data_path
|
||||
|
||||
# Keep global context: servers and modules
|
||||
self.servers = dict()
|
||||
self.modules = dict()
|
||||
|
||||
# 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
|
||||
|
||||
# Stop the class thread
|
||||
self.stop = False
|
||||
|
||||
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 srv, msg: print ("ACTION receive")
|
||||
self.ctcp_capabilities["CLIENTINFO"] = self._ctcp_clientinfo
|
||||
self.ctcp_capabilities["DCC"] = self._ctcp_dcc
|
||||
self.ctcp_capabilities["NEMUBOT"] = lambda srv, msg: _ctcp_response(
|
||||
msg.sender, "NEMUBOT %f" % __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" % __version__)
|
||||
|
||||
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]))
|
||||
|
||||
|
||||
# Events methods
|
||||
|
||||
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_event_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_event_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_event_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_event_timer()
|
||||
|
||||
|
||||
# Server methods
|
||||
|
||||
def addServer(self, node, nick, owner, realname, ssl=False):
|
||||
"""Add a new server to the context"""
|
||||
srv = IRCServer(node, nick, owner, realname, ssl)
|
||||
srv.add_hook = lambda h: self.hooks.add_hook("irc_hook", h, self)
|
||||
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
|
||||
|
||||
|
||||
# Modules methods
|
||||
|
||||
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_paths:
|
||||
self.modules_paths.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
|
||||
|
||||
|
||||
# Consumers methods
|
||||
|
||||
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_paths)
|
||||
|
||||
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)
|
||||
102
lib/channel.py
Normal file
102
lib/channel.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
# coding=utf-8
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2014 nemunaire
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class 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)
|
||||
144
lib/consumer.py
Normal file
144
lib/consumer.py
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2014 nemunaire
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import queue
|
||||
import re
|
||||
import threading
|
||||
import traceback
|
||||
import sys
|
||||
|
||||
from nemubot.DCC import DCC
|
||||
from nemubot.message import Message
|
||||
from nemubot import response
|
||||
from nemubot 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)
|
||||
self.context.cnsr_queue.task_done()
|
||||
|
||||
except queue.Empty:
|
||||
pass
|
||||
finally:
|
||||
self.context.cnsr_thrd_size -= 2
|
||||
self.context.cnsr_thrd.remove(self)
|
||||
43
lib/credits.py
Normal file
43
lib/credits.py
Normal 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))
|
||||
118
lib/event.py
Normal file
118
lib/event.py
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2014 nemunaire
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from datetime import datetime
|
||||
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)
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
# coding=utf-8
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2015 Mercier Pierre-Olivier
|
||||
# Copyright (C) 2012-2014 nemunaire
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
|
|
@ -14,21 +16,14 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class IMException(Exception):
|
||||
from nemubot.response import Response
|
||||
|
||||
class IRCException(Exception):
|
||||
|
||||
def __init__(self, message, personnal=True):
|
||||
super(IMException, self).__init__(message)
|
||||
super(IRCException, self).__init__(message)
|
||||
self.message = message
|
||||
self.personnal = personnal
|
||||
|
||||
|
||||
def fill_response(self, msg):
|
||||
if self.personnal:
|
||||
from nemubot.message import DirectAsk
|
||||
return DirectAsk(msg.frm, *self.args,
|
||||
server=msg.server, to=msg.to_response)
|
||||
|
||||
else:
|
||||
from nemubot.message import Text
|
||||
return Text(*self.args,
|
||||
server=msg.server, to=msg.to_response)
|
||||
return Response(msg.sender, self.message, channel=msg.channel, nick=(msg.nick if self.personnal else None))
|
||||
224
lib/hooks.py
Normal file
224
lib/hooks.py
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2014 nemunaire
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import re
|
||||
|
||||
from nemubot.response import Response
|
||||
from nemubot.exception import IRCException
|
||||
|
||||
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
|
||||
|
||||
try:
|
||||
if self.data is None:
|
||||
if data2 is None:
|
||||
return call(msg)
|
||||
elif isinstance(data2, dict):
|
||||
return call(msg, **data2)
|
||||
else:
|
||||
return call(msg, data2)
|
||||
elif isinstance(self.data, dict):
|
||||
if data2 is None:
|
||||
return call(msg, **self.data)
|
||||
else:
|
||||
return call(msg, data2, **self.data)
|
||||
else:
|
||||
if data2 is None:
|
||||
return call(msg, self.data)
|
||||
elif isinstance(data2, dict):
|
||||
return call(msg, self.data, **data2)
|
||||
else:
|
||||
return call(msg, self.data, data2)
|
||||
except IRCException as e:
|
||||
return e.fill_response(msg)
|
||||
256
lib/importer.py
Normal file
256
lib/importer.py
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2014 nemunaire
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from distutils.version import StrictVersion
|
||||
from importlib.abc import Finder
|
||||
from importlib.abc import SourceLoader
|
||||
import imp
|
||||
import os
|
||||
import sys
|
||||
|
||||
import nemubot
|
||||
from nemubot import event
|
||||
from nemubot import exception
|
||||
from nemubot.hooks import Hook
|
||||
from nemubot import response
|
||||
from nemubot 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_paths:
|
||||
#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 StrictVersion(module.nemubotversion) != StrictVersion(nemubot.__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.ModuleEvent = event.ModuleEvent
|
||||
module.ModuleState = xmlparser.module_state.ModuleState
|
||||
module.Response = response.Response
|
||||
module.IRCException = exception.IRCException
|
||||
|
||||
# Load dependancies
|
||||
if module.CONF is not None and module.CONF.hasNode("dependson"):
|
||||
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_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
lib/message.py
Normal file
294
lib/message.py
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2014 nemunaire
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from datetime import datetime
|
||||
import re
|
||||
import shlex
|
||||
import time
|
||||
|
||||
import nemubot.credits
|
||||
from nemubot.credits import Credits
|
||||
from nemubot.response import Response
|
||||
import nemubot.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
|
||||
240
lib/networkbot.py
Normal file
240
lib/networkbot.py
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2014 nemunaire
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import json
|
||||
import random
|
||||
import shlex
|
||||
import urllib.parse
|
||||
import zlib
|
||||
|
||||
from nemubot.DCC import DCC
|
||||
import nemubot.hooks
|
||||
from nemubot.response import Response
|
||||
|
||||
class NetworkBot:
|
||||
def __init__(self, context, srv, dest, dcc=None):
|
||||
# General informations
|
||||
self.context = context
|
||||
self.srv = srv
|
||||
self.dest = dest
|
||||
|
||||
self.dcc = dcc # DCC connection to the other bot
|
||||
if self.dcc is not None:
|
||||
self.dcc.closing_event = self.closing_event
|
||||
|
||||
self.hooks = list()
|
||||
self.REGISTERED_HOOKS = list()
|
||||
|
||||
# Tags monitor
|
||||
self.my_tag = random.randint(0,255)
|
||||
self.inc_tag = 0
|
||||
self.tags = dict()
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self.dcc.id
|
||||
@property
|
||||
def sender(self):
|
||||
if self.dcc is not None:
|
||||
return self.dcc.sender
|
||||
return None
|
||||
@property
|
||||
def nick(self):
|
||||
if self.dcc is not None:
|
||||
return self.dcc.nick
|
||||
return None
|
||||
@property
|
||||
def realname(self):
|
||||
if self.dcc is not None:
|
||||
return self.dcc.realname
|
||||
return None
|
||||
@property
|
||||
def owner(self):
|
||||
return self.srv.owner
|
||||
|
||||
def isDCC(self, someone):
|
||||
"""Abstract implementation"""
|
||||
return True
|
||||
|
||||
def accepted_channel(self, chan, sender=None):
|
||||
return True
|
||||
|
||||
def send_cmd(self, cmd, data=None):
|
||||
"""Create a tag and send the command"""
|
||||
# First, define a tag
|
||||
self.inc_tag = (self.inc_tag + 1) % 256
|
||||
while self.inc_tag in self.tags:
|
||||
self.inc_tag = (self.inc_tag + 1) % 256
|
||||
tag = ("%c%c" % (self.my_tag, self.inc_tag)).encode()
|
||||
|
||||
self.tags[tag] = (cmd, data)
|
||||
|
||||
# Send the command with the tag
|
||||
self.send_response_final(tag, cmd)
|
||||
|
||||
def send_response(self, res, tag):
|
||||
self.send_response_final(tag, [res.sender, res.channel, res.nick, res.nomore, res.title, res.more, res.count, json.dumps(res.messages)])
|
||||
|
||||
def msg_treated(self, tag):
|
||||
self.send_ack(tag)
|
||||
|
||||
def send_response_final(self, tag, msg):
|
||||
"""Send a response with a tag"""
|
||||
if isinstance(msg, list):
|
||||
cnt = b''
|
||||
for i in msg:
|
||||
if i is None:
|
||||
cnt += b' ""'
|
||||
elif isinstance(i, int):
|
||||
cnt += (' %d' % i).encode()
|
||||
elif isinstance(i, float):
|
||||
cnt += (' %f' % i).encode()
|
||||
else:
|
||||
cnt += b' "' + urllib.parse.quote(i).encode() + b'"'
|
||||
if False and len(cnt) > 10:
|
||||
cnt = b' Z ' + zlib.compress(cnt)
|
||||
print (cnt)
|
||||
self.dcc.send_dcc_raw(tag + cnt)
|
||||
else:
|
||||
for line in msg.split("\n"):
|
||||
self.dcc.send_dcc_raw(tag + b' ' + line.encode())
|
||||
|
||||
def send_ack(self, tag):
|
||||
"""Acknowledge a command"""
|
||||
if tag in self.tags:
|
||||
del self.tags[tag]
|
||||
self.send_response_final(tag, "ACK")
|
||||
|
||||
def connect(self):
|
||||
"""Making the connexion with dest through srv"""
|
||||
if self.dcc is None or not self.dcc.connected:
|
||||
self.dcc = DCC(self.srv, self.dest)
|
||||
self.dcc.closing_event = self.closing_event
|
||||
self.dcc.treatement = self.hello
|
||||
self.dcc.send_dcc("NEMUBOT###")
|
||||
else:
|
||||
self.send_cmd("FETCH")
|
||||
|
||||
def disconnect(self, reason=""):
|
||||
"""Close the connection and remove the bot from network list"""
|
||||
del self.context.network[self.dcc.id]
|
||||
self.dcc.send_dcc("DISCONNECT :%s" % reason)
|
||||
self.dcc.disconnect()
|
||||
|
||||
def hello(self, line):
|
||||
if line == b'NEMUBOT###':
|
||||
self.dcc.treatement = self.treat_msg
|
||||
self.send_cmd("MYTAG %c" % self.my_tag)
|
||||
self.send_cmd("FETCH")
|
||||
elif line != b'Hello ' + self.srv.nick.encode() + b'!':
|
||||
self.disconnect("Sorry, I think you were a bot")
|
||||
|
||||
def treat_msg(self, line, cmd=None):
|
||||
words = line.split(b' ')
|
||||
|
||||
# Ignore invalid commands
|
||||
if len(words) >= 2:
|
||||
tag = words[0]
|
||||
|
||||
# Is it a response?
|
||||
if tag in self.tags:
|
||||
# Is it compressed content?
|
||||
if words[1] == b'Z':
|
||||
#print (line)
|
||||
line = zlib.decompress(line[len(tag) + 3:])
|
||||
self.response(line, tag, [urllib.parse.unquote(arg) for arg in shlex.split(line[len(tag) + 1:].decode())], self.tags[tag])
|
||||
else:
|
||||
cmd = words[1]
|
||||
if len(words) > 2:
|
||||
args = shlex.split(line[len(tag) + len(cmd) + 2:].decode())
|
||||
args = [urllib.parse.unquote(arg) for arg in args]
|
||||
else:
|
||||
args = list()
|
||||
#print ("request:", line)
|
||||
self.request(tag, cmd, args)
|
||||
|
||||
def closing_event(self):
|
||||
for lvl in self.hooks:
|
||||
lvl.clear()
|
||||
|
||||
def response(self, line, tag, args, t):
|
||||
(cmds, data) = t
|
||||
#print ("response for", cmds, ":", args)
|
||||
|
||||
if isinstance(cmds, list):
|
||||
cmd = cmds[0]
|
||||
else:
|
||||
cmd = cmds
|
||||
cmds = list(cmd)
|
||||
|
||||
if args[0] == 'ACK': # Acknowledge a command
|
||||
del self.tags[tag]
|
||||
|
||||
elif cmd == "FETCH" and len(args) >= 5:
|
||||
level = int(args[1])
|
||||
while len(self.hooks) <= level:
|
||||
self.hooks.append(hooks.MessagesHook(self.context, self))
|
||||
|
||||
if args[2] == "": args[2] = None
|
||||
if args[3] == "": args[3] = None
|
||||
if args[4] == "": args[4] = list()
|
||||
else: args[4] = args[4].split(',')
|
||||
|
||||
self.hooks[level].add_hook(args[0], hooks.Hook(self.exec_hook, args[2], None, args[3], args[4]), self)
|
||||
|
||||
elif cmd == "HOOK" and len(args) >= 8:
|
||||
# Rebuild the response
|
||||
if args[1] == '': args[1] = None
|
||||
if args[2] == '': args[2] = None
|
||||
if args[3] == '': args[3] = None
|
||||
if args[4] == '': args[4] = None
|
||||
if args[5] == '': args[5] = None
|
||||
if args[6] == '': args[6] = None
|
||||
res = Response(args[0], channel=args[1], nick=args[2], nomore=args[3], title=args[4], more=args[5], count=args[6])
|
||||
for msg in json.loads(args[7]):
|
||||
res.append_message(msg)
|
||||
if len(res.messages) <= 1:
|
||||
res.alone = True
|
||||
self.srv.send_response(res, None)
|
||||
|
||||
|
||||
def request(self, tag, cmd, args):
|
||||
# Parse
|
||||
if cmd == b'MYTAG' and len(args) > 0: # Inform about choosen tag
|
||||
while args[0] == self.my_tag:
|
||||
self.my_tag = random.randint(0,255)
|
||||
self.send_ack(tag)
|
||||
|
||||
elif cmd == b'FETCH': # Get known commands
|
||||
for name in ["cmd_hook", "ask_hook", "msg_hook"]:
|
||||
elts = self.context.create_cache(name)
|
||||
for elt in elts:
|
||||
(hooks, lvl, store, bot) = elts[elt]
|
||||
for h in hooks:
|
||||
self.send_response_final(tag, [name, lvl, elt, h.regexp, ','.join(h.channels)])
|
||||
self.send_ack(tag)
|
||||
|
||||
elif (cmd == b'HOOK' or cmd == b'"HOOK"') and len(args) > 0: # Action requested
|
||||
self.context.receive_message(self, args[0].encode(), True, tag)
|
||||
|
||||
elif (cmd == b'NOMORE' or cmd == b'"NOMORE"') and len(args) > 0: # Reset !more feature
|
||||
if args[0] in self.srv.moremessages:
|
||||
del self.srv.moremessages[args[0]]
|
||||
|
||||
def exec_hook(self, msg):
|
||||
self.send_cmd(["HOOK", msg.raw])
|
||||
105
lib/prompt/__init__.py
Normal file
105
lib/prompt/__init__.py
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2014 nemunaire
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import imp
|
||||
import os
|
||||
import shlex
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from . import builtins
|
||||
|
||||
class Prompt:
|
||||
def __init__(self, hc=dict(), hl=dict()):
|
||||
self.selectedServer = None
|
||||
|
||||
self.HOOKS_CAPS = hc
|
||||
self.HOOKS_LIST = hl
|
||||
|
||||
def add_cap_hook(self, name, call, data=None):
|
||||
self.HOOKS_CAPS[name] = (lambda d, t, c, p: call(d, t, c, p), data)
|
||||
|
||||
|
||||
def lex_cmd(self, line):
|
||||
"""Return an array of tokens"""
|
||||
ret = list()
|
||||
try:
|
||||
cmds = shlex.split(line)
|
||||
bgn = 0
|
||||
for i in range(0, len(cmds)):
|
||||
if cmds[i] == ';':
|
||||
if i != bgn:
|
||||
cmds[bgn] = cmds[bgn].lower()
|
||||
ret.append(cmds[bgn:i])
|
||||
bgn = i + 1
|
||||
|
||||
if bgn != len(cmds):
|
||||
cmds[bgn] = cmds[bgn].lower()
|
||||
ret.append(cmds[bgn:len(cmds)])
|
||||
|
||||
return ret
|
||||
except:
|
||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||
sys.stderr.write (traceback.format_exception_only(
|
||||
exc_type, exc_value)[0])
|
||||
return ret
|
||||
|
||||
def exec_cmd(self, toks, context):
|
||||
"""Execute the command"""
|
||||
if toks[0] in builtins.CAPS:
|
||||
return builtins.CAPS[toks[0]](toks, context, self)
|
||||
elif toks[0] in self.HOOKS_CAPS:
|
||||
(f,d) = self.HOOKS_CAPS[toks[0]]
|
||||
return f(d, toks, context, self)
|
||||
else:
|
||||
print ("Unknown command: `%s'" % toks[0])
|
||||
return ""
|
||||
|
||||
def getPS1(self):
|
||||
"""Get the PS1 associated to the selected server"""
|
||||
if self.selectedServer is None:
|
||||
return "nemubot"
|
||||
else:
|
||||
return self.selectedServer.id
|
||||
|
||||
def run(self, context):
|
||||
"""Launch the prompt"""
|
||||
ret = ""
|
||||
while ret != "quit" and ret != "reset" and ret != "refresh":
|
||||
sys.stdout.write("\033[0;33m%s§\033[0m " % self.getPS1())
|
||||
sys.stdout.flush()
|
||||
|
||||
try:
|
||||
line = sys.stdin.readline()
|
||||
if len(line) <= 0:
|
||||
line = "quit"
|
||||
print ("quit")
|
||||
cmds = self.lex_cmd(line.strip())
|
||||
for toks in cmds:
|
||||
try:
|
||||
ret = self.exec_cmd(toks, context)
|
||||
except:
|
||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||
traceback.print_exception(exc_type, exc_value, exc_traceback)
|
||||
except KeyboardInterrupt:
|
||||
print ("")
|
||||
return ret != "quit"
|
||||
|
||||
|
||||
def hotswap(prompt):
|
||||
return Prompt(prompt.HOOKS_CAPS, prompt.HOOKS_LIST)
|
||||
159
lib/prompt/builtins.py
Normal file
159
lib/prompt/builtins.py
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2014 nemunaire
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
|
||||
from nemubot import xmlparser
|
||||
|
||||
def end(toks, context, prompt):
|
||||
"""Quit the prompt for reload or exit"""
|
||||
if toks[0] == "refresh":
|
||||
return "refresh"
|
||||
elif toks[0] == "reset":
|
||||
return "reset"
|
||||
else:
|
||||
context.quit()
|
||||
return "quit"
|
||||
|
||||
|
||||
def liste(toks, context, prompt):
|
||||
"""Show some lists"""
|
||||
if len(toks) > 1:
|
||||
for l in toks[1:]:
|
||||
l = l.lower()
|
||||
if l == "server" or l == "servers":
|
||||
for srv in context.servers.keys():
|
||||
print (" - %s ;" % srv)
|
||||
else:
|
||||
print (" > No server loaded")
|
||||
elif l == "mod" or l == "mods" or l == "module" or l == "modules":
|
||||
for mod in context.modules.keys():
|
||||
print (" - %s ;" % mod)
|
||||
else:
|
||||
print (" > No module loaded")
|
||||
elif l in prompt.HOOKS_LIST:
|
||||
(f,d) = prompt.HOOKS_LIST[l]
|
||||
f(d, context, prompt)
|
||||
else:
|
||||
print (" Unknown list `%s'" % l)
|
||||
else:
|
||||
print (" Please give a list to show: servers, ...")
|
||||
|
||||
|
||||
def load_file(filename, context):
|
||||
if os.path.isfile(filename):
|
||||
config = xmlparser.parse_file(filename)
|
||||
|
||||
# This is a true nemubot configuration file, load it!
|
||||
if (config.getName() == "nemubotconfig"
|
||||
or config.getName() == "config"):
|
||||
# Preset each server in this file
|
||||
for server in config.getNodes("server"):
|
||||
if context.addServer(server, config["nick"],
|
||||
config["owner"], config["realname"],
|
||||
server.hasAttribute("ssl")):
|
||||
print (" Server `%s:%s' successfully added."
|
||||
% (server["server"], server["port"]))
|
||||
else:
|
||||
print (" Server `%s:%s' already added, skiped."
|
||||
% (server["server"], server["port"]))
|
||||
|
||||
# Load files asked by the configuration file
|
||||
for load in config.getNodes("load"):
|
||||
load_file(load["path"], context)
|
||||
|
||||
# This is a nemubot module configuration file, load the module
|
||||
elif config.getName() == "nemubotmodule":
|
||||
__import__(config["name"])
|
||||
|
||||
# Other formats
|
||||
else:
|
||||
print (" Can't load `%s'; this is not a valid nemubot "
|
||||
"configuration file." % filename)
|
||||
|
||||
# Unexisting file, assume a name was passed, import the module!
|
||||
else:
|
||||
__import__(filename)
|
||||
|
||||
|
||||
def load(toks, context, prompt):
|
||||
"""Load an XML configuration file"""
|
||||
if len(toks) > 1:
|
||||
for filename in toks[1:]:
|
||||
load_file(filename, context)
|
||||
else:
|
||||
print ("Not enough arguments. `load' takes a filename.")
|
||||
return
|
||||
|
||||
|
||||
def select(toks, context, prompt):
|
||||
"""Select the current server"""
|
||||
if (len(toks) == 2 and toks[1] != "None"
|
||||
and toks[1] != "nemubot" and toks[1] != "none"):
|
||||
if toks[1] in context.servers:
|
||||
prompt.selectedServer = context.servers[toks[1]]
|
||||
else:
|
||||
print ("select: server `%s' not found." % toks[1])
|
||||
else:
|
||||
prompt.selectedServer = None
|
||||
return
|
||||
|
||||
|
||||
def unload(toks, context, prompt):
|
||||
"""Unload a module"""
|
||||
if len(toks) == 2 and toks[1] == "all":
|
||||
for name in context.modules.keys():
|
||||
context.unload_module(name)
|
||||
elif len(toks) > 1:
|
||||
for name in toks[1:]:
|
||||
if context.unload_module(name):
|
||||
print (" Module `%s' successfully unloaded." % name)
|
||||
else:
|
||||
print (" No module `%s' loaded, can't unload!" % name)
|
||||
else:
|
||||
print ("Not enough arguments. `unload' takes a module name.")
|
||||
|
||||
|
||||
def debug(toks, context, prompt):
|
||||
"""Enable/Disable debug mode on a module"""
|
||||
if len(toks) > 1:
|
||||
for name in toks[1:]:
|
||||
if name in context.modules:
|
||||
context.modules[name].DEBUG = not context.modules[name].DEBUG
|
||||
if context.modules[name].DEBUG:
|
||||
print (" Module `%s' now in DEBUG mode." % name)
|
||||
else:
|
||||
print (" Debug for module module `%s' disabled." % name)
|
||||
else:
|
||||
print (" No module `%s' loaded, can't debug!" % name)
|
||||
else:
|
||||
print ("Not enough arguments. `debug' takes a module name.")
|
||||
|
||||
|
||||
#Register build-ins
|
||||
CAPS = {
|
||||
'quit': end, #Disconnect all server and quit
|
||||
'exit': end, #Alias for quit
|
||||
'reset': end, #Reload the prompt
|
||||
'refresh': end, #Reload the prompt but save modules
|
||||
'load': load, #Load a servers or module configuration file
|
||||
'unload': unload, #Unload a module and remove it from the list
|
||||
'select': select, #Select a server
|
||||
'list': liste, #Show lists
|
||||
'debug': debug, #Pass a module in debug mode
|
||||
}
|
||||
176
lib/response.py
Normal file
176
lib/response.py
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2014 nemunaire
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import traceback
|
||||
import sys
|
||||
|
||||
class Response:
|
||||
def __init__(self, sender, message=None, channel=None, nick=None, server=None,
|
||||
nomore="No more message", title=None, more="(suite) ", count=None,
|
||||
ctcp=False, shown_first_count=-1):
|
||||
self.nomore = nomore
|
||||
self.more = more
|
||||
self.rawtitle = title
|
||||
self.server = server
|
||||
self.messages = list()
|
||||
self.alone = True
|
||||
self.ctcp = ctcp
|
||||
if message is not None:
|
||||
self.append_message(message, shown_first_count=shown_first_count)
|
||||
self.elt = 0 # Next element to display
|
||||
|
||||
self.channel = channel
|
||||
self.nick = nick
|
||||
self.set_sender(sender)
|
||||
self.count = count
|
||||
|
||||
@property
|
||||
def content(self):
|
||||
#FIXME: error when messages in self.messages are list!
|
||||
try:
|
||||
if self.title is not None:
|
||||
return self.title + ", ".join(self.messages)
|
||||
else:
|
||||
return ", ".join(self.messages)
|
||||
except:
|
||||
return ""
|
||||
|
||||
def set_sender(self, sender):
|
||||
if sender is None or sender.find("!") < 0:
|
||||
if sender is not None:
|
||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||
traceback.print_exception(exc_type, "\033[1;35mWarning:\033[0m bad sender provided in Response, it will be ignored.", exc_traceback)
|
||||
self.sender = None
|
||||
else:
|
||||
self.sender = sender
|
||||
|
||||
def append_message(self, message, title=None, shown_first_count=-1):
|
||||
if message is not None and len(message) > 0:
|
||||
if shown_first_count >= 0:
|
||||
self.messages.append(message[:shown_first_count])
|
||||
message = message[shown_first_count:]
|
||||
self.messages.append(message)
|
||||
self.alone = self.alone and len(self.messages) <= 1
|
||||
if isinstance(self.rawtitle, list):
|
||||
self.rawtitle.append(title)
|
||||
elif title is not None:
|
||||
rawtitle = self.rawtitle
|
||||
self.rawtitle = list()
|
||||
for osef in self.messages:
|
||||
self.rawtitle.append(rawtitle)
|
||||
self.rawtitle.pop()
|
||||
self.rawtitle.append(title)
|
||||
|
||||
def append_content(self, message):
|
||||
if message is not None and len(message) > 0:
|
||||
if self.messages is None or len(self.messages) == 0:
|
||||
self.messages = list(message)
|
||||
self.alone = True
|
||||
else:
|
||||
self.messages[len(self.messages)-1] += message
|
||||
self.alone = self.alone and len(self.messages) <= 1
|
||||
|
||||
@property
|
||||
def empty(self):
|
||||
return len(self.messages) <= 0
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
if isinstance(self.rawtitle, list):
|
||||
return self.rawtitle[0]
|
||||
else:
|
||||
return self.rawtitle
|
||||
|
||||
def pop(self):
|
||||
self.messages.pop(0)
|
||||
if isinstance(self.rawtitle, list):
|
||||
self.rawtitle.pop(0)
|
||||
if len(self.rawtitle) <= 0:
|
||||
self.rawtitle = None
|
||||
|
||||
def get_message(self):
|
||||
if self.alone and len(self.messages) > 1:
|
||||
self.alone = False
|
||||
|
||||
if self.empty:
|
||||
return self.nomore
|
||||
|
||||
msg = ""
|
||||
if self.channel is not None and self.nick is not None:
|
||||
msg += self.nick + ": "
|
||||
|
||||
if self.title is not None:
|
||||
if self.elt > 0:
|
||||
msg += self.title + " " + self.more + ": "
|
||||
else:
|
||||
msg += self.title + ": "
|
||||
|
||||
if self.elt > 0:
|
||||
msg += "[…] "
|
||||
|
||||
elts = self.messages[0][self.elt:]
|
||||
if isinstance(elts, list):
|
||||
for e in elts:
|
||||
if len(msg) + len(e) > 430:
|
||||
msg += "[…]"
|
||||
self.alone = False
|
||||
return msg
|
||||
else:
|
||||
msg += e + ", "
|
||||
self.elt += 1
|
||||
self.pop()
|
||||
self.elt = 0
|
||||
return msg[:len(msg)-2]
|
||||
|
||||
else:
|
||||
if len(elts) <= 432:
|
||||
self.pop()
|
||||
self.elt = 0
|
||||
if self.count is not None:
|
||||
return msg + elts + (self.count % len(self.messages))
|
||||
else:
|
||||
return msg + elts
|
||||
|
||||
else:
|
||||
words = elts.split(' ')
|
||||
|
||||
if len(words[0]) > 432 - len(msg):
|
||||
self.elt += 432 - len(msg)
|
||||
return msg + elts[:self.elt] + "[…]"
|
||||
|
||||
for w in words:
|
||||
if len(msg) + len(w) > 431:
|
||||
msg += "[…]"
|
||||
self.alone = False
|
||||
return msg
|
||||
else:
|
||||
msg += w + " "
|
||||
self.elt += len(w) + 1
|
||||
self.pop()
|
||||
self.elt = 0
|
||||
return msg
|
||||
|
||||
import nemubot.hooks
|
||||
class Hook:
|
||||
def __init__(self, TYPE, call, name=None, data=None, regexp=None,
|
||||
channels=list(), server=None, end=None, call_end=None,
|
||||
SRC=None):
|
||||
self.hook = hooks.Hook(call, name, data, regexp, channels,
|
||||
server, end, call_end)
|
||||
self.type = TYPE
|
||||
self.src = SRC
|
||||
169
lib/server.py
Normal file
169
lib/server.py
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2014 nemunaire
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import socket
|
||||
import threading
|
||||
|
||||
class Server(threading.Thread):
|
||||
def __init__(self, socket = None):
|
||||
self.stop = False
|
||||
self.stopping = threading.Event()
|
||||
self.s = socket
|
||||
self.connected = self.s is not None
|
||||
self.closing_event = None
|
||||
|
||||
self.moremessages = dict()
|
||||
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
def isDCC(self, to=None):
|
||||
return to is not None and to in self.dcc_clients
|
||||
|
||||
@property
|
||||
def ip(self):
|
||||
"""Convert common IP representation to little-endian integer representation"""
|
||||
sum = 0
|
||||
if self.node.hasAttribute("ip"):
|
||||
ip = self.node["ip"]
|
||||
else:
|
||||
#TODO: find the external IP
|
||||
ip = "0.0.0.0"
|
||||
for b in ip.split("."):
|
||||
sum = 256 * sum + int(b)
|
||||
return sum
|
||||
|
||||
def toIP(self, input):
|
||||
"""Convert little-endian int to IPv4 adress"""
|
||||
ip = ""
|
||||
for i in range(0,4):
|
||||
mod = input % 256
|
||||
ip = "%d.%s" % (mod, ip)
|
||||
input = (input - mod) / 256
|
||||
return ip[:len(ip) - 1]
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
"""Gives the server identifiant"""
|
||||
raise NotImplemented()
|
||||
|
||||
def accepted_channel(self, msg, sender=None):
|
||||
return True
|
||||
|
||||
def msg_treated(self, origin):
|
||||
"""Action done on server when a message was treated"""
|
||||
raise NotImplemented()
|
||||
|
||||
def send_response(self, res, origin):
|
||||
"""Analyse a Response and send it"""
|
||||
# TODO: how to send a CTCP message to a different person
|
||||
if res.ctcp:
|
||||
self.send_ctcp(res.sender, res.get_message())
|
||||
|
||||
elif res.channel is not None and res.channel != self.nick:
|
||||
self.send_msg(res.channel, res.get_message())
|
||||
|
||||
if not res.alone:
|
||||
if hasattr(self, "send_bot"):
|
||||
self.send_bot("NOMORE %s" % res.channel)
|
||||
self.moremessages[res.channel] = res
|
||||
elif res.sender is not None:
|
||||
self.send_msg_usr(res.sender, res.get_message())
|
||||
|
||||
if not res.alone:
|
||||
self.moremessages[res.sender] = res
|
||||
|
||||
def send_ctcp(self, to, msg, cmd="NOTICE", endl="\r\n"):
|
||||
"""Send a message as CTCP response"""
|
||||
if msg is not None and to is not None:
|
||||
for line in msg.split("\n"):
|
||||
if line != "":
|
||||
self.send_msg_final(to.split("!")[0], "\x01" + line + "\x01", cmd, endl)
|
||||
|
||||
def send_dcc(self, msg, to):
|
||||
"""Send a message through DCC connection"""
|
||||
raise NotImplemented()
|
||||
|
||||
def send_msg_final(self, channel, msg, cmd="PRIVMSG", endl="\r\n"):
|
||||
"""Send a message without checks or format"""
|
||||
raise NotImplemented()
|
||||
|
||||
def send_msg_usr(self, user, msg):
|
||||
"""Send a message to a user instead of a channel"""
|
||||
raise NotImplemented()
|
||||
|
||||
def send_msg(self, channel, msg, cmd="PRIVMSG", endl="\r\n"):
|
||||
"""Send a message to a channel"""
|
||||
if msg is not None:
|
||||
for line in msg.split("\n"):
|
||||
if line != "":
|
||||
self.send_msg_final(channel, line, cmd, endl)
|
||||
|
||||
def send_msg_verified(self, sender, channel, msg, cmd="PRIVMSG", endl="\r\n"):
|
||||
"""A more secure way to send messages"""
|
||||
raise NotImplemented()
|
||||
|
||||
def send_global(self, msg, cmd="PRIVMSG", endl="\r\n"):
|
||||
"""Send a message to all channels on this server"""
|
||||
raise NotImplemented()
|
||||
|
||||
def disconnect(self):
|
||||
"""Close the socket with the server"""
|
||||
if self.connected:
|
||||
self.stop = True
|
||||
try:
|
||||
self.s.shutdown(socket.SHUT_RDWR)
|
||||
except socket.error:
|
||||
pass
|
||||
|
||||
self.stopping.wait()
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def kill(self):
|
||||
"""Just stop the main loop, don't close the socket directly"""
|
||||
if self.connected:
|
||||
self.stop = True
|
||||
self.connected = False
|
||||
#Send a message in order to close the socket
|
||||
try:
|
||||
self.s.send(("Bye!\r\n" % self.nick).encode ())
|
||||
except:
|
||||
pass
|
||||
self.stopping.wait()
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def launch(self, receive_action, verb=True):
|
||||
"""Connect to the server if it is no yet connected"""
|
||||
self._receive_action = receive_action
|
||||
if not self.connected:
|
||||
self.stop = False
|
||||
try:
|
||||
self.start()
|
||||
except RuntimeError:
|
||||
pass
|
||||
elif verb:
|
||||
print (" Already connected.")
|
||||
|
||||
def treat_msg(self, line, private=False):
|
||||
self._receive_action(self, line, private)
|
||||
|
||||
def run(self):
|
||||
raise NotImplemented()
|
||||
0
lib/tools/__init__.py
Normal file
0
lib/tools/__init__.py
Normal file
148
lib/tools/web.py
Normal file
148
lib/tools/web.py
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
# coding=utf-8
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2014 nemunaire
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from html.entities import name2codepoint
|
||||
import http.client
|
||||
import json
|
||||
import re
|
||||
import socket
|
||||
from urllib.parse import quote
|
||||
from urllib.parse import urlparse
|
||||
from urllib.request import urlopen
|
||||
|
||||
import xmlparser
|
||||
|
||||
def isURL(url):
|
||||
"""Return True if the URL can be parsed"""
|
||||
o = urlparse(url)
|
||||
return o.scheme == "" and o.netloc == "" and o.path == ""
|
||||
|
||||
def getScheme(url):
|
||||
"""Return the protocol of a given URL"""
|
||||
o = urlparse(url)
|
||||
return o.scheme
|
||||
|
||||
def getHost(url):
|
||||
"""Return the domain of a given URL"""
|
||||
return urlparse(url).netloc
|
||||
|
||||
def getPort(url):
|
||||
"""Return the port of a given URL"""
|
||||
return urlparse(url).port
|
||||
|
||||
def getPath(url):
|
||||
"""Return the page request of a given URL"""
|
||||
return urlparse(url).path
|
||||
|
||||
def getUser(url):
|
||||
"""Return the page request of a given URL"""
|
||||
return urlparse(url).username
|
||||
def getPassword(url):
|
||||
"""Return the page request of a given URL"""
|
||||
return urlparse(url).password
|
||||
|
||||
|
||||
# Get real pages
|
||||
|
||||
def getURLContent(url, timeout=15):
|
||||
"""Return page content corresponding to URL or None if any error occurs"""
|
||||
o = urlparse(url)
|
||||
if o.netloc == "":
|
||||
o = urlparse("http://" + url)
|
||||
|
||||
if o.scheme == "http":
|
||||
conn = http.client.HTTPConnection(o.netloc, port=o.port, timeout=timeout)
|
||||
elif o.scheme == "https":
|
||||
conn = http.client.HTTPSConnection(o.netloc, port=o.port, timeout=timeout)
|
||||
elif o.scheme is None or o.scheme == "":
|
||||
conn = http.client.HTTPConnection(o.netloc, port=80, timeout=timeout)
|
||||
else:
|
||||
return None
|
||||
try:
|
||||
if o.query != '':
|
||||
conn.request("GET", o.path + "?" + o.query, None, {"User-agent": "Nemubot v3"})
|
||||
else:
|
||||
conn.request("GET", o.path, None, {"User-agent": "Nemubot v3"})
|
||||
except socket.timeout:
|
||||
return None
|
||||
except socket.gaierror:
|
||||
print ("<tools.web> Unable to receive page %s on %s from %s."
|
||||
% (o.path, o.netloc, url))
|
||||
return None
|
||||
|
||||
try:
|
||||
res = conn.getresponse()
|
||||
size = int(res.getheader("Content-Length", 200000))
|
||||
cntype = res.getheader("Content-Type")
|
||||
|
||||
if size > 200000 or (cntype[:4] != "text" and cntype[:4] != "appl"):
|
||||
return None
|
||||
|
||||
data = res.read(size)
|
||||
|
||||
# Decode content
|
||||
charset = "utf-8"
|
||||
lcharset = res.getheader("Content-Type").split(";")
|
||||
if len(lcharset) > 1:
|
||||
for c in charset:
|
||||
ch = c.split("=")
|
||||
if ch[0].strip().lower() == "charset" and len(ch) > 1:
|
||||
cha = ch[1].split(".")
|
||||
if len(cha) > 1:
|
||||
charset = cha[1]
|
||||
else:
|
||||
charset = cha[0]
|
||||
except http.client.BadStatusLine:
|
||||
return None
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
if res.status == http.client.OK or res.status == http.client.SEE_OTHER:
|
||||
return data.decode(charset)
|
||||
elif res.status == http.client.FOUND or res.status == http.client.MOVED_PERMANENTLY:
|
||||
return getURLContent(res.getheader("Location"), timeout)
|
||||
else:
|
||||
return None
|
||||
|
||||
def getXML(url, timeout=15):
|
||||
"""Get content page and return XML parsed content"""
|
||||
cnt = getURLContent(url, timeout)
|
||||
if cnt is None:
|
||||
return None
|
||||
else:
|
||||
return xmlparser.parse_string(cnt)
|
||||
|
||||
def getJSON(url, timeout=15):
|
||||
"""Get content page and return JSON content"""
|
||||
cnt = getURLContent(url, timeout)
|
||||
if cnt is None:
|
||||
return None
|
||||
else:
|
||||
return json.loads(cnt.decode())
|
||||
|
||||
# Other utils
|
||||
|
||||
def htmlentitydecode(s):
|
||||
"""Decode htmlentities"""
|
||||
return re.sub('&(%s);' % '|'.join(name2codepoint),
|
||||
lambda m: chr(name2codepoint[m.group(1)]), s)
|
||||
|
||||
def striphtml(data):
|
||||
"""Remove HTML tags from text"""
|
||||
p = re.compile(r'<.*?>')
|
||||
return htmlentitydecode(p.sub('', data).replace("(", "/(").replace(")", ")/").replace(""", "\""))
|
||||
66
lib/tools/wrapper.py
Normal file
66
lib/tools/wrapper.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# coding=utf-8
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2014 nemunaire
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from xmlparser.node import ModuleState
|
||||
|
||||
class Wrapper:
|
||||
"""Simulate a hash table
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.stateName = "state"
|
||||
self.attName = "name"
|
||||
self.cache = dict()
|
||||
|
||||
def items(self):
|
||||
ret = list()
|
||||
for k in self.DATAS.index.keys():
|
||||
ret.append((k, self[k]))
|
||||
return ret
|
||||
|
||||
def __contains__(self, i):
|
||||
return i in self.DATAS.index
|
||||
|
||||
def __getitem__(self, i):
|
||||
return self.DATAS.index[i]
|
||||
|
||||
def __setitem__(self, i, j):
|
||||
ms = ModuleState(self.stateName)
|
||||
ms.setAttribute(self.attName, i)
|
||||
j.save(ms)
|
||||
self.DATAS.addChild(ms)
|
||||
self.DATAS.setIndex(self.attName, self.stateName)
|
||||
|
||||
def __delitem__(self, i):
|
||||
self.DATAS.delChild(self.DATAS.index[i])
|
||||
|
||||
def save(self, i):
|
||||
if i in self.cache:
|
||||
self.cache[i].save(self.DATAS.index[i])
|
||||
del self.cache[i]
|
||||
|
||||
def flush(self):
|
||||
"""Remove all cached datas"""
|
||||
self.cache = dict()
|
||||
|
||||
def reset(self):
|
||||
"""Erase the list and flush the cache"""
|
||||
for child in self.DATAS.getNodes(self.stateName):
|
||||
self.DATAS.delChild(child)
|
||||
self.flush()
|
||||
74
lib/xmlparser/__init__.py
Normal file
74
lib/xmlparser/__init__.py
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Nemubot is a smart and modulable IM bot.
|
||||
# Copyright (C) 2012-2014 nemunaire
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import imp
|
||||
import xml.sax
|
||||
|
||||
from . import node as module_state
|
||||
|
||||
class ModuleStatesFile(xml.sax.ContentHandler):
|
||||
def startDocument(self):
|
||||
self.root = None
|
||||
self.stack = list()
|
||||
|
||||
def startElement(self, name, attrs):
|
||||
cur = module_state.ModuleState(name)
|
||||
|
||||
for name in attrs.keys():
|
||||
cur.setAttribute(name, attrs.getValue(name))
|
||||
|
||||
self.stack.append(cur)
|
||||
|
||||
def characters(self, content):
|
||||
self.stack[len(self.stack)-1].content += content
|
||||
|
||||
def endElement(self, name):
|
||||
child = self.stack.pop()
|
||||
size = len(self.stack)
|
||||
if size > 0:
|
||||
self.stack[size - 1].content = self.stack[size - 1].content.strip()
|
||||
self.stack[size - 1].addChild(child)
|
||||
else:
|
||||
self.root = child
|
||||
|
||||
def parse_file(filename):
|
||||
parser = xml.sax.make_parser()
|
||||
mod = ModuleStatesFile()
|
||||
parser.setContentHandler(mod)
|
||||
try:
|
||||
parser.parse(open(filename, "r"))
|
||||
return mod.root
|
||||
except IOError:
|
||||
return module_state.ModuleState("nemubotstate")
|
||||
except:
|
||||
if mod.root is None:
|
||||
return module_state.ModuleState("nemubotstate")
|
||||
else:
|
||||
return mod.root
|
||||
|
||||
def parse_string(string):
|
||||
mod = ModuleStatesFile()
|
||||
try:
|
||||
xml.sax.parseString(string, mod)
|
||||
return mod.root
|
||||
except:
|
||||
if mod.root is None:
|
||||
return module_state.ModuleState("nemubotstate")
|
||||
else:
|
||||
return mod.root
|
||||
203
lib/xmlparser/node.py
Normal file
203
lib/xmlparser/node.py
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
# coding=utf-8
|
||||
|
||||
import xml.sax
|
||||
from datetime import datetime
|
||||
from datetime import date
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
|
||||
class ModuleState:
|
||||
"""Tiny tree representation of an XML file"""
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.content = ""
|
||||
self.attributes = dict()
|
||||
self.childs = list()
|
||||
self.index = dict()
|
||||
self.index_fieldname = None
|
||||
self.index_tagname = None
|
||||
|
||||
def getName(self):
|
||||
"""Get the name of the current node"""
|
||||
return self.name
|
||||
|
||||
def display(self, level = 0):
|
||||
ret = ""
|
||||
out = list()
|
||||
for k in self.attributes:
|
||||
out.append("%s : %s" % (k, self.attributes[k]))
|
||||
ret += "%s%s { %s } = '%s'\n" % (' ' * level, self.name, ' ; '.join(out), self.content)
|
||||
for c in self.childs:
|
||||
ret += c.display(level + 2)
|
||||
return ret
|
||||
|
||||
def __str__(self):
|
||||
return self.display()
|
||||
|
||||
def __getitem__(self, i):
|
||||
"""Return the attribute asked"""
|
||||
return self.getAttribute(i)
|
||||
|
||||
def __setitem__(self, i, c):
|
||||
"""Set the attribute"""
|
||||
return self.setAttribute(i, c)
|
||||
|
||||
def getAttribute(self, name):
|
||||
"""Get the asked argument or return None if doesn't exist"""
|
||||
if name in self.attributes:
|
||||
return self.attributes[name]
|
||||
else:
|
||||
return None
|
||||
|
||||
def getDate(self, name=None):
|
||||
"""Get the asked argument and return it as a date"""
|
||||
if name is None:
|
||||
source = self.content
|
||||
elif name in self.attributes.keys():
|
||||
source = self.attributes[name]
|
||||
else:
|
||||
return None
|
||||
|
||||
if isinstance(source, datetime):
|
||||
return source
|
||||
else:
|
||||
try:
|
||||
return datetime.fromtimestamp(float(source))
|
||||
except ValueError:
|
||||
while True:
|
||||
try:
|
||||
return datetime.fromtimestamp(time.mktime(
|
||||
time.strptime(source[:19], "%Y-%m-%d %H:%M:%S")))
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
def getInt(self, name=None):
|
||||
"""Get the asked argument and return it as an integer"""
|
||||
if name is None:
|
||||
source = self.content
|
||||
elif name in self.attributes.keys():
|
||||
source = self.attributes[name]
|
||||
else:
|
||||
return None
|
||||
|
||||
return int(float(source))
|
||||
|
||||
def getBool(self, name=None):
|
||||
"""Get the asked argument and return it as an integer"""
|
||||
if name is None:
|
||||
source = self.content
|
||||
elif name in self.attributes.keys():
|
||||
source = self.attributes[name]
|
||||
else:
|
||||
return False
|
||||
|
||||
return (isinstance(source, bool) and source) or source == "True"
|
||||
|
||||
def tmpIndex(self, fieldname="name", tagname=None):
|
||||
index = dict()
|
||||
for child in self.childs:
|
||||
if (tagname is None or tagname == child.name) and child.hasAttribute(fieldname):
|
||||
index[child[fieldname]] = child
|
||||
return index
|
||||
|
||||
def setIndex(self, fieldname="name", tagname=None):
|
||||
"""Defines an hash table to accelerate childs search. You have just to define a common attribute"""
|
||||
self.index = self.tmpIndex(fieldname, tagname)
|
||||
self.index_fieldname = fieldname
|
||||
self.index_tagname = tagname
|
||||
|
||||
def __contains__(self, i):
|
||||
"""Return true if i is found in the index"""
|
||||
return i in self.index
|
||||
|
||||
def hasAttribute(self, name):
|
||||
"""DOM like method"""
|
||||
return (name in self.attributes)
|
||||
|
||||
def setAttribute(self, name, value):
|
||||
"""DOM like method"""
|
||||
self.attributes[name] = value
|
||||
|
||||
def getContent(self):
|
||||
return self.content
|
||||
|
||||
def getChilds(self):
|
||||
"""Return a full list of direct child of this node"""
|
||||
return self.childs
|
||||
|
||||
def getNode(self, tagname):
|
||||
"""Get a unique node (or the last one) with the given tagname"""
|
||||
ret = None
|
||||
for child in self.childs:
|
||||
if tagname is None or tagname == child.name:
|
||||
ret = child
|
||||
return ret
|
||||
|
||||
def getFirstNode(self, tagname):
|
||||
"""Get a unique node (or the last one) with the given tagname"""
|
||||
for child in self.childs:
|
||||
if tagname is None or tagname == child.name:
|
||||
return child
|
||||
return None
|
||||
|
||||
def getNodes(self, tagname):
|
||||
"""Get all direct childs that have the given tagname"""
|
||||
ret = list()
|
||||
for child in self.childs:
|
||||
if tagname is None or tagname == child.name:
|
||||
ret.append(child)
|
||||
return ret
|
||||
|
||||
def hasNode(self, tagname):
|
||||
"""Return True if at least one node with the given tagname exists"""
|
||||
ret = list()
|
||||
for child in self.childs:
|
||||
if tagname is None or tagname == child.name:
|
||||
return True
|
||||
return False
|
||||
|
||||
def addChild(self, child):
|
||||
"""Add a child to this node"""
|
||||
self.childs.append(child)
|
||||
if self.index_fieldname is not None:
|
||||
self.setIndex(self.index_fieldname, self.index_tagname)
|
||||
|
||||
def delChild(self, child):
|
||||
"""Remove the given child from this node"""
|
||||
self.childs.remove(child)
|
||||
if self.index_fieldname is not None:
|
||||
self.setIndex(self.index_fieldname, self.index_tagname)
|
||||
|
||||
def save_node(self, gen):
|
||||
"""Serialize this node as a XML node"""
|
||||
attribs = {}
|
||||
for att in self.attributes.keys():
|
||||
if att[0] != "_": # Don't save attribute starting by _
|
||||
if isinstance(self.attributes[att], datetime):
|
||||
attribs[att] = str(time.mktime(self.attributes[att].timetuple()))
|
||||
else:
|
||||
attribs[att] = str(self.attributes[att])
|
||||
attrs = xml.sax.xmlreader.AttributesImpl(attribs)
|
||||
|
||||
try:
|
||||
gen.startElement(self.name, attrs)
|
||||
|
||||
for child in self.childs:
|
||||
child.save_node(gen)
|
||||
|
||||
gen.endElement(self.name)
|
||||
except:
|
||||
print ("\033[1;31mERROR:\033[0m occurred when saving the "
|
||||
"following XML node: %s with %s" % (self.name, attrs))
|
||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||
traceback.print_exception(exc_type, exc_value, exc_traceback)
|
||||
|
||||
def save(self, filename):
|
||||
"""Save the current node as root node in a XML file"""
|
||||
with open(filename,"w") as f:
|
||||
gen = xml.sax.saxutils.XMLGenerator(f, "utf-8")
|
||||
gen.startDocument()
|
||||
self.save_node(gen)
|
||||
gen.endDocument()
|
||||
398
modules/alias.py
398
modules/alias.py
|
|
@ -1,277 +1,185 @@
|
|||
"""Create alias of commands"""
|
||||
|
||||
# PYTHON STUFFS #######################################################
|
||||
# coding=utf-8
|
||||
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
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 #############################################################
|
||||
nemubotversion = 3.3
|
||||
|
||||
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")
|
||||
from hooks import Hook
|
||||
add_hook("cmd_hook", Hook(cmd_listalias, "listalias"))
|
||||
add_hook("cmd_hook", Hook(cmd_listvars, "listvars"))
|
||||
add_hook("cmd_hook", Hook(cmd_unalias, "unalias"))
|
||||
add_hook("cmd_hook", Hook(cmd_alias, "alias"))
|
||||
add_hook("cmd_hook", Hook(cmd_set, "set"))
|
||||
add_hook("all_pre", Hook(treat_alias))
|
||||
add_hook("all_post", Hook(treat_variables))
|
||||
|
||||
global DATAS
|
||||
if not DATAS.hasNode("aliases"):
|
||||
DATAS.addChild(ModuleState("aliases"))
|
||||
DATAS.getNode("aliases").setIndex("alias")
|
||||
if not DATAS.hasNode("variables"):
|
||||
DATAS.addChild(ModuleState("variables"))
|
||||
DATAS.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 help_tiny ():
|
||||
"""Line inserted in the response to the command !help"""
|
||||
return "alias module"
|
||||
|
||||
def help_full ():
|
||||
return "TODO"
|
||||
|
||||
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()
|
||||
DATAS.getNode("variables").addChild(var)
|
||||
|
||||
|
||||
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)
|
||||
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:
|
||||
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)
|
||||
return ""
|
||||
|
||||
# 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)
|
||||
if len (msg.cmds) > 2:
|
||||
set_variable(msg.cmds[1], " ".join(msg.cmds[2:]), msg.nick)
|
||||
res = Response(msg.sender, "Variable \$%s définie." % msg.cmds[1])
|
||||
save()
|
||||
return res
|
||||
return Response(msg.sender, "!set prend au minimum deux arguments : le nom de la variable et sa valeur.")
|
||||
|
||||
|
||||
## 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?")
|
||||
if len(msg.cmds) > 1:
|
||||
res = list()
|
||||
for alias in msg.args:
|
||||
if alias[0] == "!" and len(alias) > 1:
|
||||
for user in msg.cmds[1:]:
|
||||
als = [x["alias"] for x in DATAS.getNode("aliases").index.values() if x["creator"] == user]
|
||||
if len(als) > 0:
|
||||
res.append("Alias créés par %s : %s" % (user, ", ".join(als)))
|
||||
else:
|
||||
res.append("%s n'a pas encore créé d'alias" % user)
|
||||
return Response(msg.sender, " ; ".join(res), channel=msg.channel)
|
||||
else:
|
||||
return Response(msg.sender, "Alias connus : %s." % ", ".join(DATAS.getNode("aliases").index.keys()), channel=msg.channel)
|
||||
|
||||
def cmd_listvars(msg):
|
||||
if len(msg.cmds) > 1:
|
||||
res = list()
|
||||
for user in msg.cmds[1:]:
|
||||
als = [x["alias"] for x in DATAS.getNode("variables").index.values() if x["creator"] == user]
|
||||
if len(als) > 0:
|
||||
res.append("Variables créées par %s : %s" % (user, ", ".join(als)))
|
||||
else:
|
||||
res.append("%s n'a pas encore créé de variable" % user)
|
||||
return Response(msg.sender, " ; ".join(res), channel=msg.channel)
|
||||
else:
|
||||
return Response(msg.sender, "Variables connues : %s." % ", ".join(DATAS.getNode("variables").index.keys()), channel=msg.channel)
|
||||
|
||||
def cmd_alias(msg):
|
||||
if len (msg.cmds) > 1:
|
||||
res = list()
|
||||
for alias in msg.cmds[1:]:
|
||||
if alias[0] == "!":
|
||||
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,
|
||||
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("%s is not an alias" % alias,
|
||||
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)
|
||||
|
||||
|
||||
## Alias replacement
|
||||
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
|
||||
|
||||
@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 '!'?).")
|
||||
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)
|
||||
|
||||
# Avoid infinite recursion
|
||||
if not isinstance(rpl_msg, Command) or msg.cmd != rpl_msg.cmd:
|
||||
return rpl_msg
|
||||
msg.content = replace_variables(msg.content, msg)
|
||||
|
||||
return 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
|
||||
|
|
|
|||
|
|
@ -1,68 +1,56 @@
|
|||
"""People birthdays and ages"""
|
||||
|
||||
# PYTHON STUFFS #######################################################
|
||||
# coding=utf-8
|
||||
|
||||
import re
|
||||
import sys
|
||||
from datetime import date, datetime
|
||||
from datetime import datetime
|
||||
from datetime import date
|
||||
|
||||
from nemubot import context
|
||||
from nemubot.exception import IMException
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.tools.countdown import countdown_format
|
||||
from nemubot.tools.date import extractDate
|
||||
from nemubot.tools.xmlparser.node import ModuleState
|
||||
from xmlparser.node import ModuleState
|
||||
|
||||
from nemubot.module.more import Response
|
||||
|
||||
|
||||
# LOADING #############################################################
|
||||
nemubotversion = 3.3
|
||||
|
||||
def load(context):
|
||||
context.data.setIndex("name", "birthday")
|
||||
global DATAS
|
||||
DATAS.setIndex("name", "birthday")
|
||||
|
||||
|
||||
# MODULE CORE #########################################################
|
||||
def help_tiny ():
|
||||
"""Line inserted in the response to the command !help"""
|
||||
return "People birthdays and ages"
|
||||
|
||||
|
||||
def help_full ():
|
||||
return "!anniv /who/: gives the remaining time before the anniversary of /who/\n!age /who/: gives the age of /who/\nIf /who/ is not given, gives the remaining time before your anniversary.\n\n To set yout birthday, say it to nemubot :)"
|
||||
|
||||
|
||||
def findName(msg):
|
||||
if (not len(msg.args) or msg.args[0].lower() == "moi" or
|
||||
msg.args[0].lower() == "me"):
|
||||
name = msg.frm.lower()
|
||||
if len(msg.cmds) < 2 or msg.cmds[1].lower() == "moi" or msg.cmds[1].lower() == "me":
|
||||
name = msg.nick.lower()
|
||||
else:
|
||||
name = msg.args[0].lower()
|
||||
name = msg.cmds[1].lower()
|
||||
|
||||
matches = []
|
||||
|
||||
if name in context.data.index:
|
||||
if name in DATAS.index:
|
||||
matches.append(name)
|
||||
else:
|
||||
for k in context.data.index.keys():
|
||||
for k in DATAS.index.keys ():
|
||||
if k.find (name) == 0:
|
||||
matches.append (k)
|
||||
return (matches, name)
|
||||
|
||||
|
||||
# MODULE INTERFACE ####################################################
|
||||
|
||||
## Commands
|
||||
|
||||
@hook.command("anniv",
|
||||
help="gives the remaining time before the anniversary of known people",
|
||||
help_usage={
|
||||
None: "Calculate the time remaining before your birthday",
|
||||
"WHO": "Calculate the time remaining before WHO's birthday",
|
||||
})
|
||||
def cmd_anniv(msg):
|
||||
(matches, name) = findName(msg)
|
||||
if len(matches) == 1:
|
||||
name = matches[0]
|
||||
tyd = context.data.index[name].getDate("born")
|
||||
tyd = DATAS.index[name].getDate("born")
|
||||
tyd = datetime(date.today().year, tyd.month, tyd.day)
|
||||
|
||||
if (tyd.day == datetime.today().day and
|
||||
tyd.month == datetime.today().month):
|
||||
return Response(countdown_format(
|
||||
context.data.index[name].getDate("born"), "",
|
||||
return Response(msg.sender, msg.countdown_format(
|
||||
DATAS.index[name].getDate("born"), "",
|
||||
"C'est aujourd'hui l'anniversaire de %s !"
|
||||
" Il a %s. Joyeux anniversaire :)" % (name, "%s")),
|
||||
msg.channel)
|
||||
|
|
@ -70,65 +58,54 @@ def cmd_anniv(msg):
|
|||
if tyd < datetime.today():
|
||||
tyd = datetime(date.today().year + 1, tyd.month, tyd.day)
|
||||
|
||||
return Response(countdown_format(tyd,
|
||||
return Response(msg.sender, msg.countdown_format(tyd,
|
||||
"Il reste %s avant l'anniversaire de %s !" % ("%s",
|
||||
name), ""),
|
||||
msg.channel)
|
||||
else:
|
||||
return Response("désolé, je ne connais pas la date d'anniversaire"
|
||||
return Response(msg.sender, "désolé, je ne connais pas la date d'anniversaire"
|
||||
" de %s. Quand est-il né ?" % name,
|
||||
msg.channel, msg.frm)
|
||||
msg.channel, msg.nick)
|
||||
|
||||
|
||||
@hook.command("age",
|
||||
help="Calculate age of known people",
|
||||
help_usage={
|
||||
None: "Calculate your age",
|
||||
"WHO": "Calculate the age of WHO"
|
||||
})
|
||||
def cmd_age(msg):
|
||||
(matches, name) = findName(msg)
|
||||
if len(matches) == 1:
|
||||
name = matches[0]
|
||||
d = context.data.index[name].getDate("born")
|
||||
d = DATAS.index[name].getDate("born")
|
||||
|
||||
return Response(countdown_format(d,
|
||||
return Response(msg.sender, msg.countdown_format(d,
|
||||
"%s va naître dans %s." % (name, "%s"),
|
||||
"%s a %s." % (name, "%s")),
|
||||
msg.channel)
|
||||
else:
|
||||
return Response("désolé, je ne connais pas l'âge de %s."
|
||||
" Quand est-il né ?" % name, msg.channel, msg.frm)
|
||||
return Response(msg.sender, "désolé, je ne connais pas l'âge de %s."
|
||||
" Quand est-il né ?" % name, msg.channel, msg.nick)
|
||||
return True
|
||||
|
||||
|
||||
## Input parsing
|
||||
|
||||
@hook.ask()
|
||||
def parseask(msg):
|
||||
res = re.match(r"^(\S+)\s*('s|suis|est|is|was|were)?\s+(birthday|geburtstag|née? |nee? le|born on).*$", msg.message, re.I)
|
||||
if res is not None:
|
||||
msgl = msg.content.lower ()
|
||||
if re.match("^.*(date de naissance|birthday|geburtstag|née? |nee? le|born on).*$", msgl) is not None:
|
||||
try:
|
||||
extDate = extractDate(msg.message)
|
||||
extDate = msg.extractDate()
|
||||
if extDate is None or extDate.year > datetime.now().year:
|
||||
return Response("la date de naissance ne paraît pas valide...",
|
||||
return Response(msg.sender,
|
||||
"ta date de naissance ne paraît pas valide...",
|
||||
msg.channel,
|
||||
msg.frm)
|
||||
msg.nick)
|
||||
else:
|
||||
nick = res.group(1)
|
||||
if nick == "my" or nick == "I" or nick == "i" or nick == "je" or nick == "mon" or nick == "ma":
|
||||
nick = msg.frm
|
||||
if nick.lower() in context.data.index:
|
||||
context.data.index[nick.lower()]["born"] = extDate
|
||||
if msg.nick.lower() in DATAS.index:
|
||||
DATAS.index[msg.nick.lower()]["born"] = extDate
|
||||
else:
|
||||
ms = ModuleState("birthday")
|
||||
ms.setAttribute("name", nick.lower())
|
||||
ms.setAttribute("name", msg.nick.lower())
|
||||
ms.setAttribute("born", extDate)
|
||||
context.data.addChild(ms)
|
||||
context.save()
|
||||
return Response("ok, c'est noté, %s est né le %s"
|
||||
% (nick, extDate.strftime("%A %d %B %Y à %H:%M")),
|
||||
DATAS.addChild(ms)
|
||||
save()
|
||||
return Response(msg.sender,
|
||||
"ok, c'est noté, ta date de naissance est le %s"
|
||||
% extDate.strftime("%A %d %B %Y à %H:%M"),
|
||||
msg.channel,
|
||||
msg.frm)
|
||||
msg.nick)
|
||||
except:
|
||||
raise IMException("la date de naissance ne paraît pas valide.")
|
||||
return Response(msg.sender, "ta date de naissance ne paraît pas valide...",
|
||||
msg.channel, msg.nick)
|
||||
|
|
|
|||
5
modules/birthday.xml
Normal file
5
modules/birthday.xml
Normal 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>
|
||||
|
|
@ -1,74 +1,51 @@
|
|||
"""Wishes Happy New Year when the time comes"""
|
||||
# coding=utf-8
|
||||
|
||||
# PYTHON STUFFS #######################################################
|
||||
from datetime import datetime
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from nemubot.event import ModuleEvent
|
||||
from nemubot.hooks import hook
|
||||
from nemubot.tools.countdown import countdown_format
|
||||
|
||||
from nemubot.module.more import Response
|
||||
|
||||
|
||||
# GLOBALS #############################################################
|
||||
|
||||
yr = datetime.now(timezone.utc).year
|
||||
yrn = datetime.now(timezone.utc).year + 1
|
||||
|
||||
|
||||
# LOADING #############################################################
|
||||
nemubotversion = 3.3
|
||||