Compare commits

..

12 commits

135 changed files with 3656 additions and 6630 deletions

View file

@ -1,26 +0,0 @@
---
kind: pipeline
type: docker
name: default-arm64
platform:
os: linux
arch: arm64
steps:
- name: build
image: python:3.11-alpine
commands:
- pip install --no-cache-dir -r requirements.txt
- pip install .
- name: docker
image: plugins/docker
settings:
repo: nemunaire/nemubot
auto_tag: true
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
username:
from_secret: docker_username
password:
from_secret: docker_password

3
.gitmodules vendored Normal file
View file

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

View file

@ -1,9 +1,8 @@
language: python language: python
python: python:
- 3.3
- 3.4 - 3.4
- 3.5 - 3.5
- 3.6
- 3.7
- nightly - nightly
install: install:
- pip install -r requirements.txt - pip install -r requirements.txt

View file

@ -1,21 +0,0 @@
FROM python:3.11-alpine
WORKDIR /usr/src/app
COPY requirements.txt /usr/src/app/
RUN apk add --no-cache bash build-base capstone-dev mandoc-doc man-db w3m youtube-dl aspell aspell-fr py3-matrix-nio && \
pip install --no-cache-dir --ignore-installed -r requirements.txt && \
pip install bs4 capstone dnspython openai && \
apk del build-base capstone-dev && \
ln -s /var/lib/nemubot/home /home/nemubot
VOLUME /var/lib/nemubot
COPY . /usr/src/app/
RUN ./setup.py install
WORKDIR /var/lib/nemubot
USER guest
ENTRYPOINT [ "python", "-m", "nemubot", "-d", "-P", "", "-M", "/usr/src/app/modules" ]
CMD [ "-D", "/var/lib/nemubot" ]

View file

@ -1,50 +1,17 @@
nemubot # *nemubot*
=======
An extremely modulable IRC bot, built around XML configuration files! An extremely modulable IRC bot, built around XML configuration files!
Requirements ## Requirements
------------
*nemubot* requires at least Python 3.3 to work. *nemubot* requires at least Python 3.3 to work.
Some modules (like `cve`, `nextstop` or `laposte`) require the Some modules (like `cve`, `nextstop` or `laposte`) require the
[BeautifulSoup module](https://www.crummy.com/software/BeautifulSoup/), [BeautifulSoup module](http://www.crummy.com/software/BeautifulSoup/),
but the core and framework has no dependency. but the core and framework has no dependency.
Installation ## Documentation
------------
Use the `setup.py` file: `python setup.py install`. Have a look to the wiki at https://github.com/nemunaire/nemubot/wiki
### VirtualEnv setup
The easiest way to do this is through a virtualenv:
```sh
virtualenv venv
. venv/bin/activate
python setup.py install
```
### Create a new configuration file
There is a sample configuration file, called `bot_sample.xml`. You can
create your own configuration file from it.
Usage
-----
Don't forget to activate your virtualenv in further terminals, if you
use it.
To launch the bot, run:
```sh
nemubot bot.xml
```
Where `bot.xml` is your configuration file.

View file

@ -1,7 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3.3
# -*- coding: utf-8 -*-
# Nemubot is a smart and modulable IM bot. # Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2016 Mercier Pierre-Olivier # Copyright (C) 2012-2015 Mercier Pierre-Olivier
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by

View file

@ -1,11 +1,11 @@
<nemubotconfig nick="nemubot" realname="nemubot clone" owner="someone"> <nemubotconfig nick="nemubot" realname="nemubot clone" owner="someone">
<server uri="irc://irc.rezosup.org:6667" autoconnect="true" caps="znc.in/server-time-iso"> <server host="irc.rezosup.org" port="6667" autoconnect="true" caps="znc.in/server-time-iso">
<channel name="#nemutest" /> <channel name="#nemutest" />
</server> </server>
<!-- <!--
<server host="ircs://my_host.local:6667" password="secret" autoconnect="true"> <server host="my_host.local" port="6667" password="secret" autoconnect="true" ip="10.69.42.23" ssl="on">
<channel name="#nemutest" /> <channel name="#nemutest" />
</server> </server>
--> -->

View file

@ -3,16 +3,17 @@
# PYTHON STUFFS ####################################################### # PYTHON STUFFS #######################################################
import re import re
import sys
from datetime import datetime, timezone from datetime import datetime, timezone
import shlex
from nemubot import context from nemubot import context
from nemubot.exception import IMException from nemubot.exception import IRCException
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.message import Command from nemubot.message import Command
from nemubot.tools.human import guess
from nemubot.tools.xmlparser.node import ModuleState from nemubot.tools.xmlparser.node import ModuleState
from nemubot.module.more import Response from more import Response
# LOADING ############################################################# # LOADING #############################################################
@ -76,7 +77,7 @@ def get_variable(name, msg=None):
elif name in context.data.getNode("variables").index: elif name in context.data.getNode("variables").index:
return context.data.getNode("variables").index[name]["value"] return context.data.getNode("variables").index[name]["value"]
else: else:
return None return ""
def list_variables(user=None): def list_variables(user=None):
@ -108,12 +109,12 @@ def set_variable(name, value, creator):
context.save() context.save()
def replace_variables(cnts, msg): def replace_variables(cnts, msg=None):
"""Replace variables contained in the content """Replace variables contained in the content
Arguments: Arguments:
cnt -- content where search variables cnt -- content where search variables
msg -- Message where pick some variables msg -- optional message where pick some variables
""" """
unsetCnt = list() unsetCnt = list()
@ -122,12 +123,12 @@ def replace_variables(cnts, msg):
resultCnt = list() resultCnt = list()
for cnt in cnts: for cnt in cnts:
for res, name, default in re.findall("\\$\{(([a-zA-Z0-9:]+)(?:-([^}]+))?)\}", cnt): for res in re.findall("\\$\{(?P<name>[a-zA-Z0-9:]+)\}", cnt):
rv = re.match("([0-9]+)(:([0-9]*))?", name) rv = re.match("([0-9]+)(:([0-9]*))?", res)
if rv is not None: if rv is not None:
varI = int(rv.group(1)) - 1 varI = int(rv.group(1)) - 1
if varI >= len(msg.args): if varI > len(msg.args):
cnt = cnt.replace("${%s}" % res, default, 1) cnt = cnt.replace("${%s}" % res, "", 1)
elif rv.group(2) is not None: elif rv.group(2) is not None:
if rv.group(3) is not None and len(rv.group(3)): if rv.group(3) is not None and len(rv.group(3)):
varJ = int(rv.group(3)) - 1 varJ = int(rv.group(3)) - 1
@ -142,12 +143,11 @@ def replace_variables(cnts, msg):
cnt = cnt.replace("${%s}" % res, msg.args[varI], 1) cnt = cnt.replace("${%s}" % res, msg.args[varI], 1)
unsetCnt.append(varI) unsetCnt.append(varI)
else: else:
cnt = cnt.replace("${%s}" % res, get_variable(name) or default, 1) cnt = cnt.replace("${%s}" % res, get_variable(res), 1)
resultCnt.append(cnt) resultCnt.append(cnt)
# Remove used content
for u in sorted(set(unsetCnt), reverse=True): for u in sorted(set(unsetCnt), reverse=True):
msg.args.pop(u) k = msg.args.pop(u)
return resultCnt return resultCnt
@ -156,7 +156,7 @@ def replace_variables(cnts, msg):
## Variables management ## Variables management
@hook.command("listvars", @hook("cmd_hook", "listvars",
help="list defined variables for substitution in input commands", help="list defined variables for substitution in input commands",
help_usage={ help_usage={
None: "List all known variables", None: "List all known variables",
@ -179,20 +179,20 @@ def cmd_listvars(msg):
return Response("There is currently no variable stored.", channel=msg.channel) return Response("There is currently no variable stored.", channel=msg.channel)
@hook.command("set", @hook("cmd_hook", "set",
help="Create or set variables for substitution in input commands", 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"}) help_usage={"KEY VALUE": "Define the variable named KEY and fill it with VALUE as content"})
def cmd_set(msg): def cmd_set(msg):
if len(msg.args) < 2: if len(msg.args) < 2:
raise IMException("!set take two args: the key and the value.") raise IRCException("!set take two args: the key and the value.")
set_variable(msg.args[0], " ".join(msg.args[1:]), msg.frm) set_variable(msg.args[0], " ".join(msg.args[1:]), msg.nick)
return Response("Variable $%s successfully defined." % msg.args[0], return Response("Variable $%s successfully defined." % msg.args[0],
channel=msg.channel) channel=msg.channel)
## Alias management ## Alias management
@hook.command("listalias", @hook("cmd_hook", "listalias",
help="List registered aliases", help="List registered aliases",
help_usage={ help_usage={
None: "List all registered aliases", None: "List all registered aliases",
@ -206,42 +206,27 @@ def cmd_listalias(msg):
return Response("There is no alias currently.", channel=msg.channel) return Response("There is no alias currently.", channel=msg.channel)
@hook.command("alias", @hook("cmd_hook", "alias",
help="Display or define the replacement command for a given alias", help="Display 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): def cmd_alias(msg):
if not len(msg.args): if not len(msg.args):
raise IMException("!alias takes as argument an alias to extend.") raise IRCException("!alias takes as argument an alias to extend.")
res = list()
alias = context.subparse(msg, msg.args[0]) for alias in msg.args:
if alias is None or not isinstance(alias, Command): if alias[0] == "!":
raise IMException("%s is not a valid alias" % msg.args[0]) alias = alias[1:]
if alias in context.data.getNode("aliases").index:
if alias.cmd in context.data.getNode("aliases").index: res.append("!%s correspond to %s" % (alias, context.data.getNode("aliases").index[alias]["origin"]))
return Response("%s corresponds to %s" % (alias.cmd, context.data.getNode("aliases").index[alias.cmd]["origin"]), else:
channel=msg.channel, nick=msg.frm) res.append("!%s is not an alias" % alias)
return Response(res, channel=msg.channel, nick=msg.nick)
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", @hook("cmd_hook", "unalias",
help="Remove a previously created alias") help="Remove a previously created alias")
def cmd_unalias(msg): def cmd_unalias(msg):
if not len(msg.args): if not len(msg.args):
raise IMException("Which alias would you want to remove?") raise IRCException("Which alias would you want to remove?")
res = list() res = list()
for alias in msg.args: for alias in msg.args:
if alias[0] == "!" and len(alias) > 1: if alias[0] == "!" and len(alias) > 1:
@ -258,20 +243,39 @@ def cmd_unalias(msg):
## Alias replacement ## Alias replacement
@hook.add(["pre","Command"]) @hook("pre_Command")
def treat_alias(msg): def treat_alias(msg):
if context.data.getNode("aliases") is not None and msg.cmd in context.data.getNode("aliases").index: if msg.cmd in context.data.getNode("aliases").index:
origin = context.data.getNode("aliases").index[msg.cmd]["origin"] txt = context.data.getNode("aliases").index[msg.cmd]["origin"]
rpl_msg = context.subparse(msg, origin) # TODO: for legacy compatibility
if isinstance(rpl_msg, Command): if txt[0] == "!":
rpl_msg.args = replace_variables(rpl_msg.args, msg) txt = txt[1:]
rpl_msg.args += msg.args try:
rpl_msg.kwargs.update(msg.kwargs) args = shlex.split(txt)
elif len(msg.args) or len(msg.kwargs): except ValueError:
raise IMException("This kind of alias doesn't take any argument (haven't you forgotten the '!'?).") args = txt.split(' ')
nmsg = Command(args[0], replace_variables(args[1:], msg) + msg.args, **msg.export_args())
# Avoid infinite recursion # Avoid infinite recursion
if not isinstance(rpl_msg, Command) or msg.cmd != rpl_msg.cmd: if msg.cmd != nmsg.cmd:
return rpl_msg # Also return origin message, if it can be treated as well
return [msg, nmsg]
return msg return msg
@hook("ask_default")
def parseask(msg):
if re.match(".*(register|set|cr[ée]{2}|new|nouvel(le)?) alias.*", msg.text) is not None:
result = re.match(".*alias !?([^ ]+) ?(pour|for|=|:) ?(.+)$", msg.text)
if result.group(1) in context.data.getNode("aliases").index:
raise IRCException("this alias is already defined.")
else:
create_alias(result.group(1),
result.group(3),
channel=msg.channel,
creator=msg.nick)
res = Response("New alias %s successfully registered." %
result.group(1), channel=msg.channel)
return res
return None

View file

@ -7,13 +7,13 @@ import sys
from datetime import date, datetime from datetime import date, datetime
from nemubot import context from nemubot import context
from nemubot.exception import IMException from nemubot.exception import IRCException
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.tools.countdown import countdown_format from nemubot.tools.countdown import countdown_format
from nemubot.tools.date import extractDate from nemubot.tools.date import extractDate
from nemubot.tools.xmlparser.node import ModuleState from nemubot.tools.xmlparser.node import ModuleState
from nemubot.module.more import Response from more import Response
# LOADING ############################################################# # LOADING #############################################################
@ -27,7 +27,7 @@ def load(context):
def findName(msg): def findName(msg):
if (not len(msg.args) or msg.args[0].lower() == "moi" or if (not len(msg.args) or msg.args[0].lower() == "moi" or
msg.args[0].lower() == "me"): msg.args[0].lower() == "me"):
name = msg.frm.lower() name = msg.nick.lower()
else: else:
name = msg.args[0].lower() name = msg.args[0].lower()
@ -46,7 +46,7 @@ def findName(msg):
## Commands ## Commands
@hook.command("anniv", @hook("cmd_hook", "anniv",
help="gives the remaining time before the anniversary of known people", help="gives the remaining time before the anniversary of known people",
help_usage={ help_usage={
None: "Calculate the time remaining before your birthday", None: "Calculate the time remaining before your birthday",
@ -77,10 +77,10 @@ def cmd_anniv(msg):
else: else:
return Response("désolé, je ne connais pas la date d'anniversaire" return Response("désolé, je ne connais pas la date d'anniversaire"
" de %s. Quand est-il né ?" % name, " de %s. Quand est-il né ?" % name,
msg.channel, msg.frm) msg.channel, msg.nick)
@hook.command("age", @hook("cmd_hook", "age",
help="Calculate age of known people", help="Calculate age of known people",
help_usage={ help_usage={
None: "Calculate your age", None: "Calculate your age",
@ -98,26 +98,26 @@ def cmd_age(msg):
msg.channel) msg.channel)
else: else:
return Response("désolé, je ne connais pas l'âge de %s." return Response("désolé, je ne connais pas l'âge de %s."
" Quand est-il né ?" % name, msg.channel, msg.frm) " Quand est-il né ?" % name, msg.channel, msg.nick)
return True return True
## Input parsing ## Input parsing
@hook.ask() @hook("ask_default")
def parseask(msg): def parseask(msg):
res = re.match(r"^(\S+)\s*('s|suis|est|is|was|were)?\s+(birthday|geburtstag|née? |nee? le|born on).*$", msg.message, re.I) res = re.match(r"^(\S+)\s*('s|suis|est|is|was|were)?\s+(birthday|geburtstag|née? |nee? le|born on).*$", msg.text, re.I)
if res is not None: if res is not None:
try: try:
extDate = extractDate(msg.message) extDate = extractDate(msg.text)
if extDate is None or extDate.year > datetime.now().year: if extDate is None or extDate.year > datetime.now().year:
return Response("la date de naissance ne paraît pas valide...", return Response("la date de naissance ne paraît pas valide...",
msg.channel, msg.channel,
msg.frm) msg.nick)
else: else:
nick = res.group(1) nick = res.group(1)
if nick == "my" or nick == "I" or nick == "i" or nick == "je" or nick == "mon" or nick == "ma": if nick == "my" or nick == "I" or nick == "i" or nick == "je" or nick == "mon" or nick == "ma":
nick = msg.frm nick = msg.nick
if nick.lower() in context.data.index: if nick.lower() in context.data.index:
context.data.index[nick.lower()]["born"] = extDate context.data.index[nick.lower()]["born"] = extDate
else: else:
@ -129,6 +129,6 @@ def parseask(msg):
return Response("ok, c'est noté, %s est né le %s" return Response("ok, c'est noté, %s est né le %s"
% (nick, extDate.strftime("%A %d %B %Y à %H:%M")), % (nick, extDate.strftime("%A %d %B %Y à %H:%M")),
msg.channel, msg.channel,
msg.frm) msg.nick)
except: except:
raise IMException("la date de naissance ne paraît pas valide.") raise IRCException("la date de naissance ne paraît pas valide.")

View file

@ -4,11 +4,12 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from nemubot import context
from nemubot.event import ModuleEvent from nemubot.event import ModuleEvent
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.tools.countdown import countdown_format from nemubot.tools.countdown import countdown_format
from nemubot.module.more import Response from more import Response
# GLOBALS ############################################################# # GLOBALS #############################################################
@ -46,9 +47,9 @@ def load(context):
# MODULE INTERFACE #################################################### # MODULE INTERFACE ####################################################
@hook.command("newyear", @hook("cmd_hook", "newyear",
help="Display the remaining time before the next new year") help="Display the remaining time before the next new year")
@hook.command(str(yrn), @hook("cmd_hook", str(yrn),
help="Display the remaining time before %d" % yrn) help="Display the remaining time before %d" % yrn)
def cmd_newyear(msg): def cmd_newyear(msg):
return Response(countdown_format(datetime(yrn, 1, 1, 0, 0, 1, 0, return Response(countdown_format(datetime(yrn, 1, 1, 0, 0, 1, 0,
@ -58,7 +59,7 @@ def cmd_newyear(msg):
channel=msg.channel) channel=msg.channel)
@hook.command(data=yrn, regexp="^[0-9]{4}$", @hook("cmd_rgxp", data=yrn, regexp="^[0-9]{4}$",
help="Calculate time remaining/passed before/since the requested year") help="Calculate time remaining/passed before/since the requested year")
def cmd_timetoyear(msg, cur): def cmd_timetoyear(msg, cur):
yr = int(msg.cmd) yr = int(msg.cmd)

View file

@ -5,17 +5,17 @@
import urllib import urllib
from nemubot import context from nemubot import context
from nemubot.exception import IMException from nemubot.exception import IRCException
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.tools import web from nemubot.tools import web
from nemubot.module.more import Response from more import Response
# LOADING ############################################################# # LOADING #############################################################
def load(context): def load(context):
if not context.config or "goodreadskey" not in context.config: if not context.config or not context.config.getAttribute("goodreadskey"):
raise ImportError("You need a Goodreads API key in order to use this " raise ImportError("You need a Goodreads API key in order to use this "
"module. Add it to the module configuration file:\n" "module. Add it to the module configuration file:\n"
"<module name=\"books\" goodreadskey=\"XXXXXX\" />\n" "<module name=\"books\" goodreadskey=\"XXXXXX\" />\n"
@ -28,8 +28,8 @@ def get_book(title):
"""Retrieve a book from its title""" """Retrieve a book from its title"""
response = web.getXML("https://www.goodreads.com/book/title.xml?key=%s&title=%s" % response = web.getXML("https://www.goodreads.com/book/title.xml?key=%s&title=%s" %
(context.config["goodreadskey"], urllib.parse.quote(title))) (context.config["goodreadskey"], urllib.parse.quote(title)))
if response is not None and len(response.getElementsByTagName("book")): if response is not None and response.hasNode("book"):
return response.getElementsByTagName("book")[0] return response.getNode("book")
else: else:
return None return None
@ -38,8 +38,8 @@ def search_books(title):
"""Get a list of book matching given title""" """Get a list of book matching given title"""
response = web.getXML("https://www.goodreads.com/search.xml?key=%s&q=%s" % response = web.getXML("https://www.goodreads.com/search.xml?key=%s&q=%s" %
(context.config["goodreadskey"], urllib.parse.quote(title))) (context.config["goodreadskey"], urllib.parse.quote(title)))
if response is not None and len(response.getElementsByTagName("search")): if response is not None and response.hasNode("search"):
return response.getElementsByTagName("search")[0].getElementsByTagName("results")[0].getElementsByTagName("work") return response.getNode("search").getNode("results").getNodes("work")
else: else:
return [] return []
@ -48,43 +48,43 @@ def search_author(name):
"""Looking for an author""" """Looking for an author"""
response = web.getXML("https://www.goodreads.com/api/author_url/%s?key=%s" % response = web.getXML("https://www.goodreads.com/api/author_url/%s?key=%s" %
(urllib.parse.quote(name), context.config["goodreadskey"])) (urllib.parse.quote(name), context.config["goodreadskey"]))
if response is not None and len(response.getElementsByTagName("author")) and response.getElementsByTagName("author")[0].hasAttribute("id"): if response is not None and response.hasNode("author") and response.getNode("author").hasAttribute("id"):
response = web.getXML("https://www.goodreads.com/author/show/%s.xml?key=%s" % response = web.getXML("https://www.goodreads.com/author/show/%s.xml?key=%s" %
(urllib.parse.quote(response.getElementsByTagName("author")[0].getAttribute("id")), context.config["goodreadskey"])) (urllib.parse.quote(response.getNode("author")["id"]), context.config["goodreadskey"]))
if response is not None and len(response.getElementsByTagName("author")): if response is not None and response.hasNode("author"):
return response.getElementsByTagName("author")[0] return response.getNode("author")
return None return None
# MODULE INTERFACE #################################################### # MODULE INTERFACE ####################################################
@hook.command("book", @hook("cmd_hook", "book",
help="Get information about a book from its title", help="Get information about a book from its title",
help_usage={ help_usage={
"TITLE": "Get information about a book titled TITLE" "TITLE": "Get information about a book titled TITLE"
}) })
def cmd_book(msg): def cmd_book(msg):
if not len(msg.args): if not len(msg.args):
raise IMException("please give me a title to search") raise IRCException("please give me a title to search")
book = get_book(" ".join(msg.args)) book = get_book(" ".join(msg.args))
if book is None: if book is None:
raise IMException("unable to find book named like this") raise IRCException("unable to find book named like this")
res = Response(channel=msg.channel) res = Response(channel=msg.channel)
res.append_message("%s, written by %s: %s" % (book.getElementsByTagName("title")[0].firstChild.nodeValue, res.append_message("%s, writed by %s: %s" % (book.getNode("title").getContent(),
book.getElementsByTagName("author")[0].getElementsByTagName("name")[0].firstChild.nodeValue, book.getNode("authors").getNode("author").getNode("name").getContent(),
web.striphtml(book.getElementsByTagName("description")[0].firstChild.nodeValue if book.getElementsByTagName("description")[0].firstChild else ""))) web.striphtml(book.getNode("description").getContent())))
return res return res
@hook.command("search_books", @hook("cmd_hook", "search_books",
help="Search book's title", help="Search book's title",
help_usage={ help_usage={
"APPROX_TITLE": "Search for a book approximately titled APPROX_TITLE" "APPROX_TITLE": "Search for a book approximately titled APPROX_TITLE"
}) })
def cmd_books(msg): def cmd_books(msg):
if not len(msg.args): if not len(msg.args):
raise IMException("please give me a title to search") raise IRCException("please give me a title to search")
title = " ".join(msg.args) title = " ".join(msg.args)
res = Response(channel=msg.channel, res = Response(channel=msg.channel,
@ -92,24 +92,21 @@ def cmd_books(msg):
count=" (%d more books)") count=" (%d more books)")
for book in search_books(title): for book in search_books(title):
res.append_message("%s, writed by %s" % (book.getElementsByTagName("best_book")[0].getElementsByTagName("title")[0].firstChild.nodeValue, res.append_message("%s, writed by %s" % (book.getNode("best_book").getNode("title").getContent(),
book.getElementsByTagName("best_book")[0].getElementsByTagName("author")[0].getElementsByTagName("name")[0].firstChild.nodeValue)) book.getNode("best_book").getNode("author").getNode("name").getContent()))
return res return res
@hook.command("author_books", @hook("cmd_hook", "author_books",
help="Looking for books writen by a given author", help="Looking for books writen by a given author",
help_usage={ help_usage={
"AUTHOR": "Looking for books writen by AUTHOR" "AUTHOR": "Looking for books writen by AUTHOR"
}) })
def cmd_author(msg): def cmd_author(msg):
if not len(msg.args): if not len(msg.args):
raise IMException("please give me an author to search") raise IRCException("please give me an author to search")
name = " ".join(msg.args) ath = search_author(" ".join(msg.args))
ath = search_author(name) return Response([b.getNode("title").getContent() for b in ath.getNode("books").getNodes("book")],
if ath is None:
raise IMException("%s does not appear to be a published author." % name)
return Response([b.getElementsByTagName("title")[0].firstChild.nodeValue for b in ath.getElementsByTagName("book")],
channel=msg.channel, channel=msg.channel,
title=ath.getElementsByTagName("name")[0].firstChild.nodeValue) title=ath.getNode("name").getContent())

View file

@ -1,55 +0,0 @@
"""Concatenate commands"""
# PYTHON STUFFS #######################################################
from nemubot import context
from nemubot.exception import IMException
from nemubot.hooks import hook
from nemubot.message import Command, DirectAsk, Text
from nemubot.module.more import Response
# MODULE CORE #########################################################
def cat(msg, *terms):
res = Response(channel=msg.to_response, server=msg.server)
for term in terms:
m = context.subparse(msg, term)
if isinstance(m, Command) or isinstance(m, DirectAsk):
for r in context.subtreat(m):
if isinstance(r, Response):
for t in range(len(r.messages)):
res.append_message(r.messages[t],
title=r.rawtitle if not isinstance(r.rawtitle, list) else r.rawtitle[t])
elif isinstance(r, Text):
res.append_message(r.message)
elif isinstance(r, str):
res.append_message(r)
else:
res.append_message(term)
return res
# MODULE INTERFACE ####################################################
@hook.command("cat",
help="Concatenate responses of commands given as argument",
help_usage={"!SUBCMD [!SUBCMD [...]]": "Concatenate response of subcommands"},
keywords={
"merge": "Merge messages into the same",
})
def cmd_cat(msg):
if len(msg.args) < 1:
raise IMException("No subcommand to concatenate")
r = cat(msg, *msg.args)
if "merge" in msg.kwargs and len(r.messages) > 1:
r.messages = [ r.messages ]
return r

View file

@ -6,12 +6,12 @@ from collections import defaultdict
import re import re
from urllib.parse import quote from urllib.parse import quote
from nemubot.exception import IMException from nemubot.exception import IRCException
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.tools import web from nemubot.tools import web
from nemubot.tools.web import striphtml from nemubot.tools.web import striphtml
from nemubot.module.more import Response from more import Response
# GLOBALS ############################################################# # GLOBALS #############################################################
@ -36,7 +36,7 @@ for k, v in s:
# MODULE CORE ######################################################### # MODULE CORE #########################################################
def get_conjug(verb, stringTens): def get_conjug(verb, stringTens):
url = ("https://leconjugueur.lefigaro.fr/conjugaison/verbe/%s.html" % url = ("http://leconjugueur.lefigaro.fr/conjugaison/verbe/%s.html" %
quote(verb.encode("ISO-8859-1"))) quote(verb.encode("ISO-8859-1")))
page = web.getURLContent(url) page = web.getURLContent(url)
@ -51,10 +51,10 @@ def compute_line(line, stringTens):
try: try:
idTemps = d[stringTens] idTemps = d[stringTens]
except: except:
raise IMException("le temps demandé n'existe pas") raise IRCException("le temps demandé n'existe pas")
if len(idTemps) == 0: if len(idTemps) == 0:
raise IMException("le temps demandé n'existe pas") raise IRCException("le temps demandé n'existe pas")
index = line.index('<div id="temps' + idTemps[0] + '\"') index = line.index('<div id="temps' + idTemps[0] + '\"')
endIndex = line[index:].index('<div class=\"conjugBloc\"') endIndex = line[index:].index('<div class=\"conjugBloc\"')
@ -72,13 +72,13 @@ def compute_line(line, stringTens):
# MODULE INTERFACE #################################################### # MODULE INTERFACE ####################################################
@hook.command("conjugaison", @hook("cmd_hook", "conjugaison",
help_usage={ help_usage={
"TENS VERB": "give the conjugaison for VERB in TENS." "TENS VERB": "give the conjugaison for VERB in TENS."
}) })
def cmd_conjug(msg): def cmd_conjug(msg):
if len(msg.args) < 2: if len(msg.args) < 2:
raise IMException("donne moi un temps et un verbe, et je te donnerai " raise IRCException("donne moi un temps et un verbe, et je te donnerai "
"sa conjugaison!") "sa conjugaison!")
tens = ' '.join(msg.args[:-1]) tens = ' '.join(msg.args[:-1])
@ -91,4 +91,4 @@ def cmd_conjug(msg):
return Response(conjug, channel=msg.channel, return Response(conjug, channel=msg.channel,
title="Conjugaison de %s" % verb) title="Conjugaison de %s" % verb)
else: else:
raise IMException("aucune conjugaison de '%s' n'a été trouvé" % verb) raise IRCException("aucune conjugaison de '%s' n'a été trouvé" % verb)

View file

@ -6,7 +6,7 @@ from bs4 import BeautifulSoup
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.tools.web import getURLContent, striphtml from nemubot.tools.web import getURLContent, striphtml
from nemubot.module.more import Response from more import Response
# GLOBALS ############################################################# # GLOBALS #############################################################
@ -16,7 +16,7 @@ URL = 'https://ctftime.org/event/list/upcoming'
# MODULE INTERFACE #################################################### # MODULE INTERFACE ####################################################
@hook.command("ctfs", @hook("cmd_hook", "ctfs",
help="Display the upcoming CTFs") help="Display the upcoming CTFs")
def get_info_yt(msg): def get_info_yt(msg):
soup = BeautifulSoup(getURLContent(URL)) soup = BeautifulSoup(getURLContent(URL))
@ -25,8 +25,10 @@ def get_info_yt(msg):
for line in soup.body.find_all('tr'): for line in soup.body.find_all('tr'):
n = line.find_all('td') n = line.find_all('td')
if len(n) == 7: if len(n) == 5:
res.append_message("\x02%s:\x0F from %s type %s at %s. Weight: %s. %s%s" % try:
tuple([striphtml(x.text).strip() for x in n])) res.append_message("\x02%s:\x0F from %s type %s at %s. %s" %
tuple([striphtml(x.text) for x in n]))
except:
pass
return res return res

View file

@ -1,78 +1,26 @@
"""Read CVE in your IM client"""
# PYTHON STUFFS #######################################################
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from urllib.parse import quote from urllib.parse import quote
from nemubot.exception import IMException
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.tools.web import getURLContent, striphtml from nemubot.tools.web import getURLContent
from more import Response
from nemubot.module.more import Response """CVE description"""
BASEURL_NIST = 'https://nvd.nist.gov/vuln/detail/' nemubotversion = 4.0
BASEURL_MITRE = 'http://cve.mitre.org/cgi-bin/cvename.cgi?name='
# MODULE CORE #########################################################
VULN_DATAS = {
"alert-title": "vuln-warning-status-name",
"alert-content": "vuln-warning-banner-content",
"description": "vuln-description",
"published": "vuln-published-on",
"last_modified": "vuln-last-modified-on",
"base_score": "vuln-cvssv3-base-score-link",
"severity": "vuln-cvssv3-base-score-severity",
"impact_score": "vuln-cvssv3-impact-score",
"exploitability_score": "vuln-cvssv3-exploitability-score",
"av": "vuln-cvssv3-av",
"ac": "vuln-cvssv3-ac",
"pr": "vuln-cvssv3-pr",
"ui": "vuln-cvssv3-ui",
"s": "vuln-cvssv3-s",
"c": "vuln-cvssv3-c",
"i": "vuln-cvssv3-i",
"a": "vuln-cvssv3-a",
}
def get_cve(cve_id): def get_cve(cve_id):
search_url = BASEURL_NIST + quote(cve_id.upper()) search_url = BASEURL_MITRE + quote(cve_id.upper())
soup = BeautifulSoup(getURLContent(search_url)) soup = BeautifulSoup(getURLContent(search_url))
desc = soup.body.findAll('td')
vuln = {} return desc[17].text.replace("\n", " ") + " Moar at " + search_url
for vd in VULN_DATAS: @hook("cmd_hook", "cve")
r = soup.body.find(attrs={"data-testid": VULN_DATAS[vd]})
if r:
vuln[vd] = r.text.strip()
return vuln
def display_metrics(av, ac, pr, ui, s, c, i, a, **kwargs):
ret = []
if av != "None": ret.append("Attack Vector: \x02%s\x0F" % av)
if ac != "None": ret.append("Attack Complexity: \x02%s\x0F" % ac)
if pr != "None": ret.append("Privileges Required: \x02%s\x0F" % pr)
if ui != "None": ret.append("User Interaction: \x02%s\x0F" % ui)
if s != "Unchanged": ret.append("Scope: \x02%s\x0F" % s)
if c != "None": ret.append("Confidentiality: \x02%s\x0F" % c)
if i != "None": ret.append("Integrity: \x02%s\x0F" % i)
if a != "None": ret.append("Availability: \x02%s\x0F" % a)
return ', '.join(ret)
# MODULE INTERFACE ####################################################
@hook.command("cve",
help="Display given CVE",
help_usage={"CVE_ID": "Display the description of the given CVE"})
def get_cve_desc(msg): def get_cve_desc(msg):
res = Response(channel=msg.channel) res = Response(channel=msg.channel)
@ -80,20 +28,6 @@ def get_cve_desc(msg):
if cve_id[:3].lower() != 'cve': if cve_id[:3].lower() != 'cve':
cve_id = 'cve-' + cve_id cve_id = 'cve-' + cve_id
cve = get_cve(cve_id) res.append_message(get_cve(cve_id))
if not cve:
raise IMException("CVE %s doesn't exists." % cve_id)
if "alert-title" in cve or "alert-content" in cve:
alert = "\x02%s:\x0F %s " % (cve["alert-title"] if "alert-title" in cve else "",
cve["alert-content"] if "alert-content" in cve else "")
else:
alert = ""
if "base_score" not in cve and "description" in cve:
res.append_message("{alert}Last modified on \x02{last_modified}\x0F. {description}".format(alert=alert, **cve), title=cve_id)
else:
metrics = display_metrics(**cve)
res.append_message("{alert}Base score: \x02{base_score} {severity}\x0F (impact: \x02{impact_score}\x0F, exploitability: \x02{exploitability_score}\x0F; {metrics}), last modified on \x02{last_modified}\x0F. {description}".format(alert=alert, metrics=metrics, **cve), title=cve_id)
return res return res

View file

@ -1,138 +0,0 @@
"""Search around DuckDuckGo search engine"""
# PYTHON STUFFS #######################################################
from urllib.parse import quote
from nemubot.exception import IMException
from nemubot.hooks import hook
from nemubot.tools import web
from nemubot.module.more import Response
# MODULE CORE #########################################################
def do_search(terms):
if "!safeoff" in terms:
terms.remove("!safeoff")
safeoff = True
else:
safeoff = False
sterm = " ".join(terms)
return DDGResult(sterm, web.getJSON(
"https://api.duckduckgo.com/?q=%s&format=json&no_redirect=1%s" %
(quote(sterm), "&kp=-1" if safeoff else "")))
class DDGResult:
def __init__(self, terms, res):
if res is None:
raise IMException("An error occurs during search")
self.terms = terms
self.ddgres = res
@property
def type(self):
if not self.ddgres or "Type" not in self.ddgres:
return ""
return self.ddgres["Type"]
@property
def definition(self):
if "Definition" not in self.ddgres or not self.ddgres["Definition"]:
return None
return self.ddgres["Definition"] + " <" + self.ddgres["DefinitionURL"] + "> from " + self.ddgres["DefinitionSource"]
@property
def relatedTopics(self):
if "RelatedTopics" in self.ddgres:
for rt in self.ddgres["RelatedTopics"]:
if "Text" in rt:
yield rt["Text"] + " <" + rt["FirstURL"] + ">"
elif "Topics" in rt:
yield rt["Name"] + ": " + "; ".join([srt["Text"] + " <" + srt["FirstURL"] + ">" for srt in rt["Topics"]])
@property
def redirect(self):
if "Redirect" not in self.ddgres or not self.ddgres["Redirect"]:
return None
return self.ddgres["Redirect"]
@property
def entity(self):
if "Entity" not in self.ddgres or not self.ddgres["Entity"]:
return None
return self.ddgres["Entity"]
@property
def heading(self):
if "Heading" not in self.ddgres or not self.ddgres["Heading"]:
return " ".join(self.terms)
return self.ddgres["Heading"]
@property
def result(self):
if "Results" in self.ddgres:
for res in self.ddgres["Results"]:
yield res["Text"] + " <" + res["FirstURL"] + ">"
@property
def answer(self):
if "Answer" not in self.ddgres or not self.ddgres["Answer"]:
return None
return web.striphtml(self.ddgres["Answer"])
@property
def abstract(self):
if "Abstract" not in self.ddgres or not self.ddgres["Abstract"]:
return None
return self.ddgres["AbstractText"] + " <" + self.ddgres["AbstractURL"] + "> from " + self.ddgres["AbstractSource"]
# MODULE INTERFACE ####################################################
@hook.command("define")
def define(msg):
if not len(msg.args):
raise IMException("Indicate a term to define")
s = do_search(msg.args)
if not s.definition:
raise IMException("no definition found for '%s'." % " ".join(msg.args))
return Response(s.definition, channel=msg.channel)
@hook.command("search")
def search(msg):
if not len(msg.args):
raise IMException("Indicate a term to search")
s = do_search(msg.args)
res = Response(channel=msg.channel, nomore="No more results",
count=" (%d more results)")
res.append_message(s.redirect)
res.append_message(s.answer)
res.append_message(s.abstract)
res.append_message([r for r in s.result])
for rt in s.relatedTopics:
res.append_message(rt)
res.append_message(s.definition)
return res

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

@ -0,0 +1,71 @@
# coding=utf-8
from urllib.parse import quote
from nemubot.tools import web
from nemubot.tools.xmlparser import parse_string
class DDGSearch:
def __init__(self, terms, safeoff=False):
self.terms = terms
self.ddgres = web.getXML(
"https://api.duckduckgo.com/?q=%s&format=xml&no_redirect=1%s" %
(quote(terms), "&kp=-1" if safeoff else ""),
timeout=10)
@property
def type(self):
if self.ddgres and self.ddgres.hasNode("Type"):
return self.ddgres.getFirstNode("Type").getContent()
else:
return ""
@property
def definition(self):
if self.ddgres.hasNode("Definition"):
return self.ddgres.getFirstNode("Definition").getContent()
else:
return "Sorry, no definition found for %s" % self.terms
@property
def relatedTopics(self):
try:
for rt in self.ddgres.getFirstNode("RelatedTopics").getNodes("RelatedTopic"):
yield rt.getFirstNode("Text").getContent()
except:
pass
@property
def redirect(self):
try:
return self.ddgres.getFirstNode("Redirect").getContent()
except:
return None
@property
def result(self):
try:
node = self.ddgres.getFirstNode("Results").getFirstNode("Result")
return node.getFirstNode("Text").getContent() + ": " + node.getFirstNode("FirstURL").getContent()
except:
return None
@property
def answer(self):
try:
return web.striphtml(self.ddgres.getFirstNode("Answer").getContent())
except:
return None
@property
def abstract(self):
try:
if self.ddgres.getNode("Abstract").getContent() != "":
return self.ddgres.getNode("Abstract").getContent() + " <" + self.ddgres.getNode("AbstractURL").getContent() + ">"
else:
return None
except:
return None

View file

@ -0,0 +1,30 @@
# coding=utf-8
from urllib.parse import quote
from nemubot.tools import web
class UrbanDictionnary:
def __init__(self, terms):
self.terms = terms
self.udres = web.getJSON(
"http://api.urbandictionary.com/v0/define?term=%s" % quote(terms),
timeout=10)
@property
def result_type(self):
if self.udres and "result_type" in self.udres:
return self.udres["result_type"]
else:
return ""
@property
def definitions(self):
if self.udres and "list" in self.udres:
for d in self.udres["list"]:
yield d["definition"] + "\n" + d["example"]
else:
yield "Sorry, no definition found for %s" % self.terms

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

@ -0,0 +1,70 @@
# coding=utf-8
"""Search around various search engine or knowledges database"""
import imp
from nemubot import context
from nemubot.exception import IRCException
from nemubot.hooks import hook
nemubotversion = 3.4
from more import Response
from . import DDGSearch
from . import UrbanDictionnary
@hook("cmd_hook", "define")
def define(msg):
if not len(msg.args):
raise IRCException("Indicate a term to define")
s = DDGSearch.DDGSearch(' '.join(msg.args))
return Response(s.definition, channel=msg.channel)
@hook("cmd_hook", "search")
def search(msg):
if not len(msg.args):
raise IRCException("Indicate a term to search")
if "!safeoff" in msg.args:
msg.args.remove("!safeoff")
safeoff = True
else:
safeoff = False
s = DDGSearch.DDGSearch(' '.join(msg.args), safeoff)
res = Response(channel=msg.channel, nomore="No more results",
count=" (%d more results)")
res.append_message(s.redirect)
res.append_message(s.abstract)
res.append_message(s.result)
res.append_message(s.answer)
for rt in s.relatedTopics:
res.append_message(rt)
res.append_message(s.definition)
return res
@hook("cmd_hook", "urbandictionnary")
def udsearch(msg):
if not len(msg.args):
raise IRCException("Indicate a term to search")
s = UrbanDictionnary.UrbanDictionnary(' '.join(msg.args))
res = Response(channel=msg.channel, nomore="No more results",
count=" (%d more definitions)")
for d in s.definitions:
res.append_message(d.replace("\n", " "))
return res

View file

@ -1,94 +0,0 @@
"""DNS resolver"""
# PYTHON STUFFS #######################################################
import ipaddress
import socket
import dns.exception
import dns.name
import dns.rdataclass
import dns.rdatatype
import dns.resolver
from nemubot.exception import IMException
from nemubot.hooks import hook
from nemubot.module.more import Response
# MODULE INTERFACE ####################################################
@hook.command("dig",
help="Resolve domain name with a basic syntax similar to dig(1)")
def dig(msg):
lclass = "IN"
ltype = "A"
ledns = None
ltimeout = 6.0
ldomain = None
lnameservers = []
lsearchlist = []
loptions = []
for a in msg.args:
if a in dns.rdatatype._by_text:
ltype = a
elif a in dns.rdataclass._by_text:
lclass = a
elif a[0] == "@":
try:
lnameservers.append(str(ipaddress.ip_address(a[1:])))
except ValueError:
for r in socket.getaddrinfo(a[1:], 53, proto=socket.IPPROTO_UDP):
lnameservers.append(r[4][0])
elif a[0:8] == "+domain=":
lsearchlist.append(dns.name.from_unicode(a[8:]))
elif a[0:6] == "+edns=":
ledns = int(a[6:])
elif a[0:6] == "+time=":
ltimeout = float(a[6:])
elif a[0] == "+":
loptions.append(a[1:])
else:
ldomain = a
if not ldomain:
raise IMException("indicate a domain to resolve")
resolv = dns.resolver.Resolver()
if ledns:
resolv.edns = ledns
resolv.lifetime = ltimeout
resolv.timeout = ltimeout
resolv.flags = (
dns.flags.QR | dns.flags.RA |
dns.flags.AA if "aaonly" in loptions or "aaflag" in loptions else 0 |
dns.flags.AD if "adflag" in loptions else 0 |
dns.flags.CD if "cdflag" in loptions else 0 |
dns.flags.RD if "norecurse" not in loptions else 0
)
if lsearchlist:
resolv.search = lsearchlist
else:
resolv.search = [dns.name.from_text(".")]
if lnameservers:
resolv.nameservers = lnameservers
try:
answers = resolv.query(ldomain, ltype, lclass, tcp="tcp" in loptions)
except dns.exception.DNSException as e:
raise IMException(str(e))
res = Response(channel=msg.channel, count=" (%s others entries)")
for rdata in answers:
res.append_message("%s %s %s %s %s" % (
answers.qname.to_text(),
answers.ttl if not "nottlid" in loptions else "",
dns.rdataclass.to_text(answers.rdclass) if not "nocl" in loptions else "",
dns.rdatatype.to_text(answers.rdtype),
rdata.to_text())
)
return res

View file

@ -1,89 +0,0 @@
"""The Ultimate Disassembler Module"""
# PYTHON STUFFS #######################################################
import capstone
from nemubot.exception import IMException
from nemubot.hooks import hook
from nemubot.module.more import Response
# MODULE CORE #########################################################
ARCHITECTURES = {
"arm": capstone.CS_ARCH_ARM,
"arm64": capstone.CS_ARCH_ARM64,
"mips": capstone.CS_ARCH_MIPS,
"ppc": capstone.CS_ARCH_PPC,
"sparc": capstone.CS_ARCH_SPARC,
"sysz": capstone.CS_ARCH_SYSZ,
"x86": capstone.CS_ARCH_X86,
"xcore": capstone.CS_ARCH_XCORE,
}
MODES = {
"arm": capstone.CS_MODE_ARM,
"thumb": capstone.CS_MODE_THUMB,
"mips32": capstone.CS_MODE_MIPS32,
"mips64": capstone.CS_MODE_MIPS64,
"mips32r6": capstone.CS_MODE_MIPS32R6,
"16": capstone.CS_MODE_16,
"32": capstone.CS_MODE_32,
"64": capstone.CS_MODE_64,
"le": capstone.CS_MODE_LITTLE_ENDIAN,
"be": capstone.CS_MODE_BIG_ENDIAN,
"micro": capstone.CS_MODE_MICRO,
"mclass": capstone.CS_MODE_MCLASS,
"v8": capstone.CS_MODE_V8,
"v9": capstone.CS_MODE_V9,
}
# MODULE INTERFACE ####################################################
@hook.command("disas",
help="Display assembly code",
help_usage={"CODE": "Display assembly code corresponding to the given CODE"},
keywords={
"arch=ARCH": "Specify the architecture of the code to disassemble (default: x86, choose between: %s)" % ', '.join(ARCHITECTURES.keys()),
"modes=MODE[,MODE]": "Specify hardware mode of the code to disassemble (default: 32, between: %s)" % ', '.join(MODES.keys()),
})
def cmd_disas(msg):
if not len(msg.args):
raise IMException("please give me some code")
# Determine the architecture
if "arch" in msg.kwargs:
if msg.kwargs["arch"] not in ARCHITECTURES:
raise IMException("unknown architectures '%s'" % msg.kwargs["arch"])
architecture = ARCHITECTURES[msg.kwargs["arch"]]
else:
architecture = capstone.CS_ARCH_X86
# Determine hardware modes
modes = 0
if "modes" in msg.kwargs:
for mode in msg.kwargs["modes"].split(','):
if mode not in MODES:
raise IMException("unknown mode '%s'" % mode)
modes += MODES[mode]
elif architecture == capstone.CS_ARCH_X86 or architecture == capstone.CS_ARCH_PPC:
modes = capstone.CS_MODE_32
elif architecture == capstone.CS_ARCH_ARM or architecture == capstone.CS_ARCH_ARM64:
modes = capstone.CS_MODE_ARM
elif architecture == capstone.CS_ARCH_MIPS:
modes = capstone.CS_MODE_MIPS32
# Get the code
code = bytearray.fromhex(''.join([a.replace("0x", "") for a in msg.args]))
# Setup capstone
md = capstone.Cs(architecture, modes)
res = Response(channel=msg.channel, nomore="No more instruction")
for isn in md.disasm(code, 0x1000):
res.append_message("%s %s" %(isn.mnemonic, isn.op_str), title="0x%x" % isn.address)
return res

View file

@ -1,99 +1,49 @@
# coding=utf-8
"""Create countdowns and reminders""" """Create countdowns and reminders"""
import calendar import imp
from datetime import datetime, timedelta, timezone
from functools import partial
import re import re
import sys
from datetime import datetime, timedelta, timezone
import time
import threading
import traceback
from nemubot import context from nemubot import context
from nemubot.exception import IMException from nemubot.exception import IRCException
from nemubot.event import ModuleEvent from nemubot.event import ModuleEvent
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.message import Command
from nemubot.tools.countdown import countdown_format, countdown from nemubot.tools.countdown import countdown_format, countdown
from nemubot.tools.date import extractDate from nemubot.tools.date import extractDate
from nemubot.tools.xmlparser.basic import DictNode from nemubot.tools.xmlparser.node import ModuleState
from nemubot.module.more import Response nemubotversion = 3.4
class Event:
def __init__(self, server, channel, creator, start_time, end_time=None):
self._server = server
self._channel = channel
self._creator = creator
self._start = datetime.utcfromtimestamp(float(start_time)).replace(tzinfo=timezone.utc) if not isinstance(start_time, datetime) else start_time
self._end = datetime.utcfromtimestamp(float(end_time)).replace(tzinfo=timezone.utc) if end_time else None
self._evt = None
def __del__(self):
if self._evt is not None:
context.del_event(self._evt)
self._evt = None
def saveElement(self, store, tag="event"):
attrs = {
"server": str(self._server),
"channel": str(self._channel),
"creator": str(self._creator),
"start_time": str(calendar.timegm(self._start.timetuple())),
}
if self._end:
attrs["end_time"] = str(calendar.timegm(self._end.timetuple()))
store.startElement(tag, attrs)
store.endElement(tag)
@property
def creator(self):
return self._creator
@property
def start(self):
return self._start
@property
def end(self):
return self._end
@end.setter
def end(self, c):
self._end = c
@end.deleter
def end(self):
self._end = None
from more import Response
def help_full (): def help_full ():
return "This module store a lot of events: ny, we, " + (", ".join(context.datas.keys()) if hasattr(context, "datas") else "") + "\n!eventslist: gets list of timer\n!start /something/: launch a timer" return "This module store a lot of events: ny, we, " + (", ".join(context.datas.index.keys())) + "\n!eventslist: gets list of timer\n!start /something/: launch a timer"
def load(context): def load(context):
context.set_knodes({ #Define the index
"dict": DictNode, context.data.setIndex("name")
"event": Event,
})
if context.data is None: for evt in context.data.index.keys():
context.set_default(DictNode()) if context.data.index[evt].hasAttribute("end"):
event = ModuleEvent(call=fini, call_data=dict(strend=context.data.index[evt]))
# Relaunch all timers event._end = context.data.index[evt].getDate("end")
for kevt in context.data: idt = context.add_event(event)
if context.data[kevt].end: if idt is not None:
context.data[kevt]._evt = context.add_event(ModuleEvent(partial(fini, kevt, context.data[kevt]), offset=context.data[kevt].end - datetime.now(timezone.utc), interval=0)) context.data.index[evt]["_id"] = idt
def fini(name, evt): def fini(d, strend):
context.send_response(evt._server, Response("%s arrivé à échéance." % name, channel=evt._channel, nick=evt.creator)) context.send_response(strend["server"], Response("%s arrivé à échéance." % strend["name"], channel=strend["channel"], nick=strend["proprio"]))
evt._evt = None context.data.delChild(context.data.index[strend["name"]])
del context.data[name]
context.save() context.save()
@hook("cmd_hook", "goûter")
@hook.command("goûter")
def cmd_gouter(msg): def cmd_gouter(msg):
ndate = datetime.now(timezone.utc) ndate = datetime.now(timezone.utc)
ndate = datetime(ndate.year, ndate.month, ndate.day, 16, 42, 0, 0, timezone.utc) ndate = datetime(ndate.year, ndate.month, ndate.day, 16, 42, 0, 0, timezone.utc)
@ -102,8 +52,7 @@ def cmd_gouter(msg):
"Nous avons %s de retard pour le goûter :("), "Nous avons %s de retard pour le goûter :("),
channel=msg.channel) channel=msg.channel)
@hook("cmd_hook", "week-end")
@hook.command("week-end")
def cmd_we(msg): def cmd_we(msg):
ndate = datetime.now(timezone.utc) + timedelta(5 - datetime.today().weekday()) ndate = datetime.now(timezone.utc) + timedelta(5 - datetime.today().weekday())
ndate = datetime(ndate.year, ndate.month, ndate.day, 0, 0, 1, 0, timezone.utc) ndate = datetime(ndate.year, ndate.month, ndate.day, 0, 0, 1, 0, timezone.utc)
@ -112,16 +61,23 @@ def cmd_we(msg):
"Youhou, on est en week-end depuis %s."), "Youhou, on est en week-end depuis %s."),
channel=msg.channel) channel=msg.channel)
@hook("cmd_hook", "start")
@hook.command("start")
def start_countdown(msg): def start_countdown(msg):
"""!start /something/: launch a timer""" """!start /something/: launch a timer"""
if len(msg.args) < 1: if len(msg.args) < 1:
raise IMException("indique le nom d'un événement à chronométrer") raise IRCException("indique le nom d'un événement à chronométrer")
if msg.args[0] in context.data: if msg.args[0] in context.data.index:
raise IMException("%s existe déjà." % msg.args[0]) raise IRCException("%s existe déjà." % msg.args[0])
evt = Event(server=msg.server, channel=msg.channel, creator=msg.frm, start_time=msg.date) strnd = ModuleState("strend")
strnd["server"] = msg.server
strnd["channel"] = msg.channel
strnd["proprio"] = msg.nick
strnd["start"] = msg.date
strnd["name"] = msg.args[0]
context.data.addChild(strnd)
evt = ModuleEvent(call=fini, call_data=dict(strend=strnd))
if len(msg.args) > 1: if len(msg.args) > 1:
result1 = re.findall("([0-9]+)([smhdjwyaSMHDJWYA])?", msg.args[1]) result1 = re.findall("([0-9]+)([smhdjwyaSMHDJWYA])?", msg.args[1])
@ -139,158 +95,156 @@ def start_countdown(msg):
if result2 is None or result2.group(4) is None: yea = now.year if result2 is None or result2.group(4) is None: yea = now.year
else: yea = int(result2.group(4)) else: yea = int(result2.group(4))
if result2 is not None and result3 is not None: if result2 is not None and result3 is not None:
evt.end = datetime(yea, int(result2.group(3)), int(result2.group(2)), hou, minu, sec, timezone.utc) strnd["end"] = datetime(yea, int(result2.group(3)), int(result2.group(2)), hou, minu, sec, timezone.utc)
elif result2 is not None: elif result2 is not None:
evt.end = datetime(int(result2.group(4)), int(result2.group(3)), int(result2.group(2)), 0, 0, 0, timezone.utc) strnd["end"] = datetime(int(result2.group(4)), int(result2.group(3)), int(result2.group(2)), 0, 0, 0, timezone.utc)
elif result3 is not None: elif result3 is not None:
if hou * 3600 + minu * 60 + sec > now.hour * 3600 + now.minute * 60 + now.second: if hou * 3600 + minu * 60 + sec > now.hour * 3600 + now.minute * 60 + now.second:
evt.end = datetime(now.year, now.month, now.day, hou, minu, sec, timezone.utc) strnd["end"] = datetime(now.year, now.month, now.day, hou, minu, sec, timezone.utc)
else: else:
evt.end = datetime(now.year, now.month, now.day + 1, hou, minu, sec, timezone.utc) strnd["end"] = datetime(now.year, now.month, now.day + 1, hou, minu, sec, timezone.utc)
evt._end = strnd.getDate("end")
strnd["_id"] = context.add_event(evt)
except: except:
raise IMException("Mauvais format de date pour l'événement %s. Il n'a pas été créé." % msg.args[0]) context.data.delChild(strnd)
raise IRCException("Mauvais format de date pour l'événement %s. Il n'a pas été créé." % msg.args[0])
elif result1 is not None and len(result1) > 0: elif result1 is not None and len(result1) > 0:
evt.end = msg.date strnd["end"] = msg.date
for (t, g) in result1: for (t, g) in result1:
if g is None or g == "" or g == "m" or g == "M": if g is None or g == "" or g == "m" or g == "M":
evt.end += timedelta(minutes=int(t)) strnd["end"] += timedelta(minutes=int(t))
elif g == "h" or g == "H": elif g == "h" or g == "H":
evt.end += timedelta(hours=int(t)) strnd["end"] += timedelta(hours=int(t))
elif g == "d" or g == "D" or g == "j" or g == "J": elif g == "d" or g == "D" or g == "j" or g == "J":
evt.end += timedelta(days=int(t)) strnd["end"] += timedelta(days=int(t))
elif g == "w" or g == "W": elif g == "w" or g == "W":
evt.end += timedelta(days=int(t)*7) strnd["end"] += timedelta(days=int(t)*7)
elif g == "y" or g == "Y" or g == "a" or g == "A": elif g == "y" or g == "Y" or g == "a" or g == "A":
evt.end += timedelta(days=int(t)*365) strnd["end"] += timedelta(days=int(t)*365)
else: else:
evt.end += timedelta(seconds=int(t)) strnd["end"] += timedelta(seconds=int(t))
evt._end = strnd.getDate("end")
eid = context.add_event(evt)
if eid is not None:
strnd["_id"] = eid
else:
raise IMException("Mauvais format de date pour l'événement %s. Il n'a pas été créé." % msg.args[0])
context.data[msg.args[0]] = evt
context.save() context.save()
if "end" in strnd:
if evt.end is not None:
context.add_event(ModuleEvent(partial(fini, msg.args[0], evt),
offset=evt.end - datetime.now(timezone.utc),
interval=0))
return Response("%s commencé le %s et se terminera le %s." % return Response("%s commencé le %s et se terminera le %s." %
(msg.args[0], msg.date.strftime("%A %d %B %Y à %H:%M:%S"), (msg.args[0], msg.date.strftime("%A %d %B %Y à %H:%M:%S"),
evt.end.strftime("%A %d %B %Y à %H:%M:%S")), strnd.getDate("end").strftime("%A %d %B %Y à %H:%M:%S")),
channel=msg.channel) nick=msg.frm)
else: else:
return Response("%s commencé le %s"% (msg.args[0], return Response("%s commencé le %s"% (msg.args[0],
msg.date.strftime("%A %d %B %Y à %H:%M:%S")), msg.date.strftime("%A %d %B %Y à %H:%M:%S")),
channel=msg.channel) nick=msg.frm)
@hook("cmd_hook", "end")
@hook.command("end") @hook("cmd_hook", "forceend")
@hook.command("forceend")
def end_countdown(msg): def end_countdown(msg):
if len(msg.args) < 1: if len(msg.args) < 1:
raise IMException("quel événement terminer ?") raise IRCException("quel événement terminer ?")
if msg.args[0] in context.data: if msg.args[0] in context.data.index:
if context.data[msg.args[0]].creator == msg.frm or (msg.cmd == "forceend" and msg.frm_owner): if context.data.index[msg.args[0]]["proprio"] == msg.nick or (msg.cmd == "forceend" and msg.frm_owner):
duration = countdown(msg.date - context.data[msg.args[0]].start) duration = countdown(msg.date - context.data.index[msg.args[0]].getDate("start"))
del context.data[msg.args[0]] context.del_event(context.data.index[msg.args[0]]["_id"])
context.data.delChild(context.data.index[msg.args[0]])
context.save() context.save()
return Response("%s a duré %s." % (msg.args[0], duration), return Response("%s a duré %s." % (msg.args[0], duration),
channel=msg.channel, nick=msg.frm) channel=msg.channel, nick=msg.nick)
else: else:
raise IMException("Vous ne pouvez pas terminer le compteur %s, créé par %s." % (msg.args[0], context.data[msg.args[0]].creator)) raise IRCException("Vous ne pouvez pas terminer le compteur %s, créé par %s." % (msg.args[0], context.data.index[msg.args[0]]["proprio"]))
else: else:
return Response("%s n'est pas un compteur connu."% (msg.args[0]), channel=msg.channel, nick=msg.frm) return Response("%s n'est pas un compteur connu."% (msg.args[0]), channel=msg.channel, nick=msg.nick)
@hook("cmd_hook", "eventslist")
@hook.command("eventslist")
def liste(msg): def liste(msg):
"""!eventslist: gets list of timer""" """!eventslist: gets list of timer"""
if len(msg.args): if len(msg.args):
res = Response(channel=msg.channel) res = list()
for user in msg.args: for user in msg.args:
cmptr = [k for k in context.data if context.data[k].creator == user] cmptr = [x["name"] for x in context.data.index.values() if x["proprio"] == user]
if len(cmptr) > 0: if len(cmptr) > 0:
res.append_message(cmptr, title="Events created by %s" % user) res.append("Compteurs créés par %s : %s" % (user, ", ".join(cmptr)))
else: else:
res.append_message("%s doesn't have any counting events" % user) res.append("%s n'a pas créé de compteur" % user)
return res return Response(" ; ".join(res), channel=msg.channel)
else: else:
return Response(list(context.data.keys()), channel=msg.channel, title="Known events") return Response("Compteurs connus : %s." % ", ".join(context.data.index.keys()), channel=msg.channel)
@hook("cmd_default")
@hook.command(match=lambda msg: isinstance(msg, Command) and msg.cmd in context.data)
def parseanswer(msg): def parseanswer(msg):
res = Response(channel=msg.channel) if msg.cmd in context.data.index:
res = Response(channel=msg.channel)
# Avoid message starting by ! which can be interpreted as command by other bots # Avoid message starting by ! which can be interpreted as command by other bots
if msg.cmd[0] == "!": if msg.cmd[0] == "!":
res.nick = msg.frm res.nick = msg.nick
if msg.cmd in context.data: if context.data.index[msg.cmd].name == "strend":
if context.data[msg.cmd].end: if context.data.index[msg.cmd].hasAttribute("end"):
res.append_message("%s commencé il y a %s et se terminera dans %s." % (msg.cmd, countdown(msg.date - context.data[msg.cmd].start), countdown(context.data[msg.cmd].end - msg.date))) res.append_message("%s commencé il y a %s et se terminera dans %s." % (msg.cmd, countdown(msg.date - context.data.index[msg.cmd].getDate("start")), countdown(context.data.index[msg.cmd].getDate("end") - msg.date)))
else:
res.append_message("%s commencé il y a %s." % (msg.cmd, countdown(msg.date - context.data.index[msg.cmd].getDate("start"))))
else: else:
res.append_message("%s commencé il y a %s." % (msg.cmd, countdown(msg.date - context.data[msg.cmd].start))) res.append_message(countdown_format(context.data.index[msg.cmd].getDate("start"), context.data.index[msg.cmd]["msg_before"], context.data.index[msg.cmd]["msg_after"]))
else: return res
res.append_message(countdown_format(context.data[msg.cmd].start, context.data[msg.cmd]["msg_before"], context.data[msg.cmd]["msg_after"]))
return res
RGXP_ask = re.compile(r"^.*((create|new)\s+(a|an|a\s*new|an\s*other)?\s*(events?|commande?)|(nouvel(le)?|ajoute|cr[ée]{1,3})\s+(un)?\s*([eé]v[ée]nements?|commande?)).*$", re.I) RGXP_ask = re.compile(r"^.*((create|new)\s+(a|an|a\s*new|an\s*other)?\s*(events?|commande?)|(nouvel(le)?|ajoute|cr[ée]{1,3})\s+(un)?\s*([eé]v[ée]nements?|commande?)).*$", re.I)
@hook.ask(match=lambda msg: RGXP_ask.match(msg.message)) @hook("ask_default")
def parseask(msg): def parseask(msg):
name = re.match("^.*!([^ \"'@!]+).*$", msg.message) if RGXP_ask.match(msg.text) is not None:
if name is None: name = re.match("^.*!([^ \"'@!]+).*$", msg.text)
raise IMException("il faut que tu attribues une commande à l'événement.") if name is None:
if name.group(1) in context.data: raise IRCException("il faut que tu attribues une commande à l'événement.")
raise IMException("un événement portant ce nom existe déjà.") if name.group(1) in context.data.index:
raise IRCException("un événement portant ce nom existe déjà.")
texts = re.match("^[^\"]*(avant|après|apres|before|after)?[^\"]*\"([^\"]+)\"[^\"]*((avant|après|apres|before|after)?.*\"([^\"]+)\".*)?$", msg.message, re.I) texts = re.match("^[^\"]*(avant|après|apres|before|after)?[^\"]*\"([^\"]+)\"[^\"]*((avant|après|apres|before|after)?.*\"([^\"]+)\".*)?$", msg.text, re.I)
if texts is not None and texts.group(3) is not None: if texts is not None and texts.group(3) is not None:
extDate = extractDate(msg.message) extDate = extractDate(msg.text)
if extDate is None or extDate == "": if extDate is None or extDate == "":
raise IMException("la date de l'événement est invalide !") raise IRCException("la date de l'événement est invalide !")
if texts.group(1) is not None and (texts.group(1) == "après" or texts.group(1) == "apres" or texts.group(1) == "after"): if texts.group(1) is not None and (texts.group(1) == "après" or texts.group(1) == "apres" or texts.group(1) == "after"):
msg_after = texts.group(2) msg_after = texts.group (2)
msg_before = texts.group(5) msg_before = texts.group (5)
if (texts.group(4) is not None and (texts.group(4) == "après" or texts.group(4) == "apres" or texts.group(4) == "after")) or texts.group(1) is None: if (texts.group(4) is not None and (texts.group(4) == "après" or texts.group(4) == "apres" or texts.group(4) == "after")) or texts.group(1) is None:
msg_before = texts.group(2) msg_before = texts.group (2)
msg_after = texts.group(5) msg_after = texts.group (5)
if msg_before.find("%s") == -1 or msg_after.find("%s") == -1: if msg_before.find("%s") == -1 or msg_after.find("%s") == -1:
raise IMException("Pour que l'événement soit valide, ajouter %s à" raise IRCException("Pour que l'événement soit valide, ajouter %s à"
" l'endroit où vous voulez que soit ajouté le" " l'endroit où vous voulez que soit ajouté le"
" compte à rebours.") " compte à rebours.")
evt = ModuleState("event") evt = ModuleState("event")
evt["server"] = msg.server evt["server"] = msg.server
evt["channel"] = msg.channel evt["channel"] = msg.channel
evt["proprio"] = msg.frm evt["proprio"] = msg.nick
evt["name"] = name.group(1) evt["name"] = name.group(1)
evt["start"] = extDate evt["start"] = extDate
evt["msg_after"] = msg_after evt["msg_after"] = msg_after
evt["msg_before"] = msg_before evt["msg_before"] = msg_before
context.data.addChild(evt) context.data.addChild(evt)
context.save() context.save()
return Response("Nouvel événement !%s ajouté avec succès." % name.group(1), return Response("Nouvel événement !%s ajouté avec succès." % name.group(1),
channel=msg.channel) channel=msg.channel)
elif texts is not None and texts.group(2) is not None: elif texts is not None and texts.group (2) is not None:
evt = ModuleState("event") evt = ModuleState("event")
evt["server"] = msg.server evt["server"] = msg.server
evt["channel"] = msg.channel evt["channel"] = msg.channel
evt["proprio"] = msg.frm evt["proprio"] = msg.nick
evt["name"] = name.group(1) evt["name"] = name.group(1)
evt["msg_before"] = texts.group (2) evt["msg_before"] = texts.group (2)
context.data.addChild(evt) context.data.addChild(evt)
context.save() context.save()
return Response("Nouvelle commande !%s ajoutée avec succès." % name.group(1), return Response("Nouvelle commande !%s ajoutée avec succès." % name.group(1),
channel=msg.channel) channel=msg.channel)
else: else:
raise IMException("Veuillez indiquez les messages d'attente et d'après événement entre guillemets.") raise IRCException("Veuillez indiquez les messages d'attente et d'après événement entre guillemets.")

127
modules/framalink.py Normal file
View file

@ -0,0 +1,127 @@
"""URL reducer module"""
# PYTHON STUFFS #######################################################
import re
import json
from urllib.parse import urlparse
from urllib.parse import quote
from nemubot.exception import IRCException
from nemubot.hooks import hook
from nemubot.message import Text
from nemubot.tools import web
# MODULE FUCNTIONS ####################################################
def default_reducer(url, data):
snd_url = url + quote(data, "/:%@&=?")
return web.getURLContent(snd_url)
def framalink_reducer(url, data):
json_data = json.loads(web.getURLContent(url, "lsturl="
+ quote(data, "/:%@&=?"),
header={"Content-Type": "application/x-www-form-urlencoded"}))
return json_data['short']
# MODULE VARIABLES ####################################################
PROVIDERS = {
"tinyurl": (default_reducer, "http://tinyurl.com/api-create.php?url="),
"ycc": (default_reducer, "http://ycc.fr/redirection/create/"),
"framalink": (framalink_reducer, "https://frama.link/a?format=json")
}
DEFAULT_PROVIDER = "framalink"
PROVIDERS_NETLOC = [urlparse(web.getNormalizedURL(url), "http").netloc for f, url in PROVIDERS.values()]
# LOADING #############################################################
def load(context):
global DEFAULT_PROVIDER
if "provider" in context.config:
if context.config["provider"] == "custom":
PROVIDERS["custom"] = context.config["provider_url"]
DEFAULT_PROVIDER = context.config["provider"]
# MODULE CORE #########################################################
def reduce(url):
"""Ask the url shortner website to reduce given URL
Argument:
url -- the URL to reduce
"""
return PROVIDERS[DEFAULT_PROVIDER][0](PROVIDERS[DEFAULT_PROVIDER][1], url)
def gen_response(res, msg, srv):
if res is None:
raise IRCException("bad URL : %s" % srv)
else:
return Text("URL for %s: %s" % (srv, res), server=None,
to=msg.to_response)
## URL stack
LAST_URLS = dict()
@hook("msg_default")
def parselisten(msg):
parseresponse(msg)
return None
@hook("all_post")
def parseresponse(msg):
global LAST_URLS
if hasattr(msg, "text") and msg.text:
urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?[^ :]+)", msg.text)
for url in urls:
o = urlparse(web._getNormalizedURL(url), "http")
# Skip short URLs
if o.netloc == "" or o.netloc in PROVIDERS or len(o.netloc) + len(o.path) < 17:
continue
for recv in msg.receivers:
if recv not in LAST_URLS:
LAST_URLS[recv] = list()
LAST_URLS[recv].append(url)
return msg
# MODULE INTERFACE ####################################################
@hook("cmd_hook", "framalink",
help="Reduce any given URL",
help_usage={None: "Reduce the last URL said on the channel",
"URL [URL ...]": "Reduce the given URL(s)"})
def cmd_reduceurl(msg):
minify = list()
if not len(msg.args):
global LAST_URLS
if msg.channel in LAST_URLS and len(LAST_URLS[msg.channel]) > 0:
minify.append(LAST_URLS[msg.channel].pop())
else:
raise IRCException("I have no more URL to reduce.")
if len(msg.args) > 4:
raise IRCException("I cannot reduce as much URL at once.")
else:
minify += msg.args
res = list()
for url in minify:
o = urlparse(web.getNormalizedURL(url), "http")
minief_url = reduce(url)
if o.netloc == "":
res.append(gen_response(minief_url, msg, o.scheme))
else:
res.append(gen_response(minief_url, msg, o.netloc))
return res

View file

@ -1,64 +0,0 @@
"""Inform about Free Mobile tarifs"""
# PYTHON STUFFS #######################################################
import urllib.parse
from bs4 import BeautifulSoup
from nemubot.exception import IMException
from nemubot.hooks import hook
from nemubot.tools import web
from nemubot.module.more import Response
# MODULE CORE #########################################################
ACT = {
"ff_toFixe": "Appel vers les fixes",
"ff_toMobile": "Appel vers les mobiles",
"ff_smsSendedToCountry": "SMS vers le pays",
"ff_mmsSendedToCountry": "MMS vers le pays",
"fc_callToFrance": "Appel vers la France",
"fc_smsToFrance": "SMS vers la france",
"fc_mmsSended": "MMS vers la france",
"fc_callToSameCountry": "Réception des appels",
"fc_callReceived": "Appel dans le pays",
"fc_smsReceived": "SMS (Réception)",
"fc_mmsReceived": "MMS (Réception)",
"fc_moDataFromCountry": "Data",
}
def get_land_tarif(country, forfait="pkgFREE"):
url = "http://mobile.international.free.fr/?" + urllib.parse.urlencode({'pays': country})
page = web.getURLContent(url)
soup = BeautifulSoup(page)
fact = soup.find(class_=forfait)
if fact is None:
raise IMException("Country or forfait not found.")
res = {}
for s in ACT.keys():
try:
res[s] = fact.find(attrs={"data-bind": "text: " + s}).text + " " + fact.find(attrs={"data-bind": "html: " + s + "Unit"}).text
except AttributeError:
res[s] = "inclus"
return res
@hook.command("freetarifs",
help="Show Free Mobile tarifs for given contries",
help_usage={"COUNTRY": "Show Free Mobile tarifs for given CONTRY"},
keywords={
"forfait=FORFAIT": "Related forfait between Free (default) and 2euro"
})
def get_freetarif(msg):
res = Response(channel=msg.channel)
for country in msg.args:
t = get_land_tarif(country.lower().capitalize(), "pkg" + (msg.kwargs["forfait"] if "forfait" in msg.kwargs else "FREE").upper())
res.append_message(["\x02%s\x0F : %s" % (ACT[k], t[k]) for k in sorted(ACT.keys(), reverse=True)], title=country)
return res

View file

@ -1,38 +1,40 @@
"""Repositories, users or issues on GitHub""" # coding=utf-8
# PYTHON STUFFS ####################################################### """Repositories, users or issues on GitHub"""
import re import re
from urllib.parse import quote from urllib.parse import quote
from nemubot.exception import IMException from nemubot.exception import IRCException
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.tools import web from nemubot.tools import web
from nemubot.module.more import Response nemubotversion = 3.4
from more import Response
# MODULE CORE ######################################################### def help_full():
return ("!github /repo/: Display information about /repo/.\n"
"!github_user /user/: Display information about /user/.")
def info_repos(repo): def info_repos(repo):
return web.getJSON("https://api.github.com/search/repositories?q=%s" % return web.getJSON("https://api.github.com/search/repositories?q=%s" %
quote(repo)) quote(repo), timeout=10)
def info_user(username): def info_user(username):
user = web.getJSON("https://api.github.com/users/%s" % quote(username)) user = web.getJSON("https://api.github.com/users/%s" % quote(username),
timeout=10)
user["repos"] = web.getJSON("https://api.github.com/users/%s/" user["repos"] = web.getJSON("https://api.github.com/users/%s/"
"repos?sort=updated" % quote(username)) "repos?sort=updated" % quote(username),
timeout=10)
return user return user
def user_keys(username):
keys = web.getURLContent("https://github.com/%s.keys" % quote(username))
return keys.split('\n')
def info_issue(repo, issue=None): def info_issue(repo, issue=None):
rp = info_repos(repo) rp = info_repos(repo)
if rp["items"]: if rp["items"]:
@ -63,16 +65,10 @@ def info_commit(repo, commit=None):
quote(fullname)) quote(fullname))
# MODULE INTERFACE #################################################### @hook("cmd_hook", "github")
@hook.command("github",
help="Display information about some repositories",
help_usage={
"REPO": "Display information about the repository REPO",
})
def cmd_github(msg): def cmd_github(msg):
if not len(msg.args): if not len(msg.args):
raise IMException("indicate a repository name to search") raise IRCException("indicate a repository name to search")
repos = info_repos(" ".join(msg.args)) repos = info_repos(" ".join(msg.args))
@ -97,14 +93,10 @@ def cmd_github(msg):
return res return res
@hook.command("github_user", @hook("cmd_hook", "github_user")
help="Display information about users", def cmd_github(msg):
help_usage={
"USERNAME": "Display information about the user USERNAME",
})
def cmd_github_user(msg):
if not len(msg.args): if not len(msg.args):
raise IMException("indicate a user name to search") raise IRCException("indicate a user name to search")
res = Response(channel=msg.channel, nomore="No more user") res = Response(channel=msg.channel, nomore="No more user")
@ -129,37 +121,15 @@ def cmd_github_user(msg):
user["html_url"], user["html_url"],
kf)) kf))
else: else:
raise IMException("User not found") raise IRCException("User not found")
return res return res
@hook.command("github_user_keys", @hook("cmd_hook", "github_issue")
help="Display user SSH keys", def cmd_github(msg):
help_usage={
"USERNAME": "Show USERNAME's SSH keys",
})
def cmd_github_user_keys(msg):
if not len(msg.args): if not len(msg.args):
raise IMException("indicate a user name to search") raise IRCException("indicate a repository to view its issues")
res = Response(channel=msg.channel, nomore="No more keys")
for k in user_keys(" ".join(msg.args)):
res.append_message(k)
return res
@hook.command("github_issue",
help="Display repository's issues",
help_usage={
"REPO": "Display latest issues created on REPO",
"REPO #ISSUE": "Display the issue number #ISSUE for REPO",
})
def cmd_github_issue(msg):
if not len(msg.args):
raise IMException("indicate a repository to view its issues")
issue = None issue = None
@ -180,7 +150,7 @@ def cmd_github_issue(msg):
issues = info_issue(repo, issue) issues = info_issue(repo, issue)
if issues is None: if issues is None:
raise IMException("Repository not found") raise IRCException("Repository not found")
for issue in issues: for issue in issues:
res.append_message("%s%s issue #%d: \x03\x02%s\x03\x02 opened by %s on %s: %s" % res.append_message("%s%s issue #%d: \x03\x02%s\x03\x02 opened by %s on %s: %s" %
@ -194,15 +164,10 @@ def cmd_github_issue(msg):
return res return res
@hook.command("github_commit", @hook("cmd_hook", "github_commit")
help="Display repository's commits", def cmd_github(msg):
help_usage={
"REPO": "Display latest commits on REPO",
"REPO COMMIT": "Display details for the COMMIT on REPO",
})
def cmd_github_commit(msg):
if not len(msg.args): if not len(msg.args):
raise IMException("indicate a repository to view its commits") raise IRCException("indicate a repository to view its commits")
commit = None commit = None
if re.match("^[a-fA-F0-9]+$", msg.args[0]): if re.match("^[a-fA-F0-9]+$", msg.args[0]):
@ -220,7 +185,7 @@ def cmd_github_commit(msg):
commits = info_commit(repo, commit) commits = info_commit(repo, commit)
if commits is None: if commits is None:
raise IMException("Repository or commit not found") raise IRCException("Repository not found")
for commit in commits: for commit in commits:
res.append_message("Commit %s by %s on %s: %s" % res.append_message("Commit %s by %s on %s: %s" %

View file

@ -1,85 +0,0 @@
"""Filter messages, displaying lines matching a pattern"""
# PYTHON STUFFS #######################################################
import re
from nemubot import context
from nemubot.exception import IMException
from nemubot.hooks import hook
from nemubot.message import Command, Text
from nemubot.module.more import Response
# MODULE CORE #########################################################
def grep(fltr, cmd, msg, icase=False, only=False):
"""Perform a grep like on known nemubot structures
Arguments:
fltr -- The filter regexp
cmd -- The subcommand to execute
msg -- The original message
icase -- like the --ignore-case parameter of grep
only -- like the --only-matching parameter of grep
"""
fltr = re.compile(fltr, re.I if icase else 0)
for r in context.subtreat(context.subparse(msg, cmd)):
if isinstance(r, Response):
for i in range(len(r.messages) - 1, -1, -1):
if isinstance(r.messages[i], list):
for j in range(len(r.messages[i]) - 1, -1, -1):
res = fltr.match(r.messages[i][j])
if not res:
r.messages[i].pop(j)
elif only:
r.messages[i][j] = res.group(1) if fltr.groups else res.group(0)
if len(r.messages[i]) <= 0:
r.messages.pop(i)
elif isinstance(r.messages[i], str):
res = fltr.match(r.messages[i])
if not res:
r.messages.pop(i)
elif only:
r.messages[i] = res.group(1) if fltr.groups else res.group(0)
yield r
elif isinstance(r, Text):
res = fltr.match(r.message)
if res:
if only:
r.message = res.group(1) if fltr.groups else res.group(0)
yield r
else:
yield r
# MODULE INTERFACE ####################################################
@hook.command("grep",
help="Display only lines from a subcommand matching the given pattern",
help_usage={"PTRN !SUBCMD": "Filter SUBCMD command using the pattern PTRN"},
keywords={
"nocase": "Perform case-insensitive matching",
"only": "Print only the matched parts of a matching line",
})
def cmd_grep(msg):
if len(msg.args) < 2:
raise IMException("Please provide a filter and a command")
only = "only" in msg.kwargs
l = [m for m in grep(msg.args[0] if len(msg.args[0]) and msg.args[0][0] == "^" else ".*?(" + msg.args[0] + ").*?",
" ".join(msg.args[1:]),
msg,
icase="nocase" in msg.kwargs,
only=only) if m is not None]
if len(l) <= 0:
raise IMException("Pattern not found in output")
return l

View file

@ -1,115 +1,112 @@
"""Show many information about a movie or serie""" # coding=utf-8
# PYTHON STUFFS ####################################################### """Show many information about a movie or serie"""
import re import re
import urllib.parse import urllib.parse
from bs4 import BeautifulSoup from nemubot.exception import IRCException
from nemubot.exception import IMException
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.tools import web from nemubot.tools import web
from nemubot.module.more import Response nemubotversion = 3.4
from more import Response
# MODULE CORE ######################################################### def help_full():
return "Search a movie title with: !imdbs <approximative title> ; View movie details with !imdb <title>"
def get_movie_by_id(imdbid):
def get_movie(title=None, year=None, imdbid=None, fullplot=True, tomatoes=False):
"""Returns the information about the matching movie""" """Returns the information about the matching movie"""
url = "http://www.imdb.com/title/" + urllib.parse.quote(imdbid)
soup = BeautifulSoup(web.getURLContent(url))
return {
"imdbID": imdbid,
"Title": soup.body.find('h1').contents[0].strip(),
"Year": soup.body.find(id="titleYear").find("a").text.strip() if soup.body.find(id="titleYear") else ", ".join([y.text.strip() for y in soup.body.find(attrs={"class": "seasons-and-year-nav"}).find_all("a")[1:]]),
"Duration": soup.body.find(attrs={"class": "title_wrapper"}).find("time").text.strip() if soup.body.find(attrs={"class": "title_wrapper"}).find("time") else None,
"imdbRating": soup.body.find(attrs={"class": "ratingValue"}).find("strong").text.strip() if soup.body.find(attrs={"class": "ratingValue"}) else None,
"imdbVotes": soup.body.find(attrs={"class": "imdbRating"}).find("a").text.strip() if soup.body.find(attrs={"class": "imdbRating"}) else None,
"Plot": re.sub(r"\s+", " ", soup.body.find(attrs={"class": "summary_text"}).text).strip(),
"Type": "TV Series" if soup.find(id="title-episode-widget") else "Movie",
"Genre": ", ".join([x.text.strip() for x in soup.body.find(id="titleStoryLine").find_all("a") if x.get("href") is not None and x.get("href")[:21] == "/search/title?genres="]),
"Country": ", ".join([x.text.strip() for x in soup.body.find(id="titleDetails").find_all("a") if x.get("href") is not None and x.get("href")[:32] == "/search/title?country_of_origin="]),
"Credits": " ; ".join([x.find("h4").text.strip() + " " + (", ".join([y.text.strip() for y in x.find_all("a") if y.get("href") is not None and y.get("href")[:6] == "/name/"])) for x in soup.body.find_all(attrs={"class": "credit_summary_item"})]),
}
def find_movies(title, year=None):
"""Find existing movies matching a approximate title"""
title = title.lower()
# Built URL # Built URL
url = "https://v2.sg.media-imdb.com/suggests/%s/%s.json" % (urllib.parse.quote(title[0]), urllib.parse.quote(title.replace(" ", "_"))) url = "http://www.omdbapi.com/?"
if title is not None:
url += "t=%s&" % urllib.parse.quote(title)
if year is not None:
url += "y=%s&" % urllib.parse.quote(year)
if imdbid is not None:
url += "i=%s&" % urllib.parse.quote(imdbid)
if fullplot:
url += "plot=full&"
if tomatoes:
url += "tomatoes=true&"
# Make the request # Make the request
data = web.getJSON(url, remove_callback=True) data = web.getJSON(url)
# Return data
if "Error" in data:
raise IRCException(data["Error"])
elif "Response" in data and data["Response"] == "True":
return data
if "d" not in data:
return None
elif year is None:
return data["d"]
else: else:
return [d for d in data["d"] if "y" in d and str(d["y"]) == year] raise IRCException("An error occurs during movie search")
# MODULE INTERFACE #################################################### def find_movies(title):
"""Find existing movies matching a approximate title"""
@hook.command("imdb", # Built URL
help="View movie/serie details, using OMDB", url = "http://www.omdbapi.com/?s=%s" % urllib.parse.quote(title)
help_usage={
"TITLE": "Look for a movie titled TITLE", # Make the request
"IMDB_ID": "Look for the movie with the given IMDB_ID", data = web.getJSON(url)
})
# Return data
if "Error" in data:
raise IRCException(data["Error"])
elif "Search" in data:
return data
else:
raise IRCException("An error occurs during movie search")
@hook("cmd_hook", "imdb")
def cmd_imdb(msg): def cmd_imdb(msg):
"""View movie details with !imdb <title>"""
if not len(msg.args): if not len(msg.args):
raise IMException("precise a movie/serie title!") raise IRCException("precise a movie/serie title!")
title = ' '.join(msg.args) title = ' '.join(msg.args)
if re.match("^tt[0-9]{7}$", title) is not None: if re.match("^tt[0-9]{7}$", title) is not None:
data = get_movie_by_id(imdbid=title) data = get_movie(imdbid=title)
else: else:
rm = re.match(r"^(.+)\s\(([0-9]{4})\)$", title) rm = re.match(r"^(.+)\s\(([0-9]{4})\)$", title)
if rm is not None: if rm is not None:
data = find_movies(rm.group(1), year=rm.group(2)) data = get_movie(title=rm.group(1), year=rm.group(2))
else: else:
data = find_movies(title) data = get_movie(title=title)
if not data:
raise IMException("Movie/series not found")
data = get_movie_by_id(data[0]["id"])
res = Response(channel=msg.channel, res = Response(channel=msg.channel,
title="%s (%s)" % (data['Title'], data['Year']), title="%s (%s)" % (data['Title'], data['Year']),
nomore="No more information, more at http://www.imdb.com/title/%s" % data['imdbID']) nomore="No more information, more at http://www.imdb.com/title/%s" % data['imdbID'])
res.append_message("%s \x02genre:\x0F %s; \x02rating\x0F: %s (%s votes); \x02plot\x0F: %s" % res.append_message("\x02rating\x0F: %s (%s votes); \x02plot\x0F: %s" %
(data['Type'], data['Genre'], data['imdbRating'], data['imdbVotes'], data['Plot'])) (data['imdbRating'], data['imdbVotes'], data['Plot']))
res.append_message("%s \x02from\x0F %s; %s"
% (data['Type'], data['Country'], data['Credits']))
res.append_message("%s \x02from\x0F %s \x02released on\x0F %s; \x02genre:\x0F %s; \x02directed by:\x0F %s; \x02written by:\x0F %s; \x02main actors:\x0F %s"
% (data['Type'], data['Country'], data['Released'], data['Genre'], data['Director'], data['Writer'], data['Actors']))
return res return res
@hook.command("imdbs", @hook("cmd_hook", "imdbs")
help="Search a movie/serie by title",
help_usage={
"TITLE": "Search a movie/serie by TITLE",
})
def cmd_search(msg): def cmd_search(msg):
"""!imdbs <approximative title> to search a movie title"""
if not len(msg.args): if not len(msg.args):
raise IMException("precise a movie/serie title!") raise IRCException("precise a movie/serie title!")
data = find_movies(' '.join(msg.args)) data = find_movies(' '.join(msg.args))
movies = list() movies = list()
for m in data: for m in data['Search']:
movies.append("\x02%s\x0F%s with %s" % (m['l'], (" (" + str(m['y']) + ")") if "y" in m else "", m['s'])) movies.append("\x02%s\x0F (%s of %s)" % (m['Title'], m['Type'], m['Year']))
return Response(movies, title="Titles found", channel=msg.channel) return Response(movies, title="Titles found", channel=msg.channel)

View file

@ -1,7 +1,9 @@
from bs4 import BeautifulSoup
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.exception import IMException from nemubot.exception import IRCException
from nemubot.tools import web from nemubot.tools import web
from nemubot.module.more import Response from more import Response
import json import json
nemubotversion = 3.4 nemubotversion = 3.4
@ -39,18 +41,18 @@ def getJsonKeys(data):
else: else:
return data.keys() return data.keys()
@hook.command("json") @hook("cmd_hook", "json")
def get_json_info(msg): def get_json_info(msg):
if not len(msg.args): if not len(msg.args):
raise IMException("Please specify a url and a list of JSON keys.") raise IRCException("Please specify a url and a list of JSON keys.")
request_data = web.getURLContent(msg.args[0].replace(' ', "%20")) request_data = web.getURLContent(msg.args[0].replace(' ', "%20"))
if not request_data: if not request_data:
raise IMException("Please specify a valid url.") raise IRCException("Please specify a valid url.")
json_data = json.loads(request_data) json_data = json.loads(request_data)
if len(msg.args) == 1: if len(msg.args) == 1:
raise IMException("Please specify the keys to return (%s)" % ", ".join(getJsonKeys(json_data))) raise IRCException("Please specify the keys to return (%s)" % ", ".join(getJsonKeys(json_data)))
tags = ','.join(msg.args[1:]).split(',') tags = ','.join(msg.args[1:]).split(',')
response = getRequestedTags(tags, json_data) response = getRequestedTags(tags, json_data)

View file

@ -1,6 +1,6 @@
"""Read manual pages on IRC""" # coding=utf-8
# PYTHON STUFFS ####################################################### "Read manual pages on IRC"
import subprocess import subprocess
import re import re
@ -8,22 +8,18 @@ import os
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.module.more import Response nemubotversion = 3.4
from more import Response
# GLOBALS ############################################################# def help_full():
return "!man [0-9] /what/: gives informations about /what/."
RGXP_s = re.compile(b'\x1b\\[[0-9]+m') RGXP_s = re.compile(b'\x1b\\[[0-9]+m')
# MODULE INTERFACE #################################################### @hook("cmd_hook", "MAN")
@hook.command("MAN",
help="Show man pages",
help_usage={
"SUBJECT": "Display the default man page for SUBJECT",
"SECTION SUBJECT": "Display the man page in SECTION for SUBJECT"
})
def cmd_man(msg): def cmd_man(msg):
args = ["man"] args = ["man"]
num = None num = None
@ -56,11 +52,7 @@ def cmd_man(msg):
return res return res
@hook.command("man", @hook("cmd_hook", "man")
help="Show man pages synopsis (in one line)",
help_usage={
"SUBJECT": "Display man page synopsis for SUBJECT",
})
def cmd_whatis(msg): def cmd_whatis(msg):
args = ["whatis", " ".join(msg.args)] args = ["whatis", " ".join(msg.args)]
@ -73,6 +65,10 @@ def cmd_whatis(msg):
res.append_message(" ".join(line.decode().split())) res.append_message(" ".join(line.decode().split()))
if len(res.messages) <= 0: if len(res.messages) <= 0:
res.append_message("There is no man page for %s." % msg.args[0]) if num is not None:
res.append_message("There is no entry %s in section %d." %
(msg.args[0], num))
else:
res.append_message("There is no man page for %s." % msg.args[0])
return res return res

View file

@ -1,34 +1,33 @@
"""Transform name location to GPS coordinates""" # coding=utf-8
# PYTHON STUFFS ####################################################### """Transform name location to GPS coordinates"""
import re import re
from urllib.parse import quote from urllib.parse import quote
from nemubot.exception import IMException from nemubot.exception import IRCException
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.tools import web from nemubot.tools import web
from nemubot.module.more import Response nemubotversion = 4.0
# GLOBALS ############################################################# from more import Response
URL_API = "https://open.mapquestapi.com/geocoding/v1/address?key=%s&location=%%s" URL_API = "http://open.mapquestapi.com/geocoding/v1/address?key=%s&location=%%s"
# LOADING #############################################################
def load(context): def load(context):
if not context.config or "apikey" not in context.config: if not context.config or not context.config.hasAttribute("apikey"):
raise ImportError("You need a MapQuest API key in order to use this " raise ImportError("You need a MapQuest API key in order to use this "
"module. Add it to the module configuration file:\n" "module. Add it to the module configuration file:\n"
"<module name=\"mapquest\" key=\"XXXXXXXXXXXXXXXX\" " "<module name=\"mapquest\" key=\"XXXXXXXXXXXXXXXX\" "
"/>\nRegister at https://developer.mapquest.com/") "/>\nRegister at http://developer.mapquest.com/")
global URL_API global URL_API
URL_API = URL_API % context.config["apikey"].replace("%", "%%") URL_API = URL_API % context.config["apikey"].replace("%", "%%")
# MODULE CORE ######################################################### def help_full():
return "!geocode /place/: get coordinate of /place/."
def geocode(location): def geocode(location):
obj = web.getJSON(URL_API % quote(location)) obj = web.getJSON(URL_API % quote(location))
@ -44,18 +43,12 @@ def where(loc):
"{adminArea1}".format(**loc)).strip() "{adminArea1}".format(**loc)).strip()
# MODULE INTERFACE #################################################### @hook("cmd_hook", "geocode")
@hook.command("geocode",
help="Get GPS coordinates of a place",
help_usage={
"PLACE": "Get GPS coordinates of PLACE"
})
def cmd_geocode(msg): def cmd_geocode(msg):
if not len(msg.args): if not len(msg.args):
raise IMException("indicate a name") raise IRCException("indicate a name")
res = Response(channel=msg.channel, nick=msg.frm, res = Response(channel=msg.channel, nick=msg.nick,
nomore="No more geocode", count=" (%s more geocode)") nomore="No more geocode", count=" (%s more geocode)")
for loc in geocode(' '.join(msg.args)): for loc in geocode(' '.join(msg.args)):

View file

@ -2,24 +2,25 @@
"""Use MediaWiki API to get pages""" """Use MediaWiki API to get pages"""
import json
import re import re
import urllib.parse import urllib.parse
from nemubot.exception import IMException from nemubot.exception import IRCException
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.tools import web from nemubot.tools import web
nemubotversion = 3.4 nemubotversion = 3.4
from nemubot.module.more import Response from more import Response
# MEDIAWIKI REQUESTS ################################################## # MEDIAWIKI REQUESTS ##################################################
def get_namespaces(site, ssl=False, path="/w/api.php"): def get_namespaces(site, ssl=False):
# Built URL # Built URL
url = "http%s://%s%s?format=json&action=query&meta=siteinfo&siprop=namespaces" % ( url = "http%s://%s/w/api.php?format=json&action=query&meta=siteinfo&siprop=namespaces" % (
"s" if ssl else "", site, path) "s" if ssl else "", site)
# Make the request # Make the request
data = web.getJSON(url) data = web.getJSON(url)
@ -30,10 +31,10 @@ def get_namespaces(site, ssl=False, path="/w/api.php"):
return namespaces return namespaces
def get_raw_page(site, term, ssl=False, path="/w/api.php"): def get_raw_page(site, term, ssl=False):
# Built URL # Built URL
url = "http%s://%s%s?format=json&redirects&action=query&prop=revisions&rvprop=content&titles=%s" % ( url = "http%s://%s/w/api.php?format=json&redirects&action=query&prop=revisions&rvprop=content&titles=%s" % (
"s" if ssl else "", site, path, urllib.parse.quote(term)) "s" if ssl else "", site, urllib.parse.quote(term))
# Make the request # Make the request
data = web.getJSON(url) data = web.getJSON(url)
@ -42,13 +43,13 @@ def get_raw_page(site, term, ssl=False, path="/w/api.php"):
try: try:
return data["query"]["pages"][k]["revisions"][0]["*"] return data["query"]["pages"][k]["revisions"][0]["*"]
except: except:
raise IMException("article not found") raise IRCException("article not found")
def get_unwikitextified(site, wikitext, ssl=False, path="/w/api.php"): def get_unwikitextified(site, wikitext, ssl=False):
# Built URL # Built URL
url = "http%s://%s%s?format=json&action=expandtemplates&text=%s" % ( url = "http%s://%s/w/api.php?format=json&action=expandtemplates&text=%s" % (
"s" if ssl else "", site, path, urllib.parse.quote(wikitext)) "s" if ssl else "", site, urllib.parse.quote(wikitext))
# Make the request # Make the request
data = web.getJSON(url) data = web.getJSON(url)
@ -58,25 +59,25 @@ def get_unwikitextified(site, wikitext, ssl=False, path="/w/api.php"):
## Search ## Search
def opensearch(site, term, ssl=False, path="/w/api.php"): def opensearch(site, term, ssl=False):
# Built URL # Built URL
url = "http%s://%s%s?format=json&action=opensearch&search=%s" % ( url = "http%s://%s/w/api.php?format=xml&action=opensearch&search=%s" % (
"s" if ssl else "", site, path, urllib.parse.quote(term)) "s" if ssl else "", site, urllib.parse.quote(term))
# Make the request # Make the request
response = web.getJSON(url) response = web.getXML(url)
if response is not None and len(response) >= 4: if response is not None and response.hasNode("Section"):
for k in range(len(response[1])): for itm in response.getNode("Section").getNodes("Item"):
yield (response[1][k], yield (itm.getNode("Text").getContent(),
response[2][k], itm.getNode("Description").getContent() if itm.hasNode("Description") else "",
response[3][k]) itm.getNode("Url").getContent())
def search(site, term, ssl=False, path="/w/api.php"): def search(site, term, ssl=False):
# Built URL # Built URL
url = "http%s://%s%s?format=json&action=query&list=search&srsearch=%s&srprop=titlesnippet|snippet" % ( url = "http%s://%s/w/api.php?format=json&action=query&list=search&srsearch=%s&srprop=titlesnippet|snippet" % (
"s" if ssl else "", site, path, urllib.parse.quote(term)) "s" if ssl else "", site, urllib.parse.quote(term))
# Make the request # Make the request
data = web.getJSON(url) data = web.getJSON(url)
@ -89,11 +90,6 @@ def search(site, term, ssl=False, path="/w/api.php"):
# PARSING FUNCTIONS ################################################### # PARSING FUNCTIONS ###################################################
def get_model(cnt, model="Infobox"):
for full in re.findall(r"(\{\{" + model + " .*?(?:\{\{.*?}}.*?)*}})", cnt, flags=re.DOTALL):
return full[3 + len(model):-2].replace("\n", " ").strip()
def strip_model(cnt): def strip_model(cnt):
# Strip models at begin: mostly useless # Strip models at begin: mostly useless
cnt = re.sub(r"^(({{([^{]|\s|({{([^{]|\s|{{.*?}})*?}})*?)*?}}|\[\[([^[]|\s|\[\[.*?\]\])*?\]\])\s*)+", "", cnt, flags=re.DOTALL) cnt = re.sub(r"^(({{([^{]|\s|({{([^{]|\s|{{.*?}})*?}})*?)*?}}|\[\[([^[]|\s|\[\[.*?\]\])*?\]\])\s*)+", "", cnt, flags=re.DOTALL)
@ -113,9 +109,9 @@ def strip_model(cnt):
return cnt return cnt
def parse_wikitext(site, cnt, namespaces=dict(), **kwargs): def parse_wikitext(site, cnt, namespaces=dict(), ssl=False):
for i, _, _, _ in re.findall(r"({{([^{]|\s|({{(.|\s|{{.*?}})*?}})*?)*?}})", cnt): for i, _, _, _ in re.findall(r"({{([^{]|\s|({{(.|\s|{{.*?}})*?}})*?)*?}})", cnt):
cnt = cnt.replace(i, get_unwikitextified(site, i, **kwargs), 1) cnt = cnt.replace(i, get_unwikitextified(site, i, ssl), 1)
# Strip [[...]] # Strip [[...]]
for full, args, lnk in re.findall(r"(\[\[(.*?|)?([^|]*?)\]\])", cnt): for full, args, lnk in re.findall(r"(\[\[(.*?|)?([^|]*?)\]\])", cnt):
@ -144,106 +140,71 @@ def irc_format(cnt):
return cnt.replace("'''", "\x03\x02").replace("''", "\x03\x1f") return cnt.replace("'''", "\x03\x02").replace("''", "\x03\x1f")
def parse_infobox(cnt): def get_page(site, term, ssl=False, subpart=None):
for v in cnt.split("|"): raw = get_raw_page(site, term, ssl)
try:
yield re.sub(r"^\s*([^=]*[^=\s])\s*=\s*(.+)\s*$", "\x03\x02" + r"\1" + ":\x03\x02 " + r"\2", v).replace("<br />", ", ").replace("<br/>", ", ").strip()
except:
yield re.sub(r"^\s+(.+)\s+$", "\x03\x02" + r"\1" + "\x03\x02", v).replace("<br />", ", ").replace("<br/>", ", ").strip()
def get_page(site, term, subpart=None, **kwargs):
raw = get_raw_page(site, term, **kwargs)
if subpart is not None: if subpart is not None:
subpart = subpart.replace("_", " ") subpart = subpart.replace("_", " ")
raw = re.sub(r"^.*(?P<title>==+)\s*(" + subpart + r")\s*(?P=title)", r"\1 \2 \1", raw, flags=re.DOTALL) raw = re.sub(r"^.*(?P<title>==+)\s*(" + subpart + r")\s*(?P=title)", r"\1 \2 \1", raw, flags=re.DOTALL)
return raw return strip_model(raw)
# NEMUBOT ############################################################# # NEMUBOT #############################################################
def mediawiki_response(site, term, to, **kwargs): def mediawiki_response(site, term, receivers):
ns = get_namespaces(site, **kwargs) ns = get_namespaces(site)
terms = term.split("#", 1) terms = term.split("#", 1)
try: try:
# Print the article if it exists # Print the article if it exists
return Response(strip_model(get_page(site, terms[0], subpart=terms[1] if len(terms) > 1 else None, **kwargs)), return Response(get_page(site, terms[0], subpart=terms[1] if len(terms) > 1 else None),
line_treat=lambda line: irc_format(parse_wikitext(site, line, ns, **kwargs)), line_treat=lambda line: irc_format(parse_wikitext(site, line, ns)),
channel=to) channel=receivers)
except: except:
pass pass
# Try looking at opensearch # Try looking at opensearch
os = [x for x, _, _ in opensearch(site, terms[0], **kwargs)] os = [x for x, _, _ in opensearch(site, terms[0])]
print(os)
# Fallback to global search # Fallback to global search
if not len(os): if not len(os):
os = [x for x, _ in search(site, terms[0], **kwargs) if x is not None and x != ""] os = [x for x, _ in search(site, terms[0]) if x is not None and x != ""]
return Response(os, return Response(os,
channel=to, channel=receivers,
title="Article not found, would you mean") title="Article not found, would you mean")
@hook.command("mediawiki", @hook("cmd_hook", "mediawiki")
help="Read an article on a MediaWiki",
keywords={
"ssl": "query over https instead of http",
"path=PATH": "absolute path to the API",
})
def cmd_mediawiki(msg): def cmd_mediawiki(msg):
"""Read an article on a MediaWiki"""
if len(msg.args) < 2: if len(msg.args) < 2:
raise IMException("indicate a domain and a term to search") raise IRCException("indicate a domain and a term to search")
return mediawiki_response(msg.args[0], return mediawiki_response(msg.args[0],
" ".join(msg.args[1:]), " ".join(msg.args[1:]),
msg.to_response, msg.receivers)
**msg.kwargs)
@hook.command("mediawiki_search", @hook("cmd_hook", "search_mediawiki")
help="Search an article on a MediaWiki",
keywords={
"ssl": "query over https instead of http",
"path=PATH": "absolute path to the API",
})
def cmd_srchmediawiki(msg): def cmd_srchmediawiki(msg):
"""Search an article on a MediaWiki"""
if len(msg.args) < 2: if len(msg.args) < 2:
raise IMException("indicate a domain and a term to search") raise IRCException("indicate a domain and a term to search")
res = Response(channel=msg.to_response, nomore="No more results", count=" (%d more results)") res = Response(channel=msg.receivers, nomore="No more results", count=" (%d more results)")
for r in search(msg.args[0], " ".join(msg.args[1:]), **msg.kwargs): for r in search(msg.args[0], " ".join(msg.args[1:])):
res.append_message("%s: %s" % r) res.append_message("%s: %s" % r)
return res return res
@hook.command("mediawiki_infobox", @hook("cmd_hook", "wikipedia")
help="Highlight information from an article on a MediaWiki",
keywords={
"ssl": "query over https instead of http",
"path=PATH": "absolute path to the API",
})
def cmd_infobox(msg):
if len(msg.args) < 2:
raise IMException("indicate a domain and a term to search")
ns = get_namespaces(msg.args[0], **msg.kwargs)
return Response(", ".join([x for x in parse_infobox(get_model(get_page(msg.args[0], " ".join(msg.args[1:]), **msg.kwargs), "Infobox"))]),
line_treat=lambda line: irc_format(parse_wikitext(msg.args[0], line, ns, **msg.kwargs)),
channel=msg.to_response)
@hook.command("wikipedia")
def cmd_wikipedia(msg): def cmd_wikipedia(msg):
if len(msg.args) < 2: if len(msg.args) < 2:
raise IMException("indicate a lang and a term to search") raise IRCException("indicate a lang and a term to search")
return mediawiki_response(msg.args[0] + ".wikipedia.org", return mediawiki_response(msg.args[0] + ".wikipedia.org",
" ".join(msg.args[1:]), " ".join(msg.args[1:]),
msg.to_response) msg.receivers)

View file

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
# Nemubot is a smart and modulable IM bot. # Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2015 Mercier Pierre-Olivier # Copyright (C) 2012-2015 Mercier Pierre-Olivier
# #
@ -16,18 +18,17 @@
"""Progressive display of very long messages""" """Progressive display of very long messages"""
# PYTHON STUFFS #######################################################
import logging import logging
import sys
from nemubot.message import Text, DirectAsk from nemubot.message import Text, DirectAsk
from nemubot.hooks import hook from nemubot.hooks import hook
nemubotversion = 3.4
logger = logging.getLogger("nemubot.response") logger = logging.getLogger("nemubot.response")
# MODULE CORE #########################################################
class Response: class Response:
def __init__(self, message=None, channel=None, nick=None, server=None, def __init__(self, message=None, channel=None, nick=None, server=None,
@ -50,7 +51,7 @@ class Response:
@property @property
def to(self): def receivers(self):
if self.channel is None: if self.channel is None:
if self.nick is not None: if self.nick is not None:
return [self.nick] return [self.nick]
@ -60,7 +61,6 @@ class Response:
else: else:
return [self.channel] return [self.channel]
def append_message(self, message, title=None, shown_first_count=-1): def append_message(self, message, title=None, shown_first_count=-1):
if type(message) is str: if type(message) is str:
message = message.split('\n') message = message.split('\n')
@ -91,7 +91,7 @@ class Response:
def append_content(self, message): def append_content(self, message):
if message is not None and len(message) > 0: if message is not None and len(message) > 0:
if self.messages is None or len(self.messages) == 0: if self.messages is None or len(self.messages) == 0:
self.messages = [message] self.messages = list(message)
self.alone = True self.alone = True
else: else:
self.messages[len(self.messages)-1] += message self.messages[len(self.messages)-1] += message
@ -141,10 +141,10 @@ class Response:
if self.nick: if self.nick:
return DirectAsk(self.nick, return DirectAsk(self.nick,
self.get_message(maxlen - len(self.nick) - 2), self.get_message(maxlen - len(self.nick) - 2),
server=None, to=self.to) server=None, to=self.receivers)
else: else:
return Text(self.get_message(maxlen), return Text(self.get_message(maxlen),
server=None, to=self.to) server=None, to=self.receivers)
def __str__(self): def __str__(self):
@ -181,16 +181,8 @@ class Response:
return self.nomore return self.nomore
if self.line_treat is not None and self.elt == 0: if self.line_treat is not None and self.elt == 0:
try: self.messages[0] = (self.line_treat(self.messages[0])
if isinstance(self.messages[0], list): .replace("\n", " ").strip())
for x in self.messages[0]:
print(x, self.line_treat(x))
self.messages[0] = [self.line_treat(x).replace("\n", " ").strip() for x in self.messages[0]]
else:
self.messages[0] = (self.line_treat(self.messages[0])
.replace("\n", " ").strip())
except Exception as e:
logger.exception(e)
msg = "" msg = ""
if self.title is not None: if self.title is not None:
@ -200,9 +192,7 @@ class Response:
msg += self.title + ": " msg += self.title + ": "
elif self.elt > 0: elif self.elt > 0:
msg += "[…]" msg += "[…] "
if self.messages[0][self.elt - 1] == ' ':
msg += " "
elts = self.messages[0][self.elt:] elts = self.messages[0][self.elt:]
if isinstance(elts, list): if isinstance(elts, list):
@ -220,7 +210,7 @@ class Response:
else: else:
if len(elts.encode()) <= maxlen: if len(elts.encode()) <= maxlen:
self.pop() self.pop()
if self.count is not None and not self.alone: if self.count is not None:
return msg + elts + (self.count % len(self.messages)) return msg + elts + (self.count % len(self.messages))
else: else:
return msg + elts return msg + elts
@ -247,16 +237,14 @@ class Response:
SERVERS = dict() SERVERS = dict()
# MODULE INTERFACE #################################################### @hook("all_post")
@hook.post()
def parseresponse(res): def parseresponse(res):
# TODO: handle inter-bot communication NOMORE # TODO: handle inter-bot communication NOMORE
# TODO: check that the response is not the one already saved # TODO: check that the response is not the one already saved
if isinstance(res, Response): if isinstance(res, Response):
if res.server not in SERVERS: if res.server not in SERVERS:
SERVERS[res.server] = dict() SERVERS[res.server] = dict()
for receiver in res.to: for receiver in res.receivers:
if receiver in SERVERS[res.server]: if receiver in SERVERS[res.server]:
nw, bk = SERVERS[res.server][receiver] nw, bk = SERVERS[res.server][receiver]
else: else:
@ -266,7 +254,7 @@ def parseresponse(res):
return res return res
@hook.command("more") @hook("cmd_hook", "more")
def cmd_more(msg): def cmd_more(msg):
"""Display next chunck of the message""" """Display next chunck of the message"""
res = list() res = list()
@ -282,7 +270,7 @@ def cmd_more(msg):
return res return res
@hook.command("next") @hook("cmd_hook", "next")
def cmd_next(msg): def cmd_next(msg):
"""Display the next information include in the message""" """Display the next information include in the message"""
res = list() res = list()

View file

@ -5,10 +5,10 @@
import logging import logging
import re import re
from nemubot.exception import IMException from nemubot.exception import IRCException
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.module.more import Response from more import Response
from . import isup from . import isup
from . import page from . import page
@ -38,28 +38,28 @@ def load(context):
# MODULE INTERFACE #################################################### # MODULE INTERFACE ####################################################
@hook.command("title", @hook("cmd_hook", "title",
help="Retrieve webpage's title", help="Retrieve webpage's title",
help_usage={"URL": "Display the title of the given URL"}) help_usage={"URL": "Display the title of the given URL"})
def cmd_title(msg): def cmd_title(msg):
if not len(msg.args): if not len(msg.args):
raise IMException("Indicate the URL to visit.") raise IRCException("Indicate the URL to visit.")
url = " ".join(msg.args) url = " ".join(msg.args)
res = re.search("<title>(.*?)</title>", page.fetch(" ".join(msg.args)), re.DOTALL) res = re.search("<title>(.*?)</title>", page.fetch(" ".join(msg.args)), re.DOTALL)
if res is None: if res is None:
raise IMException("The page %s has no title" % url) raise IRCException("The page %s has no title" % url)
else: else:
return Response("%s: %s" % (url, res.group(1).replace("\n", " ")), channel=msg.channel) return Response("%s: %s" % (url, res.group(1).replace("\n", " ")), channel=msg.channel)
@hook.command("curly", @hook("cmd_hook", "curly",
help="Retrieve webpage's headers", help="Retrieve webpage's headers",
help_usage={"URL": "Display HTTP headers of the given URL"}) help_usage={"URL": "Display HTTP headers of the given URL"})
def cmd_curly(msg): def cmd_curly(msg):
if not len(msg.args): if not len(msg.args):
raise IMException("Indicate the URL to visit.") raise IRCException("Indicate the URL to visit.")
url = " ".join(msg.args) url = " ".join(msg.args)
version, status, reason, headers = page.headers(url) version, status, reason, headers = page.headers(url)
@ -67,12 +67,12 @@ def cmd_curly(msg):
return Response("Entêtes de la page %s : HTTP/%s, statut : %d %s ; headers : %s" % (url, version, status, reason, ", ".join(["\x03\x02" + h + "\x03\x02: " + v for h, v in headers])), channel=msg.channel) return Response("Entêtes de la page %s : HTTP/%s, statut : %d %s ; headers : %s" % (url, version, status, reason, ", ".join(["\x03\x02" + h + "\x03\x02: " + v for h, v in headers])), channel=msg.channel)
@hook.command("curl", @hook("cmd_hook", "curl",
help="Retrieve webpage's body", help="Retrieve webpage's body",
help_usage={"URL": "Display raw HTTP body of the given URL"}) help_usage={"URL": "Display raw HTTP body of the given URL"})
def cmd_curl(msg): def cmd_curl(msg):
if not len(msg.args): if not len(msg.args):
raise IMException("Indicate the URL to visit.") raise IRCException("Indicate the URL to visit.")
res = Response(channel=msg.channel) res = Response(channel=msg.channel)
for m in page.fetch(" ".join(msg.args)).split("\n"): for m in page.fetch(" ".join(msg.args)).split("\n"):
@ -80,24 +80,24 @@ def cmd_curl(msg):
return res return res
@hook.command("w3m", @hook("cmd_hook", "w3m",
help="Retrieve and format webpage's content", help="Retrieve and format webpage's content",
help_usage={"URL": "Display and format HTTP content of the given URL"}) help_usage={"URL": "Display and format HTTP content of the given URL"})
def cmd_w3m(msg): def cmd_w3m(msg):
if not len(msg.args): if not len(msg.args):
raise IMException("Indicate the URL to visit.") raise IRCException("Indicate the URL to visit.")
res = Response(channel=msg.channel) res = Response(channel=msg.channel)
for line in page.render(" ".join(msg.args)).split("\n"): for line in page.render(" ".join(msg.args)).split("\n"):
res.append_message(line) res.append_message(line)
return res return res
@hook.command("traceurl", @hook("cmd_hook", "traceurl",
help="Follow redirections of a given URL and display each step", help="Follow redirections of a given URL and display each step",
help_usage={"URL": "Display redirections steps for the given URL"}) help_usage={"URL": "Display redirections steps for the given URL"})
def cmd_traceurl(msg): def cmd_traceurl(msg):
if not len(msg.args): if not len(msg.args):
raise IMException("Indicate an URL to trace!") raise IRCException("Indicate an URL to trace!")
res = list() res = list()
for url in msg.args[:4]: for url in msg.args[:4]:
@ -109,12 +109,12 @@ def cmd_traceurl(msg):
return res return res
@hook.command("isup", @hook("cmd_hook", "isup",
help="Check if a website is up", help="Check if a website is up",
help_usage={"DOMAIN": "Check if a DOMAIN is up"}) help_usage={"DOMAIN": "Check if a DOMAIN is up"})
def cmd_isup(msg): def cmd_isup(msg):
if not len(msg.args): if not len(msg.args):
raise IMException("Indicate an domain name to check!") raise IRCException("Indicate an domain name to check!")
res = list() res = list()
for url in msg.args[:4]: for url in msg.args[:4]:
@ -126,12 +126,12 @@ def cmd_isup(msg):
return res return res
@hook.command("w3c", @hook("cmd_hook", "w3c",
help="Perform a w3c HTML validator check", help="Perform a w3c HTML validator check",
help_usage={"URL": "Do W3C HTML validation on the given URL"}) help_usage={"URL": "Do W3C HTML validation on the given URL"})
def cmd_w3c(msg): def cmd_w3c(msg):
if not len(msg.args): if not len(msg.args):
raise IMException("Indicate an URL to validate!") raise IRCException("Indicate an URL to validate!")
headers, validator = w3c.validator(msg.args[0]) headers, validator = w3c.validator(msg.args[0])
@ -149,20 +149,20 @@ def cmd_w3c(msg):
@hook.command("watch", data="diff", @hook("cmd_hook", "watch", data="diff",
help="Alert on webpage change", help="Alert on webpage change",
help_usage={"URL": "Watch the given URL and alert when it changes"}) help_usage={"URL": "Watch the given URL and alert when it changes"})
@hook.command("updown", data="updown", @hook("cmd_hook", "updown", data="updown",
help="Alert on server availability change", help="Alert on server availability change",
help_usage={"URL": "Watch the given domain and alert when it availability status changes"}) help_usage={"URL": "Watch the given domain and alert when it availability status changes"})
def cmd_watch(msg, diffType="diff"): def cmd_watch(msg, diffType="diff"):
if not len(msg.args): if not len(msg.args):
raise IMException("indicate an URL to watch!") raise IRCException("indicate an URL to watch!")
return watchWebsite.add_site(msg.args[0], msg.frm, msg.channel, msg.server, diffType) return watchWebsite.add_site(msg.args[0], msg.frm, msg.channel, msg.server, diffType)
@hook.command("listwatch", @hook("cmd_hook", "listwatch",
help="List URL watched for the channel", help="List URL watched for the channel",
help_usage={None: "List URL watched for the channel"}) help_usage={None: "List URL watched for the channel"})
def cmd_listwatch(msg): def cmd_listwatch(msg):
@ -173,12 +173,12 @@ def cmd_listwatch(msg):
return Response("No URL are currently watched. Use !watch URL to watch one.", channel=msg.channel) return Response("No URL are currently watched. Use !watch URL to watch one.", channel=msg.channel)
@hook.command("unwatch", @hook("cmd_hook", "unwatch",
help="Unwatch a previously watched URL", help="Unwatch a previously watched URL",
help_usage={"URL": "Unwatch the given URL"}) help_usage={"URL": "Unwatch the given URL"})
def cmd_unwatch(msg): def cmd_unwatch(msg):
if not len(msg.args): if not len(msg.args):
raise IMException("which URL should I stop watching?") raise IRCException("which URL should I stop watching?")
for arg in msg.args: for arg in msg.args:
return watchWebsite.del_site(arg, msg.frm, msg.channel, msg.frm_owner) return watchWebsite.del_site(arg, msg.frm, msg.channel, msg.frm_owner)

View file

@ -11,7 +11,7 @@ def isup(url):
o = urllib.parse.urlparse(getNormalizedURL(url), "http") o = urllib.parse.urlparse(getNormalizedURL(url), "http")
if o.netloc != "": if o.netloc != "":
isup = getJSON("https://isitup.org/%s.json" % o.netloc) isup = getJSON("http://isitup.org/%s.json" % o.netloc)
if isup is not None and "status_code" in isup and isup["status_code"] == 1: if isup is not None and "status_code" in isup and isup["status_code"] == 1:
return isup["response_time"] return isup["response_time"]

View file

@ -5,7 +5,7 @@ import tempfile
import urllib import urllib
from nemubot import __version__ from nemubot import __version__
from nemubot.exception import IMException from nemubot.exception import IRCException
from nemubot.tools import web from nemubot.tools import web
@ -23,7 +23,7 @@ def headers(url):
o = urllib.parse.urlparse(web.getNormalizedURL(url), "http") o = urllib.parse.urlparse(web.getNormalizedURL(url), "http")
if o.netloc == "": if o.netloc == "":
raise IMException("invalid URL") raise IRCException("invalid URL")
if o.scheme == "http": if o.scheme == "http":
conn = http.client.HTTPConnection(o.hostname, port=o.port, timeout=5) conn = http.client.HTTPConnection(o.hostname, port=o.port, timeout=5)
else: else:
@ -32,18 +32,18 @@ def headers(url):
conn.request("HEAD", o.path, None, {"User-agent": conn.request("HEAD", o.path, None, {"User-agent":
"Nemubot v%s" % __version__}) "Nemubot v%s" % __version__})
except ConnectionError as e: except ConnectionError as e:
raise IMException(e.strerror) raise IRCException(e.strerror)
except socket.timeout: except socket.timeout:
raise IMException("request timeout") raise IRCException("request timeout")
except socket.gaierror: except socket.gaierror:
print ("<tools.web> Unable to receive page %s from %s on %d." print ("<tools.web> Unable to receive page %s from %s on %d."
% (o.path, o.hostname, o.port if o.port is not None else 0)) % (o.path, o.hostname, o.port if o.port is not None else 0))
raise IMException("an unexpected error occurs") raise IRCException("an unexpected error occurs")
try: try:
res = conn.getresponse() res = conn.getresponse()
except http.client.BadStatusLine: except http.client.BadStatusLine:
raise IMException("An error occurs") raise IRCException("An error occurs")
finally: finally:
conn.close() conn.close()
@ -51,7 +51,7 @@ def headers(url):
def _onNoneDefault(): def _onNoneDefault():
raise IMException("An error occurs when trying to access the page") raise IRCException("An error occurs when trying to access the page")
def fetch(url, onNone=_onNoneDefault): def fetch(url, onNone=_onNoneDefault):
@ -71,11 +71,11 @@ def fetch(url, onNone=_onNoneDefault):
else: else:
return None return None
except ConnectionError as e: except ConnectionError as e:
raise IMException(e.strerror) raise IRCException(e.strerror)
except socket.timeout: except socket.timeout:
raise IMException("The request timeout when trying to access the page") raise IRCException("The request timeout when trying to access the page")
except socket.error as e: except socket.error as e:
raise IMException(e.strerror) raise IRCException(e.strerror)
def _render(cnt): def _render(cnt):

View file

@ -2,7 +2,7 @@ import json
import urllib import urllib
from nemubot import __version__ from nemubot import __version__
from nemubot.exception import IMException from nemubot.exception import IRCException
from nemubot.tools.web import getNormalizedURL from nemubot.tools.web import getNormalizedURL
def validator(url): def validator(url):
@ -14,19 +14,19 @@ def validator(url):
o = urllib.parse.urlparse(getNormalizedURL(url), "http") o = urllib.parse.urlparse(getNormalizedURL(url), "http")
if o.netloc == "": if o.netloc == "":
raise IMException("Indicate a valid URL!") raise IRCException("Indicate a valid URL!")
try: try:
req = urllib.request.Request("https://validator.w3.org/check?uri=%s&output=json" % (urllib.parse.quote(o.geturl())), headers={ 'User-Agent' : "Nemubot v%s" % __version__}) req = urllib.request.Request("http://validator.w3.org/check?uri=%s&output=json" % (urllib.parse.quote(o.geturl())), headers={ 'User-Agent' : "Nemubot v%s" % __version__})
raw = urllib.request.urlopen(req, timeout=10) raw = urllib.request.urlopen(req, timeout=10)
except urllib.error.HTTPError as e: except urllib.error.HTTPError as e:
raise IMException("HTTP error occurs: %s %s" % (e.code, e.reason)) raise IRCException("HTTP error occurs: %s %s" % (e.code, e.reason))
headers = dict() headers = dict()
for Hname, Hval in raw.getheaders(): for Hname, Hval in raw.getheaders():
headers[Hname] = Hval headers[Hname] = Hval
if "X-W3C-Validator-Status" not in headers or (headers["X-W3C-Validator-Status"] != "Valid" and headers["X-W3C-Validator-Status"] != "Invalid"): if "X-W3C-Validator-Status" not in headers or (headers["X-W3C-Validator-Status"] != "Valid" and headers["X-W3C-Validator-Status"] != "Invalid"):
raise IMException("Unexpected error on W3C servers" + (" (" + headers["X-W3C-Validator-Status"] + ")" if "X-W3C-Validator-Status" in headers else "")) raise IRCException("Unexpected error on W3C servers" + (" (" + headers["X-W3C-Validator-Status"] + ")" if "X-W3C-Validator-Status" in headers else ""))
return headers, json.loads(raw.read().decode()) return headers, json.loads(raw.read().decode())

View file

@ -1,19 +1,19 @@
"""Alert on changes on websites""" """Alert on changes on websites"""
from functools import partial
import logging import logging
from random import randint from random import randint
import urllib.parse import urllib.parse
from urllib.parse import urlparse from urllib.parse import urlparse
from nemubot.event import ModuleEvent from nemubot.event import ModuleEvent
from nemubot.exception import IMException from nemubot.exception import IRCException
from nemubot.hooks import hook
from nemubot.tools.web import getNormalizedURL from nemubot.tools.web import getNormalizedURL
from nemubot.tools.xmlparser.node import ModuleState from nemubot.tools.xmlparser.node import ModuleState
logger = logging.getLogger("nemubot.module.networking.watchWebsite") logger = logging.getLogger("nemubot.module.networking.watchWebsite")
from nemubot.module.more import Response from more import Response
from . import page from . import page
@ -62,7 +62,7 @@ def del_site(url, nick, channel, frm_owner):
for a in site.getNodes("alert"): for a in site.getNodes("alert"):
if a["channel"] == channel: if a["channel"] == channel:
# if not (nick == a["nick"] or frm_owner): # if not (nick == a["nick"] or frm_owner):
# raise IMException("you cannot unwatch this URL.") # raise IRCException("you cannot unwatch this URL.")
site.delChild(a) site.delChild(a)
if not site.hasNode("alert"): if not site.hasNode("alert"):
del_event(site["_evt_id"]) del_event(site["_evt_id"])
@ -70,7 +70,7 @@ def del_site(url, nick, channel, frm_owner):
save() save()
return Response("I don't watch this URL anymore.", return Response("I don't watch this URL anymore.",
channel=channel, nick=nick) channel=channel, nick=nick)
raise IMException("I didn't watch this URL!") raise IRCException("I didn't watch this URL!")
def add_site(url, nick, channel, server, diffType="diff"): def add_site(url, nick, channel, server, diffType="diff"):
@ -82,7 +82,7 @@ def add_site(url, nick, channel, server, diffType="diff"):
o = urlparse(getNormalizedURL(url), "http") o = urlparse(getNormalizedURL(url), "http")
if o.netloc == "": if o.netloc == "":
raise IMException("sorry, I can't watch this URL :(") raise IRCException("sorry, I can't watch this URL :(")
alert = ModuleState("alert") alert = ModuleState("alert")
alert["nick"] = nick alert["nick"] = nick
@ -210,14 +210,15 @@ def start_watching(site, offset=0):
offset -- offset time to delay the launch of the first check offset -- offset time to delay the launch of the first check
""" """
#o = urlparse(getNormalizedURL(site["url"]), "http") o = urlparse(getNormalizedURL(site["url"]), "http")
#print("Add %s event for site: %s" % (site["type"], o.netloc)) #print_debug("Add %s event for site: %s" % (site["type"], o.netloc))
try: try:
evt = ModuleEvent(func=partial(fwatch, url=site["url"]), evt = ModuleEvent(func=fwatch,
cmp=site["lastcontent"], cmp_data=site["lastcontent"],
offset=offset, interval=site.getInt("time"), func_data=site["url"], offset=offset,
call=partial(alert_change, site=site)) interval=site.getInt("time"),
call=alert_change, call_data=site)
site["_evt_id"] = add_event(evt) site["_evt_id"] = add_event(evt)
except IMException: except IRCException:
logger.exception("Unable to watch %s", site["url"]) logger.exception("Unable to watch %s", site["url"])

View file

@ -1,51 +1,61 @@
# PYTHON STUFFS #######################################################
import datetime import datetime
import urllib import urllib
from nemubot.exception import IMException from nemubot.exception import IRCException
from nemubot.tools.web import getJSON from nemubot.tools.web import getJSON
from nemubot.module.more import Response from more import Response
URL_AVAIL = "https://www.whoisxmlapi.com/whoisserver/WhoisService?cmd=GET_DN_AVAILABILITY&domainName=%%s&outputFormat=json&username=%s&password=%s" URL_WHOIS = "http://www.whoisxmlapi.com/whoisserver/WhoisService?rid=1&domainName=%%s&outputFormat=json&userName=%s&password=%s"
URL_WHOIS = "https://www.whoisxmlapi.com/whoisserver/WhoisService?da=2&domainName=%%s&outputFormat=json&userName=%s&password=%s"
# LOADING #############################################################
def load(CONF, add_hook): def load(CONF, add_hook):
global URL_AVAIL, URL_WHOIS global URL_WHOIS
if not CONF or not CONF.hasNode("whoisxmlapi") or "username" not in CONF.getNode("whoisxmlapi") or "password" not in CONF.getNode("whoisxmlapi"): if not CONF or not CONF.hasNode("whoisxmlapi") or not CONF.getNode("whoisxmlapi").hasAttribute("username") or not CONF.getNode("whoisxmlapi").hasAttribute("password"):
raise ImportError("You need a WhoisXML API account in order to use " raise ImportError("You need a WhoisXML API account in order to use "
"the !netwhois feature. Add it to the module " "the !netwhois feature. Add it to the module "
"configuration file:\n<whoisxmlapi username=\"XX\" " "configuration file:\n<whoisxmlapi username=\"XX\" "
"password=\"XXX\" />\nRegister at " "password=\"XXX\" />\nRegister at "
"https://www.whoisxmlapi.com/newaccount.php") "http://www.whoisxmlapi.com/newaccount.php")
URL_AVAIL = URL_AVAIL % (urllib.parse.quote(CONF.getNode("whoisxmlapi")["username"]), urllib.parse.quote(CONF.getNode("whoisxmlapi")["password"]))
URL_WHOIS = URL_WHOIS % (urllib.parse.quote(CONF.getNode("whoisxmlapi")["username"]), urllib.parse.quote(CONF.getNode("whoisxmlapi")["password"])) URL_WHOIS = URL_WHOIS % (urllib.parse.quote(CONF.getNode("whoisxmlapi")["username"]), urllib.parse.quote(CONF.getNode("whoisxmlapi")["password"]))
import nemubot.hooks import nemubot.hooks
add_hook(nemubot.hooks.Command(cmd_whois, "netwhois", add_hook("cmd_hook", nemubot.hooks.Message(cmd_whois, "netwhois",
help="Get whois information about given domains", help="Get whois information about given domains",
help_usage={"DOMAIN": "Return whois information on the given DOMAIN"}), help_usage={"DOMAIN": "Return whois information on the given DOMAIN"}))
"in","Command")
add_hook(nemubot.hooks.Command(cmd_avail, "domain_available",
help="Domain availability check using whoisxmlapi.com",
help_usage={"DOMAIN": "Check if the given DOMAIN is available or not"}),
"in","Command")
# MODULE CORE ######################################################### def extractdate(str):
tries = [
"%Y-%m-%dT%H:%M:%S.0%Z",
"%Y-%m-%dT%H:%M:%S%Z",
"%Y-%m-%dT%H:%M:%S%z",
"%Y-%m-%dT%H:%M:%SZ",
"%Y-%m-%dT%H:%M:%S.0Z",
"%Y-%m-%dT%H:%M:%S",
"%Y-%m-%d %H:%M:%S.0%Z",
"%Y-%m-%d %H:%M:%S%Z",
"%Y-%m-%d %H:%M:%S%z",
"%Y-%m-%d %H:%M:%S.0Z",
"%Y-%m-%d %H:%M:%SZ",
"%Y-%m-%d %H:%M:%S",
"%Y-%m-%d",
"%d/%m/%Y",
]
for t in tries:
try:
return datetime.datetime.strptime(str, t)
except ValueError:
pass
return datetime.datetime.strptime(str, t)
def whois_entityformat(entity): def whois_entityformat(entity):
ret = "" ret = ""
if "organization" in entity: if "organization" in entity:
ret += entity["organization"] ret += entity["organization"]
if "organization" in entity and "name" in entity:
ret += " "
if "name" in entity: if "name" in entity:
ret += entity["name"] ret += entity["name"]
@ -67,70 +77,30 @@ def whois_entityformat(entity):
return ret.lstrip() return ret.lstrip()
def available(dom):
js = getJSON(URL_AVAIL % urllib.parse.quote(dom))
if "ErrorMessage" in js:
raise IMException(js["ErrorMessage"]["msg"])
return js["DomainInfo"]["domainAvailability"] == "AVAILABLE"
# MODULE INTERFACE ####################################################
def cmd_avail(msg):
if not len(msg.args):
raise IMException("Indicate a domain name for having its availability status!")
return Response(["%s: %s" % (dom, "available" if available(dom) else "unavailable") for dom in msg.args],
channel=msg.channel)
def cmd_whois(msg): def cmd_whois(msg):
if not len(msg.args): if not len(msg.args):
raise IMException("Indiquer un domaine ou une IP à whois !") raise IRCException("Indiquer un domaine ou une IP à whois !")
dom = msg.args[0] dom = msg.args[0]
js = getJSON(URL_WHOIS % urllib.parse.quote(dom)) js = getJSON(URL_WHOIS % urllib.parse.quote(dom))
if "ErrorMessage" in js: if "ErrorMessage" in js:
raise IMException(js["ErrorMessage"]["msg"]) err = js["ErrorMessage"]
raise IRCException(js["ErrorMessage"]["msg"])
whois = js["WhoisRecord"] whois = js["WhoisRecord"]
res = [] res = Response(channel=msg.channel, nomore="No more whois information")
if "registrarName" in whois: res.append_message("%s: %s%s%s%s\x03\x02registered by\x03\x02 %s, \x03\x02administrated by\x03\x02 %s, \x03\x02managed by\x03\x02 %s" % (whois["domainName"],
res.append("\x03\x02registered by\x03\x02 " + whois["registrarName"]) whois["status"].replace("\n", ", ") + " " if "status" in whois else "",
"\x03\x02created on\x03\x02 " + extractdate(whois["createdDate"]).strftime("%c") + ", " if "createdDate" in whois else "",
if "domainAvailability" in whois: "\x03\x02updated on\x03\x02 " + extractdate(whois["updatedDate"]).strftime("%c") + ", " if "updatedDate" in whois else "",
res.append(whois["domainAvailability"]) "\x03\x02expires on\x03\x02 " + extractdate(whois["expiresDate"]).strftime("%c") + ", " if "expiresDate" in whois else "",
whois_entityformat(whois["registrant"]) if "registrant" in whois else "unknown",
if "contactEmail" in whois: whois_entityformat(whois["administrativeContact"]) if "administrativeContact" in whois else "unknown",
res.append("\x03\x02contact email\x03\x02 " + whois["contactEmail"]) whois_entityformat(whois["technicalContact"]) if "technicalContact" in whois else "unknown",
))
if "audit" in whois: return res
if "createdDate" in whois["audit"] and "$" in whois["audit"]["createdDate"]:
res.append("\x03\x02created on\x03\x02 " + whois["audit"]["createdDate"]["$"])
if "updatedDate" in whois["audit"] and "$" in whois["audit"]["updatedDate"]:
res.append("\x03\x02updated on\x03\x02 " + whois["audit"]["updatedDate"]["$"])
if "registryData" in whois:
if "expiresDateNormalized" in whois["registryData"]:
res.append("\x03\x02expire on\x03\x02 " + whois["registryData"]["expiresDateNormalized"])
if "registrant" in whois["registryData"]:
res.append("\x03\x02registrant:\x03\x02 " + whois_entityformat(whois["registryData"]["registrant"]))
if "zoneContact" in whois["registryData"]:
res.append("\x03\x02zone contact:\x03\x02 " + whois_entityformat(whois["registryData"]["zoneContact"]))
if "technicalContact" in whois["registryData"]:
res.append("\x03\x02technical contact:\x03\x02 " + whois_entityformat(whois["registryData"]["technicalContact"]))
if "administrativeContact" in whois["registryData"]:
res.append("\x03\x02administrative contact:\x03\x02 " + whois_entityformat(whois["registryData"]["administrativeContact"]))
if "billingContact" in whois["registryData"]:
res.append("\x03\x02billing contact:\x03\x02 " + whois_entityformat(whois["registryData"]["billingContact"]))
return Response(res,
title=whois["domainName"],
channel=msg.channel,
nomore="No more whois information")

View file

@ -8,12 +8,11 @@ from urllib.parse import urljoin
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from nemubot.exception import IMException from nemubot.exception import IRCException
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.tools import web from nemubot.tools import web
from nemubot.module.more import Response from more import Response
from nemubot.module.urlreducer import reduce_inline
from nemubot.tools.feed import Feed, AtomEntry from nemubot.tools.feed import Feed, AtomEntry
@ -42,20 +41,19 @@ def get_last_news(url):
# MODULE INTERFACE #################################################### # MODULE INTERFACE ####################################################
@hook.command("news") @hook("cmd_hook", "news")
def cmd_news(msg): def cmd_news(msg):
if not len(msg.args): if not len(msg.args):
raise IMException("Indicate the URL to visit.") raise IRCException("Indicate the URL to visit.")
url = " ".join(msg.args) url = " ".join(msg.args)
links = [x for x in find_rss_links(url)] links = [x for x in find_rss_links(url)]
if len(links) == 0: links = [ url ] if len(links) == 0: links = [ url ]
res = Response(channel=msg.channel, nomore="No more news from %s" % url, line_treat=reduce_inline) res = Response(channel=msg.channel, nomore="No more news from %s" % url)
for n in get_last_news(links[0]): for n in get_last_news(links[0]):
res.append_message("%s published %s: %s %s" % (("\x02" + web.striphtml(n.title) + "\x0F") if n.title else "An article without title", res.append_message("%s published %s: %s %s" % (("\x02" + web.striphtml(n.title) + "\x0F") if n.title else "An article without title",
(n.updated.strftime("on %A %d. %B %Y at %H:%M") if n.updated else "someday") if isinstance(n, AtomEntry) else n.pubDate, (n.updated.strftime("on %A %d. %B %Y at %H:%M") if n.updated else "someday") if isinstance(n, AtomEntry) else n.pubDate,
web.striphtml(n.summary) if n.summary else "", web.striphtml(n.summary) if n.summary else "",
n.link if n.link else "")) n.link if n.link else ""))
return res return res

4
modules/nextstop.xml Normal file
View file

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

View file

@ -0,0 +1,55 @@
# coding=utf-8
"""Informe les usagers des prochains passages des transports en communs de la RATP"""
from nemubot.exception import IRCException
from nemubot.hooks import hook
from more import Response
nemubotversion = 3.4
from .external.src import ratp
def help_full ():
return "!ratp transport line [station]: Donne des informations sur les prochains passages du transport en commun séléctionné à l'arrêt désiré. Si aucune station n'est précisée, les liste toutes."
@hook("cmd_hook", "ratp")
def ask_ratp(msg):
"""Hook entry from !ratp"""
if len(msg.args) >= 3:
transport = msg.args[0]
line = msg.args[1]
station = msg.args[2]
if len(msg.args) == 4:
times = ratp.getNextStopsAtStation(transport, line, station, msg.args[3])
else:
times = ratp.getNextStopsAtStation(transport, line, station)
if len(times) == 0:
raise IRCException("la station %s n'existe pas sur le %s ligne %s." % (station, transport, line))
(time, direction, stationname) = times[0]
return Response(message=["\x03\x02%s\x03\x02 direction %s" % (time, direction) for time, direction, stationname in times],
title="Prochains passages du %s ligne %s à l'arrêt %s" % (transport, line, stationname),
channel=msg.channel)
elif len(msg.args) == 2:
stations = ratp.getAllStations(msg.args[0], msg.args[1])
if len(stations) == 0:
raise IRCException("aucune station trouvée.")
return Response([s for s in stations], title="Stations", channel=msg.channel)
else:
raise IRCException("Mauvais usage, merci de spécifier un type de transport et une ligne, ou de consulter l'aide du module.")
@hook("cmd_hook", "ratp_alert")
def ratp_alert(msg):
if len(msg.args) == 2:
transport = msg.args[0]
cause = msg.args[1]
incidents = ratp.getDisturbance(cause, transport)
return Response(incidents, channel=msg.channel, nomore="No more incidents", count=" (%d more incidents)")
else:
raise IRCException("Mauvais usage, merci de spécifier un type de transport et un type d'alerte (alerte, manif, travaux), ou de consulter l'aide du module.")

@ -0,0 +1 @@
Subproject commit 3d5c9b2d52fbd214f5aaad00e5f3952de919b3e5

View file

@ -1,229 +0,0 @@
"""The NNTP module"""
# PYTHON STUFFS #######################################################
import email
import email.policy
from email.utils import mktime_tz, parseaddr, parsedate_tz
from functools import partial
from nntplib import NNTP, decode_header
import re
import time
from datetime import datetime
from zlib import adler32
from nemubot import context
from nemubot.event import ModuleEvent
from nemubot.exception import IMException
from nemubot.hooks import hook
from nemubot.tools.xmlparser.node import ModuleState
from nemubot.module.more import Response
# LOADING #############################################################
def load(context):
for wn in context.data.getNodes("watched_newsgroup"):
watch(**wn.attributes)
# MODULE CORE #########################################################
def list_groups(group_pattern="*", **server):
with NNTP(**server) as srv:
response, l = srv.list(group_pattern)
for i in l:
yield i.group, srv.description(i.group), i.flag
def read_group(group, **server):
with NNTP(**server) as srv:
response, count, first, last, name = srv.group(group)
resp, overviews = srv.over((first, last))
for art_num, over in reversed(overviews):
yield over
def read_article(msg_id, **server):
with NNTP(**server) as srv:
response, info = srv.article(msg_id)
return email.message_from_bytes(b"\r\n".join(info.lines), policy=email.policy.SMTPUTF8)
servers_lastcheck = dict()
servers_lastseen = dict()
def whatsnew(group="*", **server):
fill = dict()
if "user" in server: fill["user"] = server["user"]
if "password" in server: fill["password"] = server["password"]
if "host" in server: fill["host"] = server["host"]
if "port" in server: fill["port"] = server["port"]
idx = _indexServer(**server)
if idx in servers_lastcheck and servers_lastcheck[idx] is not None:
date_last_check = servers_lastcheck[idx]
else:
date_last_check = datetime.now()
if idx not in servers_lastseen:
servers_lastseen[idx] = []
with NNTP(**fill) as srv:
response, servers_lastcheck[idx] = srv.date()
response, groups = srv.newgroups(date_last_check)
for g in groups:
yield g
response, articles = srv.newnews(group, date_last_check)
for msg_id in articles:
if msg_id not in servers_lastseen[idx]:
servers_lastseen[idx].append(msg_id)
response, info = srv.article(msg_id)
yield email.message_from_bytes(b"\r\n".join(info.lines))
# Clean huge lists
if len(servers_lastseen[idx]) > 42:
servers_lastseen[idx] = servers_lastseen[idx][23:]
def format_article(art, **response_args):
art["X-FromName"], art["X-FromEmail"] = parseaddr(art["From"] if "From" in art else "")
if art["X-FromName"] == '': art["X-FromName"] = art["X-FromEmail"]
date = mktime_tz(parsedate_tz(art["Date"]))
if date < time.time() - 120:
title = "\x0314In \x0F\x03{0:02d}{Newsgroups}\x0F\x0314: on \x0F{Date}\x0314 by \x0F\x03{0:02d}{X-FromName}\x0F \x02{Subject}\x0F"
else:
title = "\x0314In \x0F\x03{0:02d}{Newsgroups}\x0F\x0314: by \x0F\x03{0:02d}{X-FromName}\x0F \x02{Subject}\x0F"
return Response(art.get_payload().replace('\n', ' '),
title=title.format(adler32(art["Newsgroups"].encode()) & 0xf, adler32(art["X-FromEmail"].encode()) & 0xf, **{h: decode_header(i) for h,i in art.items()}),
**response_args)
watches = dict()
def _indexServer(**kwargs):
if "user" not in kwargs: kwargs["user"] = ""
if "password" not in kwargs: kwargs["password"] = ""
if "host" not in kwargs: kwargs["host"] = ""
if "port" not in kwargs: kwargs["port"] = 119
return "{user}:{password}@{host}:{port}".format(**kwargs)
def _newevt(**args):
context.add_event(ModuleEvent(call=partial(_ticker, **args), interval=42))
def _ticker(to_server, to_channel, group, server):
_newevt(to_server=to_server, to_channel=to_channel, group=group, server=server)
n = 0
for art in whatsnew(group, **server):
n += 1
if n > 10:
continue
context.send_response(to_server, format_article(art, channel=to_channel))
if n > 10:
context.send_response(to_server, Response("... and %s others news" % (n - 10), channel=to_channel))
def watch(to_server, to_channel, group="*", **server):
_newevt(to_server=to_server, to_channel=to_channel, group=group, server=server)
# MODULE INTERFACE ####################################################
keywords_server = {
"host=HOST": "hostname or IP of the NNTP server",
"port=PORT": "port of the NNTP server",
"user=USERNAME": "username to use to connect to the server",
"password=PASSWORD": "password to use to connect to the server",
}
@hook.command("nntp_groups",
help="Show list of existing groups",
help_usage={
None: "Display all groups",
"PATTERN": "Filter on group matching the PATTERN"
},
keywords=keywords_server)
def cmd_groups(msg):
if "host" not in msg.kwargs:
raise IMException("please give a hostname in keywords")
return Response(["\x02\x03{0:02d}{1}\x0F: {2}".format(adler32(g[0].encode()) & 0xf, *g) for g in list_groups(msg.args[0] if len(msg.args) > 0 else "*", **msg.kwargs)],
channel=msg.channel,
title="Matching groups on %s" % msg.kwargs["host"])
@hook.command("nntp_overview",
help="Show an overview of articles in given group(s)",
help_usage={
"GROUP": "Filter on group matching the PATTERN"
},
keywords=keywords_server)
def cmd_overview(msg):
if "host" not in msg.kwargs:
raise IMException("please give a hostname in keywords")
if not len(msg.args):
raise IMException("which group would you overview?")
for g in msg.args:
arts = []
for grp in read_group(g, **msg.kwargs):
grp["X-FromName"], grp["X-FromEmail"] = parseaddr(grp["from"] if "from" in grp else "")
if grp["X-FromName"] == '': grp["X-FromName"] = grp["X-FromEmail"]
arts.append("On {date}, from \x03{0:02d}{X-FromName}\x0F \x02{subject}\x0F: \x0314{message-id}\x0F".format(adler32(grp["X-FromEmail"].encode()) & 0xf, **{h: decode_header(i) for h,i in grp.items()}))
if len(arts):
yield Response(arts,
channel=msg.channel,
title="In \x03{0:02d}{1}\x0F".format(adler32(g[0].encode()) & 0xf, g))
@hook.command("nntp_read",
help="Read an article from a server",
help_usage={
"MSG_ID": "Read the given message"
},
keywords=keywords_server)
def cmd_read(msg):
if "host" not in msg.kwargs:
raise IMException("please give a hostname in keywords")
for msgid in msg.args:
if not re.match("<.*>", msgid):
msgid = "<" + msgid + ">"
art = read_article(msgid, **msg.kwargs)
yield format_article(art, channel=msg.channel)
@hook.command("nntp_watch",
help="Launch an event looking for new groups and articles on a server",
help_usage={
None: "Watch all groups",
"PATTERN": "Limit the watch on group matching this PATTERN"
},
keywords=keywords_server)
def cmd_watch(msg):
if "host" not in msg.kwargs:
raise IMException("please give a hostname in keywords")
if not msg.frm_owner:
raise IMException("sorry, this command is currently limited to the owner")
wnnode = ModuleState("watched_newsgroup")
wnnode["id"] = _indexServer(**msg.kwargs)
wnnode["to_server"] = msg.server
wnnode["to_channel"] = msg.channel
wnnode["group"] = msg.args[0] if len(msg.args) > 0 else "*"
wnnode["user"] = msg.kwargs["user"] if "user" in msg.kwargs else ""
wnnode["password"] = msg.kwargs["password"] if "password" in msg.kwargs else ""
wnnode["host"] = msg.kwargs["host"] if "host" in msg.kwargs else ""
wnnode["port"] = msg.kwargs["port"] if "port" in msg.kwargs else 119
context.data.addChild(wnnode)
watch(**wnnode.attributes)
return Response("Ok ok, I watch this newsgroup!", channel=msg.channel)

View file

@ -1,87 +0,0 @@
"""Perform requests to openai"""
# PYTHON STUFFS #######################################################
from openai import OpenAI
from nemubot import context
from nemubot.hooks import hook
from nemubot.tools import web
from nemubot.module.more import Response
# LOADING #############################################################
CLIENT = None
MODEL = "gpt-4"
ENDPOINT = None
def load(context):
global CLIENT, ENDPOINT, MODEL
if not context.config or ("apikey" not in context.config and "endpoint" not in context.config):
raise ImportError ("You need a OpenAI API key in order to use "
"this module. Add it to the module configuration: "
"\n<module name=\"openai\" "
"apikey=\"XXXXXX-XXXXXXXXXX\" endpoint=\"https://...\" model=\"gpt-4\" />")
kwargs = {
"api_key": context.config["apikey"] or "",
}
if "endpoint" in context.config:
ENDPOINT = context.config["endpoint"]
kwargs["base_url"] = ENDPOINT
CLIENT = OpenAI(**kwargs)
if "model" in context.config:
MODEL = context.config["model"]
# MODULE INTERFACE ####################################################
@hook.command("list_models",
help="list available LLM")
def cmd_listllm(msg):
llms = web.getJSON(ENDPOINT + "/models", timeout=6)
return Response(message=[m for m in map(lambda i: i["id"], llms["data"])], title="Here is the available models", channel=msg.channel)
@hook.command("set_model",
help="Set the model to use when talking to nemubot")
def cmd_setllm(msg):
if len(msg.args) != 1:
raise IMException("Indicate 1 model to use")
wanted_model = msg.args[0]
llms = web.getJSON(ENDPOINT + "/models", timeout=6)
for model in llms["data"]:
if wanted_model == model["id"]:
break
else:
raise IMException("Unable to set such model: unknown")
MODEL = wanted_model
return Response("New model in use: " + wanted_model, channel=msg.channel)
@hook.ask()
def parseask(msg):
chat_completion = CLIENT.chat.completions.create(
messages=[
{
"role": "system",
"content": "You are a kind multilingual assistant. Respond to the user request in 255 characters maximum. Be conscise, go directly to the point. Never add useless terms.",
},
{
"role": "user",
"content": msg.message,
}
],
model=MODEL,
)
return Response(chat_completion.choices[0].message.content,
msg.channel,
msg.frm)

View file

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

View file

@ -1,68 +0,0 @@
"""Get information about common software"""
# PYTHON STUFFS #######################################################
import portage
from nemubot import context
from nemubot.exception import IMException
from nemubot.hooks import hook
from nemubot.module.more import Response
DB = None
# MODULE CORE #########################################################
def get_db():
global DB
if DB is None:
DB = portage.db[portage.root]["porttree"].dbapi
return DB
def package_info(pkgname):
pv = get_db().xmatch("match-all", pkgname)
if not pv:
raise IMException("No package named '%s' found" % pkgname)
bv = get_db().xmatch("bestmatch-visible", pkgname)
pvsplit = portage.catpkgsplit(bv if bv else pv[-1])
info = get_db().aux_get(bv if bv else pv[-1], ["DESCRIPTION", "HOMEPAGE", "LICENSE", "IUSE", "KEYWORDS"])
return {
"pkgname": '/'.join(pvsplit[:2]),
"category": pvsplit[0],
"shortname": pvsplit[1],
"lastvers": '-'.join(pvsplit[2:]) if pvsplit[3] != "r0" else pvsplit[2],
"othersvers": ['-'.join(portage.catpkgsplit(p)[2:]) for p in pv if p != bv],
"description": info[0],
"homepage": info[1],
"license": info[2],
"uses": info[3],
"keywords": info[4],
}
# MODULE INTERFACE ####################################################
@hook.command("eix",
help="Get information about a package",
help_usage={
"NAME": "Get information about a software NAME"
})
def cmd_eix(msg):
if not len(msg.args):
raise IMException("please give me a package to search")
def srch(term):
try:
yield package_info(term)
except portage.exception.AmbiguousPackageName as e:
for i in e.args[0]:
yield package_info(i)
res = Response(channel=msg.channel, count=" (%d more packages)", nomore="No more package '%s'" % msg.args[0])
for pi in srch(msg.args[0]):
res.append_message("\x03\x02{pkgname}:\x03\x02 {description} - {homepage} - {license} - last revisions: \x03\x02{lastvers}\x03\x02{ov}".format(ov=(", " + ', '.join(pi["othersvers"])) if pi["othersvers"] else "", **pi))
return res

View file

@ -1,74 +0,0 @@
"""Informe les usagers des prochains passages des transports en communs de la RATP"""
# PYTHON STUFFS #######################################################
from nemubot.exception import IMException
from nemubot.hooks import hook
from nemubot.module.more import Response
from nextstop import ratp
@hook.command("ratp",
help="Affiche les prochains horaires de passage",
help_usage={
"TRANSPORT": "Affiche les lignes du moyen de transport donné",
"TRANSPORT LINE": "Affiche les stations sur la ligne de transport donnée",
"TRANSPORT LINE STATION": "Affiche les prochains horaires de passage à l'arrêt donné",
"TRANSPORT LINE STATION DESTINATION": "Affiche les prochains horaires de passage dans la direction donnée",
})
def ask_ratp(msg):
l = len(msg.args)
transport = msg.args[0] if l > 0 else None
line = msg.args[1] if l > 1 else None
station = msg.args[2] if l > 2 else None
direction = msg.args[3] if l > 3 else None
if station is not None:
times = sorted(ratp.getNextStopsAtStation(transport, line, station, direction), key=lambda i: i[0])
if len(times) == 0:
raise IMException("la station %s n'existe pas sur le %s ligne %s." % (station, transport, line))
(time, direction, stationname) = times[0]
return Response(message=["\x03\x02%s\x03\x02 direction %s" % (time, direction) for time, direction, stationname in times],
title="Prochains passages du %s ligne %s à l'arrêt %s" % (transport, line, stationname),
channel=msg.channel)
elif line is not None:
stations = ratp.getAllStations(transport, line)
if len(stations) == 0:
raise IMException("aucune station trouvée.")
return Response(stations, title="Stations", channel=msg.channel)
elif transport is not None:
lines = ratp.getTransportLines(transport)
if len(lines) == 0:
raise IMException("aucune ligne trouvée.")
return Response(lines, title="Lignes", channel=msg.channel)
else:
raise IMException("précise au moins un moyen de transport.")
@hook.command("ratp_alert",
help="Affiche les perturbations en cours sur le réseau")
def ratp_alert(msg):
if len(msg.args) == 0:
raise IMException("précise au moins un moyen de transport.")
l = len(msg.args)
transport = msg.args[0] if l > 0 else None
line = msg.args[1] if l > 1 else None
if line is not None:
d = ratp.getDisturbanceFromLine(transport, line)
if "date" in d and d["date"] is not None:
incidents = "Au {date[date]}, {title}: {message}".format(**d)
else:
incidents = "{title}: {message}".format(**d)
else:
incidents = ratp.getDisturbance(None, transport)
return Response(incidents, channel=msg.channel, nomore="No more incidents", count=" (%d more incidents)")

View file

@ -4,13 +4,13 @@
import re import re
from nemubot.exception import IMException from nemubot.exception import IRCException
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.tools import web from nemubot.tools import web
nemubotversion = 3.4 nemubotversion = 3.4
from nemubot.module.more import Response from more import Response
def help_full(): def help_full():
@ -19,14 +19,14 @@ def help_full():
LAST_SUBS = dict() LAST_SUBS = dict()
@hook.command("subreddit") @hook("cmd_hook", "subreddit")
def cmd_subreddit(msg): def cmd_subreddit(msg):
global LAST_SUBS global LAST_SUBS
if not len(msg.args): if not len(msg.args):
if msg.channel in LAST_SUBS and len(LAST_SUBS[msg.channel]) > 0: if msg.channel in LAST_SUBS and len(LAST_SUBS[msg.channel]) > 0:
subs = [LAST_SUBS[msg.channel].pop()] subs = [LAST_SUBS[msg.channel].pop()]
else: else:
raise IMException("Which subreddit? Need inspiration? " raise IRCException("Which subreddit? Need inspiration? "
"type !horny or !bored") "type !horny or !bored")
else: else:
subs = msg.args subs = msg.args
@ -40,11 +40,11 @@ def cmd_subreddit(msg):
else: else:
where = "r" where = "r"
sbr = web.getJSON("https://www.reddit.com/%s/%s/about.json" % sbr = web.getJSON("http://www.reddit.com/%s/%s/about.json" %
(where, sub.group(2))) (where, sub.group(2)))
if sbr is None: if sbr is None:
raise IMException("subreddit not found") raise IRCException("subreddit not found")
if "title" in sbr["data"]: if "title" in sbr["data"]:
res = Response(channel=msg.channel, res = Response(channel=msg.channel,
@ -64,32 +64,25 @@ def cmd_subreddit(msg):
channel=msg.channel)) channel=msg.channel))
else: else:
all_res.append(Response("%s is not a valid subreddit" % osub, all_res.append(Response("%s is not a valid subreddit" % osub,
channel=msg.channel, nick=msg.frm)) channel=msg.channel, nick=msg.nick))
return all_res return all_res
@hook.message() @hook("msg_default")
def parselisten(msg): def parselisten(msg):
global LAST_SUBS parseresponse(msg)
return None
if hasattr(msg, "message") and msg.message and type(msg.message) == str:
urls = re.findall("www.reddit.com(/\w/\w+/?)", msg.message)
for url in urls:
for recv in msg.to:
if recv not in LAST_SUBS:
LAST_SUBS[recv] = list()
LAST_SUBS[recv].append(url)
@hook.post() @hook("all_post")
def parseresponse(msg): def parseresponse(msg):
global LAST_SUBS global LAST_SUBS
if hasattr(msg, "text") and msg.text and type(msg.text) == str: if hasattr(msg, "text") and msg.text:
urls = re.findall("www.reddit.com(/\w/\w+/?)", msg.text) urls = re.findall("www.reddit.com(/\w/\w+/?)", msg.text)
for url in urls: for url in urls:
for recv in msg.to: for recv in msg.receivers:
if recv not in LAST_SUBS: if recv not in LAST_SUBS:
LAST_SUBS[recv] = list() LAST_SUBS[recv] = list()
LAST_SUBS[recv].append(url) LAST_SUBS[recv].append(url)

View file

@ -1,94 +0,0 @@
# coding=utf-8
"""Repology.org module: the packaging hub"""
import datetime
import re
from nemubot import context
from nemubot.exception import IMException
from nemubot.hooks import hook
from nemubot.tools import web
from nemubot.tools.xmlparser.node import ModuleState
nemubotversion = 4.0
from nemubot.module.more import Response
URL_REPOAPI = "https://repology.org/api/v1/project/%s"
def get_json_project(project):
prj = web.getJSON(URL_REPOAPI % (project))
return prj
@hook.command("repology",
help="Display version information about a package",
help_usage={
"PACKAGE_NAME": "Retrieve informations about PACKAGE_NAME",
},
keywords={
"distro=DISTRO": "filter by disto",
"status=STATUS[,STATUS...]": "filter by status",
})
def cmd_repology(msg):
if len(msg.args) == 0:
raise IMException("Please provide at least a package name")
res = Response(channel=msg.channel, nomore="No more information on package")
for project in msg.args:
prj = get_json_project(project)
if len(prj) == 0:
raise IMException("Unable to find package " + project)
pkg_versions = {}
pkg_maintainers = {}
pkg_licenses = {}
summary = None
for repo in prj:
# Apply filters
if "distro" in msg.kwargs and repo["repo"].find(msg.kwargs["distro"]) < 0:
continue
if "status" in msg.kwargs and repo["status"] not in msg.kwargs["status"].split(","):
continue
name = repo["visiblename"] if "visiblename" in repo else repo["name"]
status = repo["status"] if "status" in repo else "unknown"
if name not in pkg_versions:
pkg_versions[name] = {}
if status not in pkg_versions[name]:
pkg_versions[name][status] = []
if repo["version"] not in pkg_versions[name][status]:
pkg_versions[name][status].append(repo["version"])
if "maintainers" in repo:
if name not in pkg_maintainers:
pkg_maintainers[name] = []
for maintainer in repo["maintainers"]:
if maintainer not in pkg_maintainers[name]:
pkg_maintainers[name].append(maintainer)
if "licenses" in repo:
if name not in pkg_licenses:
pkg_licenses[name] = []
for lic in repo["licenses"]:
if lic not in pkg_licenses[name]:
pkg_licenses[name].append(lic)
if "summary" in repo and summary is None:
summary = repo["summary"]
for pkgname in sorted(pkg_versions.keys()):
m = "Package " + pkgname + " (" + summary + ")"
if pkgname in pkg_licenses:
m += " under " + ", ".join(pkg_licenses[pkgname])
m += ": " + " - ".join([status + ": " + ", ".join(pkg_versions[pkgname][status]) for status in ["newest", "devel", "unique", "outdated", "legacy", "rolling", "noscheme", "untrusted", "ignored"] if status in pkg_versions[pkgname]])
if "distro" in msg.kwargs and pkgname in pkg_maintainers:
m += " - Maintained by " + ", ".join(pkg_maintainers[pkgname])
res.append_message(m)
return res

View file

@ -6,49 +6,34 @@ import random
import shlex import shlex
from nemubot import context from nemubot import context
from nemubot.exception import IMException from nemubot.exception import IRCException
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.message import Command
from nemubot.module.more import Response from more import Response
# MODULE INTERFACE #################################################### # MODULE INTERFACE ####################################################
@hook.command("choice") @hook("cmd_hook", "choice")
def cmd_choice(msg): def cmd_choice(msg):
if not len(msg.args): if not len(msg.args):
raise IMException("indicate some terms to pick!") raise IRCException("indicate some terms to pick!")
return Response(random.choice(msg.args), return Response(random.choice(msg.args),
channel=msg.channel, channel=msg.channel,
nick=msg.frm) nick=msg.nick)
@hook.command("choicecmd") @hook("cmd_hook", "choicecmd")
def cmd_choicecmd(msg): def cmd_choice(msg):
if not len(msg.args): if not len(msg.args):
raise IMException("indicate some command to pick!") raise IRCException("indicate some command to pick!")
choice = shlex.split(random.choice(msg.args)) choice = shlex.split(random.choice(msg.args))
return [x for x in context.subtreat(context.subparse(msg, choice))] return [x for x in context.subtreat(Command(choice[0][1:],
choice[1:],
to_response=msg.to_response,
@hook.command("choiceres") frm=msg.frm,
def cmd_choiceres(msg): server=msg.server))]
if not len(msg.args):
raise IMException("indicate some command to pick a message from!")
rl = [x for x in context.subtreat(context.subparse(msg, " ".join(msg.args)))]
if len(rl) <= 0:
return rl
r = random.choice(rl)
if isinstance(r, Response):
for i in range(len(r.messages) - 1, -1, -1):
if isinstance(r.messages[i], list):
r.messages = [ random.choice(random.choice(r.messages)) ]
elif isinstance(r.messages[i], str):
r.messages = [ random.choice(r.messages) ]
return r

View file

@ -2,30 +2,32 @@
"""Find information about an SAP transaction codes""" """Find information about an SAP transaction codes"""
import re
import urllib.parse import urllib.parse
import urllib.request import urllib.request
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from nemubot.exception import IMException from nemubot.exception import IRCException
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.tools import web from nemubot.tools import web
from nemubot.tools.web import striphtml
nemubotversion = 4.0 nemubotversion = 4.0
from nemubot.module.more import Response from more import Response
def help_full(): def help_full():
return "Retrieve SAP transaction codes and details using tcodes or keywords: !tcode <transaction code|keywords>" return "Retrieve SAP transaction codes and details using tcodes or keywords: !tcode <transaction code|keywords>"
@hook.command("tcode") @hook("cmd_hook", "tcode")
def cmd_tcode(msg): def cmd_tcode(msg):
if not len(msg.args): if not len(msg.args):
raise IMException("indicate a transaction code or " raise IRCException("indicate a transaction code or "
"a keyword to search!") "a keyword to search!")
url = ("https://www.tcodesearch.com/tcodes/search?q=%s" % url = ("http://www.tcodesearch.com/tcodes/search?q=%s" %
urllib.parse.quote(msg.args[0])) urllib.parse.quote(msg.args[0]))
page = web.getURLContent(url) page = web.getURLContent(url)

View file

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

View file

@ -10,7 +10,7 @@ from nemubot.hooks import hook
nemubotversion = 3.4 nemubotversion = 3.4
from nemubot.module.more import Response from more import Response
def help_full(): def help_full():
@ -19,7 +19,7 @@ def help_full():
" hh:mm") " hh:mm")
@hook.command("sleepytime") @hook("cmd_hook", "sleepytime")
def cmd_sleep(msg): def cmd_sleep(msg):
if len(msg.args) and re.match("[0-9]{1,2}[h':.,-]([0-9]{1,2})?[m'\":.,-]?", if len(msg.args) and re.match("[0-9]{1,2}[h':.,-]([0-9]{1,2})?[m'\":.,-]?",
msg.args[0]) is not None: msg.args[0]) is not None:

View file

@ -1,116 +0,0 @@
"""Summarize texts"""
# PYTHON STUFFS #######################################################
from urllib.parse import quote
from nemubot import context
from nemubot.exception import IMException
from nemubot.hooks import hook
from nemubot.tools import web
from nemubot.module.more import Response
from nemubot.module.urlreducer import LAST_URLS
# GLOBALS #############################################################
URL_API = "https://api.smmry.com/?SM_API_KEY=%s"
# LOADING #############################################################
def load(context):
if not context.config or "apikey" not in context.config:
raise ImportError("You need a Smmry API key in order to use this "
"module. Add it to the module configuration file:\n"
"<module name=\"smmry\" apikey=\"XXXXXXXXXXXXXXXX\" "
"/>\nRegister at https://smmry.com/partner")
global URL_API
URL_API = URL_API % context.config["apikey"]
# MODULE INTERFACE ####################################################
@hook.command("smmry",
help="Summarize the following words/command return",
help_usage={
"WORDS/CMD": ""
},
keywords={
"keywords?=X": "Returns keywords instead of summary (count optional)",
"length=7": "The number of sentences returned, default 7",
"break": "inserts the string [BREAK] between sentences",
"ignore_length": "returns summary regardless of quality or length",
"quote_avoid": "sentences with quotations will be excluded",
"question_avoid": "sentences with question will be excluded",
"exclamation_avoid": "sentences with exclamation marks will be excluded",
})
def cmd_smmry(msg):
if not len(msg.args):
global LAST_URLS
if msg.channel in LAST_URLS and len(LAST_URLS[msg.channel]) > 0:
msg.args.append(LAST_URLS[msg.channel].pop())
else:
raise IMException("I have no more URL to sum up.")
URL = URL_API
if "length" in msg.kwargs:
if int(msg.kwargs["length"]) > 0 :
URL += "&SM_LENGTH=" + msg.kwargs["length"]
else:
msg.kwargs["ignore_length"] = True
if "break" in msg.kwargs: URL += "&SM_WITH_BREAK"
if "ignore_length" in msg.kwargs: URL += "&SM_IGNORE_LENGTH"
if "quote_avoid" in msg.kwargs: URL += "&SM_QUOTE_AVOID"
if "question_avoid" in msg.kwargs: URL += "&SM_QUESTION_AVOID"
if "exclamation_avoid" in msg.kwargs: URL += "&SM_EXCLAMATION_AVOID"
if "keywords" in msg.kwargs and msg.kwargs["keywords"] is not None and int(msg.kwargs["keywords"]) > 0: URL += "&SM_KEYWORD_COUNT=" + msg.kwargs["keywords"]
res = Response(channel=msg.channel)
if web.isURL(" ".join(msg.args)):
smmry = web.getJSON(URL + "&SM_URL=" + quote(" ".join(msg.args)), timeout=23)
else:
cnt = ""
for r in context.subtreat(context.subparse(msg, " ".join(msg.args))):
if isinstance(r, Response):
for i in range(len(r.messages) - 1, -1, -1):
if isinstance(r.messages[i], list):
for j in range(len(r.messages[i]) - 1, -1, -1):
cnt += r.messages[i][j] + "\n"
elif isinstance(r.messages[i], str):
cnt += r.messages[i] + "\n"
else:
cnt += str(r.messages) + "\n"
elif isinstance(r, Text):
cnt += r.message + "\n"
else:
cnt += str(r) + "\n"
smmry = web.getJSON(URL, body="sm_api_input=" + quote(cnt), timeout=23)
if "sm_api_error" in smmry:
if smmry["sm_api_error"] == 0:
title = "Internal server problem (not your fault)"
elif smmry["sm_api_error"] == 1:
title = "Incorrect submission variables"
elif smmry["sm_api_error"] == 2:
title = "Intentional restriction (low credits?)"
elif smmry["sm_api_error"] == 3:
title = "Summarization error"
else:
title = "Unknown error"
raise IMException(title + ": " + smmry['sm_api_message'].lower())
if "keywords" in msg.kwargs:
smmry["sm_api_content"] = ", ".join(smmry["sm_api_keyword_array"])
if "sm_api_title" in smmry and smmry["sm_api_title"] != "":
res.append_message(smmry["sm_api_content"], title=smmry["sm_api_title"])
else:
res.append_message(smmry["sm_api_content"])
return res

View file

@ -10,13 +10,13 @@ import urllib.request
import urllib.parse import urllib.parse
from nemubot import context from nemubot import context
from nemubot.exception import IMException from nemubot.exception import IRCException
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.tools.xmlparser.node import ModuleState from nemubot.tools.xmlparser.node import ModuleState
nemubotversion = 3.4 nemubotversion = 3.4
from nemubot.module.more import Response from more import Response
def load(context): def load(context):
context.data.setIndex("name", "phone") context.data.setIndex("name", "phone")
@ -46,89 +46,47 @@ def send_sms(frm, api_usr, api_key, content):
return None return None
def check_sms_dests(dests, cur_epoch):
"""Raise exception if one of the dest is not known or has already receive a SMS recently @hook("cmd_hook", "sms")
""" def cmd_sms(msg):
for u in dests: if not len(msg.args):
raise IRCException("À qui veux-tu envoyer ce SMS ?")
# Check dests
cur_epoch = time.mktime(time.localtime());
for u in msg.args[0].split(","):
if u not in context.data.index: if u not in context.data.index:
raise IMException("Désolé, je sais pas comment envoyer de SMS à %s." % u) raise IRCException("Désolé, je sais pas comment envoyer de SMS à %s." % u)
elif cur_epoch - float(context.data.index[u]["lastuse"]) < 42: elif cur_epoch - float(context.data.index[u]["lastuse"]) < 42:
raise IMException("Un peu de calme, %s a déjà reçu un SMS il n'y a pas si longtemps." % u) raise IRCException("Un peu de calme, %s a déjà reçu un SMS il n'y a pas si longtemps." % u)
return True
# Go!
def send_sms_to_list(msg, frm, dests, content, cur_epoch):
fails = list() fails = list()
for u in dests: for u in msg.args[0].split(","):
context.data.index[u]["lastuse"] = cur_epoch context.data.index[u]["lastuse"] = cur_epoch
test = send_sms(frm, context.data.index[u]["user"], context.data.index[u]["key"], content) if msg.to_response[0] == msg.frm:
frm = msg.frm
else:
frm = msg.frm + "@" + msg.to[0]
test = send_sms(frm, context.data.index[u]["user"], context.data.index[u]["key"], " ".join(msg.args[1:]))
if test is not None: if test is not None:
fails.append( "%s: %s" % (u, test) ) fails.append( "%s: %s" % (u, test) )
if len(fails) > 0: if len(fails) > 0:
return Response("quelque chose ne s'est pas bien passé durant l'envoi du SMS : " + ", ".join(fails), msg.channel, msg.frm) return Response("quelque chose ne s'est pas bien passé durant l'envoi du SMS : " + ", ".join(fails), msg.channel, msg.nick)
else: else:
return Response("le SMS a bien été envoyé", msg.channel, msg.frm) return Response("le SMS a bien été envoyé", msg.channel, msg.nick)
@hook.command("sms")
def cmd_sms(msg):
if not len(msg.args):
raise IMException("À qui veux-tu envoyer ce SMS ?")
cur_epoch = time.mktime(time.localtime())
dests = msg.args[0].split(",")
frm = msg.frm if msg.to_response[0] == msg.frm else msg.frm + "@" + msg.to[0]
content = " ".join(msg.args[1:])
check_sms_dests(dests, cur_epoch)
return send_sms_to_list(msg, frm, dests, content, cur_epoch)
@hook.command("smscmd")
def cmd_smscmd(msg):
if not len(msg.args):
raise IMException("À qui veux-tu envoyer ce SMS ?")
cur_epoch = time.mktime(time.localtime())
dests = msg.args[0].split(",")
frm = msg.frm if msg.to_response[0] == msg.frm else msg.frm + "@" + msg.to[0]
cmd = " ".join(msg.args[1:])
content = None
for r in context.subtreat(context.subparse(msg, cmd)):
if isinstance(r, Response):
for m in r.messages:
if isinstance(m, list):
for n in m:
content = n
break
if content is not None:
break
elif isinstance(m, str):
content = m
break
elif isinstance(r, Text):
content = r.message
if content is None:
raise IMException("Aucun SMS envoyé : le résultat de la commande n'a pas retourné de contenu.")
check_sms_dests(dests, cur_epoch)
return send_sms_to_list(msg, frm, dests, content, cur_epoch)
apiuser_ask = re.compile(r"(utilisateur|user|numéro|numero|compte|abonne|abone|abonné|account)\s+(est|is)\s+(?P<user>[0-9]{7,})", re.IGNORECASE) apiuser_ask = re.compile(r"(utilisateur|user|numéro|numero|compte|abonne|abone|abonné|account)\s+(est|is)\s+(?P<user>[0-9]{7,})", re.IGNORECASE)
apikey_ask = re.compile(r"(clef|key|password|mot de passe?)\s+(?:est|is)?\s+(?P<key>[a-zA-Z0-9]{10,})", re.IGNORECASE) apikey_ask = re.compile(r"(clef|key|password|mot de passe?)\s+(?:est|is)?\s+(?P<key>[a-zA-Z0-9]{10,})", re.IGNORECASE)
@hook.ask() @hook("ask_default")
def parseask(msg): def parseask(msg):
if msg.message.find("Free") >= 0 and ( if msg.text.find("Free") >= 0 and (
msg.message.find("API") >= 0 or msg.message.find("api") >= 0) and ( msg.text.find("API") >= 0 or msg.text.find("api") >= 0) and (
msg.message.find("SMS") >= 0 or msg.message.find("sms") >= 0): msg.text.find("SMS") >= 0 or msg.text.find("sms") >= 0):
resuser = apiuser_ask.search(msg.message) resuser = apiuser_ask.search(msg.text)
reskey = apikey_ask.search(msg.message) reskey = apikey_ask.search(msg.text)
if resuser is not None and reskey is not None: if resuser is not None and reskey is not None:
apiuser = resuser.group("user") apiuser = resuser.group("user")
apikey = reskey.group("key") apikey = reskey.group("key")
@ -136,18 +94,18 @@ def parseask(msg):
test = send_sms("nemubot", apiuser, apikey, test = send_sms("nemubot", apiuser, apikey,
"Vous avez enregistré vos codes d'authentification dans nemubot, félicitation !") "Vous avez enregistré vos codes d'authentification dans nemubot, félicitation !")
if test is not None: if test is not None:
return Response("je n'ai pas pu enregistrer tes identifiants : %s" % test, msg.channel, msg.frm) return Response("je n'ai pas pu enregistrer tes identifiants : %s" % test, msg.channel, msg.nick)
if msg.frm in context.data.index: if msg.nick in context.data.index:
context.data.index[msg.frm]["user"] = apiuser context.data.index[msg.nick]["user"] = apiuser
context.data.index[msg.frm]["key"] = apikey context.data.index[msg.nick]["key"] = apikey
else: else:
ms = ModuleState("phone") ms = ModuleState("phone")
ms.setAttribute("name", msg.frm) ms.setAttribute("name", msg.nick)
ms.setAttribute("user", apiuser) ms.setAttribute("user", apiuser)
ms.setAttribute("key", apikey) ms.setAttribute("key", apikey)
ms.setAttribute("lastuse", 0) ms.setAttribute("lastuse", 0)
context.data.addChild(ms) context.data.addChild(ms)
context.save() context.save()
return Response("ok, c'est noté. Je t'ai envoyé un SMS pour tester ;)", return Response("ok, c'est noté. Je t'ai envoyé un SMS pour tester ;)",
msg.channel, msg.frm) msg.channel, msg.nick)

View file

@ -1,25 +1,53 @@
# coding=utf-8
"""Check words spelling""" """Check words spelling"""
# PYTHON STUFFS ####################################################### import re
from urllib.parse import quote
from nemubot import context from nemubot import context
from nemubot.exception import IMException from nemubot.exception import IRCException
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.tools.xmlparser.node import ModuleState from nemubot.tools.xmlparser.node import ModuleState
from .pyaspell import Aspell from .pyaspell import Aspell
from .pyaspell import AspellError from .pyaspell import AspellError
from nemubot.module.more import Response nemubotversion = 3.4
from more import Response
# LOADING ############################################################# def help_full():
return "!spell [<lang>] <word>: give the correct spelling of <word> in <lang=fr>."
def load(context): def load(context):
context.data.setIndex("name", "score") context.data.setIndex("name", "score")
@hook("cmd_hook", "spell")
def cmd_spell(msg):
if not len(msg.args):
raise IRCException("indique une orthographe approximative du mot dont tu veux vérifier l'orthographe.")
# MODULE CORE ######################################################### lang = "fr"
strRes = list()
for word in msg.args:
if len(word) <= 2 and len(msg.args) > 2:
lang = word
else:
try:
r = check_spell(word, lang)
except AspellError:
return Response("Je n'ai pas le dictionnaire `%s' :(" % lang, msg.channel, msg.nick)
if r == True:
add_score(msg.nick, "correct")
strRes.append("l'orthographe de `%s' est correcte" % word)
elif len(r) > 0:
add_score(msg.nick, "bad")
strRes.append("suggestions pour `%s' : %s" % (word, ", ".join(r)))
else:
add_score(msg.nick, "bad")
strRes.append("aucune suggestion pour `%s'" % word)
return Response(strRes, channel=msg.channel, nick=msg.nick)
def add_score(nick, t): def add_score(nick, t):
if nick not in context.data.index: if nick not in context.data.index:
@ -33,59 +61,12 @@ def add_score(nick, t):
context.data.index[nick][t] = 1 context.data.index[nick][t] = 1
context.save() context.save()
@hook("cmd_hook", "spellscore")
def check_spell(word, lang='fr'):
a = Aspell([("lang", lang)])
if a.check(word.encode("utf-8")):
ret = True
else:
ret = a.suggest(word.encode("utf-8"))
a.close()
return ret
# MODULE INTERFACE ####################################################
@hook.command("spell",
help="give the correct spelling of given words",
help_usage={"WORD": "give the correct spelling of the WORD."},
keywords={"lang=": "change the language use for checking, default fr"})
def cmd_spell(msg):
if not len(msg.args):
raise IMException("indique une orthographe approximative du mot dont tu veux vérifier l'orthographe.")
lang = msg.kwargs["lang"] if "lang" in msg.kwargs else "fr"
res = Response(channel=msg.channel)
for word in msg.args:
try:
r = check_spell(word, lang)
except AspellError:
raise IMException("Je n'ai pas le dictionnaire `%s' :(" % lang)
if r == True:
add_score(msg.frm, "correct")
res.append_message("l'orthographe de `%s' est correcte" % word)
elif len(r) > 0:
add_score(msg.frm, "bad")
res.append_message(r, title="suggestions pour `%s'" % word)
else:
add_score(msg.frm, "bad")
res.append_message("aucune suggestion pour `%s'" % word)
return res
@hook.command("spellscore",
help="Show spell score (tests, mistakes, ...) for someone",
help_usage={"USER": "Display score of USER"})
def cmd_score(msg): def cmd_score(msg):
res = list() res = list()
unknown = list() unknown = list()
if not len(msg.args): if not len(msg.args):
raise IMException("De qui veux-tu voir les scores ?") raise IRCException("De qui veux-tu voir les scores ?")
for cmd in msg.args: for cmd in msg.args:
if cmd in context.data.index: if cmd in context.data.index:
res.append(Response("%s: %s" % (cmd, " ; ".join(["%s: %d" % (a, context.data.index[cmd].getInt(a)) for a in context.data.index[cmd].attributes.keys() if a != "name"])), channel=msg.channel)) res.append(Response("%s: %s" % (cmd, " ; ".join(["%s: %d" % (a, context.data.index[cmd].getInt(a)) for a in context.data.index[cmd].attributes.keys() if a != "name"])), channel=msg.channel))
@ -95,3 +76,12 @@ def cmd_score(msg):
res.append(Response("%s inconnus" % ", ".join(unknown), channel=msg.channel)) res.append(Response("%s inconnus" % ", ".join(unknown), channel=msg.channel))
return res return res
def check_spell(word, lang='fr'):
a = Aspell([("lang", lang)])
if a.check(word.encode("utf-8")):
ret = True
else:
ret = a.suggest(word.encode("utf-8"))
a.close()
return ret

View file

@ -1,53 +1,32 @@
"""Postal tracking module""" import urllib.request
# PYTHON STUFF ############################################
import json
import urllib.parse import urllib.parse
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import re
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.exception import IMException from nemubot.exception import IRCException
from nemubot.tools.web import getURLContent, getURLHeaders, getJSON from nemubot.tools.web import getURLContent
from nemubot.module.more import Response from more import Response
nemubotversion = 4.0
# POSTAGE SERVICE PARSERS ############################################ def help_full():
return "Traquez vos courriers La Poste ou Colissimo en utilisant la commande: !laposte <tracking number> ou !colissimo <tracking number>\nCe service se base sur http://www.csuivi.courrier.laposte.fr/suivi/index et http://www.colissimo.fr/portail_colissimo/suivre.do"
def get_tnt_info(track_id):
values = []
data = getURLContent('https://www.tnt.fr/public/suivi_colis/recherche/visubontransport.do?bonTransport=%s' % track_id)
soup = BeautifulSoup(data)
status_list = soup.find('div', class_='result__content')
if not status_list:
return None
last_status = status_list.find('div', class_='roster')
if last_status:
for info in last_status.find_all('div', class_='roster__item'):
values.append(info.get_text().strip())
if len(values) == 3:
return (values[0], values[1], values[2])
def get_colissimo_info(colissimo_id): def get_colissimo_info(colissimo_id):
colissimo_data = getURLContent("https://www.laposte.fr/particulier/outils/suivre-vos-envois?code=%s" % colissimo_id) colissimo_data = getURLContent("http://www.colissimo.fr/portail_colissimo/suivre.do?colispart=%s" % colissimo_id)
soup = BeautifulSoup(colissimo_data) soup = BeautifulSoup(colissimo_data)
dataArray = soup.find(class_='results-suivi') dataArray = soup.find(class_='dataArray')
if dataArray and dataArray.table and dataArray.table.tbody and dataArray.table.tbody.tr: if dataArray and dataArray.tbody and dataArray.tbody.tr:
td = dataArray.table.tbody.tr.find_all('td') date = dataArray.tbody.tr.find(headers="Date").get_text()
if len(td) > 2: libelle = dataArray.tbody.tr.find(headers="Libelle").get_text().replace('\n', '').replace('\t', '').replace('\r', '')
date = td[0].get_text() site = dataArray.tbody.tr.find(headers="site").get_text().strip()
libelle = re.sub(r'[\n\t\r]', '', td[1].get_text()) return (date, libelle, site.strip())
site = td[2].get_text().strip()
return (date, libelle, site.strip())
def get_chronopost_info(track_id): def get_chronopost_info(track_id):
data = urllib.parse.urlencode({'listeNumeros': track_id}) data = urllib.parse.urlencode({'listeNumeros': track_id})
track_baseurl = "https://www.chronopost.fr/expedier/inputLTNumbersNoJahia.do?lang=fr_FR" track_baseurl = "http://www.chronopost.fr/expedier/inputLTNumbersNoJahia.do?lang=fr_FR"
track_data = getURLContent(track_baseurl, data.encode('utf-8')) track_data = urllib.request.urlopen(track_baseurl, data.encode('utf-8'))
soup = BeautifulSoup(track_data) soup = BeautifulSoup(track_data)
infoClass = soup.find(class_='numeroColi2') infoClass = soup.find(class_='numeroColi2')
@ -60,273 +39,104 @@ def get_chronopost_info(track_id):
libelle = info[1] libelle = info[1]
return (date, libelle) return (date, libelle)
def get_colisprive_info(track_id): def get_colisprive_info(track_id):
data = urllib.parse.urlencode({'numColis': track_id}) data = urllib.parse.urlencode({'numColis': track_id})
track_baseurl = "https://www.colisprive.com/moncolis/pages/detailColis.aspx" track_baseurl = "https://www.colisprive.com/moncolis/pages/detailColis.aspx"
track_data = getURLContent(track_baseurl, data.encode('utf-8')) track_data = urllib.request.urlopen(track_baseurl, data.encode('utf-8'))
soup = BeautifulSoup(track_data) soup = BeautifulSoup(track_data)
dataArray = soup.find(class_='BandeauInfoColis') dataArray = soup.find(class_='BandeauInfoColis')
if (dataArray and dataArray.find(class_='divStatut') if dataArray and dataArray.find(class_='divStatut') and dataArray.find(class_='divStatut').find(class_='tdText'):
and dataArray.find(class_='divStatut').find(class_='tdText')): status = dataArray.find(class_='divStatut').find(class_='tdText').get_text()
status = dataArray.find(class_='divStatut') \
.find(class_='tdText').get_text()
return status return status
def get_ups_info(track_id):
data = json.dumps({'Locale': "en_US", 'TrackingNumber': [track_id]})
track_baseurl = "https://www.ups.com/track/api/Track/GetStatus?loc=en_US"
track_data = getJSON(track_baseurl, data.encode('utf-8'), header={"Content-Type": "application/json"})
return (track_data["trackDetails"][0]["trackingNumber"],
track_data["trackDetails"][0]["packageStatus"],
track_data["trackDetails"][0]["shipmentProgressActivities"][0]["date"] + " " + track_data["trackDetails"][0]["shipmentProgressActivities"][0]["time"],
track_data["trackDetails"][0]["shipmentProgressActivities"][0]["location"],
track_data["trackDetails"][0]["shipmentProgressActivities"][0]["activityScan"])
def get_laposte_info(laposte_id): def get_laposte_info(laposte_id):
status, laposte_headers = getURLHeaders("https://www.laposte.fr/outils/suivre-vos-envois?" + urllib.parse.urlencode({'code': laposte_id})) data = urllib.parse.urlencode({'id': laposte_id})
laposte_baseurl = "http://www.part.csuivi.courrier.laposte.fr/suivi/index"
laposte_cookie = None laposte_data = urllib.request.urlopen(laposte_baseurl, data.encode('utf-8'))
for k,v in laposte_headers: soup = BeautifulSoup(laposte_data)
if k.lower() == "set-cookie" and v.find("access_token") >= 0: search_res = soup.find(class_='resultat_rech_simple_table').tbody.tr
laposte_cookie = v.split(";")[0] if (soup.find(class_='resultat_rech_simple_table').thead
and soup.find(class_='resultat_rech_simple_table').thead.tr
and len(search_res.find_all('td')) > 3):
field = search_res.find('td')
poste_id = field.get_text()
laposte_data = getJSON("https://api.laposte.fr/ssu/v1/suivi-unifie/idship/%s?lang=fr_FR" % urllib.parse.quote(laposte_id), header={"Accept": "application/json", "Cookie": laposte_cookie}) field = field.find_next('td')
poste_type = field.get_text()
shipment = laposte_data["shipment"] field = field.find_next('td')
return (shipment["product"], shipment["idShip"], shipment["event"][0]["label"], shipment["event"][0]["date"]) poste_date = field.get_text()
field = field.find_next('td')
poste_location = field.get_text()
def get_postnl_info(postnl_id): field = field.find_next('td')
data = urllib.parse.urlencode({'barcodes': postnl_id}) poste_status = field.get_text()
postnl_baseurl = "http://www.postnl.post/details/"
postnl_data = getURLContent(postnl_baseurl, data.encode('utf-8')) return (poste_type.lower(), poste_id.strip(), poste_status.lower(), poste_location, poste_date)
soup = BeautifulSoup(postnl_data)
if (soup.find(id='datatables')
and soup.find(id='datatables').tbody
and soup.find(id='datatables').tbody.tr):
search_res = soup.find(id='datatables').tbody.tr
if len(search_res.find_all('td')) >= 3:
field = field.find_next('td')
post_date = field.get_text()
field = field.find_next('td') @hook("cmd_hook", "track")
post_status = field.get_text()
field = field.find_next('td')
post_destination = field.get_text()
return (post_status.lower(), post_destination, post_date)
def get_usps_info(usps_id):
usps_parcelurl = "https://tools.usps.com/go/TrackConfirmAction_input?" + urllib.parse.urlencode({'qtc_tLabels1': usps_id})
usps_data = getURLContent(usps_parcelurl)
soup = BeautifulSoup(usps_data)
if (soup.find(id="trackingHistory_1")
and soup.find(class_="tracking_history").find(class_="row_notification")
and soup.find(class_="tracking_history").find(class_="row_top").find_all("td")):
notification = soup.find(class_="tracking_history").find(class_="row_notification").text.strip()
date = re.sub(r"\s+", " ", soup.find(class_="tracking_history").find(class_="row_top").find_all("td")[0].text.strip())
status = soup.find(class_="tracking_history").find(class_="row_top").find_all("td")[1].text.strip()
last_location = soup.find(class_="tracking_history").find(class_="row_top").find_all("td")[2].text.strip()
print(notification)
return (notification, date, status, last_location)
def get_fedex_info(fedex_id, lang="en_US"):
data = urllib.parse.urlencode({
'data': json.dumps({
"TrackPackagesRequest": {
"appType": "WTRK",
"appDeviceType": "DESKTOP",
"uniqueKey": "",
"processingParameters": {},
"trackingInfoList": [
{
"trackNumberInfo": {
"trackingNumber": str(fedex_id),
"trackingQualifier": "",
"trackingCarrier": ""
}
}
]
}
}),
'action': "trackpackages",
'locale': lang,
'version': 1,
'format': "json"
})
fedex_baseurl = "https://www.fedex.com/trackingCal/track"
fedex_data = getJSON(fedex_baseurl, data.encode('utf-8'))
if ("TrackPackagesResponse" in fedex_data and
"packageList" in fedex_data["TrackPackagesResponse"] and
len(fedex_data["TrackPackagesResponse"]["packageList"]) and
(not fedex_data["TrackPackagesResponse"]["errorList"][0]["code"] or
fedex_data["TrackPackagesResponse"]["errorList"][0]["code"] == '0') and
not fedex_data["TrackPackagesResponse"]["packageList"][0]["errorList"][0]["code"]
):
return fedex_data["TrackPackagesResponse"]["packageList"][0]
def get_dhl_info(dhl_id, lang="en"):
dhl_parcelurl = "http://www.dhl.com/shipmentTracking?" + urllib.parse.urlencode({'AWB': dhl_id})
dhl_data = getJSON(dhl_parcelurl)
if "results" in dhl_data and dhl_data["results"]:
return dhl_data["results"][0]
# TRACKING HANDLERS ###################################################
def handle_tnt(tracknum):
info = get_tnt_info(tracknum)
if info:
status, date, place = info
placestr = ''
if place:
placestr = ' à \x02{place}\x0f'
return ('Le colis \x02{trackid}\x0f a actuellement le status: '
'\x02{status}\x0F mis à jour le \x02{date}\x0f{place}.'
.format(trackid=tracknum, status=status,
date=re.sub(r'\s+', ' ', date), place=placestr))
def handle_laposte(tracknum):
info = get_laposte_info(tracknum)
if info:
poste_type, poste_id, poste_status, poste_date = info
return ("\x02%s\x0F : \x02%s\x0F est actuellement "
"\x02%s\x0F (Mis à jour le \x02%s\x0F"
")." % (poste_type, poste_id, poste_status, poste_date))
def handle_postnl(tracknum):
info = get_postnl_info(tracknum)
if info:
post_status, post_destination, post_date = info
return ("PostNL \x02%s\x0F est actuellement "
"\x02%s\x0F vers le pays \x02%s\x0F (Mis à jour le \x02%s\x0F"
")." % (tracknum, post_status, post_destination, post_date))
def handle_usps(tracknum):
info = get_usps_info(tracknum)
if info:
notif, last_date, last_status, last_location = info
return ("USPS \x02{tracknum}\x0F: {last_status} in \x02{last_location}\x0F as of {last_date}: {notif}".format(tracknum=tracknum, notif=notif, last_date=last_date, last_status=last_status.lower(), last_location=last_location))
def handle_ups(tracknum):
info = get_ups_info(tracknum)
if info:
tracknum, status, last_date, last_location, last_status = info
return ("UPS \x02{tracknum}\x0F: {status}: in \x02{last_location}\x0F as of {last_date}: {last_status}".format(tracknum=tracknum, status=status, last_date=last_date, last_status=last_status.lower(), last_location=last_location))
def handle_colissimo(tracknum):
info = get_colissimo_info(tracknum)
if info:
date, libelle, site = info
return ("Colissimo: \x02%s\x0F : \x02%s\x0F Dernière mise à jour le "
"\x02%s\x0F au site \x02%s\x0F."
% (tracknum, libelle, date, site))
def handle_chronopost(tracknum):
info = get_chronopost_info(tracknum)
if info:
date, libelle = info
return ("Colis Chronopost: \x02%s\x0F : \x02%s\x0F. Dernière mise à "
"jour \x02%s\x0F." % (tracknum, libelle, date))
def handle_coliprive(tracknum):
info = get_colisprive_info(tracknum)
if info:
return ("Colis Privé: \x02%s\x0F : \x02%s\x0F." % (tracknum, info))
def handle_fedex(tracknum):
info = get_fedex_info(tracknum)
if info:
if info["displayActDeliveryDateTime"] != "":
return ("{trackingCarrierDesc}: \x02{statusWithDetails}\x0F: in \x02{statusLocationCity}, {statusLocationCntryCD}\x0F, delivered on: {displayActDeliveryDateTime}.".format(**info))
elif info["statusLocationCity"] != "":
return ("{trackingCarrierDesc}: \x02{statusWithDetails}\x0F: estimated delivery: {displayEstDeliveryDateTime}.".format(**info))
else:
return ("{trackingCarrierDesc}: \x02{statusWithDetails}\x0F: in \x02{statusLocationCity}, {statusLocationCntryCD}\x0F, estimated delivery: {displayEstDeliveryDateTime}.".format(**info))
def handle_dhl(tracknum):
info = get_dhl_info(tracknum)
if info:
return "DHL {label} {id}: \x02{description}\x0F".format(**info)
TRACKING_HANDLERS = {
'laposte': handle_laposte,
'postnl': handle_postnl,
'colissimo': handle_colissimo,
'chronopost': handle_chronopost,
'coliprive': handle_coliprive,
'tnt': handle_tnt,
'fedex': handle_fedex,
'dhl': handle_dhl,
'usps': handle_usps,
'ups': handle_ups,
}
# HOOKS ##############################################################
@hook.command("track",
help="Track postage delivery",
help_usage={
"TRACKING_ID [...]": "Track the specified postage IDs on various tracking services."
},
keywords={
"tracker=TRK": "Precise the tracker (default: all) among: " + ', '.join(TRACKING_HANDLERS)
})
def get_tracking_info(msg): def get_tracking_info(msg):
if not len(msg.args): if not len(msg.args):
raise IMException("Renseignez un identifiant d'envoi.") raise IRCException("Renseignez un identifiant d'envoi,")
res = Response(channel=msg.channel, count=" (%d suivis supplémentaires)") info = get_colisprive_info(msg.args[0])
if info:
return Response("Colis Privé: \x02%s\x0F : \x02%s\x0F." % (msg.args[0], info), msg.channel)
if 'tracker' in msg.kwargs: info = get_chronopost_info(msg.args[0])
if msg.kwargs['tracker'] in TRACKING_HANDLERS: if info:
trackers = { date, libelle = info
msg.kwargs['tracker']: TRACKING_HANDLERS[msg.kwargs['tracker']] return Response("Colis Chronopost: \x02%s\x0F : \x02%s\x0F. Dernière mise à jour \x02%s\x0F." % (msg.args[0], libelle, date), msg.channel)
}
else:
raise IMException("No tracker named \x02{tracker}\x0F, please use"
" one of the following: \x02{trackers}\x0F"
.format(tracker=msg.kwargs['tracker'],
trackers=', '
.join(TRACKING_HANDLERS.keys())))
else:
trackers = TRACKING_HANDLERS
for tracknum in msg.args: info = get_colissimo_info(msg.args[0])
for name, tracker in trackers.items(): if info:
ret = tracker(tracknum) date, libelle, site = info
if ret: return Response("Colissimo: \x02%s\x0F : \x02%s\x0F. Dernière mise à jour le \x02%s\x0F au site \x02%s\x0F." % (msg.args[0], libelle, date, site), msg.channel)
res.append_message(ret)
break info = get_laposte_info(msg.args[0])
if not ret: if info:
res.append_message("L'identifiant \x02{id}\x0F semble incorrect," poste_type, poste_id, poste_status, poste_location, poste_date = info
" merci de vérifier son exactitude." return Response("Le courrier de type \x02%s\x0F : \x02%s\x0F est actuellement \x02%s\x0F dans la zone \x02%s\x0F (Mis à jour le \x02%s\x0F)." % (poste_type, poste_id, poste_status, poste_location, poste_date), msg.channel)
.format(id=tracknum)) return Response("L'identifiant recherché semble incorrect, merci de vérifier son exactitude.", msg.channel)
return res
@hook("cmd_hook", "colisprive")
def get_colisprive_tracking_info(msg):
if not len(msg.args):
raise IRCException("Renseignez un identifiant d'envoi,")
info = get_colisprive_info(msg.args[0])
if info:
return Response("Colis: \x02%s\x0F : \x02%s\x0F." % (msg.args[0], info), msg.channel)
return Response("L'identifiant recherché semble incorrect, merci de vérifier son exactitude.", msg.channel)
@hook("cmd_hook", "chronopost")
def get_chronopost_tracking_info(msg):
if not len(msg.args):
raise IRCException("Renseignez un identifiant d'envoi,")
info = get_chronopost_info(msg.args[0])
if info:
date, libelle = info
return Response("Colis: \x02%s\x0F : \x02%s\x0F. Dernière mise à jour \x02%s\x0F." % (msg.args[0], libelle, date), msg.channel)
return Response("L'identifiant recherché semble incorrect, merci de vérifier son exactitude.", msg.channel)
@hook("cmd_hook", "colissimo")
def get_colissimo_tracking_info(msg):
if not len(msg.args):
raise IRCException("Renseignez un identifiant d'envoi,")
info = get_colissimo_info(msg.args[0])
if info:
date, libelle, site = info
return Response("Colis: \x02%s\x0F : \x02%s\x0F. Dernière mise à jour le \x02%s\x0F au site \x02%s\x0F." % (msg.args[0], libelle, date, site), msg.channel)
return Response("L'identifiant recherché semble incorrect, merci de vérifier son exactitude.", msg.channel)
@hook("cmd_hook", "laposte")
def get_laposte_tracking_info(msg):
if not len(msg.args):
raise IRCException("Renseignez un identifiant d'envoi,")
info = get_laposte_info(msg.args[0])
if info:
poste_type, poste_id, poste_status, poste_location, poste_date = info
return Response("Le courrier de type \x02%s\x0F : \x02%s\x0F est actuellement \x02%s\x0F dans la zone \x02%s\x0F (Mis à jour le \x02%s\x0F)." % (poste_type, poste_id, poste_status, poste_location, poste_date), msg.channel)
return Response("L'identifiant recherché semble incorrect, merci de vérifier son exactitude.", msg.channel)

View file

@ -1,23 +1,27 @@
"""Find synonyms""" # coding=utf-8
# PYTHON STUFFS ####################################################### """Find synonyms"""
import re import re
from urllib.parse import quote from urllib.parse import quote
from nemubot.exception import IMException from nemubot.exception import IRCException
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.tools import web from nemubot.tools import web
from nemubot.module.more import Response nemubotversion = 4.0
from more import Response
# LOADING ############################################################# def help_full():
return "!syno [LANG] <word>: give a list of synonyms for <word>."
def load(context): def load(context):
global lang_binding global lang_binding
if not context.config or not "bighugelabskey" in context.config: if not context.config or not context.config.hasAttribute("bighugelabskey"):
logger.error("You need a NigHugeLabs API key in order to have english " logger.error("You need a NigHugeLabs API key in order to have english "
"theasorus. Add it to the module configuration file:\n" "theasorus. Add it to the module configuration file:\n"
"<module name=\"syno\" bighugelabskey=\"XXXXXXXXXXXXXXXX\"" "<module name=\"syno\" bighugelabskey=\"XXXXXXXXXXXXXXXX\""
@ -26,10 +30,8 @@ def load(context):
lang_binding["en"] = lambda word: get_english_synos(context.config["bighugelabskey"], word) lang_binding["en"] = lambda word: get_english_synos(context.config["bighugelabskey"], word)
# MODULE CORE #########################################################
def get_french_synos(word): def get_french_synos(word):
url = "https://crisco.unicaen.fr/des/synonymes/" + quote(word) url = "http://www.crisco.unicaen.fr/des/synonymes/" + quote(word.encode("ISO-8859-1"))
page = web.getURLContent(url) page = web.getURLContent(url)
best = list(); synos = list(); anton = list() best = list(); synos = list(); anton = list()
@ -53,7 +55,7 @@ def get_french_synos(word):
def get_english_synos(key, word): def get_english_synos(key, word):
cnt = web.getJSON("https://words.bighugelabs.com/api/2/%s/%s/json" % cnt = web.getJSON("http://words.bighugelabs.com/api/2/%s/%s/json" %
(quote(key), quote(word.encode("ISO-8859-1")))) (quote(key), quote(word.encode("ISO-8859-1"))))
best = list(); synos = list(); anton = list() best = list(); synos = list(); anton = list()
@ -70,29 +72,24 @@ def get_english_synos(key, word):
lang_binding = { 'fr': get_french_synos } lang_binding = { 'fr': get_french_synos }
# MODULE INTERFACE #################################################### @hook("cmd_hook", "synonymes", data="synonymes")
@hook("cmd_hook", "antonymes", data="antonymes")
@hook.command("synonymes", data="synonymes",
help="give a list of synonyms",
help_usage={"WORD": "give synonyms of the given WORD"},
keywords={
"lang=LANG": "change the dictionnary language: default fr, available: " + ", ".join(lang_binding)
})
@hook.command("antonymes", data="antonymes",
help="give a list of antonyms",
help_usage={"WORD": "give antonyms of the given WORD"},
keywords={
"lang=LANG": "change the dictionnary language: default fr, available: " + ", ".join(lang_binding)
})
def go(msg, what): def go(msg, what):
if not len(msg.args): if not len(msg.args):
raise IMException("de quel mot veux-tu connaître la liste des synonymes ?") raise IRCException("de quel mot veux-tu connaître la liste des synonymes ?")
lang = msg.kwargs["lang"] if "lang" in msg.kwargs else "fr" # Detect lang
word = ' '.join(msg.args) if msg.args[0] in lang_binding:
func = lang_binding[msg.args[0]]
word = ' '.join(msg.args[1:])
else:
func = lang_binding["fr"]
word = ' '.join(msg.args)
# TODO: depreciate usage without lang
#raise IRCException("language %s is not handled yet." % msg.args[0])
try: try:
best, synos, anton = lang_binding[lang](word) best, synos, anton = func(word)
except: except:
best, synos, anton = (list(), list(), list()) best, synos, anton = (list(), list(), list())
@ -103,7 +100,7 @@ def go(msg, what):
if len(synos) > 0: res.append_message(synos) if len(synos) > 0: res.append_message(synos)
return res return res
else: else:
raise IMException("Aucun synonyme de %s n'a été trouvé" % word) raise IRCException("Aucun synonyme de %s n'a été trouvé" % word)
elif what == "antonymes": elif what == "antonymes":
if len(anton) > 0: if len(anton) > 0:
@ -111,7 +108,7 @@ def go(msg, what):
title="Antonymes de %s" % word) title="Antonymes de %s" % word)
return res return res
else: else:
raise IMException("Aucun antonyme de %s n'a été trouvé" % word) raise IRCException("Aucun antonyme de %s n'a été trouvé" % word)
else: else:
raise IMException("WHAT?!") raise IRCException("WHAT?!")

View file

@ -1,19 +1,19 @@
from datetime import datetime from datetime import datetime
import urllib import urllib
from nemubot.exception import IMException from nemubot.exception import IRCException
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.tools import human from nemubot.tools import human
from nemubot.tools.web import getJSON from nemubot.tools.web import getJSON
nemubotversion = 4.0 nemubotversion = 4.0
from nemubot.module.more import Response from more import Response
URL_TPBAPI = None URL_TPBAPI = None
def load(context): def load(context):
if not context.config or "url" not in context.config: if not context.config or not context.config.hasAttribute("url"):
raise ImportError("You need a TPB API in order to use the !tpb feature" raise ImportError("You need a TPB API in order to use the !tpb feature"
". Add it to the module configuration file:\n<module" ". Add it to the module configuration file:\n<module"
"name=\"tpb\" url=\"http://tpbapi.org/\" />\nSample " "name=\"tpb\" url=\"http://tpbapi.org/\" />\nSample "
@ -22,10 +22,10 @@ def load(context):
global URL_TPBAPI global URL_TPBAPI
URL_TPBAPI = context.config["url"] URL_TPBAPI = context.config["url"]
@hook.command("tpb") @hook("cmd_hook", "tpb")
def cmd_tpb(msg): def cmd_tpb(msg):
if not len(msg.args): if not len(msg.args):
raise IMException("indicate an item to search!") raise IRCException("indicate an item to search!")
torrents = getJSON(URL_TPBAPI + urllib.parse.quote(" ".join(msg.args))) torrents = getJSON(URL_TPBAPI + urllib.parse.quote(" ".join(msg.args)))

View file

@ -1,27 +1,25 @@
# coding=utf-8
"""Translation module""" """Translation module"""
# PYTHON STUFFS ####################################################### import re
from urllib.parse import quote from urllib.parse import quote
from nemubot.exception import IMException from nemubot.exception import IRCException
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.tools import web from nemubot.tools import web
from nemubot.module.more import Response nemubotversion = 4.0
from more import Response
# GLOBALS #############################################################
LANG = ["ar", "zh", "cz", "en", "fr", "gr", "it", LANG = ["ar", "zh", "cz", "en", "fr", "gr", "it",
"ja", "ko", "pl", "pt", "ro", "es", "tr"] "ja", "ko", "pl", "pt", "ro", "es", "tr"]
URL = "http://api.wordreference.com/0.8/%s/json/%%s%%s/%%s" URL = "http://api.wordreference.com/0.8/%s/json/%%s%%s/%%s"
# LOADING #############################################################
def load(context): def load(context):
if not context.config or "wrapikey" not in context.config: if not context.config or not context.config.hasAttribute("wrapikey"):
raise ImportError("You need a WordReference API key in order to use " raise ImportError("You need a WordReference API key in order to use "
"this module. Add it to the module configuration " "this module. Add it to the module configuration "
"file:\n<module name=\"translate\" wrapikey=\"XXXXX\"" "file:\n<module name=\"translate\" wrapikey=\"XXXXX\""
@ -31,7 +29,57 @@ def load(context):
URL = URL % context.config["wrapikey"] URL = URL % context.config["wrapikey"]
# MODULE CORE ######################################################### def help_full():
return "!translate [lang] <term>[ <term>[...]]: Found translation of <term> from/to english to/from <lang>. Data © WordReference.com"
@hook("cmd_hook", "translate")
def cmd_translate(msg):
if not len(msg.args):
raise IRCException("which word would you translate?")
if len(msg.args) > 2 and msg.args[0] in LANG and msg.args[1] in LANG:
if msg.args[0] != "en" and msg.args[1] != "en":
raise IRCException("sorry, I can only translate to or from english")
langFrom = msg.args[0]
langTo = msg.args[1]
term = ' '.join(msg.args[2:])
elif len(msg.args) > 1 and msg.args[0] in LANG:
langFrom = msg.args[0]
if langFrom == "en":
langTo = "fr"
else:
langTo = "en"
term = ' '.join(msg.args[1:])
else:
langFrom = "en"
langTo = "fr"
term = ' '.join(msg.args)
wres = web.getJSON(URL % (langFrom, langTo, quote(term)))
if "Error" in wres:
raise IRCException(wres["Note"])
else:
res = Response(channel=msg.channel,
count=" (%d more meanings)",
nomore="No more translation")
for k in sorted(wres.keys()):
t = wres[k]
if len(k) > 4 and k[:4] == "term":
if "Entries" in t:
ent = t["Entries"]
else:
ent = t["PrincipalTranslations"]
for i in sorted(ent.keys()):
res.append_message("Translation of %s%s: %s" % (
ent[i]["OriginalTerm"]["term"],
meaning(ent[i]["OriginalTerm"]),
extract_traslation(ent[i])))
return res
def meaning(entry): def meaning(entry):
ret = list() ret = list()
@ -53,59 +101,3 @@ def extract_traslation(entry):
if "Note" in entry and entry["Note"]: if "Note" in entry and entry["Note"]:
ret.append("note: %s" % entry["Note"]) ret.append("note: %s" % entry["Note"])
return ", ".join(ret) return ", ".join(ret)
def translate(term, langFrom="en", langTo="fr"):
wres = web.getJSON(URL % (langFrom, langTo, quote(term)))
if "Error" in wres:
raise IMException(wres["Note"])
else:
for k in sorted(wres.keys()):
t = wres[k]
if len(k) > 4 and k[:4] == "term":
if "Entries" in t:
ent = t["Entries"]
else:
ent = t["PrincipalTranslations"]
for i in sorted(ent.keys()):
yield "Translation of %s%s: %s" % (
ent[i]["OriginalTerm"]["term"],
meaning(ent[i]["OriginalTerm"]),
extract_traslation(ent[i]))
# MODULE INTERFACE ####################################################
@hook.command("translate",
help="Word translation using WordReference.com",
help_usage={
"TERM": "Found translation of TERM from/to english to/from <lang>."
},
keywords={
"from=LANG": "language of the term you asked for translation between: en, " + ", ".join(LANG),
"to=LANG": "language of the translated terms between: en, " + ", ".join(LANG),
})
def cmd_translate(msg):
if not len(msg.args):
raise IMException("which word would you translate?")
langFrom = msg.kwargs["from"] if "from" in msg.kwargs else "en"
if "to" in msg.kwargs:
langTo = msg.kwargs["to"]
else:
langTo = "fr" if langFrom == "en" else "en"
if langFrom not in LANG or langTo not in LANG:
raise IMException("sorry, I can only translate to or from: " + ", ".join(LANG))
if langFrom != "en" and langTo != "en":
raise IMException("sorry, I can only translate to or from english")
res = Response(channel=msg.channel,
count=" (%d more meanings)",
nomore="No more translation")
for t in translate(" ".join(msg.args), langFrom=langFrom, langTo=langTo):
res.append_message(t)
return res

View file

@ -1,37 +0,0 @@
"""Search definition from urbandictionnary"""
# PYTHON STUFFS #######################################################
from urllib.parse import quote
from nemubot.exception import IMException
from nemubot.hooks import hook
from nemubot.tools import web
from nemubot.module.more import Response
# MODULE CORE #########################################################
def search(terms):
return web.getJSON(
"https://api.urbandictionary.com/v0/define?term=%s"
% quote(' '.join(terms)))
# MODULE INTERFACE ####################################################
@hook.command("urbandictionnary")
def udsearch(msg):
if not len(msg.args):
raise IMException("Indicate a term to search")
s = search(msg.args)
res = Response(channel=msg.channel, nomore="No more results",
count=" (%d more definitions)")
for i in s["list"]:
res.append_message(i["definition"].replace("\n", " "),
title=i["word"])
return res

View file

@ -1,173 +0,0 @@
"""URL reducer module"""
# PYTHON STUFFS #######################################################
import re
import json
from urllib.parse import urlparse
from urllib.parse import quote
from nemubot.exception import IMException
from nemubot.hooks import hook
from nemubot.message import Text
from nemubot.tools import web
# MODULE FUNCTIONS ####################################################
def default_reducer(url, data):
snd_url = url + quote(data, "/:%@&=?")
return web.getURLContent(snd_url)
def ycc_reducer(url, data):
return "https://ycc.fr/%s" % default_reducer(url, data)
def lstu_reducer(url, data):
json_data = json.loads(web.getURLContent(url, "lsturl=" + quote(data),
header={"Content-Type": "application/x-www-form-urlencoded"}))
if 'short' in json_data:
return json_data['short']
elif 'msg' in json_data:
raise IMException("Error: %s" % json_data['msg'])
else:
IMException("An error occured while shortening %s." % data)
# MODULE VARIABLES ####################################################
PROVIDERS = {
"tinyurl": (default_reducer, "https://tinyurl.com/api-create.php?url="),
"ycc": (ycc_reducer, "https://ycc.fr/redirection/create/"),
"framalink": (lstu_reducer, "https://frama.link/a?format=json"),
"huitre": (lstu_reducer, "https://huit.re/a?format=json"),
"lstu": (lstu_reducer, "https://lstu.fr/a?format=json"),
}
DEFAULT_PROVIDER = "framalink"
PROVIDERS_NETLOC = [urlparse(web.getNormalizedURL(url), "http").netloc for f, url in PROVIDERS.values()]
# LOADING #############################################################
def load(context):
global DEFAULT_PROVIDER
if "provider" in context.config:
if context.config["provider"] == "custom":
PROVIDERS["custom"] = context.config["provider_url"]
DEFAULT_PROVIDER = context.config["provider"]
# MODULE CORE #########################################################
def reduce_inline(txt, provider=None):
for url in re.findall("([a-zA-Z0-9+.-]+:(?://)?(?:[^ :/]+:[0-9]+)?[^ :]+)", txt):
txt = txt.replace(url, reduce(url, provider))
return txt
def reduce(url, provider=None):
"""Ask the url shortner website to reduce given URL
Argument:
url -- the URL to reduce
"""
if provider is None:
provider = DEFAULT_PROVIDER
return PROVIDERS[provider][0](PROVIDERS[provider][1], url)
def gen_response(res, msg, srv):
if res is None:
raise IMException("bad URL : %s" % srv)
else:
return Text("URL for %s: %s" % (srv, res), server=None,
to=msg.to_response)
## URL stack
LAST_URLS = dict()
@hook.message()
def parselisten(msg):
global LAST_URLS
if hasattr(msg, "message") and isinstance(msg.message, str):
urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?(?:[^ :/]+:[0-9]+)?[^ :]+)",
msg.message)
for url in urls:
o = urlparse(web._getNormalizedURL(url), "http")
# Skip short URLs
if (o.netloc == "" or o.netloc in PROVIDERS or
len(o.netloc) + len(o.path) < 17):
continue
for recv in msg.to:
if recv not in LAST_URLS:
LAST_URLS[recv] = list()
LAST_URLS[recv].append(url)
@hook.post()
def parseresponse(msg):
global LAST_URLS
if hasattr(msg, "text") and isinstance(msg.text, str):
urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?(?:[^ :/]+:[0-9]+)?[^ :]+)",
msg.text)
for url in urls:
o = urlparse(web._getNormalizedURL(url), "http")
# Skip short URLs
if (o.netloc == "" or o.netloc in PROVIDERS or
len(o.netloc) + len(o.path) < 17):
continue
for recv in msg.to:
if recv not in LAST_URLS:
LAST_URLS[recv] = list()
LAST_URLS[recv].append(url)
return msg
# MODULE INTERFACE ####################################################
@hook.command("framalink",
help="Reduce any long URL",
help_usage={
None: "Reduce the last URL said on the channel",
"URL [URL ...]": "Reduce the given URL(s)"
},
keywords={
"provider=SMTH": "Change the service provider used (by default: %s) among %s" % (DEFAULT_PROVIDER, ", ".join(PROVIDERS.keys()))
})
def cmd_reduceurl(msg):
minify = list()
if not len(msg.args):
global LAST_URLS
if msg.channel in LAST_URLS and len(LAST_URLS[msg.channel]) > 0:
minify.append(LAST_URLS[msg.channel].pop())
else:
raise IMException("I have no more URL to reduce.")
if len(msg.args) > 4:
raise IMException("I cannot reduce that many URLs at once.")
else:
minify += msg.args
if 'provider' in msg.kwargs and msg.kwargs['provider'] in PROVIDERS:
provider = msg.kwargs['provider']
else:
provider = DEFAULT_PROVIDER
res = list()
for url in minify:
o = urlparse(web.getNormalizedURL(url), "http")
minief_url = reduce(url, provider)
if o.netloc == "":
res.append(gen_response(minief_url, msg, o.scheme))
else:
res.append(gen_response(minief_url, msg, o.netloc))
return res

View file

@ -1,24 +1,24 @@
"""Gets information about velib stations""" # coding=utf-8
# PYTHON STUFFS ####################################################### """Gets information about velib stations"""
import re import re
from nemubot import context from nemubot import context
from nemubot.exception import IMException from nemubot.exception import IRCException
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.tools import web from nemubot.tools import web
from nemubot.module.more import Response nemubotversion = 4.0
from more import Response
# LOADING #############################################################
URL_API = None # http://www.velib.paris.fr/service/stationdetails/paris/%s URL_API = None # http://www.velib.paris.fr/service/stationdetails/paris/%s
def load(context): def load(context):
global URL_API global URL_API
if not context.config or "url" not in context.config: if not context.config or not context.config.hasAttribute("url"):
raise ImportError("Please provide url attribute in the module configuration") raise ImportError("Please provide url attribute in the module configuration")
URL_API = context.config["url"] URL_API = context.config["url"]
context.data.setIndex("name", "station") context.data.setIndex("name", "station")
@ -29,14 +29,25 @@ def load(context):
# context.add_event(evt) # context.add_event(evt)
# MODULE CORE ######################################################### def help_full():
return ("!velib /number/ ...: gives available bikes and slots at "
"the station /number/.")
def station_status(station): def station_status(station):
"""Gets available and free status of a given station""" """Gets available and free status of a given station"""
response = web.getXML(URL_API % station) response = web.getXML(URL_API % station)
if response is not None: if response is not None:
available = int(response.getElementsByTagName("available")[0].firstChild.nodeValue) available = response.getNode("available").getContent()
free = int(response.getElementsByTagName("free")[0].firstChild.nodeValue) if available is not None and len(available) > 0:
available = int(available)
else:
available = 0
free = response.getNode("free").getContent()
if free is not None and len(free) > 0:
free = int(free)
else:
free = 0
return (available, free) return (available, free)
else: else:
return (None, None) return (None, None)
@ -58,30 +69,27 @@ def print_station_status(msg, station):
"""Send message with information about the given station""" """Send message with information about the given station"""
(available, free) = station_status(station) (available, free) = station_status(station)
if available is not None and free is not None: if available is not None and free is not None:
return Response("À la station %s : %d vélib et %d points d'attache" return Response("à la station %s : %d vélib et %d points d'attache"
" disponibles." % (station, available, free), " disponibles." % (station, available, free),
channel=msg.channel) channel=msg.channel, nick=msg.nick)
raise IMException("station %s inconnue." % station) raise IRCException("station %s inconnue." % station)
# MODULE INTERFACE #################################################### @hook("cmd_hook", "velib")
@hook.command("velib",
help="gives available bikes and slots at the given station",
help_usage={
"STATION_ID": "gives available bikes and slots at the station STATION_ID"
})
def ask_stations(msg): def ask_stations(msg):
"""Hook entry from !velib"""
if len(msg.args) > 4: if len(msg.args) > 4:
raise IMException("demande-moi moins de stations à la fois.") raise IRCException("demande-moi moins de stations à la fois.")
elif not len(msg.args):
raise IMException("pour quelle station ?")
for station in msg.args: elif len(msg.args):
if re.match("^[0-9]{4,5}$", station): for station in msg.args:
return print_station_status(msg, station) if re.match("^[0-9]{4,5}$", station):
elif station in context.data.index: return print_station_status(msg, station)
return print_station_status(msg, elif station in context.data.index:
context.data.index[station]["number"]) return print_station_status(msg,
else: context.data.index[station]["number"])
raise IMException("numéro de station invalide.") else:
raise IRCException("numéro de station invalide.")
else:
raise IRCException("pour quelle station ?")

View file

@ -1,100 +0,0 @@
"""Retrieve flight information from VirtualRadar APIs"""
# PYTHON STUFFS #######################################################
import re
from urllib.parse import quote
import time
from nemubot.exception import IMException
from nemubot.hooks import hook
from nemubot.tools import web
from nemubot.module.more import Response
from nemubot.module import mapquest
# GLOBALS #############################################################
URL_API = "https://public-api.adsbexchange.com/VirtualRadar/AircraftList.json?fcallC=%s"
SPEED_TYPES = {
0: 'Ground speed',
1: 'Ground speed reversing',
2: 'Indicated air speed',
3: 'True air speed'}
WTC_CAT = {
0: 'None',
1: 'Light',
2: 'Medium',
3: 'Heavy'
}
SPECIES = {
1: 'Land plane',
2: 'Sea plane',
3: 'Amphibian',
4: 'Helicopter',
5: 'Gyrocopter',
6: 'Tiltwing',
7: 'Ground vehicle',
8: 'Tower'}
HANDLER_TABLE = {
'From': lambda x: 'From: \x02%s\x0F' % x,
'To': lambda x: 'To: \x02%s\x0F' % x,
'Op': lambda x: 'Airline: \x02%s\x0F' % x,
'Mdl': lambda x: 'Model: \x02%s\x0F' % x,
'Call': lambda x: 'Flight: \x02%s\x0F' % x,
'PosTime': lambda x: 'Last update: \x02%s\x0F' % (time.ctime(int(x)/1000)),
'Alt': lambda x: 'Altitude: \x02%s\x0F ft' % x,
'Spd': lambda x: 'Speed: \x02%s\x0F kn' % x,
'SpdTyp': lambda x: 'Speed type: \x02%s\x0F' % SPEED_TYPES[x] if x in SPEED_TYPES else None,
'Engines': lambda x: 'Engines: \x02%s\x0F' % x,
'Gnd': lambda x: 'On the ground' if x else None,
'Mil': lambda x: 'Military aicraft' if x else None,
'Species': lambda x: 'Aircraft species: \x02%s\x0F' % SPECIES[x] if x in SPECIES else None,
'WTC': lambda x: 'Turbulence level: \x02%s\x0F' % WTC_CAT[x] if x in WTC_CAT else None,
}
# MODULE CORE #########################################################
def virtual_radar(flight_call):
obj = web.getJSON(URL_API % quote(flight_call))
if "acList" in obj:
for flight in obj["acList"]:
yield flight
def flight_info(flight):
for prop in HANDLER_TABLE:
if prop in flight:
yield HANDLER_TABLE[prop](flight[prop])
# MODULE INTERFACE ####################################################
@hook.command("flight",
help="Get flight information",
help_usage={ "FLIGHT": "Get information on FLIGHT" })
def cmd_flight(msg):
if not len(msg.args):
raise IMException("please indicate a flight")
res = Response(channel=msg.channel, nick=msg.frm,
nomore="No more flights", count=" (%s more flights)")
for param in msg.args:
for flight in virtual_radar(param):
if 'Lat' in flight and 'Long' in flight:
loc = None
for location in mapquest.geocode('{Lat},{Long}'.format(**flight)):
loc = location
break
if loc:
res.append_message('\x02{0}\x0F: Position: \x02{1}\x0F, {2}'.format(flight['Call'], \
mapquest.where(loc), \
', '.join(filter(None, flight_info(flight)))))
continue
res.append_message('\x02{0}\x0F: {1}'.format(flight['Call'], \
', '.join(filter(None, flight_info(flight)))))
return res

View file

@ -1,81 +1,82 @@
# coding=utf-8 # coding=utf-8
"""The weather module. Powered by Dark Sky <https://darksky.net/poweredby/>""" """The weather module"""
import datetime import datetime
import re import re
from urllib.parse import quote
from nemubot import context from nemubot import context
from nemubot.exception import IMException from nemubot.exception import IRCException
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.tools import web from nemubot.tools import web
from nemubot.tools.xmlparser.node import ModuleState from nemubot.tools.xmlparser.node import ModuleState
from nemubot.module import mapquest import mapquest
nemubotversion = 4.0 nemubotversion = 4.0
from nemubot.module.more import Response from more import Response
URL_DSAPI = "https://api.darksky.net/forecast/%s/%%s,%%s?lang=%%s&units=%%s" URL_DSAPI = "https://api.forecast.io/forecast/%s/%%s,%%s"
UNITS = {
"ca": {
"temperature": "°C",
"distance": "km",
"precipIntensity": "mm/h",
"precip": "cm",
"speed": "km/h",
"pressure": "hPa",
},
"uk2": {
"temperature": "°C",
"distance": "mi",
"precipIntensity": "mm/h",
"precip": "cm",
"speed": "mi/h",
"pressure": "hPa",
},
"us": {
"temperature": "°F",
"distance": "mi",
"precipIntensity": "in/h",
"precip": "in",
"speed": "mi/h",
"pressure": "mbar",
},
"si": {
"temperature": "°C",
"distance": "km",
"precipIntensity": "mm/h",
"precip": "cm",
"speed": "m/s",
"pressure": "hPa",
},
}
def load(context): def load(context):
if not context.config or "darkskyapikey" not in context.config: if not context.config or not context.config.hasAttribute("darkskyapikey"):
raise ImportError("You need a Dark-Sky API key in order to use this " raise ImportError("You need a Dark-Sky API key in order to use this "
"module. Add it to the module configuration file:\n" "module. Add it to the module configuration file:\n"
"<module name=\"weather\" darkskyapikey=\"XXX\" />\n" "<module name=\"weather\" darkskyapikey=\"XXX\" />\n"
"Register at https://developer.forecast.io/") "Register at http://developer.forecast.io/")
context.data.setIndex("name", "city") context.data.setIndex("name", "city")
global URL_DSAPI global URL_DSAPI
URL_DSAPI = URL_DSAPI % context.config["darkskyapikey"] URL_DSAPI = URL_DSAPI % context.config["darkskyapikey"]
def format_wth(wth, flags): def help_full ():
units = UNITS[flags["units"] if "units" in flags and flags["units"] in UNITS else "si"] return "!weather /city/: Display the current weather in /city/."
return ("{temperature} {units[temperature]} {summary}; precipitation ({precipProbability:.0%} chance) intensity: {precipIntensity} {units[precipIntensity]}; relative humidity: {humidity:.0%}; wind speed: {windSpeed} {units[speed]} {windBearing}°; cloud coverage: {cloudCover:.0%}; pressure: {pressure} {units[pressure]}; ozone: {ozone} DU"
.format(units=units, **wth)
)
def format_forecast_daily(wth, flags): def fahrenheit2celsius(temp):
units = UNITS[flags["units"] if "units" in flags and flags["units"] in UNITS else "si"] return int((temp - 32) * 50/9)/10
print(units)
return ("{summary}; between {temperatureMin}-{temperatureMax} {units[temperature]}; precipitation ({precipProbability:.0%} chance) intensity: maximum {precipIntensity} {units[precipIntensity]}; relative humidity: {humidity:.0%}; wind speed: {windSpeed} {units[speed]} {windBearing}°; cloud coverage: {cloudCover:.0%}; pressure: {pressure} {units[pressure]}; ozone: {ozone} DU".format(units=units, **wth))
def mph2kmph(speed):
return int(speed * 160.9344)/100
def inh2mmh(size):
return int(size * 254)/10
def format_wth(wth):
return ("%s °C %s; precipitation (%s %% chance) intensity: %s mm/h; relative humidity: %s %%; wind speed: %s km/h %s°; cloud coverage: %s %%; pressure: %s hPa; ozone: %s DU" %
(
fahrenheit2celsius(wth["temperature"]),
wth["summary"],
int(wth["precipProbability"] * 100),
inh2mmh(wth["precipIntensity"]),
int(wth["humidity"] * 100),
mph2kmph(wth["windSpeed"]),
wth["windBearing"],
int(wth["cloudCover"] * 100),
int(wth["pressure"]),
int(wth["ozone"])
))
def format_forecast_daily(wth):
return ("%s; between %s-%s °C; precipitation (%s %% chance) intensity: maximum %s mm/h; relative humidity: %s %%; wind speed: %s km/h %s°; cloud coverage: %s %%; pressure: %s hPa; ozone: %s DU" %
(
wth["summary"],
fahrenheit2celsius(wth["temperatureMin"]), fahrenheit2celsius(wth["temperatureMax"]),
int(wth["precipProbability"] * 100),
inh2mmh(wth["precipIntensityMax"]),
int(wth["humidity"] * 100),
mph2kmph(wth["windSpeed"]),
wth["windBearing"],
int(wth["cloudCover"] * 100),
int(wth["pressure"]),
int(wth["ozone"])
))
def format_timestamp(timestamp, tzname, tzoffset, format="%c"): def format_timestamp(timestamp, tzname, tzoffset, format="%c"):
@ -120,82 +121,60 @@ def treat_coord(msg):
coords.append(geocode[0]["latLng"]["lng"]) coords.append(geocode[0]["latLng"]["lng"])
return mapquest.where(geocode[0]), coords, specific return mapquest.where(geocode[0]), coords, specific
raise IMException("Je ne sais pas où se trouve %s." % city) raise IRCException("Je ne sais pas où se trouve %s." % city)
else: else:
raise IMException("indique-moi un nom de ville ou des coordonnées.") raise IRCException("indique-moi un nom de ville ou des coordonnées.")
def get_json_weather(coords, lang="en", units="ca"): def get_json_weather(coords):
wth = web.getJSON(URL_DSAPI % (float(coords[0]), float(coords[1]), lang, units)) wth = web.getJSON(URL_DSAPI % (float(coords[0]), float(coords[1])))
# First read flags # First read flags
if wth is None or "darksky-unavailable" in wth["flags"]: if wth is None or "darksky-unavailable" in wth["flags"]:
raise IMException("The given location is supported but a temporary error (such as a radar station being down for maintenace) made data unavailable.") raise IRCException("The given location is supported but a temporary error (such as a radar station being down for maintenace) made data unavailable.")
return wth return wth
@hook.command("coordinates") @hook("cmd_hook", "coordinates")
def cmd_coordinates(msg): def cmd_coordinates(msg):
if len(msg.args) < 1: if len(msg.args) < 1:
raise IMException("indique-moi un nom de ville.") raise IRCException("indique-moi un nom de ville.")
j = msg.args[0].lower() j = msg.args[0].lower()
if j not in context.data.index: if j not in context.data.index:
raise IMException("%s n'est pas une ville connue" % msg.args[0]) raise IRCException("%s n'est pas une ville connue" % msg.args[0])
coords = context.data.index[j] coords = context.data.index[j]
return Response("Les coordonnées de %s sont %s,%s" % (msg.args[0], coords["lat"], coords["long"]), channel=msg.channel) return Response("Les coordonnées de %s sont %s,%s" % (msg.args[0], coords["lat"], coords["long"]), channel=msg.channel)
@hook.command("alert", @hook("cmd_hook", "alert")
keywords={
"lang=LANG": "change the output language of weather sumarry; default: en",
"units=UNITS": "return weather conditions in the requested units; default: ca",
})
def cmd_alert(msg): def cmd_alert(msg):
loc, coords, specific = treat_coord(msg) loc, coords, specific = treat_coord(msg)
wth = get_json_weather(coords, wth = get_json_weather(coords)
lang=msg.kwargs["lang"] if "lang" in msg.kwargs else "en",
units=msg.kwargs["units"] if "units" in msg.kwargs else "ca")
res = Response(channel=msg.channel, nomore="No more weather alert", count=" (%d more alerts)") res = Response(channel=msg.channel, nomore="No more weather alert", count=" (%d more alerts)")
if "alerts" in wth: if "alerts" in wth:
for alert in wth["alerts"]: for alert in wth["alerts"]:
if "expires" in alert: res.append_message("\x03\x02%s\x03\x02 (see %s expire on %s): %s" % (alert["title"], alert["uri"], format_timestamp(int(alert["expires"]), wth["timezone"], wth["offset"]), alert["description"].replace("\n", " ")))
res.append_message("\x03\x02%s\x03\x02 (see %s expire on %s): %s" % (alert["title"], alert["uri"], format_timestamp(int(alert["expires"]), wth["timezone"], wth["offset"]), alert["description"].replace("\n", " ")))
else:
res.append_message("\x03\x02%s\x03\x02 (see %s): %s" % (alert["title"], alert["uri"], alert["description"].replace("\n", " ")))
return res return res
@hook.command("météo", @hook("cmd_hook", "météo")
help="Display current weather and previsions",
help_usage={
"CITY": "Display the current weather and previsions in CITY",
},
keywords={
"lang=LANG": "change the output language of weather sumarry; default: en",
"units=UNITS": "return weather conditions in the requested units; default: ca",
})
def cmd_weather(msg): def cmd_weather(msg):
loc, coords, specific = treat_coord(msg) loc, coords, specific = treat_coord(msg)
wth = get_json_weather(coords, wth = get_json_weather(coords)
lang=msg.kwargs["lang"] if "lang" in msg.kwargs else "en",
units=msg.kwargs["units"] if "units" in msg.kwargs else "ca")
res = Response(channel=msg.channel, nomore="No more weather information") res = Response(channel=msg.channel, nomore="No more weather information")
if "alerts" in wth: if "alerts" in wth:
alert_msgs = list() alert_msgs = list()
for alert in wth["alerts"]: for alert in wth["alerts"]:
if "expires" in alert: alert_msgs.append("\x03\x02%s\x03\x02 expire on %s" % (alert["title"], format_timestamp(int(alert["expires"]), wth["timezone"], wth["offset"])))
alert_msgs.append("\x03\x02%s\x03\x02 expire on %s" % (alert["title"], format_timestamp(int(alert["expires"]), wth["timezone"], wth["offset"])))
else:
alert_msgs.append("\x03\x02%s\x03\x02" % (alert["title"]))
res.append_message("\x03\x16\x03\x02/!\\\x03\x02 Alert%s:\x03\x16 " % ("s" if len(alert_msgs) > 1 else "") + ", ".join(alert_msgs)) res.append_message("\x03\x16\x03\x02/!\\\x03\x02 Alert%s:\x03\x16 " % ("s" if len(alert_msgs) > 1 else "") + ", ".join(alert_msgs))
if specific is not None: if specific is not None:
@ -207,17 +186,17 @@ def cmd_weather(msg):
if gr.group(2).lower() == "h" and gr1 < len(wth["hourly"]["data"]): if gr.group(2).lower() == "h" and gr1 < len(wth["hourly"]["data"]):
hour = wth["hourly"]["data"][gr1] hour = wth["hourly"]["data"][gr1]
res.append_message("\x03\x02At %sh:\x03\x02 %s" % (format_timestamp(int(hour["time"]), wth["timezone"], wth["offset"], '%H'), format_wth(hour, wth["flags"]))) res.append_message("\x03\x02At %sh:\x03\x02 %s" % (format_timestamp(int(hour["time"]), wth["timezone"], wth["offset"], '%H'), format_wth(hour)))
elif gr.group(2).lower() == "d" and gr1 < len(wth["daily"]["data"]): elif gr.group(2).lower() == "d" and gr1 < len(wth["daily"]["data"]):
day = wth["daily"]["data"][gr1] day = wth["daily"]["data"][gr1]
res.append_message("\x03\x02On %s:\x03\x02 %s" % (format_timestamp(int(day["time"]), wth["timezone"], wth["offset"], '%A'), format_forecast_daily(day, wth["flags"]))) res.append_message("\x03\x02On %s:\x03\x02 %s" % (format_timestamp(int(day["time"]), wth["timezone"], wth["offset"], '%A'), format_forecast_daily(day)))
else: else:
res.append_message("I don't understand %s or information is not available" % specific) res.append_message("I don't understand %s or information is not available" % specific)
else: else:
res.append_message("\x03\x02Currently:\x03\x02 " + format_wth(wth["currently"], wth["flags"])) res.append_message("\x03\x02Currently:\x03\x02 " + format_wth(wth["currently"]))
nextres = "\x03\x02Today:\x03\x02 %s " % wth["daily"]["data"][0]["summary"] nextres = "\x03\x02Today:\x03\x02 %s " % wth["daily"]["data"][0]["summary"]
if "minutely" in wth: if "minutely" in wth:
@ -227,11 +206,11 @@ def cmd_weather(msg):
for hour in wth["hourly"]["data"][1:4]: for hour in wth["hourly"]["data"][1:4]:
res.append_message("\x03\x02At %sh:\x03\x02 %s" % (format_timestamp(int(hour["time"]), wth["timezone"], wth["offset"], '%H'), res.append_message("\x03\x02At %sh:\x03\x02 %s" % (format_timestamp(int(hour["time"]), wth["timezone"], wth["offset"], '%H'),
format_wth(hour, wth["flags"]))) format_wth(hour)))
for day in wth["daily"]["data"][1:]: for day in wth["daily"]["data"][1:]:
res.append_message("\x03\x02On %s:\x03\x02 %s" % (format_timestamp(int(day["time"]), wth["timezone"], wth["offset"], '%A'), res.append_message("\x03\x02On %s:\x03\x02 %s" % (format_timestamp(int(day["time"]), wth["timezone"], wth["offset"], '%A'),
format_forecast_daily(day, wth["flags"]))) format_forecast_daily(day)))
return res return res
@ -239,9 +218,9 @@ def cmd_weather(msg):
gps_ask = re.compile(r"^\s*(?P<city>.*\w)\s*(?:(?:se|est)\s+(?:trouve|situ[ée]*)\s+[aà])\s*(?P<lat>-?[0-9]+(?:[,.][0-9]+))[^0-9.](?P<long>-?[0-9]+(?:[,.][0-9]+))\s*$", re.IGNORECASE) gps_ask = re.compile(r"^\s*(?P<city>.*\w)\s*(?:(?:se|est)\s+(?:trouve|situ[ée]*)\s+[aà])\s*(?P<lat>-?[0-9]+(?:[,.][0-9]+))[^0-9.](?P<long>-?[0-9]+(?:[,.][0-9]+))\s*$", re.IGNORECASE)
@hook.ask() @hook("ask_default")
def parseask(msg): def parseask(msg):
res = gps_ask.match(msg.message) res = gps_ask.match(msg.text)
if res is not None: if res is not None:
city_name = res.group("city").lower() city_name = res.group("city").lower()
gps_lat = res.group("lat").replace(",", ".") gps_lat = res.group("lat").replace(",", ".")
@ -258,4 +237,4 @@ def parseask(msg):
context.data.addChild(ms) context.data.addChild(ms)
context.save() context.save()
return Response("ok, j'ai bien noté les coordonnées de %s" % res.group("city"), return Response("ok, j'ai bien noté les coordonnées de %s" % res.group("city"),
msg.channel, msg.frm) msg.channel, msg.nick)

View file

@ -1,79 +1,55 @@
# coding=utf-8 # coding=utf-8
import json
import re import re
from nemubot import context from nemubot import context
from nemubot.exception import IMException from nemubot.exception import IRCException
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.tools.xmlparser.node import ModuleState from nemubot.tools.xmlparser.node import ModuleState
nemubotversion = 3.4 nemubotversion = 3.4
from nemubot.module.more import Response from more import Response
from nemubot.module.networking.page import headers from networking.page import headers
PASSWD_FILE = None PASSWD_FILE = None
# You can get one with: curl -b "sessionid=YOURSESSIONID" 'https://accounts.cri.epita.net/api/users/?limit=10000' > users.json
APIEXTRACT_FILE = None
def load(context): def load(context):
global PASSWD_FILE global PASSWD_FILE
if not context.config or "passwd" not in context.config: if not context.config or not context.config.hasAttribute("passwd"):
print("No passwd file given") print("No passwd file given")
else:
PASSWD_FILE = context.config["passwd"]
print("passwd file loaded:", PASSWD_FILE)
global APIEXTRACT_FILE
if not context.config or "apiextract" not in context.config:
print("No passwd file given")
else:
APIEXTRACT_FILE = context.config["apiextract"]
print("JSON users file loaded:", APIEXTRACT_FILE)
if PASSWD_FILE is None and APIEXTRACT_FILE is None:
return None return None
PASSWD_FILE = context.config["passwd"]
if not context.data.hasNode("aliases"): if not context.data.hasNode("aliases"):
context.data.addChild(ModuleState("aliases")) context.data.addChild(ModuleState("aliases"))
context.data.getNode("aliases").setIndex("from", "alias") context.data.getNode("aliases").setIndex("from", "alias")
if not context.data.hasNode("pics"):
context.data.addChild(ModuleState("pics"))
context.data.getNode("pics").setIndex("login", "pict")
import nemubot.hooks import nemubot.hooks
context.add_hook(nemubot.hooks.Command(cmd_whois, "whois", keywords={"lookup": "Perform a lookup of the begining of the login instead of an exact search."}), context.add_hook("cmd_hook",
"in","Command") nemubot.hooks.Message(cmd_whois, "whois"))
class Login: class Login:
def __init__(self, line=None, login=None, uidNumber=None, firstname=None, lastname=None, promo=None, **kwargs): def __init__(self, line):
if line is not None: s = line.split(":")
s = line.split(":") self.login = s[0]
self.login = s[0] self.uid = s[2]
self.uid = s[2] self.gid = s[3]
self.gid = s[3] self.cn = s[4]
self.cn = s[4] self.home = s[5]
self.home = s[5]
else:
self.login = login
self.uid = uidNumber
self.promo = promo
self.cn = firstname + " " + lastname
try:
self.gid = "epita" + str(int(promo))
except:
self.gid = promo
def get_promo(self): def get_promo(self):
if hasattr(self, "promo"): return self.home.split("/")[2].replace("_", " ")
return self.promo
if hasattr(self, "home"):
try:
return self.home.split("/")[2].replace("_", " ")
except:
return self.gid
def get_photo(self): def get_photo(self):
for url in [ "https://photos.cri.epita.fr/%s", "https://intra-bocal.epitech.eu/trombi/%s.jpg" ]: if self.login in context.data.getNode("pics").index:
return context.data.getNode("pics").index[self.login]["url"]
for url in [ "https://static.acu.epita.fr/photos/%s", "https://static.acu.epita.fr/photos/%s/%%s" % self.gid, "https://intra-bocal.epitech.eu/trombi/%s.jpg", "http://whois.23.tf/p/%s/%%s.jpg" % self.gid ]:
url = url % self.login url = url % self.login
try: try:
_, status, _, _ = headers(url) _, status, _, _ = headers(url)
@ -84,53 +60,38 @@ class Login:
return None return None
def login_lookup(login, search=False): def found_login(login):
if login in context.data.getNode("aliases").index: if login in context.data.getNode("aliases").index:
login = context.data.getNode("aliases").index[login]["to"] login = context.data.getNode("aliases").index[login]["to"]
if APIEXTRACT_FILE: login_ = login + ":"
with open(APIEXTRACT_FILE, encoding="utf-8") as f:
api = json.load(f)
for l in api["results"]:
if (not search and l["login"] == login) or (search and (("login" in l and l["login"].find(login) != -1) or ("cn" in l and l["cn"].find(login) != -1) or ("uid" in l and str(l["uid"]) == login))):
yield Login(**l)
login_ = login + (":" if not search else "")
lsize = len(login_) lsize = len(login_)
if PASSWD_FILE: with open(PASSWD_FILE, encoding="iso-8859-15") as f:
with open(PASSWD_FILE, encoding="iso-8859-15") as f: for l in f.readlines():
for l in f.readlines(): if l[:lsize] == login_:
if l[:lsize] == login_: return Login(l.strip())
yield Login(l.strip()) return None
def cmd_whois(msg): def cmd_whois(msg):
if len(msg.args) < 1: if len(msg.args) < 1:
raise IMException("Provide a name") raise IRCException("Provide a name")
def format_response(t): res = Response(channel=msg.channel, count=" (%d more logins)")
srch, l = t
if type(l) is Login:
pic = l.get_photo()
return "%s is %s (%s %s): %s%s" % (srch, l.cn.capitalize(), l.login, l.uid, l.get_promo(), " and looks like %s" % pic if pic is not None else "")
else:
return l % srch
res = Response(channel=msg.channel, count=" (%d more logins)", line_treat=format_response)
for srch in msg.args: for srch in msg.args:
found = False l = found_login(srch)
for l in login_lookup(srch, "lookup" in msg.kwargs): if l is not None:
found = True pic = l.get_photo()
res.append_message((srch, l)) res.append_message("%s is %s (%s %s): %s%s" % (srch, l.cn.capitalize(), l.login, l.uid, l.get_promo(), " and looks like %s" % pic if pic is not None else ""))
if not found: else:
res.append_message((srch, "Unknown %s :(")) res.append_message("Unknown %s :(" % srch)
return res return res
@hook.command("nicks") @hook("cmd_hook", "nicks")
def cmd_nicks(msg): def cmd_nicks(msg):
if len(msg.args) < 1: if len(msg.args) < 1:
raise IMException("Provide a login") raise IRCException("Provide a login")
nick = login_lookup(msg.args[0]) nick = found_login(msg.args[0])
if nick is None: if nick is None:
nick = msg.args[0] nick = msg.args[0]
else: else:
@ -145,14 +106,14 @@ def cmd_nicks(msg):
else: else:
return Response("%s has no known alias." % nick, channel=msg.channel) return Response("%s has no known alias." % nick, channel=msg.channel)
@hook.ask() @hook("ask_default")
def parseask(msg): def parseask(msg):
res = re.match(r"^(\S+)\s*('s|suis|est|is|was|were)\s+([a-zA-Z0-9_-]{3,8})$", msg.message, re.I) res = re.match(r"^(\S+)\s*('s|suis|est|is|was|were)\s+([a-zA-Z0-9_-]{3,8})$", msg.text, re.I)
if res is not None: if res is not None:
nick = res.group(1) nick = res.group(1)
login = res.group(3) login = res.group(3)
if nick == "my" or nick == "I" or nick == "i" or nick == "je" or nick == "mon" or nick == "ma": if nick == "my" or nick == "I" or nick == "i" or nick == "je" or nick == "mon" or nick == "ma":
nick = msg.frm nick = msg.nick
if nick in context.data.getNode("aliases").index: if nick in context.data.getNode("aliases").index:
context.data.getNode("aliases").index[nick]["to"] = login context.data.getNode("aliases").index[nick]["to"] = login
else: else:
@ -164,4 +125,4 @@ def parseask(msg):
return Response("ok, c'est noté, %s est %s" return Response("ok, c'est noté, %s est %s"
% (nick, login), % (nick, login),
channel=msg.channel, channel=msg.channel,
nick=msg.frm) nick=msg.nick)

View file

@ -1,118 +1,99 @@
"""Performing search and calculation""" # coding=utf-8
# PYTHON STUFFS #######################################################
from urllib.parse import quote from urllib.parse import quote
import re
from nemubot import context from nemubot import context
from nemubot.exception import IMException from nemubot.exception import IRCException
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.tools import web from nemubot.tools import web
from nemubot.module.more import Response nemubotversion = 4.0
from more import Response
# LOADING ############################################################# URL_API = "http://api.wolframalpha.com/v2/query?input=%%s&appid=%s"
URL_API = "https://api.wolframalpha.com/v2/query?input=%%s&format=plaintext&appid=%s"
def load(context): def load(context):
global URL_API global URL_API
if not context.config or "apikey" not in context.config: if not context.config or not context.config.hasAttribute("apikey"):
raise ImportError ("You need a Wolfram|Alpha API key in order to use " raise ImportError ("You need a Wolfram|Alpha API key in order to use "
"this module. Add it to the module configuration: " "this module. Add it to the module configuration: "
"\n<module name=\"wolframalpha\" " "\n<module name=\"wolframalpha\" "
"apikey=\"XXXXXX-XXXXXXXXXX\" />\n" "apikey=\"XXXXXX-XXXXXXXXXX\" />\n"
"Register at https://products.wolframalpha.com/api/") "Register at http://products.wolframalpha.com/api/")
URL_API = URL_API % quote(context.config["apikey"]).replace("%", "%%") URL_API = URL_API % quote(context.config["apikey"]).replace("%", "%%")
# MODULE CORE ######################################################### class WFASearch:
class WFAResults:
def __init__(self, terms): def __init__(self, terms):
self.wfares = web.getXML(URL_API % quote(terms), self.terms = terms
timeout=12) self.wfares = web.getXML(URL_API % quote(terms))
@property @property
def success(self): def success(self):
try: try:
return self.wfares.documentElement.hasAttribute("success") and self.wfares.documentElement.getAttribute("success") == "true" return self.wfares["success"] == "true"
except: except:
return False return False
@property @property
def error(self): def error(self):
if self.wfares is None: if self.wfares is None:
return "An error occurs during computation." return "An error occurs during computation."
elif self.wfares.documentElement.hasAttribute("error") and self.wfares.documentElement.getAttribute("error") == "true": elif self.wfares["error"] == "true":
return ("An error occurs during computation: " + return ("An error occurs during computation: " +
self.wfares.getElementsByTagName("error")[0].getElementsByTagName("msg")[0].firstChild.nodeValue) self.wfares.getNode("error").getNode("msg").getContent())
elif len(self.wfares.getElementsByTagName("didyoumeans")): elif self.wfares.hasNode("didyoumeans"):
start = "Did you mean: " start = "Did you mean: "
tag = "didyoumean" tag = "didyoumean"
end = "?" end = "?"
elif len(self.wfares.getElementsByTagName("tips")): elif self.wfares.hasNode("tips"):
start = "Tips: " start = "Tips: "
tag = "tip" tag = "tip"
end = "" end = ""
elif len(self.wfares.getElementsByTagName("relatedexamples")): elif self.wfares.hasNode("relatedexamples"):
start = "Related examples: " start = "Related examples: "
tag = "relatedexample" tag = "relatedexample"
end = "" end = ""
elif len(self.wfares.getElementsByTagName("futuretopic")): elif self.wfares.hasNode("futuretopic"):
return self.wfares.getElementsByTagName("futuretopic")[0].getAttribute("msg") return self.wfares.getNode("futuretopic")["msg"]
else: else:
return "An error occurs during computation" return "An error occurs during computation"
proposal = list() proposal = list()
for dym in self.wfares.getElementsByTagName(tag): for dym in self.wfares.getNode(tag + "s").getNodes(tag):
if tag == "tip": if tag == "tip":
proposal.append(dym.getAttribute("text")) proposal.append(dym["text"])
elif tag == "relatedexample": elif tag == "relatedexample":
proposal.append(dym.getAttribute("desc")) proposal.append(dym["desc"])
else: else:
proposal.append(dym.firstChild.nodeValue) proposal.append(dym.getContent())
return start + ', '.join(proposal) + end return start + ', '.join(proposal) + end
@property @property
def results(self): def nextRes(self):
for node in self.wfares.getElementsByTagName("pod"): try:
for subnode in node.getElementsByTagName("subpod"): for node in self.wfares.getNodes("pod"):
if subnode.getElementsByTagName("plaintext")[0].firstChild: for subnode in node.getNodes("subpod"):
yield (node.getAttribute("title") + if subnode.getFirstNode("plaintext").getContent() != "":
((" / " + subnode.getAttribute("title")) if subnode.getAttribute("title") else "") + ": " + yield (node["title"] + " " + subnode["title"] + ": " +
"; ".join(subnode.getElementsByTagName("plaintext")[0].firstChild.nodeValue.split("\n"))) subnode.getFirstNode("plaintext").getContent())
except IndexError:
pass
# MODULE INTERFACE #################################################### @hook("cmd_hook", "calculate")
@hook.command("calculate",
help="Perform search and calculation using WolframAlpha",
help_usage={
"TERM": "Look at the given term on WolframAlpha",
"CALCUL": "Perform the computation over WolframAlpha service",
})
def calculate(msg): def calculate(msg):
if not len(msg.args): if not len(msg.args):
raise IMException("Indicate a calcul to compute") raise IRCException("Indicate a calcul to compute")
s = WFAResults(' '.join(msg.args)) s = WFASearch(' '.join(msg.args))
if not s.success: if s.success:
raise IMException(s.error) res = Response(channel=msg.channel, nomore="No more results")
for result in s.nextRes:
res = Response(channel=msg.channel, nomore="No more results") res.append_message(result)
if (len(res.messages) > 0):
for result in s.results: res.messages.pop(0)
res.append_message(re.sub(r' +', ' ', result)) return res
if len(res.messages): else:
res.messages.pop(0) return Response(s.error, msg.channel)
return res

View file

@ -1,28 +1,27 @@
# coding=utf-8 # coding=utf-8
"""The 2014,2018 football worldcup module""" """The 2014 football worldcup module"""
from datetime import datetime, timezone from datetime import datetime, timezone
from functools import partial
import json import json
import re import re
from urllib.parse import quote from urllib.parse import quote
from urllib.request import urlopen from urllib.request import urlopen
from nemubot import context from nemubot import context
from nemubot.event import ModuleEvent from nemubot.exception import IRCException
from nemubot.exception import IMException
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.tools.xmlparser.node import ModuleState from nemubot.tools.xmlparser.node import ModuleState
nemubotversion = 3.4 nemubotversion = 3.4
from nemubot.module.more import Response from more import Response
API_URL="http://worldcup.sfg.io/%s" API_URL="http://worldcup.sfg.io/%s"
def load(context): def load(context):
context.add_event(ModuleEvent(func=partial(lambda url: urlopen(url, timeout=10).read().decode(), API_URL % "matches/current?by_date=DESC"), call=current_match_new_action, interval=30)) from nemubot.event import ModuleEvent
context.add_event(ModuleEvent(func=lambda url: urlopen(url, timeout=10).read().decode(), func_data=API_URL % "matches/current?by_date=DESC", call=current_match_new_action, interval=30))
def help_full (): def help_full ():
@ -33,13 +32,13 @@ def start_watch(msg):
w = ModuleState("watch") w = ModuleState("watch")
w["server"] = msg.server w["server"] = msg.server
w["channel"] = msg.channel w["channel"] = msg.channel
w["proprio"] = msg.frm w["proprio"] = msg.nick
w["start"] = datetime.now(timezone.utc) w["start"] = datetime.now(timezone.utc)
context.data.addChild(w) context.data.addChild(w)
context.save() context.save()
raise IMException("This channel is now watching world cup events!") raise IRCException("This channel is now watching world cup events!")
@hook.command("watch_worldcup") @hook("cmd_hook", "watch_worldcup")
def cmd_watch(msg): def cmd_watch(msg):
# Get current state # Get current state
@ -53,23 +52,23 @@ def cmd_watch(msg):
if msg.args[0] == "stop" and node is not None: if msg.args[0] == "stop" and node is not None:
context.data.delChild(node) context.data.delChild(node)
context.save() context.save()
raise IMException("This channel will not anymore receives world cup events.") raise IRCException("This channel will not anymore receives world cup events.")
elif msg.args[0] == "start" and node is None: elif msg.args[0] == "start" and node is None:
start_watch(msg) start_watch(msg)
else: else:
raise IMException("Use only start or stop as first argument") raise IRCException("Use only start or stop as first argument")
else: else:
if node is None: if node is None:
start_watch(msg) start_watch(msg)
else: else:
context.data.delChild(node) context.data.delChild(node)
context.save() context.save()
raise IMException("This channel will not anymore receives world cup events.") raise IRCException("This channel will not anymore receives world cup events.")
def current_match_new_action(matches): def current_match_new_action(match_str, osef):
def cmp(om, nm): context.add_event(ModuleEvent(func=lambda url: urlopen(url).read().decode(), func_data=API_URL % "matches/current?by_date=DESC", call=current_match_new_action, interval=30))
return len(nm) and (len(om) == 0 or len(nm[0]["home_team_events"]) != len(om[0]["home_team_events"]) or len(nm[0]["away_team_events"]) != len(om[0]["away_team_events"]))
context.add_event(ModuleEvent(func=partial(lambda url: json.loads(urlopen(url).read().decode()), API_URL % "matches/current?by_date=DESC"), cmp=partial(cmp, matches), call=current_match_new_action, interval=30)) matches = json.loads(match_str)
for match in matches: for match in matches:
if is_valid(match): if is_valid(match):
@ -121,19 +120,20 @@ def detail_event(evt):
return evt + " par" return evt + " par"
def txt_event(e): def txt_event(e):
return "%s minute : %s %s (%s)" % (e["time"], detail_event(e["type_of_event"]), e["player"], e["team"]["code"]) return "%se minutes : %s %s (%s)" % (e["time"], detail_event(e["type_of_event"]), e["player"], e["team"]["code"])
def prettify(match): def prettify(match):
matchdate = datetime.strptime(match["datetime"].replace(':', ''), "%Y-%m-%dT%H%M%SZ").replace(tzinfo=timezone.utc) matchdate_local = datetime.strptime(match["datetime"].replace(':', ''), "%Y-%m-%dT%H%M%S.%f%z")
matchdate = matchdate_local - (matchdate_local.utcoffset() - datetime.timedelta(hours=2))
if match["status"] == "future": if match["status"] == "future":
return ["Match à venir (%s) le %s : %s vs. %s" % (match["fifa_id"], matchdate.strftime("%A %d à %H:%M"), match["home_team"]["country"], match["away_team"]["country"])] return ["Match à venir (%s) le %s : %s vs. %s" % (match["match_number"], matchdate.strftime("%A %d à %H:%M"), match["home_team"]["country"], match["away_team"]["country"])]
else: else:
msgs = list() msgs = list()
msg = "" msg = ""
if match["status"] == "completed": if match["status"] == "completed":
msg += "Match (%s) du %s terminé : " % (match["fifa_id"], matchdate.strftime("%A %d à %H:%M")) msg += "Match (%s) du %s terminé : " % (match["match_number"], matchdate.strftime("%A %d à %H:%M"))
else: else:
msg += "Match en cours (%s) depuis %d minutes : " % (match["fifa_id"], (datetime.now(tz=timezone.utc) - matchdate).total_seconds() / 60) msg += "Match en cours (%s) depuis %d minutes : " % (match["match_number"], (datetime.now(matchdate.tzinfo) - matchdate_local).seconds / 60)
msg += "%s %d - %d %s" % (match["home_team"]["country"], match["home_team"]["goals"], match["away_team"]["goals"], match["away_team"]["country"]) msg += "%s %d - %d %s" % (match["home_team"]["country"], match["home_team"]["goals"], match["away_team"]["goals"], match["away_team"]["country"])
@ -163,21 +163,21 @@ def is_valid(match):
def get_match(url, matchid): def get_match(url, matchid):
allm = get_matches(url) allm = get_matches(url)
for m in allm: for m in allm:
if int(m["fifa_id"]) == matchid: if int(m["match_number"]) == matchid:
return [ m ] return [ m ]
def get_matches(url): def get_matches(url):
try: try:
raw = urlopen(url) raw = urlopen(url)
except: except:
raise IMException("requête invalide") raise IRCException("requête invalide")
matches = json.loads(raw.read().decode()) matches = json.loads(raw.read().decode())
for match in matches: for match in matches:
if is_valid(match): if is_valid(match):
yield match yield match
@hook.command("worldcup") @hook("cmd_hook", "worldcup")
def cmd_worldcup(msg): def cmd_worldcup(msg):
res = Response(channel=msg.channel, nomore="No more match to display", count=" (%d more matches)") res = Response(channel=msg.channel, nomore="No more match to display", count=" (%d more matches)")
@ -192,9 +192,9 @@ def cmd_worldcup(msg):
elif len(msg.args[0]) == 3: elif len(msg.args[0]) == 3:
url = "matches/country?fifa_code=%s&by_date=DESC" % msg.args[0] url = "matches/country?fifa_code=%s&by_date=DESC" % msg.args[0]
elif is_int(msg.args[0]): elif is_int(msg.args[0]):
url = int(msg.args[0]) url = int(msg.arg[0])
else: else:
raise IMException("unrecognized request; choose between 'today', 'tomorrow', a FIFA country code or a match identifier") raise IRCException("unrecognized request; choose between 'today', 'tomorrow', a FIFA country code or a match identifier")
if url is None: if url is None:
url = "matches/current?by_date=ASC" url = "matches/current?by_date=ASC"

View file

@ -1,10 +1,10 @@
from urllib.parse import urlparse from urllib.parse import urlparse
import re, json, subprocess import re, json, subprocess
from nemubot.exception import IMException from nemubot.exception import IRCException
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.tools.web import _getNormalizedURL, getURLContent from nemubot.tools.web import _getNormalizedURL, getURLContent
from nemubot.module.more import Response from more import Response
"""Get information of youtube videos""" """Get information of youtube videos"""
@ -19,7 +19,7 @@ def _get_ytdl(links):
res = [] res = []
with subprocess.Popen(cmd, stdout=subprocess.PIPE) as p: with subprocess.Popen(cmd, stdout=subprocess.PIPE) as p:
if p.wait() > 0: if p.wait() > 0:
raise IMException("Error while retrieving video information.") raise IRCException("Error while retrieving video information.")
for line in p.stdout.read().split(b"\n"): for line in p.stdout.read().split(b"\n"):
localres = '' localres = ''
if not line: if not line:
@ -46,13 +46,13 @@ def _get_ytdl(links):
localres += ' | ' + info['webpage_url'] localres += ' | ' + info['webpage_url']
res.append(localres) res.append(localres)
if not res: if not res:
raise IMException("No video information to retrieve about this. Sorry!") raise IRCException("No video information to retrieve about this. Sorry!")
return res return res
LAST_URLS = dict() LAST_URLS = dict()
@hook.command("yt") @hook("cmd_hook", "yt")
def get_info_yt(msg): def get_info_yt(msg):
links = list() links = list()
@ -61,7 +61,7 @@ def get_info_yt(msg):
if msg.channel in LAST_URLS and len(LAST_URLS[msg.channel]) > 0: if msg.channel in LAST_URLS and len(LAST_URLS[msg.channel]) > 0:
links.append(LAST_URLS[msg.channel].pop()) links.append(LAST_URLS[msg.channel].pop())
else: else:
raise IMException("I don't have any youtube URL for now, please provide me one to get information!") raise IRCException("I don't have any youtube URL for now, please provide me one to get information!")
else: else:
for url in msg.args: for url in msg.args:
links.append(url) links.append(url)
@ -73,23 +73,23 @@ def get_info_yt(msg):
return res return res
@hook.message() @hook("msg_default")
def parselisten(msg): def parselisten(msg):
parseresponse(msg) parseresponse(msg)
return None return None
@hook.post() @hook("all_post")
def parseresponse(msg): def parseresponse(msg):
global LAST_URLS global LAST_URLS
if hasattr(msg, "text") and msg.text and type(msg.text) == str: if hasattr(msg, "text") and msg.text:
urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?[^ :]+)", msg.text) urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?[^ :]+)", msg.text)
for url in urls: for url in urls:
o = urlparse(_getNormalizedURL(url)) o = urlparse(_getNormalizedURL(url))
if o.scheme != "": if o.scheme != "":
if o.netloc == "" and len(o.path) < 10: if o.netloc == "" and len(o.path) < 10:
continue continue
for recv in msg.to: for recv in msg.receivers:
if recv not in LAST_URLS: if recv not in LAST_URLS:
LAST_URLS[recv] = list() LAST_URLS[recv] = list()
LAST_URLS[recv].append(url) LAST_URLS[recv].append(url)

View file

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
# Nemubot is a smart and modulable IM bot. # Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2015 Mercier Pierre-Olivier # Copyright (C) 2012-2015 Mercier Pierre-Olivier
# #
@ -17,9 +19,9 @@
__version__ = '4.0.dev3' __version__ = '4.0.dev3'
__author__ = 'nemunaire' __author__ = 'nemunaire'
from nemubot.modulecontext import _ModuleContext from nemubot.modulecontext import ModuleContext
context = _ModuleContext() context = ModuleContext(None, None)
def requires_version(min=None, max=None): def requires_version(min=None, max=None):
@ -39,15 +41,11 @@ def requires_version(min=None, max=None):
"but this is nemubot v%s." % (str(max), __version__)) "but this is nemubot v%s." % (str(max), __version__))
def attach(pidfile, socketfile): def attach(pid, socketfile):
import socket import socket
import sys import sys
# Read PID from pidfile print("nemubot is already launched with PID %d. Attaching to Unix socket at: %s" % (pid, socketfile))
with open(pidfile, "r") as f:
pid = int(f.readline())
print("nemubot is launched with PID %d. Attaching to Unix socket at: %s" % (pid, socketfile))
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try: try:
@ -57,50 +55,42 @@ def attach(pidfile, socketfile):
sys.stderr.write("\n") sys.stderr.write("\n")
return 1 return 1
import select from select import select
mypoll = select.poll()
mypoll.register(sys.stdin.fileno(), select.POLLIN | select.POLLPRI)
mypoll.register(sock.fileno(), select.POLLIN | select.POLLPRI)
try: try:
print("Connection established.")
while True: while True:
for fd, flag in mypoll.poll(): rl, wl, xl = select([sys.stdin, sock], [], [])
if flag & (select.POLLERR | select.POLLHUP | select.POLLNVAL):
sock.close()
print("Connection closed.")
return 1
if fd == sys.stdin.fileno(): if sys.stdin in rl:
line = sys.stdin.readline().strip() line = sys.stdin.readline().strip()
if line == "exit" or line == "quit": if line == "exit" or line == "quit":
return 0 return 0
elif line == "reload": elif line == "reload":
import os, signal import os, signal
os.kill(pid, signal.SIGHUP) os.kill(pid, signal.SIGHUP)
print("Reload signal sent. Please wait...") print("Reload signal sent. Please wait...")
elif line == "shutdown": elif line == "shutdown":
import os, signal import os, signal
os.kill(pid, signal.SIGTERM) os.kill(pid, signal.SIGTERM)
print("Shutdown signal sent. Please wait...") print("Shutdown signal sent. Please wait...")
elif line == "kill": elif line == "kill":
import os, signal import os, signal
os.kill(pid, signal.SIGKILL) os.kill(pid, signal.SIGKILL)
print("Signal sent...") print("Signal sent...")
return 0 return 0
elif line == "stack" or line == "stacks": elif line == "stack" or line == "stacks":
import os, signal import os, signal
os.kill(pid, signal.SIGUSR1) os.kill(pid, signal.SIGUSR1)
print("Debug signal sent. Consult logs.") print("Debug signal sent. Consult logs.")
else: else:
sock.send(line.encode() + b'\r\n') sock.send(line.encode() + b'\r\n')
if fd == sock.fileno():
sys.stdout.write(sock.recv(2048).decode())
if sock in rl:
sys.stdout.write(sock.recv(2048).decode())
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass
except: except:
@ -110,7 +100,7 @@ def attach(pidfile, socketfile):
return 0 return 0
def daemonize(socketfile=None): def daemonize():
"""Detach the running process to run as a daemon """Detach the running process to run as a daemon
""" """
@ -146,3 +136,54 @@ def daemonize(socketfile=None):
os.dup2(si.fileno(), sys.stdin.fileno()) os.dup2(si.fileno(), sys.stdin.fileno())
os.dup2(so.fileno(), sys.stdout.fileno()) os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(se.fileno(), sys.stderr.fileno()) os.dup2(se.fileno(), sys.stderr.fileno())
def reload():
"""Reload code of all Python modules used by nemubot
"""
import imp
import nemubot.bot
imp.reload(nemubot.bot)
import nemubot.channel
imp.reload(nemubot.channel)
import nemubot.consumer
imp.reload(nemubot.consumer)
import nemubot.event
imp.reload(nemubot.event)
import nemubot.exception
imp.reload(nemubot.exception)
import nemubot.hooks
imp.reload(nemubot.hooks)
nemubot.hooks.reload()
import nemubot.importer
imp.reload(nemubot.importer)
import nemubot.message
imp.reload(nemubot.message)
nemubot.message.reload()
import nemubot.server
rl = nemubot.server._rlist
wl = nemubot.server._wlist
xl = nemubot.server._xlist
imp.reload(nemubot.server)
nemubot.server._rlist = rl
nemubot.server._wlist = wl
nemubot.server._xlist = xl
nemubot.server.reload()
import nemubot.tools
imp.reload(nemubot.tools)
nemubot.tools.reload()

View file

@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
# Nemubot is a smart and modulable IM bot. # Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2017 Mercier Pierre-Olivier # Copyright (C) 2012-2015 Mercier Pierre-Olivier
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -37,9 +39,6 @@ def main():
default=["./modules/"], default=["./modules/"],
help="directory to use as modules store") help="directory to use as modules store")
parser.add_argument("-A", "--no-attach", action="store_true",
help="don't attach after fork")
parser.add_argument("-d", "--debug", action="store_true", parser.add_argument("-d", "--debug", action="store_true",
help="don't deamonize, keep in foreground") help="don't deamonize, keep in foreground")
@ -71,27 +70,35 @@ def main():
# Resolve relatives paths # Resolve relatives paths
args.data_path = os.path.abspath(os.path.expanduser(args.data_path)) args.data_path = os.path.abspath(os.path.expanduser(args.data_path))
args.pidfile = os.path.abspath(os.path.expanduser(args.pidfile)) if args.pidfile is not None and args.pidfile != "" else None args.pidfile = os.path.abspath(os.path.expanduser(args.pidfile))
args.socketfile = os.path.abspath(os.path.expanduser(args.socketfile)) if args.socketfile is not None and args.socketfile != "" else None args.socketfile = os.path.abspath(os.path.expanduser(args.socketfile))
args.logfile = os.path.abspath(os.path.expanduser(args.logfile)) args.logfile = os.path.abspath(os.path.expanduser(args.logfile))
args.files = [x for x in map(os.path.abspath, args.files)] args.files = [ x for x in map(os.path.abspath, args.files)]
args.modules_path = [x for x in map(os.path.abspath, args.modules_path)] args.modules_path = [ x for x in map(os.path.abspath, args.modules_path)]
# Prepare the attached client, before setting other stuff # Check if an instance is already launched
if not args.debug and not args.no_attach and args.socketfile is not None and args.pidfile is not None: if args.pidfile is not None and os.path.isfile(args.pidfile):
with open(args.pidfile, "r") as f:
pid = int(f.readline())
try: try:
pid = os.fork() os.kill(pid, 0)
if pid > 0: except OSError:
import time pass
os.waitpid(pid, 0) else:
time.sleep(1) from nemubot import attach
from nemubot import attach sys.exit(attach(pid, args.socketfile))
sys.exit(attach(args.pidfile, args.socketfile))
except OSError as err:
sys.stderr.write("Unable to fork: %s\n" % err)
sys.exit(1)
# Setup logging interface # Daemonize
if not args.debug:
from nemubot import daemonize
daemonize()
# Store PID to pidfile
if args.pidfile is not None:
with open(args.pidfile, "w+") as f:
f.write(str(os.getpid()))
# Setup loggin interface
import logging import logging
logger = logging.getLogger("nemubot") logger = logging.getLogger("nemubot")
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
@ -110,18 +117,6 @@ def main():
fh.setFormatter(formatter) fh.setFormatter(formatter)
logger.addHandler(fh) logger.addHandler(fh)
# Check if an instance is already launched
if args.pidfile is not None and os.path.isfile(args.pidfile):
with open(args.pidfile, "r") as f:
pid = int(f.readline())
try:
os.kill(pid, 0)
except OSError:
pass
else:
from nemubot import attach
sys.exit(attach(args.pidfile, args.socketfile))
# Add modules dir paths # Add modules dir paths
modules_paths = list() modules_paths = list()
for path in args.modules_path: for path in args.modules_path:
@ -135,7 +130,7 @@ def main():
from nemubot.bot import Bot from nemubot.bot import Bot
context = Bot(modules_paths=modules_paths, context = Bot(modules_paths=modules_paths,
data_store=datastore.XML(args.data_path), data_store=datastore.XML(args.data_path),
debug=args.verbose > 0) verbosity=args.verbose)
if args.no_connect: if args.no_connect:
context.noautoconnect = True context.noautoconnect = True
@ -147,55 +142,14 @@ def main():
# Load requested configuration files # Load requested configuration files
for path in args.files: for path in args.files:
if not os.path.isfile(path): if os.path.isfile(path):
context.sync_queue.put_nowait(["loadconf", path])
else:
logger.error("%s is not a readable file", path) logger.error("%s is not a readable file", path)
continue
config = load_config(path)
# Preset each server in this file
for server in config.servers:
# Add the server in the context
for i in [0,1,2,3]:
srv = server.server(config, trynb=i)
try:
if context.add_server(srv):
logger.info("Server '%s' successfully added.", srv.name)
else:
logger.error("Can't add server '%s'.", srv.name)
except Exception as e:
logger.error("Unable to connect to '%s': %s", srv.name, e)
continue
break
# Load module and their configuration
for mod in config.modules:
context.modules_configuration[mod.name] = mod
if mod.autoload:
try:
__import__("nemubot.module." + mod.name)
except:
logger.exception("Exception occurs when loading module"
" '%s'", mod.name)
# Load files asked by the configuration file
args.files += config.includes
if args.module: if args.module:
for module in args.module: for module in args.module:
__import__("nemubot.module." + module) __import__(module)
if args.socketfile:
from nemubot.server.socket import UnixSocketListener
context.add_server(UnixSocketListener(new_server_cb=context.add_server,
location=args.socketfile,
name="master_socket"))
# Daemonize
if not args.debug:
from nemubot import daemonize
daemonize(args.socketfile)
# Signals handling # Signals handling
def sigtermhandler(signum, frame): def sigtermhandler(signum, frame):
@ -206,34 +160,43 @@ def main():
def sighuphandler(signum, frame): def sighuphandler(signum, frame):
"""On SIGHUP, perform a deep reload""" """On SIGHUP, perform a deep reload"""
nonlocal context import imp
nonlocal nemubot, context, module_finder
logger.debug("SIGHUP receive, iniate reload procedure...") logger.debug("SIGHUP receive, iniate reload procedure...")
# Reload nemubot Python modules
imp.reload(nemubot)
nemubot.reload()
# Hotswap context
import nemubot.bot
context = nemubot.bot.hotswap(context)
# Reload ModuleFinder
sys.meta_path.remove(module_finder)
module_finder = ModuleFinder(context.modules_paths, context.add_module)
sys.meta_path.append(module_finder)
# Reload configuration file # Reload configuration file
for path in args.files: for path in args.files:
if os.path.isfile(path): if os.path.isfile(path):
sync_act("loadconf", path) context.sync_queue.put_nowait(["loadconf", path])
signal.signal(signal.SIGHUP, sighuphandler) signal.signal(signal.SIGHUP, sighuphandler)
def sigusr1handler(signum, frame): def sigusr1handler(signum, frame):
"""On SIGHUSR1, display stacktraces""" """On SIGHUSR1, display stacktraces"""
import threading, traceback import traceback
for threadId, stack in sys._current_frames().items(): for threadId, stack in sys._current_frames().items():
thName = "#%d" % threadId logger.debug("########### Thread %d:\n%s",
for th in threading.enumerate(): threadId,
if th.ident == threadId:
thName = th.name
break
logger.debug("########### Thread %s:\n%s",
thName,
"".join(traceback.format_stack(stack))) "".join(traceback.format_stack(stack)))
signal.signal(signal.SIGUSR1, sigusr1handler) signal.signal(signal.SIGUSR1, sigusr1handler)
# Store PID to pidfile if args.socketfile:
if args.pidfile is not None: from nemubot.server.socket import SocketListener
with open(args.pidfile, "w+") as f: context.add_server(SocketListener(context.add_server, "master_socket",
f.write(str(os.getpid())) sock_location=args.socketfile))
# context can change when performing an hotswap, always join the latest context # context can change when performing an hotswap, always join the latest context
oldcontext = None oldcontext = None
@ -244,36 +207,7 @@ def main():
# Wait for consumers # Wait for consumers
logger.info("Waiting for other threads shuts down...") logger.info("Waiting for other threads shuts down...")
if args.debug:
sigusr1handler(0, None)
sys.exit(0) sys.exit(0)
def load_config(filename):
"""Load a configuration file
Arguments:
filename -- the path to the file to load
"""
from nemubot.channel import Channel
from nemubot import config
from nemubot.tools.xmlparser import XMLParser
try:
p = XMLParser({
"nemubotconfig": config.Nemubot,
"server": config.Server,
"channel": Channel,
"module": config.Module,
"include": config.Include,
})
return p.parse_file(filename)
except:
logger.exception("Can't load `%s'; this is not a valid nemubot "
"configuration file.", filename)
return None
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View file

@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
# Nemubot is a smart and modulable IM bot. # Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2016 Mercier Pierre-Olivier # Copyright (C) 2012-2015 Mercier Pierre-Olivier
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -14,13 +16,10 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from datetime import datetime, timezone from datetime import datetime, timedelta, timezone
import logging import logging
from multiprocessing import JoinableQueue
import threading import threading
import select
import sys import sys
import weakref
from nemubot import __version__ from nemubot import __version__
from nemubot.consumer import Consumer, EventConsumer, MessageConsumer from nemubot.consumer import Consumer, EventConsumer, MessageConsumer
@ -29,35 +28,27 @@ import nemubot.hooks
logger = logging.getLogger("nemubot") logger = logging.getLogger("nemubot")
sync_queue = JoinableQueue()
def sync_act(*args):
sync_queue.put(list(args))
class Bot(threading.Thread): class Bot(threading.Thread):
"""Class containing the bot context and ensuring key goals""" """Class containing the bot context and ensuring key goals"""
def __init__(self, ip="127.0.0.1", modules_paths=list(), def __init__(self, ip="127.0.0.1", modules_paths=list(),
data_store=datastore.Abstract(), debug=False): data_store=datastore.Abstract(), verbosity=0):
"""Initialize the bot context """Initialize the bot context
Keyword arguments: Keyword arguments:
ip -- The external IP of the bot (default: 127.0.0.1) ip -- The external IP of the bot (default: 127.0.0.1)
modules_paths -- Paths to all directories where looking for modules modules_paths -- Paths to all directories where looking for module
data_store -- An instance of the nemubot datastore for bot's modules data_store -- An instance of the nemubot datastore for bot's modules
debug -- enable debug
""" """
super().__init__(name="Nemubot main") threading.Thread.__init__(self)
logger.info("Initiate nemubot v%s (running on Python %s.%s.%s)", logger.info("Initiate nemubot v%s", __version__)
__version__,
sys.version_info.major, sys.version_info.minor, sys.version_info.micro)
self.debug = debug self.verbosity = verbosity
self.stop = True self.stop = None
# External IP for accessing this bot # External IP for accessing this bot
import ipaddress import ipaddress
@ -69,7 +60,6 @@ class Bot(threading.Thread):
self.datastore.open() self.datastore.open()
# Keep global context: servers and modules # Keep global context: servers and modules
self._poll = select.poll()
self.servers = dict() self.servers = dict()
self.modules = dict() self.modules = dict()
self.modules_configuration = dict() self.modules_configuration = dict()
@ -84,44 +74,41 @@ class Bot(threading.Thread):
import re import re
def in_ping(msg): def in_ping(msg):
return msg.respond("pong") if re.match("^ *(m[' ]?entends?[ -]+tu|h?ear me|do you copy|ping)", msg.message, re.I) is not None:
self.treater.hm.add_hook(nemubot.hooks.Message(in_ping, return msg.respond("pong")
match=lambda msg: re.match("^ *(m[' ]?entends?[ -]+tu|h?ear me|do you copy|ping)", self.treater.hm.add_hook(nemubot.hooks.Message(in_ping), "in", "DirectAsk")
msg.message, re.I)),
"in", "DirectAsk")
def in_echo(msg): def in_echo(msg):
from nemubot.message import Text from nemubot.message import Text
return Text(msg.frm + ": " + " ".join(msg.args), to=msg.to_response) return Text(msg.nick + ": " + " ".join(msg.args), to=msg.to_response)
self.treater.hm.add_hook(nemubot.hooks.Command(in_echo, "echo"), "in", "Command") self.treater.hm.add_hook(nemubot.hooks.Message(in_echo, "echo"), "in", "Command")
def _help_msg(msg): def _help_msg(msg):
"""Parse and response to help messages""" """Parse and response to help messages"""
from nemubot.module.more import Response from more import Response
res = Response(channel=msg.to_response) res = Response(channel=msg.to_response)
if len(msg.args) >= 1: if len(msg.args) >= 1:
if "nemubot.module." + msg.args[0] in self.modules and self.modules["nemubot.module." + msg.args[0]]() is not None: if msg.args[0] in self.modules:
mname = "nemubot.module." + msg.args[0] if hasattr(self.modules[msg.args[0]], "help_full"):
if hasattr(self.modules[mname](), "help_full"): hlp = self.modules[msg.args[0]].help_full()
hlp = self.modules[mname]().help_full()
if isinstance(hlp, Response): if isinstance(hlp, Response):
return hlp return hlp
else: else:
res.append_message(hlp) res.append_message(hlp)
else: else:
res.append_message([str(h) for s,h in self.modules[mname]().__nemubot_context__.hooks], title="Available commands for module " + msg.args[0]) res.append_message([str(h) for s,h in self.modules[msg.args[0]].__nemubot_context__.hooks], title="Available commands for module " + msg.args[0])
elif msg.args[0][0] == "!": elif msg.args[0][0] == "!":
from nemubot.message.command import Command for module in self.modules:
for h in self.treater._in_hooks(Command(msg.args[0][1:])): for (s, h) in self.modules[module].__nemubot_context__.hooks:
if h.help_usage: if s == "in_Command" and (h.name is not None or h.regexp is not None) and h.is_matching(msg.args[0][1:]):
lp = ["\x03\x02%s%s\x03\x02: %s" % (msg.args[0], (" " + k if k is not None else ""), h.help_usage[k]) for k in h.help_usage] if h.help_usage:
jp = h.keywords.help() return res.append_message(["\x03\x02%s%s\x03\x02: %s" % (msg.args[0], " " + (k if k is not None else ""), h.help_usage[k]) for k in h.help_usage], title="Usage for command %s from module %s" % (msg.args[0], module))
return res.append_message(lp + ([". Moreover, you can provides some optional parameters: "] + jp if len(jp) else []), title="Usage for command %s" % msg.args[0]) elif h.help:
elif h.help: return res.append_message("Command %s from module %s: %s" % (msg.args[0], module, h.help))
return res.append_message("Command %s: %s" % (msg.args[0], h.help)) else:
else: return res.append_message("Sorry, there is currently no help for the command %s. Feel free to make a pull request at https://github.com/nemunaire/nemubot/compare" % msg.args[0])
return res.append_message("Sorry, there is currently no help for the command %s. Feel free to make a pull request at https://github.com/nemunaire/nemubot/compare" % msg.args[0]) else:
res.append_message("Sorry, there is no command %s" % msg.args[0]) res.append_message("Sorry, there is no command %s" % msg.args[0])
else: else:
res.append_message("Sorry, there is no module named %s" % msg.args[0]) res.append_message("Sorry, there is no module named %s" % msg.args[0])
else: else:
@ -135,120 +122,93 @@ class Bot(threading.Thread):
"Vous pouvez le consulter, le dupliquer, " "Vous pouvez le consulter, le dupliquer, "
"envoyer des rapports de bogues ou bien " "envoyer des rapports de bogues ou bien "
"contribuer au projet sur GitHub : " "contribuer au projet sur GitHub : "
"https://github.com/nemunaire/nemubot/") "http://github.com/nemunaire/nemubot/")
res.append_message(title="Pour plus de détails sur un module, " res.append_message(title="Pour plus de détails sur un module, "
"envoyez \"!help nomdumodule\". Voici la liste" "envoyez \"!help nomdumodule\". Voici la liste"
" de tous les modules disponibles localement", " de tous les modules disponibles localement",
message=["\x03\x02%s\x03\x02 (%s)" % (im, self.modules[im]().__doc__) for im in self.modules if self.modules[im]() is not None and self.modules[im]().__doc__]) message=["\x03\x02%s\x03\x02 (%s)" % (im, self.modules[im].__doc__) for im in self.modules if self.modules[im].__doc__])
return res return res
self.treater.hm.add_hook(nemubot.hooks.Command(_help_msg, "help"), "in", "Command") self.treater.hm.add_hook(nemubot.hooks.Message(_help_msg, "help"), "in", "Command")
import os
from queue import Queue from queue import Queue
# Messages to be treated — shared across all server connections. # Messages to be treated
# cnsr_active tracks consumers currently inside stm.run() (not idle), self.cnsr_queue = Queue()
# which lets us spawn a new thread the moment all existing ones are busy. self.cnsr_thrd = list()
self.cnsr_queue = Queue() self.cnsr_thrd_size = -1
self.cnsr_thrd = list() # Synchrone actions to be treated by main thread
self.cnsr_lock = threading.Lock() self.sync_queue = Queue()
self.cnsr_active = 0 # consumers currently executing a task
self.cnsr_max = os.cpu_count() or 4 # upper bound on concurrent consumer threads
def __del__(self):
self.datastore.close()
def run(self): def run(self):
global sync_queue from select import select
from nemubot.server import _lock, _rlist, _wlist, _xlist
# Rewrite the sync_queue, as the daemonization process tend to disturb it
old_sync_queue, sync_queue = sync_queue, JoinableQueue()
while not old_sync_queue.empty():
sync_queue.put_nowait(old_sync_queue.get())
self._poll.register(sync_queue._reader, select.POLLIN | select.POLLPRI)
self.stop = False
# Relaunch events
self._update_event_timer()
logger.info("Starting main loop") logger.info("Starting main loop")
self.stop = False
while not self.stop: while not self.stop:
for fd, flag in self._poll.poll(): with _lock:
# Handle internal socket passing orders try:
if fd != sync_queue._reader.fileno() and fd in self.servers: rl, wl, xl = select(_rlist, _wlist, _xlist, 0.1)
srv = self.servers[fd] except:
logger.error("Something went wrong in select")
if flag & (select.POLLERR | select.POLLHUP | select.POLLNVAL): fnd_smth = False
try: # Looking for invalid server
srv.exception(flag) for r in _rlist:
except: if not hasattr(r, "fileno") or not isinstance(r.fileno(), int) or r.fileno() < 0:
logger.exception("Uncatched exception on server exception") _rlist.remove(r)
logger.error("Found invalid object in _rlist: " + str(r))
if srv.fileno() > 0: fnd_smth = True
if flag & (select.POLLOUT): for w in _wlist:
try: if not hasattr(w, "fileno") or not isinstance(w.fileno(), int) or w.fileno() < 0:
srv.async_write() _wlist.remove(w)
except: logger.error("Found invalid object in _wlist: " + str(w))
logger.exception("Uncatched exception on server write") fnd_smth = True
for x in _xlist:
if flag & (select.POLLIN | select.POLLPRI): if not hasattr(x, "fileno") or not isinstance(x.fileno(), int) or x.fileno() < 0:
try: _xlist.remove(x)
for i in srv.async_read(): logger.error("Found invalid object in _xlist: " + str(x))
self.receive_message(srv, i) fnd_smth = True
except: if not fnd_smth:
logger.exception("Uncatched exception on server read") logger.exception("Can't continue, sorry")
else:
del self.servers[fd]
# Always check the sync queue
while not sync_queue.empty():
args = sync_queue.get()
action = args.pop(0)
logger.debug("Executing sync_queue action %s%s", action, args)
if action == "sckt" and len(args) >= 2:
try:
if args[0] == "write":
self._poll.modify(int(args[1]), select.POLLOUT | select.POLLIN | select.POLLPRI)
elif args[0] == "unwrite":
self._poll.modify(int(args[1]), select.POLLIN | select.POLLPRI)
elif args[0] == "register":
self._poll.register(int(args[1]), select.POLLIN | select.POLLPRI)
elif args[0] == "unregister":
try:
self._poll.unregister(int(args[1]))
except KeyError:
pass
except:
logger.exception("Unhandled excpetion during action:")
elif action == "exit":
self.quit() self.quit()
continue
elif action == "launch_consumer": for x in xl:
pass # This is treated after the loop try:
x.exception()
sync_queue.task_done() except:
logger.exception("Uncatched exception on server exception")
for w in wl:
try:
w.write_select()
except:
logger.exception("Uncatched exception on server write")
for r in rl:
for i in r.read():
try:
self.receive_message(r, i)
except:
logger.exception("Uncatched exception on server read")
# Spawn a new consumer whenever the queue has work and every # Launch new consumer threads if necessary
# existing consumer is already busy executing a task. while self.cnsr_queue.qsize() > self.cnsr_thrd_size:
with self.cnsr_lock: # Next launch if two more items in queue
while (not self.cnsr_queue.empty() self.cnsr_thrd_size += 2
and self.cnsr_active >= len(self.cnsr_thrd)
and len(self.cnsr_thrd) < self.cnsr_max): c = Consumer(self)
c = Consumer(self) self.cnsr_thrd.append(c)
self.cnsr_thrd.append(c) c.start()
c.start()
sync_queue = None while self.sync_queue.qsize() > 0:
action = self.sync_queue.get_nowait()
if action[0] == "exit":
self.quit()
elif action[0] == "loadconf":
for path in action[1:]:
from nemubot.tools.config import load_file
load_file(path, self)
self.sync_queue.task_done()
logger.info("Ending main loop") logger.info("Ending main loop")
@ -270,6 +230,10 @@ class Bot(threading.Thread):
module_src -- The module to which the event is attached to module_src -- The module to which the event is attached to
""" """
if hasattr(self, "stop") and self.stop:
logger.warn("The bot is stopped, can't register new events")
return
import uuid import uuid
# Generate the event id if no given # Generate the event id if no given
@ -280,7 +244,7 @@ class Bot(threading.Thread):
if type(eid) is uuid.UUID: if type(eid) is uuid.UUID:
evt.id = str(eid) evt.id = str(eid)
else: else:
# Ok, this is quiet useless... # Ok, this is quite useless...
try: try:
evt.id = str(uuid.UUID(eid)) evt.id = str(uuid.UUID(eid))
except ValueError: except ValueError:
@ -296,7 +260,7 @@ class Bot(threading.Thread):
break break
self.events.insert(i, evt) self.events.insert(i, evt)
if i == 0 and not self.stop: if i == 0:
# First event changed, reset timer # First event changed, reset timer
self._update_event_timer() self._update_event_timer()
if len(self.events) <= 0 or self.events[i] != evt: if len(self.events) <= 0 or self.events[i] != evt:
@ -305,10 +269,10 @@ class Bot(threading.Thread):
# Register the event in the source module # Register the event in the source module
if module_src is not None: if module_src is not None:
module_src.__nemubot_context__.events.append((evt, evt.id)) module_src.__nemubot_context__.events.append(evt.id)
evt.module_src = module_src evt.module_src = module_src
logger.info("New event registered in %d position: %s", i, t) logger.info("New event registered: %s -> %s", evt.id, evt)
return evt.id return evt.id
@ -335,10 +299,10 @@ class Bot(threading.Thread):
id = evt id = evt
if len(self.events) > 0 and id == self.events[0].id: if len(self.events) > 0 and id == self.events[0].id:
if module_src is not None:
module_src.__nemubot_context__.events.remove((self.events[0], id))
self.events.remove(self.events[0]) self.events.remove(self.events[0])
self._update_event_timer() self._update_event_timer()
if module_src is not None:
module_src.__nemubot_context__.events.remove(id)
return True return True
for evt in self.events: for evt in self.events:
@ -346,7 +310,7 @@ class Bot(threading.Thread):
self.events.remove(evt) self.events.remove(evt)
if module_src is not None: if module_src is not None:
module_src.__nemubot_context__.events.remove((evt, evt.id)) module_src.__nemubot_context__.events.remove(evt.id)
return True return True
return False return False
@ -359,15 +323,11 @@ class Bot(threading.Thread):
self.event_timer.cancel() self.event_timer.cancel()
if len(self.events): if len(self.events):
try: logger.debug("Update timer: next event in %d seconds",
remaining = self.events[0].time_left.total_seconds() self.events[0].time_left.seconds)
except: self.event_timer = threading.Timer(
logger.exception("An error occurs during event time calculation:") self.events[0].time_left.seconds + self.events[0].time_left.microseconds / 1000000 if datetime.now(timezone.utc) < self.events[0].current else 0,
self.events.pop(0) self._end_event_timer)
return self._update_event_timer()
logger.debug("Update timer: next event in %d seconds", remaining)
self.event_timer = threading.Timer(remaining if remaining > 0 else 0, self._end_event_timer)
self.event_timer.start() self.event_timer.start()
else: else:
@ -380,7 +340,6 @@ class Bot(threading.Thread):
while len(self.events) > 0 and datetime.now(timezone.utc) >= self.events[0].current: while len(self.events) > 0 and datetime.now(timezone.utc) >= self.events[0].current:
evt = self.events.pop(0) evt = self.events.pop(0)
self.cnsr_queue.put_nowait(EventConsumer(evt)) self.cnsr_queue.put_nowait(EventConsumer(evt))
sync_act("launch_consumer")
self._update_event_timer() self._update_event_timer()
@ -395,12 +354,10 @@ class Bot(threading.Thread):
autoconnect -- connect after add? autoconnect -- connect after add?
""" """
fileno = srv.fileno() if srv.id not in self.servers:
if fileno not in self.servers: self.servers[srv.id] = srv
self.servers[fileno] = srv
self.servers[srv.name] = srv
if autoconnect and not hasattr(self, "noautoconnect"): if autoconnect and not hasattr(self, "noautoconnect"):
srv.connect() srv.open()
return True return True
else: else:
@ -427,6 +384,10 @@ class Bot(threading.Thread):
old one before""" old one before"""
module_name = module.__spec__.name if hasattr(module, "__spec__") else module.__name__ module_name = module.__spec__.name if hasattr(module, "__spec__") else module.__name__
if hasattr(self, "stop") and self.stop:
logger.warn("The bot is stopped, can't register new modules")
return
# Check if the module already exists # Check if the module already exists
if module_name in self.modules: if module_name in self.modules:
self.unload_module(module_name) self.unload_module(module_name)
@ -440,7 +401,7 @@ class Bot(threading.Thread):
module.print = prnt module.print = prnt
# Create module context # Create module context
from nemubot.modulecontext import _ModuleContext, ModuleContext from nemubot.modulecontext import ModuleContext
module.__nemubot_context__ = ModuleContext(self, module) module.__nemubot_context__ = ModuleContext(self, module)
if not hasattr(module, "logger"): if not hasattr(module, "logger"):
@ -448,14 +409,14 @@ class Bot(threading.Thread):
# Replace imported context by real one # Replace imported context by real one
for attr in module.__dict__: for attr in module.__dict__:
if attr != "__nemubot_context__" and type(module.__dict__[attr]) == _ModuleContext: if attr != "__nemubot_context__" and type(module.__dict__[attr]) == ModuleContext:
module.__dict__[attr] = module.__nemubot_context__ module.__dict__[attr] = module.__nemubot_context__
# Register decorated functions # Register decorated functions
import nemubot.hooks import nemubot.hooks
for s, h in nemubot.hooks.hook.last_registered: for s, h in nemubot.hooks.last_registered:
module.__nemubot_context__.add_hook(h, *s if isinstance(s, list) else s) module.__nemubot_context__.add_hook(s, h)
nemubot.hooks.hook.last_registered = [] nemubot.hooks.last_registered = []
# Launch the module # Launch the module
if hasattr(module, "load"): if hasattr(module, "load"):
@ -466,20 +427,18 @@ class Bot(threading.Thread):
raise raise
# Save a reference to the module # Save a reference to the module
self.modules[module_name] = weakref.ref(module) self.modules[module_name] = module
logger.info("Module '%s' successfully loaded.", module_name)
def unload_module(self, name): def unload_module(self, name):
"""Unload a module""" """Unload a module"""
if name in self.modules and self.modules[name]() is not None: if name in self.modules:
module = self.modules[name]() self.modules[name].print("Unloading module %s" % name)
module.print("Unloading module %s" % name)
# Call the user defined unload method # Call the user defined unload method
if hasattr(module, "unload"): if hasattr(self.modules[name], "unload"):
module.unload(self) self.modules[name].unload(self)
module.__nemubot_context__.unload() self.modules[name].__nemubot_context__.unload()
# Remove from the nemubot dict # Remove from the nemubot dict
del self.modules[name] del self.modules[name]
@ -511,28 +470,28 @@ class Bot(threading.Thread):
def quit(self): def quit(self):
"""Save and unload modules and disconnect servers""" """Save and unload modules and disconnect servers"""
self.datastore.close()
if self.event_timer is not None: if self.event_timer is not None:
logger.info("Stop the event timer...") logger.info("Stop the event timer...")
self.event_timer.cancel() self.event_timer.cancel()
logger.info("Save and unload all modules...")
for mod in [m for m in self.modules.keys()]:
self.unload_module(mod)
logger.info("Close all servers connection...")
for srv in [self.servers[k] for k in self.servers]:
srv.close()
logger.info("Stop consumers") logger.info("Stop consumers")
with self.cnsr_lock: k = self.cnsr_thrd
k = list(self.cnsr_thrd)
for cnsr in k: for cnsr in k:
cnsr.stop = True cnsr.stop = True
if self.stop is False or sync_queue is not None: logger.info("Save and unload all modules...")
self.stop = True k = list(self.modules.keys())
sync_act("end") for mod in k:
sync_queue.join() self.unload_module(mod)
logger.info("Close all servers connection...")
k = list(self.servers.keys())
for srv in k:
self.servers[srv].close()
self.stop = True
# Treatment # Treatment
@ -546,3 +505,22 @@ class Bot(threading.Thread):
del store[hook.name] del store[hook.name]
elif isinstance(store, list): elif isinstance(store, list):
store.remove(hook) store.remove(hook)
def hotswap(bak):
bak.stop = True
if bak.event_timer is not None:
bak.event_timer.cancel()
# Unload modules
for mod in [k for k in bak.modules.keys()]:
bak.unload_module(mod)
# Save datastore
bak.datastore.close()
new = Bot(str(bak.ip), bak.modules_paths, bak.datastore)
new.servers = bak.servers
new._update_event_timer()
return new

View file

@ -1,5 +1,7 @@
# coding=utf-8
# Nemubot is a smart and modulable IM bot. # Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2016 Mercier Pierre-Olivier # Copyright (C) 2012-2015 Mercier Pierre-Olivier
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -21,18 +23,16 @@ class Channel:
"""A chat room""" """A chat room"""
def __init__(self, name, password=None, encoding=None): def __init__(self, name, password=None):
"""Initialize the channel """Initialize the channel
Arguments: Arguments:
name -- the channel name name -- the channel name
password -- the optional password use to join it password -- the optional password use to join it
encoding -- the optional encoding of the channel
""" """
self.name = name self.name = name
self.password = password self.password = password
self.encoding = encoding
self.people = dict() self.people = dict()
self.topic = "" self.topic = ""
self.logger = logging.getLogger("nemubot.channel." + name) self.logger = logging.getLogger("nemubot.channel." + name)
@ -52,11 +52,11 @@ class Channel:
elif cmd == "MODE": elif cmd == "MODE":
self.mode(msg) self.mode(msg)
elif cmd == "JOIN": elif cmd == "JOIN":
self.join(msg.frm) self.join(msg.nick)
elif cmd == "NICK": elif cmd == "NICK":
self.nick(msg.frm, msg.text) self.nick(msg.nick, msg.text)
elif cmd == "PART" or cmd == "QUIT": elif cmd == "PART" or cmd == "QUIT":
self.part(msg.frm) self.part(msg.nick)
elif cmd == "TOPIC": elif cmd == "TOPIC":
self.topic = self.text self.topic = self.text
@ -120,17 +120,17 @@ class Channel:
else: else:
self.password = msg.text[1] self.password = msg.text[1]
elif msg.text[0] == "+o": elif msg.text[0] == "+o":
self.people[msg.frm] |= 4 self.people[msg.nick] |= 4
elif msg.text[0] == "-o": elif msg.text[0] == "-o":
self.people[msg.frm] &= ~4 self.people[msg.nick] &= ~4
elif msg.text[0] == "+h": elif msg.text[0] == "+h":
self.people[msg.frm] |= 2 self.people[msg.nick] |= 2
elif msg.text[0] == "-h": elif msg.text[0] == "-h":
self.people[msg.frm] &= ~2 self.people[msg.nick] &= ~2
elif msg.text[0] == "+v": elif msg.text[0] == "+v":
self.people[msg.frm] |= 1 self.people[msg.nick] |= 1
elif msg.text[0] == "-v": elif msg.text[0] == "-v":
self.people[msg.frm] &= ~1 self.people[msg.nick] &= ~1
def parse332(self, msg): def parse332(self, msg):
"""Parse RPL_TOPIC message """Parse RPL_TOPIC message

View file

@ -1,26 +0,0 @@
# Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2016 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
def get_boolean(s):
if isinstance(s, bool):
return s
else:
return (s and s != "0" and s.lower() != "false" and s.lower() != "off")
from nemubot.config.include import Include
from nemubot.config.module import Module
from nemubot.config.nemubot import Nemubot
from nemubot.config.server import Server

View file

@ -1,20 +0,0 @@
# Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2016 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
class Include:
def __init__(self, path):
self.path = path

View file

@ -1,26 +0,0 @@
# Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2016 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from nemubot.config import get_boolean
from nemubot.tools.xmlparser.genericnode import GenericNode
class Module(GenericNode):
def __init__(self, name, autoload=True, **kwargs):
super().__init__(None, **kwargs)
self.name = name
self.autoload = get_boolean(autoload)

View file

@ -1,46 +0,0 @@
# Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2016 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from nemubot.config.include import Include
from nemubot.config.module import Module
from nemubot.config.server import Server
class Nemubot:
def __init__(self, nick="nemubot", realname="nemubot", owner=None,
ip=None, ssl=False, caps=None, encoding="utf-8"):
self.nick = nick
self.realname = realname
self.owner = owner
self.ip = ip
self.caps = caps.split(" ") if caps is not None else []
self.encoding = encoding
self.servers = []
self.modules = []
self.includes = []
def addChild(self, name, child):
if name == "module" and isinstance(child, Module):
self.modules.append(child)
return True
elif name == "server" and isinstance(child, Server):
self.servers.append(child)
return True
elif name == "include" and isinstance(child, Include):
self.includes.append(child)
return True

View file

@ -1,45 +0,0 @@
# Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2016 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from nemubot.channel import Channel
class Server:
def __init__(self, uri="irc://nemubot@localhost/", autoconnect=True, caps=None, **kwargs):
self.uri = uri
self.autoconnect = autoconnect
self.caps = caps.split(" ") if caps is not None else []
self.args = kwargs
self.channels = []
def addChild(self, name, child):
if name == "channel" and isinstance(child, Channel):
self.channels.append(child)
return True
def server(self, parent, trynb=0):
from nemubot.server import factory
for a in ["nick", "owner", "realname", "encoding"]:
if a not in self.args:
self.args[a] = getattr(parent, a)
self.caps += parent.caps
return factory(self.uri, caps=self.caps, channels=self.channels, trynb=trynb, **self.args)

View file

@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
# Nemubot is a smart and modulable IM bot. # Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2016 Mercier Pierre-Olivier # Copyright (C) 2012-2015 Mercier Pierre-Olivier
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -38,37 +40,42 @@ class MessageConsumer:
msgs = [] msgs = []
# Parse message # Parse the message
try: try:
for msg in self.srv.parse(self.orig): for msg in self.srv.parse(self.orig):
msgs.append(msg) msgs.append(msg)
except: except:
logger.exception("Error occurred during the processing of the %s: " logger.exception("Error occurred during the processing of the %s: "
"%s", type(self.orig).__name__, self.orig) "%s", type(self.msgs[0]).__name__, self.msgs[0])
# Treat message if len(msgs) <= 0:
return
# Qualify the message
if not hasattr(msg, "server") or msg.server is None:
msg.server = self.srv.id
if hasattr(msg, "frm_owner"):
msg.frm_owner = (not hasattr(self.srv, "owner") or self.srv.owner == msg.frm)
# Treat the message
for msg in msgs: for msg in msgs:
for res in context.treater.treat_msg(msg): for res in context.treater.treat_msg(msg):
# Identify destination # Identify the destination
to_server = None to_server = None
if isinstance(res, str): if isinstance(res, str):
to_server = self.srv to_server = self.srv
elif not hasattr(res, "server"):
logger.error("No server defined for response of type %s: %s", type(res).__name__, res)
continue
elif res.server is None: elif res.server is None:
to_server = self.srv to_server = self.srv
res.server = self.srv.fileno() res.server = self.srv.id
elif res.server in context.servers: elif isinstance(res.server, str) and res.server in context.servers:
to_server = context.servers[res.server] to_server = context.servers[res.server]
else:
to_server = res.server
if to_server is None or not hasattr(to_server, "send_response") or not callable(to_server.send_response): if to_server is None:
logger.error("The server defined in this response doesn't exist: %s", res.server) logger.error("The server defined in this response doesn't "
"exist: %s", res.server)
continue continue
# Sent message # Sent the message only if treat_post authorize it
to_server.send_response(res) to_server.send_response(res)
@ -94,7 +101,7 @@ class EventConsumer:
# Or remove reference of this event # Or remove reference of this event
elif (hasattr(self.evt, "module_src") and elif (hasattr(self.evt, "module_src") and
self.evt.module_src is not None): self.evt.module_src is not None):
self.evt.module_src.__nemubot_context__.events.remove((self.evt, self.evt.id)) self.evt.module_src.__nemubot_context__.events.remove(self.evt.id)
@ -105,25 +112,18 @@ class Consumer(threading.Thread):
def __init__(self, context): def __init__(self, context):
self.context = context self.context = context
self.stop = False self.stop = False
super().__init__(name="Nemubot consumer", daemon=True) threading.Thread.__init__(self)
def run(self): def run(self):
try: try:
while not self.stop: while not self.stop:
try: stm = self.context.cnsr_queue.get(True, 1)
stm = self.context.cnsr_queue.get(True, 1) stm.run(self.context)
except queue.Empty: self.context.cnsr_queue.task_done()
break
with self.context.cnsr_lock: except queue.Empty:
self.context.cnsr_active += 1 pass
try:
stm.run(self.context)
finally:
self.context.cnsr_queue.task_done()
with self.context.cnsr_lock:
self.context.cnsr_active -= 1
finally: finally:
with self.context.cnsr_lock: self.context.cnsr_thrd_size -= 2
self.context.cnsr_thrd.remove(self) self.context.cnsr_thrd.remove(self)

View file

@ -1,5 +1,5 @@
# Nemubot is a smart and modulable IM bot. # Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2016 Mercier Pierre-Olivier # Copyright (C) 2012-2015 Mercier Pierre-Olivier
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by

View file

@ -1,5 +1,5 @@
# Nemubot is a smart and modulable IM bot. # Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2016 Mercier Pierre-Olivier # Copyright (C) 2012-2015 Mercier Pierre-Olivier
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by
@ -32,20 +32,16 @@ class Abstract:
def close(self): def close(self):
return return
def load(self, module, knodes): def load(self, module):
"""Load data for the given module """Load data for the given module
Argument: Argument:
module -- the module name of data to load module -- the module name of data to load
knodes -- the schema to use to load the datas
Return: Return:
The loaded data The loaded data
""" """
if knodes is not None:
return None
return self.new() return self.new()
def save(self, module, data): def save(self, module, data):

View file

@ -83,38 +83,27 @@ class XML(Abstract):
return os.path.join(self.basedir, module + ".xml") return os.path.join(self.basedir, module + ".xml")
def load(self, module, knodes): def load(self, module):
"""Load data for the given module """Load data for the given module
Argument: Argument:
module -- the module name of data to load module -- the module name of data to load
knodes -- the schema to use to load the datas
""" """
data_file = self._get_data_file_path(module) data_file = self._get_data_file_path(module)
if knodes is None:
from nemubot.tools.xmlparser import parse_file
def _true_load(path):
return parse_file(path)
else:
from nemubot.tools.xmlparser import XMLParser
p = XMLParser(knodes)
def _true_load(path):
return p.parse_file(path)
# Try to load original file # Try to load original file
if os.path.isfile(data_file): if os.path.isfile(data_file):
from nemubot.tools.xmlparser import parse_file
try: try:
return _true_load(data_file) return parse_file(data_file)
except xml.parsers.expat.ExpatError: except xml.parsers.expat.ExpatError:
# Try to load from backup # Try to load from backup
for i in range(10): for i in range(10):
path = data_file + "." + str(i) path = data_file + "." + str(i)
if os.path.isfile(path): if os.path.isfile(path):
try: try:
cnt = _true_load(path) cnt = parse_file(path)
logger.warn("Restoring from backup: %s", path) logger.warn("Restoring from backup: %s", path)
@ -123,7 +112,7 @@ class XML(Abstract):
continue continue
# Default case: initialize a new empty datastore # Default case: initialize a new empty datastore
return super().load(module, knodes) return Abstract.load(self, module)
def _rotate(self, path): def _rotate(self, path):
"""Backup given path """Backup given path
@ -154,18 +143,4 @@ class XML(Abstract):
if self.rotate: if self.rotate:
self._rotate(path) self._rotate(path)
if data is None: return data.save(path)
return
import tempfile
_, tmpath = tempfile.mkstemp()
with open(tmpath, "w") as f:
import xml.sax.saxutils
gen = xml.sax.saxutils.XMLGenerator(f, "utf-8")
gen.startDocument()
data.saveElement(gen)
gen.endDocument()
# Atomic save
import shutil
shutil.move(tmpath, path)

View file

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
# Nemubot is a smart and modulable IM bot. # Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2015 Mercier Pierre-Olivier # Copyright (C) 2012-2015 Mercier Pierre-Olivier
# #
@ -21,84 +23,121 @@ class ModuleEvent:
"""Representation of a event initiated by a bot module""" """Representation of a event initiated by a bot module"""
def __init__(self, call=None, func=None, cmp=None, interval=60, offset=0, times=1): def __init__(self, call=None, call_data=None, func=None, func_data=None,
cmp=None, cmp_data=None, end_call=None, end_data=None,
interval=60, offset=0, times=1, max_attempt=-1):
"""Initialize the event """Initialize the event
Keyword arguments: Keyword arguments:
call -- Function to call when the event is realized call -- Function to call when the event is realized
call_data -- Argument(s) (single or dict) to pass as argument
func -- Function called to check func -- Function called to check
cmp -- Boolean function called to check changes or value to compare with func_data -- Argument(s) (single or dict) to pass as argument OR if no func, initial data to watch
cmp -- Boolean function called to check changes
cmp_data -- Argument(s) (single or dict) to pass as argument OR if no cmp, data compared to previous
end_call -- Function called when times or max_attempt reach 0 (mainly for interaction with the event manager)
end_data -- Argument(s) (single or dict) to pass as argument
interval -- Time in seconds between each check (default: 60) interval -- Time in seconds between each check (default: 60)
offset -- Time in seconds added to interval before the first check (default: 0) offset -- Time in seconds added to interval before the first check (default: 0)
times -- Number of times the event has to be realized before being removed; -1 for no limit (default: 1) times -- Number of times the event has to be realized before being removed; -1 for no limit (default: 1)
max_attempt -- Maximum number of times the event will be checked
""" """
# What have we to check? # What have we to check?
self.func = func self.func = func
self.func_data = func_data
# How detect a change? # How detect a change?
self.cmp = cmp self.cmp = cmp
if cmp_data is not None:
self.cmp_data = cmp_data
elif callable(self.func):
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
# What should we call when? # What should we call when?
self.call = call self.call = call
if call_data is not None:
# Store times self.call_data = call_data
if isinstance(offset, timedelta):
self.offset = offset # Time to wait before the first check
else: else:
self.offset = timedelta(seconds=offset) # Time to wait before the first check self.call_data = func_data
if isinstance(interval, timedelta):
self.interval = interval
else:
self.interval = timedelta(seconds=interval)
self._end = None # Cache
# Store time between each event
self.interval = timedelta(seconds=interval)
# How many times do this event? # How many times do this event?
self.times = times self.times = times
@property # Cache the time of the next occurence
def current(self): self.next_occur = datetime.now(timezone.utc) + timedelta(seconds=offset) + self.interval
"""Return the date of the near check"""
if self.times != 0:
if self._end is None:
self._end = datetime.now(timezone.utc) + self.offset + self.interval
return self._end
return None
@property
def next(self):
"""Return the date of the next check"""
if self.times != 0:
if self._end is None:
return self.current
elif self._end < datetime.now(timezone.utc):
self._end += self.interval
return self._end
return None
@property @property
def time_left(self): def time_left(self):
"""Return the time left before/after the near check""" """Return the time left before/after the near check"""
if self.current is not None:
return self.current - datetime.now(timezone.utc) return self.next_occur - datetime.now(timezone.utc)
return timedelta.max
def check(self): def check(self):
"""Run a check and realized the event if this is time""" """Run a check and realized the event if this is time"""
# Get new data self.max_attempt -= 1
if self.func is not None:
d_new = self.func() # Get initial data
if not callable(self.func):
d_init = self.func_data
elif self.func_data is None:
d_init = self.func()
elif isinstance(self.func_data, dict):
d_init = self.func(**self.func_data)
else: else:
d_new = None d_init = self.func(self.func_data)
# then compare with current data # then compare with current data
if self.cmp is None or (callable(self.cmp) and self.cmp(d_new)) or (not callable(self.cmp) and d_new != self.cmp): if not callable(self.cmp):
if self.cmp_data is None:
rlz = True
else:
rlz = (d_init != self.cmp_data)
elif self.cmp_data is None:
rlz = self.cmp(d_init)
elif isinstance(self.cmp_data, dict):
rlz = self.cmp(d_init, **self.cmp_data)
else:
rlz = self.cmp(d_init, self.cmp_data)
if rlz:
self.times -= 1 self.times -= 1
# Call attended function # Call attended function
if self.func is not None: if self.call_data is None:
self.call(d_new) if d_init is None:
self.call()
else:
self.call(d_init)
elif isinstance(self.call_data, dict):
self.call(d_init, **self.call_data)
else: else:
self.call() self.call(d_init, self.call_data)
# Is it finished?
if self.times == 0 or self.max_attempt == 0:
if not callable(self.end_call):
pass # TODO: log a WARN here
else:
if self.end_data is None:
self.end_call()
elif isinstance(self.end_data, dict):
self.end_call(**self.end_data)
else:
self.end_call(self.end_data)
# Not finished, ready to next one!
else:
self.next_occur += self.interval

View file

@ -1,3 +1,5 @@
# coding=utf-8
# Nemubot is a smart and modulable IM bot. # Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2015 Mercier Pierre-Olivier # Copyright (C) 2012-2015 Mercier Pierre-Olivier
# #
@ -14,21 +16,20 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
class IMException(Exception): class IRCException(Exception):
def __init__(self, message, personnal=True): def __init__(self, message, personnal=True):
super(IMException, self).__init__(message) super(IRCException, self).__init__(message)
self.message = message
self.personnal = personnal self.personnal = personnal
def fill_response(self, msg): def fill_response(self, msg):
if self.personnal: if self.personnal:
from nemubot.message import DirectAsk from nemubot.message import DirectAsk
return DirectAsk(msg.frm, *self.args, return DirectAsk(msg.frm, self.message,
server=msg.server, to=msg.to_response) server=msg.server, to=msg.to_response)
else: else:
from nemubot.message import Text from nemubot.message import Text
return Text(*self.args, return Text(self.message,
server=msg.server, to=msg.to_response) server=msg.server, to=msg.to_response)

View file

@ -15,37 +15,29 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from nemubot.hooks.abstract import Abstract from nemubot.hooks.abstract import Abstract
from nemubot.hooks.command import Command
from nemubot.hooks.message import Message from nemubot.hooks.message import Message
last_registered = []
class hook:
last_registered = []
def _add(store, h, *args, **kwargs): def hook(store, *args, **kargs):
"""Function used as a decorator for module loading""" """Function used as a decorator for module loading"""
def sec(call): def sec(call):
hook.last_registered.append((store, h(call, *args, **kwargs))) last_registered.append((store, Message(call, *args, **kargs)))
return call return call
return sec return sec
def add(store, *args, **kwargs): def reload():
return hook._add(store, Abstract, *args, **kwargs) global Abstract, Message
import imp
def ask(*args, store=["in","DirectAsk"], **kwargs): import nemubot.hooks.abstract
return hook._add(store, Message, *args, **kwargs) imp.reload(nemubot.hooks.abstract)
Abstract = nemubot.hooks.abstract.Abstract
import nemubot.hooks.message
imp.reload(nemubot.hooks.message)
Message = nemubot.hooks.message.Message
def command(*args, store=["in","Command"], **kwargs): import nemubot.hooks.manager
return hook._add(store, Command, *args, **kwargs) imp.reload(nemubot.hooks.manager)
def message(*args, store=["in","Text"], **kwargs):
return hook._add(store, Message, *args, **kwargs)
def post(*args, store=["post"], **kwargs):
return hook._add(store, Abstract, *args, **kwargs)
def pre(*args, store=["pre"], **kwargs):
return hook._add(store, Abstract, *args, **kwargs)

View file

@ -14,8 +14,6 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import types
def call_game(call, *args, **kargs): def call_game(call, *args, **kargs):
"""With given args, try to determine the right call to make """With given args, try to determine the right call to make
@ -44,95 +42,30 @@ class Abstract:
"""Abstract class for Hook implementation""" """Abstract class for Hook implementation"""
def __init__(self, call, data=None, channels=None, servers=None, mtimes=-1, def __init__(self, call, data=None, mtimes=-1, end_call=None):
end_call=None, check=None, match=None):
"""Create basis of the hook
Arguments:
call -- function to call to perform the hook
Keyword arguments:
data -- optional datas passed to call
"""
if channels is None: channels = list()
if servers is None: servers = list()
assert callable(call), call
assert end_call is None or callable(end_call), end_call
assert check is None or callable(check), check
assert match is None or callable(match), match
assert isinstance(channels, list), channels
assert isinstance(servers, list), servers
assert type(mtimes) is int, mtimes
self.call = call self.call = call
self.data = data self.data = data
self.mod_check = check
self.mod_match = match
# TODO: find a way to have only one list: a limit is server + channel, not only server or channel
self.channels = channels
self.servers = servers
self.times = mtimes self.times = mtimes
self.end_call = end_call self.end_call = end_call
def can_read(self, receivers=list(), server=None): def match(self, data1, server):
assert isinstance(receivers, list), receivers return NotImplemented
if server is None or len(self.servers) == 0 or server in self.servers:
if len(self.channels) == 0:
return True
for receiver in receivers:
if receiver in self.channels:
return True
return False
def __str__(self):
return ""
def can_write(self, receivers=list(), server=None):
return True
def check(self, data1):
return self.mod_check(data1) if self.mod_check is not None else True
def match(self, data1):
return self.mod_match(data1) if self.mod_match is not None else True
def run(self, data1, *args): def run(self, data1, *args):
"""Run the hook""" """Run the hook"""
from nemubot.exception import IMException from nemubot.exception import IRCException
self.times -= 1 self.times -= 1
ret = None
try: try:
if self.check(data1): ret = call_game(self.call, data1, self.data, *args)
ret = call_game(self.call, data1, self.data, *args) except IRCException as e:
if isinstance(ret, types.GeneratorType):
for r in ret:
yield r
ret = None
except IMException as e:
ret = e.fill_response(data1) ret = e.fill_response(data1)
finally: finally:
if self.times == 0: if self.times == 0:
self.call_end(ret) self.call_end(ret)
if isinstance(ret, list): return ret
for r in ret:
yield ret
elif ret is not None:
yield ret

View file

@ -1,67 +0,0 @@
# Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2015 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import re
from nemubot.hooks.message import Message
from nemubot.hooks.abstract import Abstract
from nemubot.hooks.keywords import NoKeyword
from nemubot.hooks.keywords.abstract import Abstract as AbstractKeywords
from nemubot.hooks.keywords.dict import Dict as DictKeywords
import nemubot.message
class Command(Message):
"""Class storing hook information, specialized for Command messages"""
def __init__(self, call, name=None, help_usage=dict(), keywords=NoKeyword(),
**kargs):
super().__init__(call=call, **kargs)
if isinstance(keywords, dict):
keywords = DictKeywords(keywords)
assert type(help_usage) is dict, help_usage
assert isinstance(keywords, AbstractKeywords), keywords
self.name = str(name) if name is not None else None
self.help_usage = help_usage
self.keywords = keywords
def __str__(self):
return "\x03\x02%s\x03\x02%s%s" % (
self.name if self.name is not None else "\x03\x1f" + self.regexp + "\x03\x1f" if self.regexp is not None else "",
" (restricted to %:%s)" % ((",".join(self.servers) if self.server else "*") + (",".join(self.channels) if self.channels else "*")) if len(self.channels) or len(self.servers) else "",
": %s" % self.help if self.help is not None else ""
)
def check(self, msg):
return self.keywords.check(msg.kwargs) and super().check(msg)
def match(self, msg):
if not isinstance(msg, nemubot.message.command.Command):
return False
else:
return (
(self.name is None or msg.cmd == self.name) and
(self.regexp is None or re.match(self.regexp, msg.cmd)) and
Abstract.match(self, msg)
)

View file

@ -1,47 +0,0 @@
# Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2016 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from nemubot.exception.keyword import KeywordException
from nemubot.hooks.keywords.abstract import Abstract
class NoKeyword(Abstract):
def check(self, mkw):
if len(mkw):
raise KeywordException("This command doesn't take any keyword arguments.")
return super().check(mkw)
class AnyKeyword(Abstract):
def __init__(self, h):
"""Class that accepts any passed keywords
Arguments:
h -- Help string
"""
super().__init__()
self.h = h
def check(self, mkw):
return super().check(mkw)
def help(self):
return self.h

View file

@ -1,35 +0,0 @@
# Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2016 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
class Abstract:
def __init__(self):
pass
def check(self, mkw):
"""Check that all given message keywords are valid
Argument:
mkw -- dictionnary of keywords present in the message
"""
assert type(mkw) is dict, mkw
return True
def help(self):
return ""

View file

@ -1,59 +0,0 @@
# Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2016 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from nemubot.exception.keyword import KeywordException
from nemubot.hooks.keywords.abstract import Abstract
from nemubot.tools.human import guess
class Dict(Abstract):
def __init__(self, d):
super().__init__()
self.d = d
@property
def chk_noarg(self):
if not hasattr(self, "_cache_chk_noarg"):
self._cache_chk_noarg = [k for k in self.d if "=" not in k]
return self._cache_chk_noarg
@property
def chk_args(self):
if not hasattr(self, "_cache_chk_args"):
self._cache_chk_args = [k.split("=", 1)[0] for k in self.d if "=" in k]
return self._cache_chk_args
def check(self, mkw):
for k in mkw:
if ((k + "?") not in self.chk_args) and ((mkw[k] and k not in self.chk_args) or (not mkw[k] and k not in self.chk_noarg)):
if mkw[k] and k in self.chk_noarg:
raise KeywordException("Keyword %s doesn't take value." % k)
elif not mkw[k] and k in self.chk_args:
raise KeywordException("Keyword %s requires a value." % k)
else:
ch = [c for c in guess(k, self.d)]
raise KeywordException("Unknown keyword %s." % k + (" Did you mean: " + ", ".join(ch) + "?" if len(ch) else ""))
return super().check(mkw)
def help(self):
return ["\x03\x02@%s\x03\x02: %s" % (k, self.d[k]) for k in self.d]

View file

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
# Nemubot is a smart and modulable IM bot. # Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2015 Mercier Pierre-Olivier # Copyright (C) 2012-2015 Mercier Pierre-Olivier
# #
@ -14,47 +16,15 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
class HooksManager: class HooksManager:
"""Class to manage hooks""" """Class to manage hooks"""
def __init__(self, name="core"): def __init__(self):
"""Initialize the manager""" """Initialize the manager"""
self.hooks = dict() self.hooks = dict()
self.logger = logging.getLogger("nemubot.hooks.manager." + name)
def _access(self, *triggers):
"""Access to the given triggers chain"""
h = self.hooks
for t in triggers:
if t not in h:
h[t] = dict()
h = h[t]
if "__end__" not in h:
h["__end__"] = list()
return h
def _search(self, hook, *where, start=None):
"""Search all occurence of the given hook"""
if start is None:
start = self.hooks
for k in start:
if k == "__end__":
if hook in start[k]:
yield where
else:
yield from self._search(hook, *where + (k,), start=start[k])
def add_hook(self, hook, *triggers): def add_hook(self, hook, *triggers):
@ -65,19 +35,20 @@ class HooksManager:
triggers -- string that trigger the hook triggers -- string that trigger the hook
""" """
assert hook is not None, hook trigger = "_".join(triggers)
h = self._access(*triggers) if trigger not in self.hooks:
self.hooks[trigger] = list()
h["__end__"].append(hook) self.hooks[trigger].append(hook)
self.logger.debug("New hook successfully added in %s: %s",
"/".join(triggers), hook)
def del_hooks(self, *triggers, hook=None): def del_hook(self, hook=None, *triggers):
"""Remove the given hook from the manager """Remove the given hook from the manager
Return:
Boolean value reporting the deletion success
Argument: Argument:
triggers -- trigger string to remove triggers -- trigger string to remove
@ -85,20 +56,15 @@ class HooksManager:
hook -- a Hook instance to remove from the trigger string hook -- a Hook instance to remove from the trigger string
""" """
assert hook is not None or len(triggers) trigger = "_".join(triggers)
self.logger.debug("Trying to delete hook in %s: %s", if trigger in self.hooks:
"/".join(triggers), hook) if hook is None:
del self.hooks[trigger]
if hook is not None:
for h in self._search(hook, *triggers, start=self._access(*triggers)):
self._access(*h)["__end__"].remove(hook)
else:
if len(triggers):
del self._access(*triggers[:-1])[triggers[-1]]
else: else:
self.hooks = dict() self.hooks[trigger].remove(hook)
return True
return False
def get_hooks(self, *triggers): def get_hooks(self, *triggers):
@ -106,29 +72,35 @@ class HooksManager:
Argument: Argument:
triggers -- the trigger string triggers -- the trigger string
Keyword argument:
data -- Data to pass to the hook as argument
""" """
for n in range(len(triggers) + 1): trigger = "_".join(triggers)
i = self._access(*triggers[:n])
for h in i["__end__"]: res = list()
yield h
for key in self.hooks:
if trigger.find(key) == 0:
res += self.hooks[key]
return res
def get_reverse_hooks(self, *triggers, exclude_first=False): def exec_hook(self, *triggers, **data):
"""Returns list of triggered hooks that are bellow or at the same level """Trigger hooks that match the given trigger string
Argument: Argument:
triggers -- the trigger string trigger -- the trigger string
Keyword arguments: Keyword argument:
exclude_first -- start reporting hook at the next level data -- Data to pass to the hook as argument
""" """
h = self._access(*triggers) trigger = "_".join(triggers)
for k in h:
if k == "__end__": for key in self.hooks:
if not exclude_first: if trigger.find(key) == 0:
for hk in h[k]: for hook in self.hooks[key]:
yield hk hook.run(**data)
else:
yield from self.get_reverse_hooks(*triggers + (k,))

View file

@ -1,115 +0,0 @@
#!/usr/bin/env python3
import unittest
from nemubot.hooks.manager import HooksManager
class TestHookManager(unittest.TestCase):
def test_access(self):
hm = HooksManager()
h1 = "HOOK1"
h2 = "HOOK2"
h3 = "HOOK3"
hm.add_hook(h1)
hm.add_hook(h2, "pre")
hm.add_hook(h3, "pre", "Text")
hm.add_hook(h2, "post", "Text")
self.assertIn("__end__", hm._access())
self.assertIn("__end__", hm._access("pre"))
self.assertIn("__end__", hm._access("pre", "Text"))
self.assertIn("__end__", hm._access("post", "Text"))
self.assertFalse(hm._access("inexistant")["__end__"])
self.assertTrue(hm._access()["__end__"])
self.assertTrue(hm._access("pre")["__end__"])
self.assertTrue(hm._access("pre", "Text")["__end__"])
self.assertTrue(hm._access("post", "Text")["__end__"])
def test_search(self):
hm = HooksManager()
h1 = "HOOK1"
h2 = "HOOK2"
h3 = "HOOK3"
h4 = "HOOK4"
hm.add_hook(h1)
hm.add_hook(h2, "pre")
hm.add_hook(h3, "pre", "Text")
hm.add_hook(h2, "post", "Text")
self.assertTrue([h for h in hm._search(h1)])
self.assertFalse([h for h in hm._search(h4)])
self.assertEqual(2, len([h for h in hm._search(h2)]))
self.assertEqual([("pre", "Text")], [h for h in hm._search(h3)])
def test_delete(self):
hm = HooksManager()
h1 = "HOOK1"
h2 = "HOOK2"
h3 = "HOOK3"
h4 = "HOOK4"
hm.add_hook(h1)
hm.add_hook(h2, "pre")
hm.add_hook(h3, "pre", "Text")
hm.add_hook(h2, "post", "Text")
hm.del_hooks(hook=h4)
self.assertTrue(hm._access("pre")["__end__"])
self.assertTrue(hm._access("pre", "Text")["__end__"])
hm.del_hooks("pre")
self.assertFalse(hm._access("pre")["__end__"])
self.assertTrue(hm._access("post", "Text")["__end__"])
hm.del_hooks("post", "Text", hook=h2)
self.assertFalse(hm._access("post", "Text")["__end__"])
self.assertTrue(hm._access()["__end__"])
hm.del_hooks(hook=h1)
self.assertFalse(hm._access()["__end__"])
def test_get(self):
hm = HooksManager()
h1 = "HOOK1"
h2 = "HOOK2"
h3 = "HOOK3"
hm.add_hook(h1)
hm.add_hook(h2, "pre")
hm.add_hook(h3, "pre", "Text")
hm.add_hook(h2, "post", "Text")
self.assertEqual([h1, h2], [h for h in hm.get_hooks("pre")])
self.assertEqual([h1, h2, h3], [h for h in hm.get_hooks("pre", "Text")])
def test_get_rev(self):
hm = HooksManager()
h1 = "HOOK1"
h2 = "HOOK2"
h3 = "HOOK3"
hm.add_hook(h1)
hm.add_hook(h2, "pre")
hm.add_hook(h3, "pre", "Text")
hm.add_hook(h2, "post", "Text")
self.assertEqual([h2, h3], [h for h in hm.get_reverse_hooks("pre")])
self.assertEqual([h3], [h for h in hm.get_reverse_hooks("pre", exclude_first=True)])
if __name__ == '__main__':
unittest.main()

View file

@ -24,26 +24,54 @@ class Message(Abstract):
"""Class storing hook information, specialized for a generic Message""" """Class storing hook information, specialized for a generic Message"""
def __init__(self, call, regexp=None, help=None, **kwargs): def __init__(self, call, name=None, regexp=None, channels=list(),
super().__init__(call=call, **kwargs) server=None, help=None, help_usage=dict(), **kargs):
Abstract.__init__(self, call=call, **kargs)
assert regexp is None or type(regexp) is str, regexp assert regexp is None or type(regexp) is str, regexp
assert channels is None or type(channels) is list, channels
assert server is None or type(server) is str, server
assert type(help_usage) is dict, help_usage
self.name = str(name) if name is not None else None
self.regexp = regexp self.regexp = regexp
self.server = server
self.channels = channels
self.help = help self.help = help
self.help_usage = help_usage
def __str__(self): def __str__(self):
# TODO: find a way to name the feature (like command: help) return "\x03\x02%s\x03\x02%s%s" % (
return self.help if self.help is not None else super().__str__() self.name if self.name is not None else "\x03\x1f" + self.regexp + "\x03\x1f" if self.regexp is not None else "",
" (restricted to %s)" % (self.server + ":" if self.server is not None else "") + (self.channels if self.channels else "*") if len(self.channels) or self.server else "",
": %s" % self.help if self.help is not None else ""
)
def check(self, msg): def match(self, msg, server=None):
return super().check(msg) if not isinstance(msg, nemubot.message.abstract.Abstract):
return True
elif isinstance(msg, nemubot.message.Command):
def match(self, msg): return self.is_matching(msg.cmd, msg.to, server)
if not isinstance(msg, nemubot.message.text.Text): elif isinstance(msg, nemubot.message.Text):
return False return self.is_matching(msg.message, msg.to, server)
else: else:
return (self.regexp is None or re.match(self.regexp, msg.message)) and super().match(msg) return False
def is_matching(self, strcmp, receivers=list(), server=None):
"""Test if the current hook correspond to the message"""
if ((server is None or self.server is None or self.server == server)
and ((self.name is None or strcmp == self.name) and (
self.regexp is None or re.match(self.regexp, strcmp)))):
if receivers and self.channels:
for receiver in receivers:
if receiver in self.channels:
return True
else:
return True
return False

View file

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
# Nemubot is a smart and modulable IM bot. # Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2015 Mercier Pierre-Olivier # Copyright (C) 2012-2015 Mercier Pierre-Olivier
# #
@ -29,16 +31,16 @@ class ModuleFinder(Finder):
self.add_module = add_module self.add_module = add_module
def find_module(self, fullname, path=None): def find_module(self, fullname, path=None):
if path is not None and fullname.startswith("nemubot.module."): # Search only for new nemubot modules (packages init)
module_name = fullname.split(".", 2)[2] if path is None:
for mpath in self.modules_paths: for mpath in self.modules_paths:
if os.path.isfile(os.path.join(mpath, module_name + ".py")): if os.path.isfile(os.path.join(mpath, fullname + ".py")):
return ModuleLoader(self.add_module, fullname, return ModuleLoader(self.add_module, fullname,
os.path.join(mpath, module_name + ".py")) os.path.join(mpath, fullname + ".py"))
elif os.path.isfile(os.path.join(os.path.join(mpath, module_name), "__init__.py")): elif os.path.isfile(os.path.join(os.path.join(mpath, fullname), "__init__.py")):
return ModuleLoader(self.add_module, fullname, return ModuleLoader(self.add_module, fullname,
os.path.join( os.path.join(
os.path.join(mpath, module_name), os.path.join(mpath, fullname),
"__init__.py")) "__init__.py"))
return None return None
@ -53,17 +55,17 @@ class ModuleLoader(SourceFileLoader):
def _load(self, module, name): def _load(self, module, name):
# Add the module to the global modules list # Add the module to the global modules list
self.add_module(module) self.add_module(module)
logger.info("Module '%s' successfully imported from %s.", name.split(".", 2)[2], self.path) logger.info("Module '%s' successfully loaded.", name)
return module return module
# Python 3.4 # Python 3.4
def exec_module(self, module): def exec_module(self, module):
super().exec_module(module) super(ModuleLoader, self).exec_module(module)
self._load(module, module.__spec__.name) self._load(module, module.__spec__.name)
# Python 3.3 # Python 3.3
def load_module(self, fullname): def load_module(self, fullname):
module = super().load_module(fullname) module = super(ModuleLoader, self).load_module(fullname)
return self._load(module, module.__name__) return self._load(module, module.__name__)

View file

@ -19,3 +19,27 @@ from nemubot.message.text import Text
from nemubot.message.directask import DirectAsk from nemubot.message.directask import DirectAsk
from nemubot.message.command import Command from nemubot.message.command import Command
from nemubot.message.command import OwnerCommand from nemubot.message.command import OwnerCommand
def reload():
global Abstract, Text, DirectAsk, Command, OwnerCommand
import imp
import nemubot.message.abstract
imp.reload(nemubot.message.abstract)
Abstract = nemubot.message.abstract.Abstract
imp.reload(nemubot.message.text)
Text = nemubot.message.text.Text
imp.reload(nemubot.message.directask)
DirectAsk = nemubot.message.directask.DirectAsk
imp.reload(nemubot.message.command)
Command = nemubot.message.command.Command
OwnerCommand = nemubot.message.command.OwnerCommand
import nemubot.message.visitor
imp.reload(nemubot.message.visitor)
import nemubot.message.printer
imp.reload(nemubot.message.printer)
nemubot.message.printer.reload()

View file

@ -21,7 +21,7 @@ class Abstract:
"""This class represents an abstract message""" """This class represents an abstract message"""
def __init__(self, server=None, date=None, to=None, to_response=None, frm=None, frm_owner=False): def __init__(self, server=None, date=None, to=None, to_response=None, frm=None):
"""Initialize an abstract message """Initialize an abstract message
Arguments: Arguments:
@ -40,7 +40,7 @@ class Abstract:
else [ to_response ]) else [ to_response ])
self.frm = frm # None allowed when it designate this bot self.frm = frm # None allowed when it designate this bot
self.frm_owner = frm_owner self.frm_owner = False # Filled later, in consumer
@property @property
@ -51,6 +51,11 @@ class Abstract:
return self.to return self.to
@property
def receivers(self):
# TODO: this is for legacy modules
return self.to_response
@property @property
def channel(self): def channel(self):
# TODO: this is for legacy modules # TODO: this is for legacy modules
@ -59,6 +64,12 @@ class Abstract:
else: else:
return None return None
@property
def nick(self):
# TODO: this is for legacy modules
return self.frm
def accept(self, visitor): def accept(self, visitor):
visitor.visit(self) visitor.visit(self)
@ -72,8 +83,7 @@ class Abstract:
"date": self.date, "date": self.date,
"to": self.to, "to": self.to,
"to_response": self._to_response, "to_response": self._to_response,
"frm": self.frm, "frm": self.frm
"frm_owner": self.frm_owner,
} }
for w in without: for w in without:

View file

@ -22,7 +22,7 @@ class Command(Abstract):
"""This class represents a specialized TextMessage""" """This class represents a specialized TextMessage"""
def __init__(self, cmd, args=None, kwargs=None, *nargs, **kargs): def __init__(self, cmd, args=None, kwargs=None, *nargs, **kargs):
super().__init__(*nargs, **kargs) Abstract.__init__(self, *nargs, **kargs)
self.cmd = cmd self.cmd = cmd
self.args = args if args is not None else list() self.args = args if args is not None else list()
@ -31,6 +31,11 @@ class Command(Abstract):
def __str__(self): def __str__(self):
return self.cmd + " @" + ",@".join(self.args) return self.cmd + " @" + ",@".join(self.args)
@property
def cmds(self):
# TODO: this is for legacy modules
return [self.cmd] + self.args
class OwnerCommand(Command): class OwnerCommand(Command):

View file

@ -28,7 +28,7 @@ class DirectAsk(Text):
designated -- the user designated by the message designated -- the user designated by the message
""" """
super().__init__(*args, **kargs) Text.__init__(self, *args, **kargs)
self.designated = designated self.designated = designated

View file

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
# Nemubot is a smart and modulable IM bot. # Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2015 Mercier Pierre-Olivier # Copyright (C) 2012-2015 Mercier Pierre-Olivier
# #
@ -14,10 +16,12 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from nemubot.exception import IMException from nemubot.message import Text
from nemubot.message.printer.socket import Socket as SocketPrinter
class KeywordException(IMException): class IRC(SocketPrinter):
def __init__(self, message): def visit_Text(self, msg):
super(KeywordException, self).__init__(message) self.pp += "PRIVMSG %s :" % ",".join(msg.to)
SocketPrinter.visit_Text(self, msg)

View file

@ -1,67 +0,0 @@
# Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2026 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from nemubot.message.visitor import AbstractVisitor
class IRCLib(AbstractVisitor):
"""Visitor that sends bot responses via an irc.client.ServerConnection.
Unlike the socket-based IRC printer (which builds a raw PRIVMSG string),
this calls connection.privmsg() directly so the library handles encoding,
line-length capping, and any internal locking.
"""
def __init__(self, connection):
self._conn = connection
def _send(self, target, text):
try:
self._conn.privmsg(target, text)
except Exception:
pass # drop silently during reconnection
# Visitor methods
def visit_Text(self, msg):
if isinstance(msg.message, str):
for target in msg.to:
self._send(target, msg.message)
else:
msg.message.accept(self)
def visit_DirectAsk(self, msg):
text = msg.message if isinstance(msg.message, str) else str(msg.message)
# Mirrors socket.py logic:
# rooms that are NOT the designated nick get a "nick: " prefix
others = [to for to in msg.to if to != msg.designated]
if len(others) == 0 or len(others) != len(msg.to):
for target in msg.to:
self._send(target, text)
if others:
for target in others:
self._send(target, "%s: %s" % (msg.designated, text))
def visit_Command(self, msg):
parts = ["!" + msg.cmd] + list(msg.args)
for target in msg.to:
self._send(target, " ".join(parts))
def visit_OwnerCommand(self, msg):
parts = ["`" + msg.cmd] + list(msg.args)
for target in msg.to:
self._send(target, " ".join(parts))

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