Compare commits

..

30 commits

Author SHA1 Message Date
2ac37f79d9 New class storing scope 2017-07-18 00:50:30 +02:00
e3150a6061 Messages now implements Serializable 2017-07-18 00:50:30 +02:00
e0af09f3c5 New printer and parser for bot data, XML-based 2017-07-18 00:50:30 +02:00
2992c13ca7 socket: limit getaddrinfo to TCP connections 2017-07-18 00:48:30 +02:00
49c207a4c9 events: fix help when no event is defined 2017-07-18 00:48:30 +02:00
3c13043ca3 run: recreate the sync_queue on run, it seems to have strange behaviour when created before the fork 2017-07-18 00:48:30 +02:00
5b28828ede event: ensure that enough consumers are launched at the end of an event 2017-07-18 00:48:30 +02:00
2a25f5311a rename module nextstop: ratp to avoid import loop with the inderlying Python module 2017-07-18 00:48:30 +02:00
1db63600e5 main: new option -A to run as daemon 2017-07-18 00:48:30 +02:00
5f89428562 Use getaddrinfo to create the right socket 2017-07-18 00:48:30 +02:00
205a39ad70 Try to restaure frm_owner flag 2017-07-18 00:42:11 +02:00
c51b0a9170 When launched in daemon mode, attach to the socket 2017-07-18 00:42:11 +02:00
eb70fe560b Deamonize later 2017-07-18 00:42:11 +02:00
8b6f72587d Local client now detects when server close the connection 2017-07-18 00:42:11 +02:00
02838658b0 Fix communication over unix socket 2017-07-18 00:42:11 +02:00
1a813083e5 Handle multiple SIGTERM 2017-07-18 00:42:11 +02:00
3fbeb49a6c suivi: add fedex 2017-07-18 00:42:11 +02:00
f5f13202c5 suivi: use getURLContent instead of call to urllib 2017-07-18 00:42:11 +02:00
aeba947877 tools/web: fill a default Content-Type in case of POST 2017-07-18 00:42:11 +02:00
b27e01a196 tools/web: improve redirection reliability 2017-07-18 00:42:11 +02:00
4d68410777 tools/web: forward all arguments passed to getJSON and getXML to getURLContent 2017-07-18 00:42:11 +02:00
384fbc6717 Update weather module: refleting forcastAPI changes 2017-07-18 00:42:11 +02:00
63e65b2659 modulecontext: use inheritance instead of conditional init 2017-07-18 00:42:11 +02:00
bdf8a69ff0 Avoid stack-trace and DOS if event is not well formed 2017-07-18 00:42:11 +02:00
4f1dcb8524 [nextstop] Use as system wide module 2017-07-18 00:42:11 +02:00
6d2f90fe77 Allow module function to be generators 2017-07-18 00:42:10 +02:00
2e5834a89d Parse server urls using parse_qs 2017-07-18 00:42:10 +02:00
26d1f5b6e8 Format and typo 2017-07-18 00:42:10 +02:00
40fc84fcec Implement socket server subparse 2017-07-18 00:42:10 +02:00
449cb684f9 Refactor file/socket management (use poll instead of select) 2017-07-18 00:42:08 +02:00
106 changed files with 2196 additions and 3257 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

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

@ -9,8 +9,10 @@ Requirements
*nemubot* requires at least Python 3.3 to work. *nemubot* requires at least Python 3.3 to work.
Connecting to SSL server requires [this patch](http://bugs.python.org/issue27629).
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.

View file

@ -12,7 +12,7 @@ from nemubot.message import Command
from nemubot.tools.human import guess 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 #############################################################
@ -185,7 +185,7 @@ def cmd_listvars(msg):
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 IMException("!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)
@ -222,13 +222,13 @@ def cmd_alias(msg):
if alias.cmd in context.data.getNode("aliases").index: if alias.cmd in context.data.getNode("aliases").index:
return Response("%s corresponds to %s" % (alias.cmd, context.data.getNode("aliases").index[alias.cmd]["origin"]), return Response("%s corresponds to %s" % (alias.cmd, context.data.getNode("aliases").index[alias.cmd]["origin"]),
channel=msg.channel, nick=msg.frm) channel=msg.channel, nick=msg.nick)
elif len(msg.args) > 1: elif len(msg.args) > 1:
create_alias(alias.cmd, create_alias(alias.cmd,
" ".join(msg.args[1:]), " ".join(msg.args[1:]),
channel=msg.channel, channel=msg.channel,
creator=msg.frm) creator=msg.nick)
return Response("New alias %s successfully registered." % alias.cmd, return Response("New alias %s successfully registered." % alias.cmd,
channel=msg.channel) channel=msg.channel)
@ -272,6 +272,7 @@ def treat_alias(msg):
# Avoid infinite recursion # Avoid infinite recursion
if not isinstance(rpl_msg, Command) or msg.cmd != rpl_msg.cmd: if not isinstance(rpl_msg, Command) or msg.cmd != rpl_msg.cmd:
return rpl_msg # Also return origin message, if it can be treated as well
return [msg, rpl_msg]
return msg return msg

View file

@ -13,7 +13,7 @@ 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()
@ -77,7 +77,7 @@ 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.command("age",
@ -98,7 +98,7 @@ 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
@ -106,18 +106,18 @@ def cmd_age(msg):
@hook.ask() @hook.ask()
def parseask(msg): def parseask(msg):
res = re.match(r"^(\S+)\s*('s|suis|est|is|was|were)?\s+(birthday|geburtstag|née? |nee? le|born on).*$", msg.message, re.I) 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 IMException("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 #############################################################

View file

@ -9,7 +9,7 @@ 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 from more import Response
# LOADING ############################################################# # LOADING #############################################################

View file

@ -7,7 +7,7 @@ from nemubot.exception import IMException
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.message import Command, DirectAsk, Text from nemubot.message import Command, DirectAsk, Text
from nemubot.module.more import Response from more import Response
# MODULE CORE ######################################################### # MODULE CORE #########################################################

View file

@ -11,7 +11,7 @@ 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)

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

@ -5,67 +5,29 @@
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, striphtml
from nemubot.module.more import Response from more import Response
BASEURL_NIST = 'https://nvd.nist.gov/vuln/detail/' BASEURL_NIST = 'https://web.nvd.nist.gov/view/vuln/detail?vulnId='
# MODULE CORE ######################################################### # 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_NIST + quote(cve_id.upper())
soup = BeautifulSoup(getURLContent(search_url)) soup = BeautifulSoup(getURLContent(search_url))
vuln = soup.body.find(class_="vuln-detail")
cvss = vuln.findAll('div')[4]
vuln = {} return [
"Base score: " + cvss.findAll('div')[0].findAll('a')[0].text.strip(),
for vd in VULN_DATAS: vuln.findAll('p')[0].text, # description
r = soup.body.find(attrs={"data-testid": VULN_DATAS[vd]}) striphtml(vuln.findAll('div')[0].text).strip(), # publication date
if r: striphtml(vuln.findAll('div')[1].text).strip(), # last revised
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 #################################################### # MODULE INTERFACE ####################################################
@ -80,20 +42,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

@ -8,7 +8,7 @@ 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 from more import Response
# MODULE CORE ######################################################### # MODULE CORE #########################################################

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,9 +1,7 @@
"""Create countdowns and reminders""" """Create countdowns and reminders"""
import calendar
from datetime import datetime, timedelta, timezone
from functools import partial
import re import re
from datetime import datetime, timedelta, timezone
from nemubot import context from nemubot import context
from nemubot.exception import IMException from nemubot.exception import IMException
@ -12,84 +10,31 @@ from nemubot.hooks import hook
from nemubot.message import Command 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 from more import Response
class Event:
def __init__(self, server, channel, creator, start_time, end_time=None):
self._server = server
self._channel = channel
self._creator = creator
self._start = datetime.utcfromtimestamp(float(start_time)).replace(tzinfo=timezone.utc) if not isinstance(start_time, datetime) else start_time
self._end = datetime.utcfromtimestamp(float(end_time)).replace(tzinfo=timezone.utc) if end_time else None
self._evt = None
def __del__(self):
if self._evt is not None:
context.del_event(self._evt)
self._evt = None
def saveElement(self, store, tag="event"):
attrs = {
"server": str(self._server),
"channel": str(self._channel),
"creator": str(self._creator),
"start_time": str(calendar.timegm(self._start.timetuple())),
}
if self._end:
attrs["end_time"] = str(calendar.timegm(self._end.timetuple()))
store.startElement(tag, attrs)
store.endElement(tag)
@property
def creator(self):
return self._creator
@property
def start(self):
return self._start
@property
def end(self):
return self._end
@end.setter
def end(self, c):
self._end = c
@end.deleter
def end(self):
self._end = None
def help_full (): 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() if hasattr(context, "datas") else [])) + "\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()
@ -118,10 +63,18 @@ 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 IMException("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 IMException("%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,51 +92,50 @@ 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:
context.data.delChild(strnd)
raise IMException("Mauvais format de date pour l'événement %s. Il n'a pas été créé." % msg.args[0]) raise IMException("Mauvais format de date pour l'événement %s. Il n'a pas été créé." % msg.args[0])
elif result1 is not None and len(result1) > 0: 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.command("end") @hook.command("end")
@ -192,66 +144,67 @@ def end_countdown(msg):
if len(msg.args) < 1: if len(msg.args) < 1:
raise IMException("quel événement terminer ?") raise IMException("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 IMException("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.command("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.command(match=lambda msg: isinstance(msg, Command) and msg.cmd in context.data) @hook.command(match=lambda msg: isinstance(msg, Command) and msg.cmd in context.data.index)
def parseanswer(msg): def parseanswer(msg):
res = Response(channel=msg.channel) 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: else:
res.append_message("%s commencé il y a %s." % (msg.cmd, countdown(msg.date - context.data[msg.cmd].start))) 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(countdown_format(context.data[msg.cmd].start, context.data[msg.cmd]["msg_before"], context.data[msg.cmd]["msg_after"])) 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"]))
return res 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(match=lambda msg: RGXP_ask.match(msg.text))
def parseask(msg): def parseask(msg):
name = re.match("^.*!([^ \"'@!]+).*$", msg.message) name = re.match("^.*!([^ \"'@!]+).*$", msg.text)
if name is None: if name is None:
raise IMException("il faut que tu attribues une commande à l'événement.") raise IMException("il faut que tu attribues une commande à l'événement.")
if name.group(1) in context.data: if name.group(1) in context.data.index:
raise IMException("un événement portant ce nom existe déjà.") raise IMException("un événement portant ce nom existe déjà.")
texts = re.match("^[^\"]*(avant|après|apres|before|after)?[^\"]*\"([^\"]+)\"[^\"]*((avant|après|apres|before|after)?.*\"([^\"]+)\".*)?$", msg.message, re.I) 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 IMException("la date de l'événement est invalide !")
@ -270,7 +223,7 @@ def parseask(msg):
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
@ -284,7 +237,7 @@ def parseask(msg):
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)

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

@ -9,7 +9,7 @@ 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 from more import Response
# MODULE CORE ######################################################### # MODULE CORE #########################################################

View file

@ -9,7 +9,7 @@ from nemubot.exception import IMException
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.message import Command, Text from nemubot.message import Command, Text
from nemubot.module.more import Response from more import Response
# MODULE CORE ######################################################### # MODULE CORE #########################################################
@ -73,7 +73,7 @@ def cmd_grep(msg):
only = "only" in msg.kwargs 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] + ").*?", l = [m for m in grep(msg.args[0] if msg.args[0][0] == "^" else ".*?(" + msg.args[0] + ").*?",
" ".join(msg.args[1:]), " ".join(msg.args[1:]),
msg, msg,
icase="nocase" in msg.kwargs, icase="nocase" in msg.kwargs,

View file

@ -5,56 +5,63 @@
import re import re
import urllib.parse import urllib.parse
from bs4 import BeautifulSoup
from nemubot.exception import IMException 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 from more import Response
# MODULE CORE ######################################################### # MODULE CORE #########################################################
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 IMException(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 IMException("An error occurs during movie search")
def find_movies(title):
"""Find existing movies matching a approximate title"""
# Built URL
url = "http://www.omdbapi.com/?s=%s" % urllib.parse.quote(title)
# Make the request
data = web.getJSON(url)
# Return data
if "Error" in data:
raise IMException(data["Error"])
elif "Search" in data:
return data
else:
raise IMException("An error occurs during movie search")
# MODULE INTERFACE #################################################### # MODULE INTERFACE ####################################################
@ -72,28 +79,23 @@ def cmd_imdb(msg):
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
@ -109,7 +111,7 @@ def cmd_search(msg):
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,7 @@
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.exception import IMException from nemubot.exception import IMException
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

View file

@ -8,7 +8,7 @@ import os
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.module.more import Response from more import Response
# GLOBALS ############################################################# # GLOBALS #############################################################

View file

@ -9,11 +9,11 @@ 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 from more import Response
# GLOBALS ############################################################# # GLOBALS #############################################################
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 ############################################################# # LOADING #############################################################
@ -23,7 +23,7 @@ def load(context):
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("%", "%%")
@ -55,7 +55,7 @@ def cmd_geocode(msg):
if not len(msg.args): if not len(msg.args):
raise IMException("indicate a name") raise IMException("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

@ -11,7 +11,7 @@ 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 ##################################################

View file

@ -181,16 +181,13 @@ 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: if isinstance(self.messages[0], list):
if isinstance(self.messages[0], list): for x in self.messages[0]:
for x in self.messages[0]: print(x, self.line_treat(x))
print(x, self.line_treat(x)) self.messages[0] = [self.line_treat(x).replace("\n", " ").strip() for x in self.messages[0]]
self.messages[0] = [self.line_treat(x).replace("\n", " ").strip() for x in self.messages[0]] else:
else: self.messages[0] = (self.line_treat(self.messages[0])
self.messages[0] = (self.line_treat(self.messages[0]) .replace("\n", " ").strip())
.replace("\n", " ").strip())
except Exception as e:
logger.exception(e)
msg = "" msg = ""
if self.title is not None: if self.title is not None:

View file

@ -8,7 +8,7 @@ import re
from nemubot.exception import IMException from nemubot.exception import IMException
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

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

@ -17,7 +17,7 @@ def validator(url):
raise IMException("Indicate a valid URL!") raise IMException("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 IMException("HTTP error occurs: %s %s" % (e.code, e.reason))

View file

@ -1,6 +1,5 @@
"""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
@ -13,7 +12,7 @@ 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
@ -210,14 +209,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 IMException:
logger.exception("Unable to watch %s", site["url"]) logger.exception("Unable to watch %s", site["url"])

View file

@ -6,10 +6,10 @@ import urllib
from nemubot.exception import IMException from nemubot.exception import IMException
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_AVAIL = "https://www.whoisxmlapi.com/whoisserver/WhoisService?cmd=GET_DN_AVAILABILITY&domainName=%%s&outputFormat=json&username=%s&password=%s"
URL_WHOIS = "https://www.whoisxmlapi.com/whoisserver/WhoisService?da=2&domainName=%%s&outputFormat=json&userName=%s&password=%s" URL_WHOIS = "http://www.whoisxmlapi.com/whoisserver/WhoisService?da=2&domainName=%%s&outputFormat=json&userName=%s&password=%s"
# LOADING ############################################################# # LOADING #############################################################
@ -22,7 +22,7 @@ def load(CONF, add_hook):
"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_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"]))

View file

@ -12,8 +12,7 @@ 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 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
@ -51,11 +50,10 @@ def cmd_news(msg):
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

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

@ -4,7 +4,7 @@
from nemubot.exception import IMException from nemubot.exception import IMException
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.module.more import Response from more import Response
from nextstop import ratp from nextstop import ratp

View file

@ -10,7 +10,7 @@ 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():
@ -40,7 +40,7 @@ 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:
@ -64,22 +64,15 @@ 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.message()
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.post()

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

@ -9,7 +9,7 @@ from nemubot import context
from nemubot.exception import IMException from nemubot.exception import IMException
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.module.more import Response from more import Response
# MODULE INTERFACE #################################################### # MODULE INTERFACE ####################################################
@ -21,7 +21,7 @@ def cmd_choice(msg):
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.command("choicecmd")

View file

@ -12,7 +12,7 @@ from nemubot.tools import web
nemubotversion = 4.0 nemubotversion = 4.0
from nemubot.module.more import Response from more import Response
def help_full(): def help_full():
@ -25,7 +25,7 @@ def cmd_tcode(msg):
raise IMException("indicate a transaction code or " raise IMException("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():

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

@ -16,7 +16,7 @@ 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
"""
for u in dests:
if u not in context.data.index:
raise IMException("Désolé, je sais pas comment envoyer de SMS à %s." % u)
elif cur_epoch - float(context.data.index[u]["lastuse"]) < 42:
raise IMException("Un peu de calme, %s a déjà reçu un SMS il n'y a pas si longtemps." % u)
return True
def send_sms_to_list(msg, frm, dests, content, cur_epoch):
fails = list()
for u in dests:
context.data.index[u]["lastuse"] = cur_epoch
test = send_sms(frm, context.data.index[u]["user"], context.data.index[u]["key"], content)
if test is not None:
fails.append( "%s: %s" % (u, test) )
if len(fails) > 0:
return Response("quelque chose ne s'est pas bien passé durant l'envoi du SMS : " + ", ".join(fails), msg.channel, msg.frm)
else:
return Response("le SMS a bien été envoyé", msg.channel, msg.frm)
@hook.command("sms") @hook.command("sms")
def cmd_sms(msg): def cmd_sms(msg):
if not len(msg.args): if not len(msg.args):
raise IMException("À qui veux-tu envoyer ce SMS ?") raise IMException("À qui veux-tu envoyer ce SMS ?")
cur_epoch = time.mktime(time.localtime()) # Check dests
dests = msg.args[0].split(",") cur_epoch = time.mktime(time.localtime());
frm = msg.frm if msg.to_response[0] == msg.frm else msg.frm + "@" + msg.to[0] for u in msg.args[0].split(","):
content = " ".join(msg.args[1:]) if u not in context.data.index:
raise IMException("Désolé, je sais pas comment envoyer de SMS à %s." % u)
elif cur_epoch - float(context.data.index[u]["lastuse"]) < 42:
raise IMException("Un peu de calme, %s a déjà reçu un SMS il n'y a pas si longtemps." % u)
check_sms_dests(dests, cur_epoch) # Go!
return send_sms_to_list(msg, frm, dests, content, cur_epoch) fails = list()
for u in msg.args[0].split(","):
context.data.index[u]["lastuse"] = cur_epoch
@hook.command("smscmd") if msg.to_response[0] == msg.frm:
def cmd_smscmd(msg): frm = msg.frm
if not len(msg.args): else:
raise IMException("À qui veux-tu envoyer ce SMS ?") frm = msg.frm + "@" + msg.to[0]
test = send_sms(frm, context.data.index[u]["user"], context.data.index[u]["key"], " ".join(msg.args[1:]))
cur_epoch = time.mktime(time.localtime()) if test is not None:
dests = msg.args[0].split(",") fails.append( "%s: %s" % (u, test) )
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)
if len(fails) > 0:
return Response("quelque chose ne s'est pas bien passé durant l'envoi du SMS : " + ", ".join(fails), msg.channel, msg.nick)
else:
return Response("le SMS a bien été envoyé", msg.channel, msg.nick)
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()
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

@ -10,7 +10,7 @@ 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 from more import Response
# LOADING ############################################################# # LOADING #############################################################
@ -64,15 +64,15 @@ def cmd_spell(msg):
raise IMException("Je n'ai pas le dictionnaire `%s' :(" % lang) raise IMException("Je n'ai pas le dictionnaire `%s' :(" % lang)
if r == True: if r == True:
add_score(msg.frm, "correct") add_score(msg.nick, "correct")
res.append_message("l'orthographe de `%s' est correcte" % word) res.append_message("l'orthographe de `%s' est correcte" % word)
elif len(r) > 0: elif len(r) > 0:
add_score(msg.frm, "bad") add_score(msg.nick, "bad")
res.append_message(r, title="suggestions pour `%s'" % word) res.append_message(r, title="suggestions pour `%s'" % word)
else: else:
add_score(msg.frm, "bad") add_score(msg.nick, "bad")
res.append_message("aucune suggestion pour `%s'" % word) res.append_message("aucune suggestion pour `%s'" % word)
return res return res

View file

@ -9,15 +9,15 @@ import re
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.exception import IMException from nemubot.exception import IMException
from nemubot.tools.web import getURLContent, getURLHeaders, getJSON from nemubot.tools.web import getURLContent, getJSON
from nemubot.module.more import Response from more import Response
# POSTAGE SERVICE PARSERS ############################################ # POSTAGE SERVICE PARSERS ############################################
def get_tnt_info(track_id): def get_tnt_info(track_id):
values = [] values = []
data = getURLContent('https://www.tnt.fr/public/suivi_colis/recherche/visubontransport.do?bonTransport=%s' % track_id) data = getURLContent('www.tnt.fr/public/suivi_colis/recherche/visubontransport.do?bonTransport=%s' % track_id)
soup = BeautifulSoup(data) soup = BeautifulSoup(data)
status_list = soup.find('div', class_='result__content') status_list = soup.find('div', class_='result__content')
if not status_list: if not status_list:
@ -31,22 +31,21 @@ def get_tnt_info(track_id):
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 = re.sub(r'[\n\t\r]', '',
date = td[0].get_text() dataArray.tbody.tr.find(headers="Libelle").get_text())
libelle = re.sub(r'[\n\t\r]', '', td[1].get_text()) site = dataArray.tbody.tr.find(headers="site").get_text().strip()
site = td[2].get_text().strip() return (date, libelle, site.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 = getURLContent(track_baseurl, data.encode('utf-8'))
soup = BeautifulSoup(track_data) soup = BeautifulSoup(track_data)
@ -75,29 +74,33 @@ def get_colisprive_info(track_id):
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 = getURLContent(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()
field = field.find_next('td')
poste_status = field.get_text()
return (poste_type.lower(), poste_id.strip(), poste_status.lower(),
poste_location, poste_date)
def get_postnl_info(postnl_id): def get_postnl_info(postnl_id):
@ -123,24 +126,6 @@ def get_postnl_info(postnl_id):
return (post_status.lower(), post_destination, post_date) 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"): def get_fedex_info(fedex_id, lang="en_US"):
data = urllib.parse.urlencode({ data = urllib.parse.urlencode({
'data': json.dumps({ 'data': json.dumps({
@ -172,22 +157,11 @@ def get_fedex_info(fedex_id, lang="en_US"):
if ("TrackPackagesResponse" in fedex_data and if ("TrackPackagesResponse" in fedex_data and
"packageList" in fedex_data["TrackPackagesResponse"] and "packageList" in fedex_data["TrackPackagesResponse"] and
len(fedex_data["TrackPackagesResponse"]["packageList"]) and len(fedex_data["TrackPackagesResponse"]["packageList"]) and
(not fedex_data["TrackPackagesResponse"]["errorList"][0]["code"] or not fedex_data["TrackPackagesResponse"]["packageList"][0]["isInvalid"]
fedex_data["TrackPackagesResponse"]["errorList"][0]["code"] == '0') and
not fedex_data["TrackPackagesResponse"]["packageList"][0]["errorList"][0]["code"]
): ):
return fedex_data["TrackPackagesResponse"]["packageList"][0] 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 ################################################### # TRACKING HANDLERS ###################################################
def handle_tnt(tracknum): def handle_tnt(tracknum):
@ -206,10 +180,11 @@ def handle_tnt(tracknum):
def handle_laposte(tracknum): def handle_laposte(tracknum):
info = get_laposte_info(tracknum) info = get_laposte_info(tracknum)
if info: if info:
poste_type, poste_id, poste_status, poste_date = info poste_type, poste_id, poste_status, poste_location, poste_date = info
return ("\x02%s\x0F : \x02%s\x0F est actuellement " return ("Le courrier de type \x02%s\x0F : \x02%s\x0F est actuellement "
"\x02%s\x0F (Mis à jour le \x02%s\x0F" "\x02%s\x0F dans la zone \x02%s\x0F (Mis à jour le \x02%s\x0F"
")." % (poste_type, poste_id, poste_status, poste_date)) ")." % (poste_type, poste_id, poste_status,
poste_location, poste_date))
def handle_postnl(tracknum): def handle_postnl(tracknum):
@ -221,20 +196,6 @@ def handle_postnl(tracknum):
")." % (tracknum, post_status, post_destination, post_date)) ")." % (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): def handle_colissimo(tracknum):
info = get_colissimo_info(tracknum) info = get_colissimo_info(tracknum)
if info: if info:
@ -269,12 +230,6 @@ def handle_fedex(tracknum):
return ("{trackingCarrierDesc}: \x02{statusWithDetails}\x0F: in \x02{statusLocationCity}, {statusLocationCntryCD}\x0F, estimated delivery: {displayEstDeliveryDateTime}.".format(**info)) 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 = { TRACKING_HANDLERS = {
'laposte': handle_laposte, 'laposte': handle_laposte,
'postnl': handle_postnl, 'postnl': handle_postnl,
@ -283,9 +238,6 @@ TRACKING_HANDLERS = {
'coliprive': handle_coliprive, 'coliprive': handle_coliprive,
'tnt': handle_tnt, 'tnt': handle_tnt,
'fedex': handle_fedex, 'fedex': handle_fedex,
'dhl': handle_dhl,
'usps': handle_usps,
'ups': handle_ups,
} }

View file

@ -9,7 +9,7 @@ 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 from more import Response
# LOADING ############################################################# # LOADING #############################################################
@ -29,7 +29,7 @@ def load(context):
# MODULE CORE ######################################################### # 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)
page = web.getURLContent(url) page = web.getURLContent(url)
best = list(); synos = list(); anton = list() best = list(); synos = list(); anton = list()
@ -53,7 +53,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()

View file

@ -8,7 +8,7 @@ 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

View file

@ -8,7 +8,7 @@ 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 from more import Response
# GLOBALS ############################################################# # GLOBALS #############################################################

View file

@ -8,13 +8,13 @@ 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 from more import Response
# MODULE CORE ######################################################### # MODULE CORE #########################################################
def search(terms): def search(terms):
return web.getJSON( return web.getJSON(
"https://api.urbandictionary.com/v0/define?term=%s" "http://api.urbandictionary.com/v0/define?term=%s"
% quote(' '.join(terms))) % quote(' '.join(terms)))

View file

@ -21,7 +21,7 @@ def default_reducer(url, data):
def ycc_reducer(url, data): def ycc_reducer(url, data):
return "https://ycc.fr/%s" % default_reducer(url, data) return "http://ycc.fr/%s" % default_reducer(url, data)
def lstu_reducer(url, data): def lstu_reducer(url, data):
json_data = json.loads(web.getURLContent(url, "lsturl=" + quote(data), json_data = json.loads(web.getURLContent(url, "lsturl=" + quote(data),
@ -36,8 +36,8 @@ def lstu_reducer(url, data):
# MODULE VARIABLES #################################################### # MODULE VARIABLES ####################################################
PROVIDERS = { PROVIDERS = {
"tinyurl": (default_reducer, "https://tinyurl.com/api-create.php?url="), "tinyurl": (default_reducer, "http://tinyurl.com/api-create.php?url="),
"ycc": (ycc_reducer, "https://ycc.fr/redirection/create/"), "ycc": (ycc_reducer, "http://ycc.fr/redirection/create/"),
"framalink": (lstu_reducer, "https://frama.link/a?format=json"), "framalink": (lstu_reducer, "https://frama.link/a?format=json"),
"huitre": (lstu_reducer, "https://huit.re/a?format=json"), "huitre": (lstu_reducer, "https://huit.re/a?format=json"),
"lstu": (lstu_reducer, "https://lstu.fr/a?format=json"), "lstu": (lstu_reducer, "https://lstu.fr/a?format=json"),
@ -60,20 +60,12 @@ def load(context):
# MODULE CORE ######################################################### # MODULE CORE #########################################################
def reduce_inline(txt, provider=None): def reduce(url, provider=DEFAULT_PROVIDER):
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 """Ask the url shortner website to reduce given URL
Argument: Argument:
url -- the URL to reduce url -- the URL to reduce
""" """
if provider is None:
provider = DEFAULT_PROVIDER
return PROVIDERS[provider][0](PROVIDERS[provider][1], url) return PROVIDERS[provider][0](PROVIDERS[provider][1], url)
@ -92,22 +84,8 @@ LAST_URLS = dict()
@hook.message() @hook.message()
def parselisten(msg): def parselisten(msg):
global LAST_URLS parseresponse(msg)
if hasattr(msg, "message") and isinstance(msg.message, str): return None
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() @hook.post()

View file

@ -9,7 +9,7 @@ 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 from more import Response
# LOADING ############################################################# # LOADING #############################################################

View file

@ -10,12 +10,12 @@ 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 from more import Response
from nemubot.module import mapquest import mapquest
# GLOBALS ############################################################# # GLOBALS #############################################################
URL_API = "https://public-api.adsbexchange.com/VirtualRadar/AircraftList.json?fcallC=%s" URL_API = "http://public-api.adsbexchange.com/VirtualRadar/AircraftList.json?fcallC=%s"
SPEED_TYPES = { SPEED_TYPES = {
0: 'Ground speed', 0: 'Ground speed',
@ -80,7 +80,7 @@ def cmd_flight(msg):
if not len(msg.args): if not len(msg.args):
raise IMException("please indicate a flight") raise IMException("please indicate a flight")
res = Response(channel=msg.channel, nick=msg.frm, res = Response(channel=msg.channel, nick=msg.nick,
nomore="No more flights", count=" (%s more flights)") nomore="No more flights", count=" (%s more flights)")
for param in msg.args: for param in msg.args:

View file

@ -11,71 +11,56 @@ 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.darksky.net/forecast/%s/%%s,%%s?lang=%%s&units=%%s"
UNITS = {
"ca": {
"temperature": "°C",
"distance": "km",
"precipIntensity": "mm/h",
"precip": "cm",
"speed": "km/h",
"pressure": "hPa",
},
"uk2": {
"temperature": "°C",
"distance": "mi",
"precipIntensity": "mm/h",
"precip": "cm",
"speed": "mi/h",
"pressure": "hPa",
},
"us": {
"temperature": "°F",
"distance": "mi",
"precipIntensity": "in/h",
"precip": "in",
"speed": "mi/h",
"pressure": "mbar",
},
"si": {
"temperature": "°C",
"distance": "km",
"precipIntensity": "mm/h",
"precip": "cm",
"speed": "m/s",
"pressure": "hPa",
},
}
def load(context): def load(context):
if not context.config or "darkskyapikey" not in context.config: if not context.config or "darkskyapikey" not in context.config:
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 format_wth(wth):
units = UNITS[flags["units"] if "units" in flags and flags["units"] in UNITS else "si"] return ("%s °C %s; precipitation (%s %% chance) intensity: %s mm/h; relative humidity: %s %%; wind speed: %s m/s %s°; cloud coverage: %s %%; pressure: %s hPa; visibility: %s km; ozone: %s DU" %
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) wth["temperature"],
) wth["summary"],
int(wth["precipProbability"] * 100),
wth["precipIntensity"],
int(wth["humidity"] * 100),
wth["windSpeed"],
wth["windBearing"],
int(wth["cloudCover"] * 100),
int(wth["pressure"]),
int(wth["visibility"]),
int(wth["ozone"])
))
def format_forecast_daily(wth, flags): def format_forecast_daily(wth):
units = UNITS[flags["units"] if "units" in flags and flags["units"] in UNITS else "si"] 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" %
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)) wth["summary"],
wth["temperatureMin"], 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"):
@ -126,7 +111,7 @@ def treat_coord(msg):
raise IMException("indique-moi un nom de ville ou des coordonnées.") raise IMException("indique-moi un nom de ville ou des coordonnées.")
def get_json_weather(coords, lang="en", units="ca"): def get_json_weather(coords, lang="en", units="auto"):
wth = web.getJSON(URL_DSAPI % (float(coords[0]), float(coords[1]), lang, units)) wth = web.getJSON(URL_DSAPI % (float(coords[0]), float(coords[1]), lang, units))
# First read flags # First read flags
@ -152,13 +137,13 @@ def cmd_coordinates(msg):
@hook.command("alert", @hook.command("alert",
keywords={ keywords={
"lang=LANG": "change the output language of weather sumarry; default: en", "lang=LANG": "change the output language of weather sumarry; default: en",
"units=UNITS": "return weather conditions in the requested units; default: ca", "units=UNITS": "return weather conditions in the requested units; default: auto",
}) })
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", lang=msg.kwargs["lang"] if "lang" in msg.kwargs else "en",
units=msg.kwargs["units"] if "units" in msg.kwargs else "ca") units=msg.kwargs["units"] if "units" in msg.kwargs else "auto")
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)")
@ -179,13 +164,13 @@ def cmd_alert(msg):
}, },
keywords={ keywords={
"lang=LANG": "change the output language of weather sumarry; default: en", "lang=LANG": "change the output language of weather sumarry; default: en",
"units=UNITS": "return weather conditions in the requested units; default: ca", "units=UNITS": "return weather conditions in the requested units; default: auto",
}) })
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", lang=msg.kwargs["lang"] if "lang" in msg.kwargs else "en",
units=msg.kwargs["units"] if "units" in msg.kwargs else "ca") units=msg.kwargs["units"] if "units" in msg.kwargs else "auto")
res = Response(channel=msg.channel, nomore="No more weather information") res = Response(channel=msg.channel, nomore="No more weather information")
@ -207,17 +192,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 +212,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
@ -241,7 +226,7 @@ gps_ask = re.compile(r"^\s*(?P<city>.*\w)\s*(?:(?:se|est)\s+(?:trouve|situ[ée]*
@hook.ask() @hook.ask()
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 +243,8 @@ 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)
if __name__ == "__main__":
sys.exit(main())

View file

@ -1,6 +1,5 @@
# coding=utf-8 # coding=utf-8
import json
import re import re
from nemubot import context from nemubot import context
@ -10,70 +9,47 @@ 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 "passwd" not in context.config:
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(nemubot.hooks.Command(cmd_whois, "whois", keywords={"lookup": "Perform a lookup of the begining of the login instead of an exact search."}),
"in","Command") "in","Command")
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://photos.cri.epita.net/%s", "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,25 +60,17 @@ class Login:
return None return None
def login_lookup(login, search=False): def found_login(login, search=False):
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:
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 "") 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_: yield Login(l.strip())
yield Login(l.strip())
def cmd_whois(msg): def cmd_whois(msg):
if len(msg.args) < 1: if len(msg.args) < 1:
@ -119,7 +87,7 @@ def cmd_whois(msg):
res = Response(channel=msg.channel, count=" (%d more logins)", line_treat=format_response) 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 found = False
for l in login_lookup(srch, "lookup" in msg.kwargs): for l in found_login(srch, "lookup" in msg.kwargs):
found = True found = True
res.append_message((srch, l)) res.append_message((srch, l))
if not found: if not found:
@ -130,7 +98,7 @@ def cmd_whois(msg):
def cmd_nicks(msg): def cmd_nicks(msg):
if len(msg.args) < 1: if len(msg.args) < 1:
raise IMException("Provide a login") raise IMException("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:
@ -147,12 +115,12 @@ def cmd_nicks(msg):
@hook.ask() @hook.ask()
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 +132,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

@ -10,12 +10,12 @@ 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 from more import Response
# LOADING ############################################################# # LOADING #############################################################
URL_API = "https://api.wolframalpha.com/v2/query?input=%%s&format=plaintext&appid=%s" URL_API = "http://api.wolframalpha.com/v2/query?input=%%s&format=plaintext&appid=%s"
def load(context): def load(context):
global URL_API global URL_API
@ -24,7 +24,7 @@ def load(context):
"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("%", "%%")

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 IMException 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,7 +32,7 @@ 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()
@ -66,10 +65,10 @@ def cmd_watch(msg):
context.save() context.save()
raise IMException("This channel will not anymore receives world cup events.") raise IMException("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).total_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,7 +163,7 @@ 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):
@ -192,7 +192,7 @@ 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 IMException("unrecognized request; choose between 'today', 'tomorrow', a FIFA country code or a match identifier")

View file

@ -4,7 +4,7 @@ import re, json, subprocess
from nemubot.exception import IMException from nemubot.exception import IMException
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"""

View file

@ -39,14 +39,10 @@ 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
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)) 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)
@ -110,13 +106,28 @@ def attach(pidfile, socketfile):
return 0 return 0
def daemonize(socketfile=None): def daemonize(socketfile=None, autoattach=True):
"""Detach the running process to run as a daemon """Detach the running process to run as a daemon
""" """
import os import os
import sys import sys
if socketfile is not None:
try:
pid = os.fork()
if pid > 0:
if autoattach:
import time
os.waitpid(pid, 0)
time.sleep(1)
sys.exit(attach(pid, socketfile))
else:
sys.exit(0)
except OSError as err:
sys.stderr.write("Unable to fork: %s\n" % err)
sys.exit(1)
try: try:
pid = os.fork() pid = os.fork()
if pid > 0: if pid > 0:

View file

@ -71,26 +71,12 @@ 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
if not args.debug and not args.no_attach and args.socketfile is not None and args.pidfile is not None:
try:
pid = os.fork()
if pid > 0:
import time
os.waitpid(pid, 0)
time.sleep(1)
from nemubot import attach
sys.exit(attach(args.pidfile, args.socketfile))
except OSError as err:
sys.stderr.write("Unable to fork: %s\n" % err)
sys.exit(1)
# Setup logging interface # Setup logging interface
import logging import logging
logger = logging.getLogger("nemubot") logger = logging.getLogger("nemubot")
@ -120,7 +106,7 @@ def main():
pass pass
else: else:
from nemubot import attach from nemubot import attach
sys.exit(attach(args.pidfile, args.socketfile)) sys.exit(attach(pid, args.socketfile))
# Add modules dir paths # Add modules dir paths
modules_paths = list() modules_paths = list()
@ -132,10 +118,10 @@ def main():
# Create bot context # Create bot context
from nemubot import datastore from nemubot import datastore
from nemubot.bot import Bot from nemubot.bot import Bot, sync_act
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,44 +133,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):
sync_act("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: if args.socketfile:
from nemubot.server.socket import UnixSocketListener from nemubot.server.socket import UnixSocketListener
@ -195,7 +151,7 @@ def main():
# Daemonize # Daemonize
if not args.debug: if not args.debug:
from nemubot import daemonize from nemubot import daemonize
daemonize(args.socketfile) daemonize(args.socketfile, not args.no_attach)
# Signals handling # Signals handling
def sigtermhandler(signum, frame): def sigtermhandler(signum, frame):
@ -249,31 +205,5 @@ def main():
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

@ -20,7 +20,6 @@ from multiprocessing import JoinableQueue
import threading import threading
import select 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
@ -40,14 +39,14 @@ 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 modules
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 verbosity -- verbosity level
""" """
super().__init__(name="Nemubot main") super().__init__(name="Nemubot main")
@ -56,8 +55,8 @@ class Bot(threading.Thread):
__version__, __version__,
sys.version_info.major, sys.version_info.minor, sys.version_info.micro) 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
@ -92,24 +91,23 @@ class Bot(threading.Thread):
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.Command(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 from nemubot.message.command import Command
for h in self.treater._in_hooks(Command(msg.args[0][1:])): for h in self.treater._in_hooks(Command(msg.args[0][1:])):
@ -135,28 +133,19 @@ 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.Command(_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()
self.cnsr_lock = threading.Lock()
self.cnsr_active = 0 # consumers currently executing a task
self.cnsr_max = os.cpu_count() or 4 # upper bound on concurrent consumer threads
def __del__(self):
self.datastore.close()
def run(self): def run(self):
@ -169,13 +158,8 @@ class Bot(threading.Thread):
self._poll.register(sync_queue._reader, select.POLLIN | select.POLLPRI) 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(): for fd, flag in self._poll.poll():
# Handle internal socket passing orders # Handle internal socket passing orders
@ -223,10 +207,7 @@ class Bot(threading.Thread):
elif args[0] == "register": elif args[0] == "register":
self._poll.register(int(args[1]), select.POLLIN | select.POLLPRI) self._poll.register(int(args[1]), select.POLLIN | select.POLLPRI)
elif args[0] == "unregister": elif args[0] == "unregister":
try: self._poll.unregister(int(args[1]))
self._poll.unregister(int(args[1]))
except KeyError:
pass
except: except:
logger.exception("Unhandled excpetion during action:") logger.exception("Unhandled excpetion during action:")
@ -236,23 +217,86 @@ class Bot(threading.Thread):
elif action == "launch_consumer": elif action == "launch_consumer":
pass # This is treated after the loop pass # This is treated after the loop
elif action == "loadconf":
for path in args:
logger.debug("Load configuration from %s", path)
self.load_file(path)
logger.info("Configurations successfully loaded")
sync_queue.task_done() sync_queue.task_done()
# 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 sync_queue = None
logger.info("Ending main loop") logger.info("Ending main loop")
# Config methods
def load_file(self, filename):
"""Load a configuration file
Arguments:
filename -- the path to the file to load
"""
import os
# Unexisting file, assume a name was passed, import the module!
if not os.path.isfile(filename):
return self.import_module(filename)
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,
})
config = p.parse_file(filename)
except:
logger.exception("Can't load `%s'; this is not a valid nemubot "
"configuration file." % filename)
return False
# Preset each server in this file
for server in config.servers:
srv = server.server(config)
# Add the server in the context
if self.add_server(srv, server.autoconnect):
logger.info("Server '%s' successfully added." % srv.name)
else:
logger.error("Can't add server '%s'." % srv.name)
# Load module and their configuration
for mod in config.modules:
self.modules_configuration[mod.name] = mod
if mod.autoload:
try:
__import__(mod.name)
except:
logger.exception("Exception occurs when loading module"
" '%s'", mod.name)
# Load files asked by the configuration file
for load in config.includes:
self.load_file(load.path)
# Events methods # Events methods
def add_event(self, evt, eid=None, module_src=None): def add_event(self, evt, eid=None, module_src=None):
@ -270,6 +314,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
@ -296,7 +344,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,7 +353,7 @@ 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 in %d position: %s", i, t)
@ -335,10 +383,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 +394,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
@ -427,6 +475,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)
@ -466,20 +518,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]
@ -516,7 +566,7 @@ class Bot(threading.Thread):
self.event_timer.cancel() self.event_timer.cancel()
logger.info("Save and unload all modules...") logger.info("Save and unload all modules...")
for mod in [m for m in self.modules.keys()]: for mod in self.modules.items():
self.unload_module(mod) self.unload_module(mod)
logger.info("Close all servers connection...") logger.info("Close all servers connection...")
@ -524,11 +574,12 @@ class Bot(threading.Thread):
srv.close() 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
self.datastore.close()
if self.stop is False or sync_queue is not None: if self.stop is False or sync_queue is not None:
self.stop = True self.stop = True
sync_act("end") sync_act("end")

View file

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

@ -15,7 +15,7 @@
# 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.config import get_boolean from nemubot.config import get_boolean
from nemubot.tools.xmlparser.genericnode import GenericNode from nemubot.datastore.nodes.generic import GenericNode
class Module(GenericNode): class Module(GenericNode):

View file

@ -33,7 +33,7 @@ class Server:
return True return True
def server(self, parent, trynb=0): def server(self, parent):
from nemubot.server import factory from nemubot.server import factory
for a in ["nick", "owner", "realname", "encoding"]: for a in ["nick", "owner", "realname", "encoding"]:
@ -42,4 +42,4 @@ class Server:
self.caps += parent.caps self.caps += parent.caps
return factory(self.uri, caps=self.caps, channels=self.channels, trynb=trynb, **self.args) return factory(self.uri, caps=self.caps, channels=self.channels, **self.args)

View file

@ -94,7 +94,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 +105,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) super().__init__(name="Nemubot consumer")
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

@ -23,8 +23,7 @@ class Abstract:
"""Initialize a new empty storage tree """Initialize a new empty storage tree
""" """
from nemubot.tools.xmlparser import module_state return None
return module_state.ModuleState("nemubotstate")
def open(self): def open(self):
return return
@ -32,20 +31,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

@ -0,0 +1,18 @@
# 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.datastore.nodes.generic import ParsingNode
from nemubot.datastore.nodes.serializable import Serializable

View file

@ -14,11 +14,16 @@
# 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 ListNode: from nemubot.datastore.nodes.serializable import Serializable
class ListNode(Serializable):
"""XML node representing a Python dictionnnary """XML node representing a Python dictionnnary
""" """
serializetag = "list"
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.items = list() self.items = list()
@ -27,6 +32,9 @@ class ListNode:
self.items.append(child) self.items.append(child)
return True return True
def parsedForm(self):
return self.items
def __len__(self): def __len__(self):
return len(self.items) return len(self.items)
@ -44,18 +52,21 @@ class ListNode:
return self.items.__repr__() return self.items.__repr__()
def saveElement(self, store, tag="list"): def serialize(self):
store.startElement(tag, {}) from nemubot.datastore.nodes.generic import ParsingNode
node = ParsingNode(tag=self.serializetag)
for i in self.items: for i in self.items:
i.saveElement(store) node.children.append(ParsingNode.serialize_node(i))
store.endElement(tag) return node
class DictNode: class DictNode(Serializable):
"""XML node representing a Python dictionnnary """XML node representing a Python dictionnnary
""" """
serializetag = "dict"
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.items = dict() self.items = dict()
self._cur = None self._cur = None
@ -63,44 +74,20 @@ class DictNode:
def startElement(self, name, attrs): def startElement(self, name, attrs):
if self._cur is None and "key" in attrs: if self._cur is None and "key" in attrs:
self._cur = (attrs["key"], "") self._cur = attrs["key"]
return True
return False return False
def characters(self, content):
if self._cur is not None:
key, cnt = self._cur
if isinstance(cnt, str):
cnt += content
self._cur = key, cnt
def endElement(self, name):
if name is not None or self._cur is None:
return
key, cnt = self._cur
if isinstance(cnt, list) and len(cnt) == 1:
self.items[key] = cnt[0]
else:
self.items[key] = cnt
self._cur = None
return True
def addChild(self, name, child): def addChild(self, name, child):
if self._cur is None: if self._cur is None:
return False return False
key, cnt = self._cur self.items[self._cur] = child
if not isinstance(cnt, list): self._cur = None
cnt = []
cnt.append(child)
self._cur = key, cnt
return True return True
def parsedForm(self):
return self.items
def __getitem__(self, item): def __getitem__(self, item):
return self.items[item] return self.items[item]
@ -115,39 +102,11 @@ class DictNode:
return self.items.__repr__() return self.items.__repr__()
def saveElement(self, store, tag="dict"): def serialize(self):
store.startElement(tag, {}) from nemubot.datastore.nodes.generic import ParsingNode
for k, v in self.items.items(): node = ParsingNode(tag=self.serializetag)
store.startElement("item", {"key": k}) for k in self.items:
if isinstance(v, str): chld = ParsingNode.serialize_node(self.items[k])
store.characters(v) chld.attrs["key"] = k
else: node.children.append(chld)
if hasattr(v, "__iter__"): return node
for i in v:
i.saveElement(store)
else:
v.saveElement(store)
store.endElement("item")
store.endElement(tag)
def __contain__(self, i):
return i in self.items
def __getitem__(self, i):
return self.items[i]
def __setitem__(self, i, c):
self.items[i] = c
def __delitem__(self, k):
del self.items[k]
def __iter__(self):
return self.items.__iter__()
def keys(self):
return self.items.keys()
def items(self):
return self.items.items()

View file

@ -14,6 +14,9 @@
# 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.datastore.nodes.serializable import Serializable
class ParsingNode: class ParsingNode:
"""Allow any kind of subtags, just keep parsed ones """Allow any kind of subtags, just keep parsed ones
@ -53,12 +56,45 @@ class ParsingNode:
return item in self.attrs return item in self.attrs
def saveElement(self, store, tag=None): def serialize_node(node, **def_kwargs):
store.startElement(tag if tag is not None else self.tag, self.attrs) """Serialize any node or basic data to a ParsingNode instance"""
for child in self.children:
child.saveElement(store) if isinstance(node, Serializable):
store.characters(self.content) node = node.serialize()
store.endElement(tag if tag is not None else self.tag)
if isinstance(node, str):
from nemubot.datastore.nodes.python import StringNode
pn = StringNode(**def_kwargs)
pn.value = node
return pn
elif isinstance(node, int):
from nemubot.datastore.nodes.python import IntNode
pn = IntNode(**def_kwargs)
pn.value = node
return pn
elif isinstance(node, float):
from nemubot.datastore.nodes.python import FloatNode
pn = FloatNode(**def_kwargs)
pn.value = node
return pn
elif isinstance(node, list):
from nemubot.datastore.nodes.basic import ListNode
pn = ListNode(**def_kwargs)
pn.items = node
return pn.serialize()
elif isinstance(node, dict):
from nemubot.datastore.nodes.basic import DictNode
pn = DictNode(**def_kwargs)
pn.items = node
return pn.serialize()
else:
assert isinstance(node, ParsingNode)
return node
class GenericNode(ParsingNode): class GenericNode(ParsingNode):

View file

@ -0,0 +1,91 @@
# 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.datastore.nodes.serializable import Serializable
class PythonTypeNode(Serializable):
"""XML node representing a Python simple type
"""
def __init__(self, **kwargs):
self.value = None
self._cnt = ""
def characters(self, content):
self._cnt += content
def endElement(self, name):
raise NotImplemented
def __repr__(self):
return self.value.__repr__()
def parsedForm(self):
return self.value
def serialize(self):
raise NotImplemented
class IntNode(PythonTypeNode):
serializetag = "int"
def endElement(self, name):
self.value = int(self._cnt)
return True
def serialize(self):
from nemubot.datastore.nodes.generic import ParsingNode
node = ParsingNode(tag=self.serializetag)
node.content = str(self.value)
return node
class FloatNode(PythonTypeNode):
serializetag = "float"
def endElement(self, name):
self.value = float(self._cnt)
return True
def serialize(self):
from nemubot.datastore.nodes.generic import ParsingNode
node = ParsingNode(tag=self.serializetag)
node.content = str(self.value)
return node
class StringNode(PythonTypeNode):
serializetag = "str"
def endElement(self, name):
self.value = str(self._cnt)
return True
def serialize(self):
from nemubot.datastore.nodes.generic import ParsingNode
node = ParsingNode(tag=self.serializetag)
node.content = str(self.value)
return node

View file

@ -0,0 +1,22 @@
# 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 Serializable:
def serialize(self):
# Implementations of this function should return ParsingNode items
return NotImplemented

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-2015 Mercier Pierre-Olivier # Copyright (C) 2012-2016 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
@ -36,17 +36,24 @@ class XML(Abstract):
rotate -- auto-backup files? rotate -- auto-backup files?
""" """
self.basedir = basedir self.basedir = os.path.abspath(basedir)
self.rotate = rotate self.rotate = rotate
self.nb_save = 0 self.nb_save = 0
logger.info("Initiate XML datastore at %s, rotation %s",
self.basedir,
"enabled" if self.rotate else "disabled")
def open(self): def open(self):
"""Lock the directory""" """Lock the directory"""
if not os.path.isdir(self.basedir): if not os.path.isdir(self.basedir):
logger.debug("Datastore directory not found, creating: %s", self.basedir)
os.mkdir(self.basedir) os.mkdir(self.basedir)
lock_path = os.path.join(self.basedir, ".used_by_nemubot") lock_path = self._get_lock_file_path()
logger.debug("Locking datastore directory via %s", lock_path)
self.lock_file = open(lock_path, 'a+') self.lock_file = open(lock_path, 'a+')
ok = True ok = True
@ -64,66 +71,94 @@ class XML(Abstract):
self.lock_file.write(str(os.getpid())) self.lock_file.write(str(os.getpid()))
self.lock_file.flush() self.lock_file.flush()
logger.info("Datastore successfuly opened at %s", self.basedir)
return True return True
def close(self): def close(self):
"""Release a locked path""" """Release a locked path"""
if hasattr(self, "lock_file"): if hasattr(self, "lock_file"):
self.lock_file.close() self.lock_file.close()
lock_path = os.path.join(self.basedir, ".used_by_nemubot") lock_path = self._get_lock_file_path()
if os.path.isdir(self.basedir) and os.path.exists(lock_path): if os.path.isdir(self.basedir) and os.path.exists(lock_path):
os.unlink(lock_path) os.unlink(lock_path)
del self.lock_file del self.lock_file
logger.info("Datastore successfully closed at %s", self.basedir)
return True return True
else:
logger.warn("Datastore not open/locked or lock file not found")
return False return False
def _get_data_file_path(self, module): def _get_data_file_path(self, module):
"""Get the path to the module data file""" """Get the path to the module data file"""
return os.path.join(self.basedir, module + ".xml") return os.path.join(self.basedir, module + ".xml")
def load(self, module, knodes):
def _get_lock_file_path(self):
"""Get the path to the datastore lock file"""
return os.path.join(self.basedir, ".used_by_nemubot")
def load(self, module, extendsTags={}):
"""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
""" """
logger.debug("Trying to load data for %s%s",
module,
(" with tags: " + ", ".join(extendsTags.keys())) if len(extendsTags) else "")
data_file = self._get_data_file_path(module) data_file = self._get_data_file_path(module)
if knodes is None: def parse(path):
from nemubot.tools.xmlparser import parse_file
def _true_load(path):
return parse_file(path)
else:
from nemubot.tools.xmlparser import XMLParser from nemubot.tools.xmlparser import XMLParser
p = XMLParser(knodes) from nemubot.datastore.nodes import basic as basicNodes
def _true_load(path): from nemubot.datastore.nodes import python as pythonNodes
return p.parse_file(path) from nemubot.message.command import Command
from nemubot.scope import Scope
d = {
basicNodes.ListNode.serializetag: basicNodes.ListNode,
basicNodes.DictNode.serializetag: basicNodes.DictNode,
pythonNodes.IntNode.serializetag: pythonNodes.IntNode,
pythonNodes.FloatNode.serializetag: pythonNodes.FloatNode,
pythonNodes.StringNode.serializetag: pythonNodes.StringNode,
Command.serializetag: Command,
Scope.serializetag: Scope,
}
d.update(extendsTags)
p = XMLParser(d)
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):
try: try:
return _true_load(data_file) return parse(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(path)
logger.warn("Restoring from backup: %s", path) logger.warn("Restoring data from backup: %s", path)
return cnt return cnt
except xml.parsers.expat.ExpatError: except xml.parsers.expat.ExpatError:
continue continue
# Default case: initialize a new empty datastore # Default case: initialize a new empty datastore
return super().load(module, knodes) logger.warn("No data found in store for %s, creating new set", module)
return Abstract.load(self, module)
def _rotate(self, path): def _rotate(self, path):
"""Backup given path """Backup given path
@ -141,6 +176,25 @@ class XML(Abstract):
if os.path.isfile(src): if os.path.isfile(src):
os.rename(src, dst) os.rename(src, dst)
def _save_node(self, gen, node):
from nemubot.datastore.nodes.generic import ParsingNode
# First, get the serialized form of the node
node = ParsingNode.serialize_node(node)
assert node.tag is not None, "Undefined tag name"
gen.startElement(node.tag, {k: str(node.attrs[k]) for k in node.attrs})
gen.characters(node.content)
for child in node.children:
self._save_node(gen, child)
gen.endElement(node.tag)
def save(self, module, data): def save(self, module, data):
"""Load data for the given module """Load data for the given module
@ -150,22 +204,22 @@ class XML(Abstract):
""" """
path = self._get_data_file_path(module) path = self._get_data_file_path(module)
logger.debug("Trying to save data for module %s in %s", module, path)
if self.rotate: if self.rotate:
self._rotate(path) self._rotate(path)
if data is None:
return
import tempfile import tempfile
_, tmpath = tempfile.mkstemp() _, tmpath = tempfile.mkstemp()
with open(tmpath, "w") as f: with open(tmpath, "w") as f:
import xml.sax.saxutils import xml.sax.saxutils
gen = xml.sax.saxutils.XMLGenerator(f, "utf-8") gen = xml.sax.saxutils.XMLGenerator(f, "utf-8")
gen.startDocument() gen.startDocument()
data.saveElement(gen) self._save_node(gen, data)
gen.endDocument() gen.endDocument()
# Atomic save # Atomic save
import shutil import shutil
shutil.move(tmpath, path) shutil.move(tmpath, path)
return True

View file

@ -21,14 +21,18 @@ 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, interval=60, offset=0, times=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
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)
@ -36,22 +40,31 @@ class ModuleEvent:
# 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
self.cmp_data = None
if cmp_data is not None:
self.cmp_data = cmp_data
elif self.func is not None:
if self.func_data is None:
self.cmp_data = self.func()
elif isinstance(self.func_data, dict):
self.cmp_data = self.func(**self.func_data)
else:
self.cmp_data = self.func(self.func_data)
# What should we call when? # What should we call when?
self.call = call self.call = call
if call_data is not None:
self.call_data = call_data
else:
self.call_data = func_data
# Store times # Store times
if isinstance(offset, timedelta): self.offset = timedelta(seconds=offset) # Time to wait before the first check
self.offset = offset # Time to wait before the first check self.interval = timedelta(seconds=interval)
else:
self.offset = timedelta(seconds=offset) # Time to wait before the first check
if isinstance(interval, timedelta):
self.interval = interval
else:
self.interval = timedelta(seconds=interval)
self._end = None # Cache self._end = None # Cache
# How many times do this event? # How many times do this event?
@ -87,18 +100,41 @@ class ModuleEvent:
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 # Get initial data
if self.func is not None: if self.func is None:
d_new = 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 self.cmp is None:
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)
elif d_init is None:
self.call(self.call_data)
else: else:
self.call() self.call(d_init, self.call_data)

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
@ -121,18 +119,10 @@ class Abstract:
try: try:
if self.check(data1): if self.check(data1):
ret = call_game(self.call, data1, self.data, *args) ret = call_game(self.call, data1, self.data, *args)
if isinstance(ret, types.GeneratorType):
for r in ret:
yield r
ret = None
except IMException as e: 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

@ -43,7 +43,7 @@ class Dict(Abstract):
def check(self, mkw): def check(self, mkw):
for k in 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 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: if mkw[k] and k in self.chk_noarg:
raise KeywordException("Keyword %s doesn't take value." % k) raise KeywordException("Keyword %s doesn't take value." % k)
elif not mkw[k] and k in self.chk_args: elif not mkw[k] and k in self.chk_args:

View file

@ -29,16 +29,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 +53,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

@ -16,11 +16,16 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from nemubot.datastore.nodes import Serializable
class Abstract:
class Abstract(Serializable):
"""This class represents an abstract message""" """This class represents an abstract message"""
serializetag = "nemubotAMessage"
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, frm_owner=False):
"""Initialize an abstract message """Initialize an abstract message
@ -59,6 +64,20 @@ class Abstract:
else: else:
return None return None
@property
def nick(self):
# TODO: this is for legacy modules
return self.frm
@property
def scope(self):
from nemubot.scope import Scope
return Scope(server=self.server,
channel=self.to_response[0],
nick=self.frm)
def accept(self, visitor): def accept(self, visitor):
visitor.visit(self) visitor.visit(self)
@ -81,3 +100,8 @@ class Abstract:
del ret[w] del ret[w]
return ret return ret
def serialize(self):
from nemubot.datastore.nodes import ParsingNode
return ParsingNode(tag=Abstract.serializetag, **self.export_args())

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-2015 Mercier Pierre-Olivier # Copyright (C) 2012-2016 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,6 +21,9 @@ class Command(Abstract):
"""This class represents a specialized TextMessage""" """This class represents a specialized TextMessage"""
serializetag = "nemubotCommand"
def __init__(self, cmd, args=None, kwargs=None, *nargs, **kargs): def __init__(self, cmd, args=None, kwargs=None, *nargs, **kargs):
super().__init__(*nargs, **kargs) super().__init__(*nargs, **kargs)
@ -28,12 +31,35 @@ class Command(Abstract):
self.args = args if args is not None else list() self.args = args if args is not None else list()
self.kwargs = kwargs if kwargs is not None else dict() self.kwargs = kwargs if kwargs is not None else dict()
def __str__(self):
def __repr__(self):
return self.cmd + " @" + ",@".join(self.args) return self.cmd + " @" + ",@".join(self.args)
def addChild(self, name, child):
if name == "list":
self.args = child
elif name == "dict":
self.kwargs = child
else:
return False
return True
def serialize(self):
from nemubot.datastore.nodes import ParsingNode
node = ParsingNode(tag=Command.serializetag, cmd=self.cmd)
if len(self.args):
node.children.append(ParsingNode.serialize_node(self.args))
if len(self.kwargs):
node.children.append(ParsingNode.serialize_node(self.kwargs))
return node
class OwnerCommand(Command): class OwnerCommand(Command):
"""This class represents a special command incomming from the owner""" """This class represents a special command incomming from the owner"""
serializetag = "nemubotOCommand"
pass pass

View file

@ -0,0 +1,25 @@
# 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.message import Text
from nemubot.message.printer.socket import Socket as SocketPrinter
class IRC(SocketPrinter):
def visit_Text(self, msg):
self.pp += "PRIVMSG %s :" % ",".join(msg.to)
super().visit_Text(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))

View file

@ -1,69 +0,0 @@
# Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2026 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from nemubot.message.visitor import AbstractVisitor
class Matrix(AbstractVisitor):
"""Visitor that sends bot responses as Matrix room messages.
Instead of accumulating text like the IRC printer does, each visit_*
method calls send_func(room_id, text) directly for every destination room.
"""
def __init__(self, send_func):
"""
Argument:
send_func -- callable(room_id: str, text: str) that sends a plain-text
message to the given Matrix room
"""
self._send = send_func
def visit_Text(self, msg):
if isinstance(msg.message, str):
for room in msg.to:
self._send(room, msg.message)
else:
# Nested message object — let it visit itself
msg.message.accept(self)
def visit_DirectAsk(self, msg):
text = msg.message if isinstance(msg.message, str) else str(msg.message)
# Rooms that are NOT the designated nick → prefix with "nick: "
others = [to for to in msg.to if to != msg.designated]
if len(others) == 0 or len(others) != len(msg.to):
# At least one room IS the designated target → send plain
for room in msg.to:
self._send(room, text)
if len(others):
# Other rooms → prefix with nick
for room in others:
self._send(room, "%s: %s" % (msg.designated, text))
def visit_Command(self, msg):
parts = ["!" + msg.cmd]
if msg.args:
parts.extend(msg.args)
for room in msg.to:
self._send(room, " ".join(parts))
def visit_OwnerCommand(self, msg):
parts = ["`" + msg.cmd]
if msg.args:
parts.extend(msg.args)
for room in msg.to:
self._send(room, " ".join(parts))

View file

@ -27,3 +27,8 @@ class Response(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

View file

@ -1,7 +0,0 @@
#
# This directory aims to store nemubot core modules.
#
# Custom modules should be placed into a separate directory.
# By default, this is the directory modules in your current directory.
# Use the --modules-path argument to define a custom directory for your modules.
#

View file

@ -16,34 +16,24 @@
class _ModuleContext: class _ModuleContext:
def __init__(self, module=None, knodes=None): def __init__(self, module=None):
self.module = module self.module = module
if module is not None: if module is not None:
self.module_name = (module.__spec__.name if hasattr(module, "__spec__") else module.__name__).replace("nemubot.module.", "") self.module_name = module.__spec__.name if hasattr(module, "__spec__") else module.__name__
else: else:
self.module_name = "" self.module_name = ""
self.hooks = list() self.hooks = list()
self.events = list() self.events = list()
self.extendtags = dict()
self.debug = False self.debug = False
from nemubot.config.module import Module from nemubot.config.module import Module
self.config = Module(self.module_name) self.config = Module(self.module_name)
self._knodes = knodes
def load_data(self): def load_data(self):
from nemubot.tools.xmlparser import module_state return None
return module_state.ModuleState("nemubotstate")
def set_knodes(self, knodes):
self._knodes = knodes
def set_default(self, default):
# Access to data will trigger the load of data
if self.data is None:
self._data = default
def add_hook(self, hook, *triggers): def add_hook(self, hook, *triggers):
from nemubot.hooks import Abstract as AbstractHook from nemubot.hooks import Abstract as AbstractHook
@ -73,7 +63,9 @@ class _ModuleContext:
self.module.logger.info("Send response: %s", res) self.module.logger.info("Send response: %s", res)
def save(self): def save(self):
self.context.datastore.save(self.module_name, self.data) # Don't save if no data has been access
if hasattr(self, "_data"):
context.datastore.save(self.module_name, self.data)
def subparse(self, orig, cnt): def subparse(self, orig, cnt):
if orig.server in self.context.servers: if orig.server in self.context.servers:
@ -85,6 +77,21 @@ class _ModuleContext:
self._data = self.load_data() self._data = self.load_data()
return self._data return self._data
@data.setter
def data(self, value):
assert value is not None
self._data = value
def register_tags(self, **tags):
self.extendtags.update(tags)
def unregister_tags(self, *tags):
for t in tags:
del self.extendtags[t]
def unload(self): def unload(self):
"""Perform actions for unloading the module""" """Perform actions for unloading the module"""
@ -94,7 +101,7 @@ class _ModuleContext:
self.del_hook(h, *s) self.del_hook(h, *s)
# Remove registered events # Remove registered events
for evt, eid in self.events: for evt, eid, module_src in self.events:
self.del_event(evt) self.del_event(evt)
self.save() self.save()
@ -117,11 +124,11 @@ class ModuleContext(_ModuleContext):
self.config = context.modules_configuration[self.module_name] self.config = context.modules_configuration[self.module_name]
self.context = context self.context = context
self.debug = context.debug self.debug = context.verbosity > 0
def load_data(self): def load_data(self):
return self.context.datastore.load(self.module_name, self._knodes) return self.context.datastore.load(self.module_name, extendsTags=self.extendtags)
def add_hook(self, hook, *triggers): def add_hook(self, hook, *triggers):
from nemubot.hooks import Abstract as AbstractHook from nemubot.hooks import Abstract as AbstractHook

83
nemubot/scope.py Normal file
View file

@ -0,0 +1,83 @@
# 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.datastore.nodes import Serializable
class Scope(Serializable):
serializetag = "nemubot-scope"
default_limit = "channel"
def __init__(self, server, channel, nick, limit=default_limit):
self._server = server
self._channel = channel
self._nick = nick
self._limit = limit
def sameServer(self, server):
return self._server is None or self._server == server
def sameChannel(self, server, channel):
return self.sameServer(server) and (self._channel is None or self._channel == channel)
def sameNick(self, server, channel, nick):
return self.sameChannel(server, channel) and (self._nick is None or self._nick == nick)
def check(self, scope, limit=None):
return self.checkScope(scope._server, scope._channel, scope._nick, limit)
def checkScope(self, server, channel, nick, limit=None):
if limit is None: limit = self._limit
assert limit == "global" or limit == "server" or limit == "channel" or limit == "nick"
if limit == "server":
return self.sameServer(server)
elif limit == "channel":
return self.sameChannel(server, channel)
elif limit == "nick":
return self.sameNick(server, channel, nick)
else:
return True
def narrow(self, scope):
return scope is None or (
scope._limit == "global" or
(scope._limit == "server" and (self._limit == "nick" or self._limit == "channel")) or
(scope._limit == "channel" and self._limit == "nick")
)
def serialize(self):
from nemubot.datastore.nodes import ParsingNode
args = {}
if self._server is not None:
args["server"] = self._server
if self._channel is not None:
args["channel"] = self._channel
if self._nick is not None:
args["nick"] = self._nick
if self._limit is not None:
args["limit"] = self._limit
return ParsingNode(tag=self.serializetag, **args)

239
nemubot/server/DCC.py Normal file
View file

@ -0,0 +1,239 @@
# 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 imp
import os
import re
import socket
import sys
import time
import threading
import traceback
import nemubot.message as message
import nemubot.server as server
#Store all used ports
PORTS = list()
class DCC(server.AbstractServer):
def __init__(self, srv, dest, socket=None):
super().__init__(name="Nemubot DCC server")
self.error = False # An error has occur, closing the connection?
self.messages = list() # Message queued before connexion
# Informations about the sender
self.sender = dest
if self.sender is not None:
self.nick = (self.sender.split('!'))[0]
if self.nick != self.sender:
self.realname = (self.sender.split('!'))[1]
else:
self.realname = self.nick
# Keep the server
self.srv = srv
self.treatement = self.treat_msg
# Found a port for the connection
self.port = self.foundPort()
if self.port is None:
self.logger.critical("No more available slot for DCC connection")
self.setError("Il n'y a plus de place disponible sur le serveur"
" pour initialiser une session DCC.")
def foundPort(self):
"""Found a free port for the connection"""
for p in range(65432, 65535):
if p not in PORTS:
PORTS.append(p)
return p
return None
@property
def id(self):
"""Gives the server identifiant"""
return self.srv.id + "/" + self.sender
def setError(self, msg):
self.error = True
self.srv.send_msg_usr(self.sender, msg)
def accept_user(self, host, port):
"""Accept a DCC connection"""
self.s = socket.socket()
try:
self.s.connect((host, port))
self.logger.info("Accepted user from %s:%d for %s", host, port, self.sender)
self.connected = True
self.stop = False
except:
self.connected = False
self.error = True
return False
self.start()
return True
def request_user(self, type="CHAT", filename="CHAT", size=""):
"""Create a DCC connection"""
#Open the port
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.bind(('', self.port))
except:
try:
self.port = self.foundPort()
s.bind(('', self.port))
except:
self.setError("Une erreur s'est produite durant la tentative"
" d'ouverture d'une session DCC.")
return False
self.logger.info("Listening on %d for %s", self.port, self.sender)
#Send CTCP request for DCC
self.srv.send_ctcp(self.sender,
"DCC %s %s %d %d %s" % (type, filename, self.srv.ip,
self.port, size),
"PRIVMSG")
s.listen(1)
#Waiting for the client
(self.s, addr) = s.accept()
self.logger.info("Connected by %d", addr)
self.connected = True
return True
def send_dcc_raw(self, line):
self.s.sendall(line + b'\n')
def send_dcc(self, msg, to = None):
"""If we talk to this user, send a message through this connection
else, send the message to the server class"""
if to is None or to == self.sender or to == self.nick:
if self.error:
self.srv.send_msg_final(self.nick, msg)
elif not self.connected or self.s is None:
try:
self.start()
except RuntimeError:
pass
self.messages.append(msg)
else:
for line in msg.split("\n"):
self.send_dcc_raw(line.encode())
else:
self.srv.send_dcc(msg, to)
def send_file(self, filename):
"""Send a file over DCC"""
if os.path.isfile(filename):
self.messages = filename
try:
self.start()
except RuntimeError:
pass
else:
self.logger.error("File not found `%s'", filename)
def run(self):
self.stopping.clear()
# Send file connection
if not isinstance(self.messages, list):
self.request_user("SEND",
os.path.basename(self.messages),
os.path.getsize(self.messages))
if self.connected:
with open(self.messages, 'rb') as f:
d = f.read(268435456) #Packets size: 256Mo
while d:
self.s.sendall(d)
self.s.recv(4) #The client send a confirmation after each packet
d = f.read(268435456) #Packets size: 256Mo
# Messages connection
else:
if not self.connected:
if not self.request_user():
#TODO: do something here
return False
#Start by sending all queued messages
for mess in self.messages:
self.send_dcc(mess)
time.sleep(1)
readbuffer = b''
self.nicksize = len(self.srv.nick)
self.Bnick = self.srv.nick.encode()
while not self.stop:
raw = self.s.recv(1024) #recieve server messages
if not raw:
break
readbuffer = readbuffer + raw
temp = readbuffer.split(b'\n')
readbuffer = temp.pop()
for line in temp:
self.treatement(line)
if self.connected:
self.s.close()
self.connected = False
#Remove from DCC connections server list
if self.realname in self.srv.dcc_clients:
del self.srv.dcc_clients[self.realname]
self.logger.info("Closing connection with %s", self.nick)
self.stopping.set()
if self.closing_event is not None:
self.closing_event()
#Rearm Thread
threading.Thread.__init__(self)
def treat_msg(self, line):
"""Treat a receive message, *can be overwritten*"""
if line == b'NEMUBOT###':
bot = self.srv.add_networkbot(self.srv, self.sender, self)
self.treatement = bot.treat_msg
self.send_dcc("NEMUBOT###")
elif (line[:self.nicksize] == self.Bnick and
line[self.nicksize+1:].strip()[:10] == b'my name is'):
name = line[self.nicksize+1:].strip()[11:].decode('utf-8',
'replace')
if re.match("^[a-zA-Z0-9_-]+$", name):
if name not in self.srv.dcc_clients:
del self.srv.dcc_clients[self.sender]
self.nick = name
self.sender = self.nick + "!" + self.realname
self.srv.dcc_clients[self.realname] = self
self.send_dcc("Hi " + self.nick)
else:
self.send_dcc("This nickname is already in use"
", please choose another one.")
else:
self.send_dcc("The name you entered contain"
" invalid char.")
else:
self.srv.treat_msg(
(":%s PRIVMSG %s :" % (
self.sender,self.srv.nick)).encode() + line,
True)

283
nemubot/server/IRC.py Normal file
View file

@ -0,0 +1,283 @@
# 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/>.
from datetime import datetime
import re
import socket
from nemubot.channel import Channel
from nemubot.message.printer.IRC import IRC as IRCPrinter
from nemubot.server.message.IRC import IRC as IRCMessage
from nemubot.server.socket import SocketServer, SecureSocketServer
class _IRC:
"""Concrete implementation of a connexion to an IRC server"""
def __init__(self, host="localhost", port=6667, owner=None,
nick="nemubot", username=None, password=None,
realname="Nemubot", encoding="utf-8", caps=None,
channels=list(), on_connect=None, **kwargs):
"""Prepare a connection with an IRC server
Keyword arguments:
host -- host to join
port -- port on the host to reach
ssl -- is this server using a TLS socket
owner -- bot's owner
nick -- bot's nick
username -- the username as sent to server
password -- if a password is required to connect to the server
realname -- the bot's realname
encoding -- the encoding used on the whole server
caps -- client capabilities to register on the server
channels -- list of channels to join on connection
on_connect -- generator to call when connection is done
"""
self.username = username if username is not None else nick
self.password = password
self.nick = nick
self.owner = owner
self.realname = realname
super().__init__(name=self.username + "@" + host + ":" + str(port),
host=host, port=port, **kwargs)
self.printer = IRCPrinter
self.encoding = encoding
# Keep a list of joined channels
self.channels = dict()
# Server/client capabilities
self.capabilities = caps
# Register CTCP capabilities
self.ctcp_capabilities = dict()
def _ctcp_clientinfo(msg, cmds):
"""Response to CLIENTINFO CTCP message"""
return " ".join(self.ctcp_capabilities.keys())
def _ctcp_dcc(msg, cmds):
"""Response to DCC CTCP message"""
try:
import ipaddress
ip = ipaddress.ip_address(int(cmds[3]))
port = int(cmds[4])
conn = DCC(srv, msg.sender)
except:
return "ERRMSG invalid parameters provided as DCC CTCP request"
self.logger.info("Receive DCC connection request from %s to %s:%d", conn.sender, ip, port)
if conn.accept_user(ip, port):
srv.dcc_clients[conn.sender] = conn
conn.send_dcc("Hello %s!" % conn.nick)
else:
self.logger.error("DCC: unable to connect to %s:%d", ip, port)
return "ERRMSG unable to connect to %s:%d" % (ip, port)
import nemubot
self.ctcp_capabilities["ACTION"] = lambda msg, cmds: None
self.ctcp_capabilities["CLIENTINFO"] = _ctcp_clientinfo
#self.ctcp_capabilities["DCC"] = _ctcp_dcc
self.ctcp_capabilities["FINGER"] = lambda msg, cmds: "VERSION nemubot v%s" % nemubot.__version__
self.ctcp_capabilities["NEMUBOT"] = lambda msg, cmds: "NEMUBOT %s" % nemubot.__version__
self.ctcp_capabilities["PING"] = lambda msg, cmds: "PING %s" % " ".join(cmds[1:])
self.ctcp_capabilities["SOURCE"] = lambda msg, cmds: "SOURCE https://github.com/nemunaire/nemubot"
self.ctcp_capabilities["TIME"] = lambda msg, cmds: "TIME %s" % (datetime.now())
self.ctcp_capabilities["USERINFO"] = lambda msg, cmds: "USERINFO %s" % self.realname
self.ctcp_capabilities["VERSION"] = lambda msg, cmds: "VERSION nemubot v%s" % nemubot.__version__
# TODO: Temporary fix, waiting for hook based CTCP management
self.ctcp_capabilities["TYPING"] = lambda msg, cmds: None
self.logger.debug("CTCP capabilities setup: %s", ", ".join(self.ctcp_capabilities))
# Register hooks on some IRC CMD
self.hookscmd = dict()
# Respond to PING
def _on_ping(msg):
self.write(b"PONG :" + msg.params[0])
self.hookscmd["PING"] = _on_ping
# Respond to 001
def _on_connect(msg):
# First, send user defined command
if on_connect is not None:
if callable(on_connect):
toc = on_connect()
else:
toc = on_connect
if toc is not None:
for oc in toc:
self.write(oc)
# Then, JOIN some channels
for chn in channels:
if chn.password:
self.write("JOIN %s %s" % (chn.name, chn.password))
else:
self.write("JOIN %s" % chn.name)
self.hookscmd["001"] = _on_connect
# Respond to ERROR
def _on_error(msg):
self.close()
self.hookscmd["ERROR"] = _on_error
# Respond to CAP
def _on_cap(msg):
if len(msg.params) != 3 or msg.params[1] != b"LS":
return
server_caps = msg.params[2].decode().split(" ")
for cap in self.capabilities:
if cap not in server_caps:
self.capabilities.remove(cap)
if len(self.capabilities) > 0:
self.write("CAP REQ :" + " ".join(self.capabilities))
self.write("CAP END")
self.hookscmd["CAP"] = _on_cap
# Respond to JOIN
def _on_join(msg):
if len(msg.params) == 0:
return
for chname in msg.decode(msg.params[0]).split(","):
# Register the channel
chan = Channel(chname)
self.channels[chname] = chan
self.hookscmd["JOIN"] = _on_join
# Respond to PART
def _on_part(msg):
if len(msg.params) != 1 and len(msg.params) != 2:
return
for chname in msg.params[0].split(b","):
if chname in self.channels:
if msg.nick == self.nick:
del self.channels[chname]
elif msg.nick in self.channels[chname].people:
del self.channels[chname].people[msg.nick]
self.hookscmd["PART"] = _on_part
# Respond to 331/RPL_NOTOPIC,332/RPL_TOPIC,TOPIC
def _on_topic(msg):
if len(msg.params) != 1 and len(msg.params) != 2:
return
if msg.params[0] in self.channels:
if len(msg.params) == 1 or len(msg.params[1]) == 0:
self.channels[msg.params[0]].topic = None
else:
self.channels[msg.params[0]].topic = msg.decode(msg.params[1])
self.hookscmd["331"] = _on_topic
self.hookscmd["332"] = _on_topic
self.hookscmd["TOPIC"] = _on_topic
# Respond to 353/RPL_NAMREPLY
def _on_353(msg):
if len(msg.params) == 3:
msg.params.pop(0) # 353: like RFC 1459
if len(msg.params) != 2:
return
if msg.params[0] in self.channels:
for nk in msg.decode(msg.params[1]).split(" "):
res = re.match("^(?P<level>[^a-zA-Z[\]\\`_^{|}])(?P<nickname>[a-zA-Z[\]\\`_^{|}][a-zA-Z0-9[\]\\`_^{|}-]*)$")
self.channels[msg.params[0]].people[res.group("nickname")] = res.group("level")
self.hookscmd["353"] = _on_353
# Respond to INVITE
def _on_invite(msg):
if len(msg.params) != 2:
return
self.write("JOIN " + msg.decode(msg.params[1]))
self.hookscmd["INVITE"] = _on_invite
# Respond to ERR_NICKCOLLISION
def _on_nickcollision(msg):
self.nick += "_"
self.write("NICK " + self.nick)
self.hookscmd["433"] = _on_nickcollision
self.hookscmd["436"] = _on_nickcollision
# Handle CTCP requests
def _on_ctcp(msg):
if len(msg.params) != 2 or not msg.is_ctcp:
return
cmds = msg.decode(msg.params[1][1:len(msg.params[1])-1]).split(' ')
if cmds[0] in self.ctcp_capabilities:
res = self.ctcp_capabilities[cmds[0]](msg, cmds)
else:
res = "ERRMSG Unknown or unimplemented CTCP request"
if res is not None:
self.write("NOTICE %s :\x01%s\x01" % (msg.nick, res))
self.hookscmd["PRIVMSG"] = _on_ctcp
# Open/close
def connect(self):
super().connect()
if self.password is not None:
self.write("PASS :" + self.password)
if self.capabilities is not None:
self.write("CAP LS")
self.write("NICK :" + self.nick)
self.write("USER %s %s bla :%s" % (self.username, socket.getfqdn(), self.realname))
def close(self):
if not self._closed:
self.write("QUIT")
return super().close()
# Writes: as inherited
# Read
def async_read(self):
for line in super().async_read():
# PING should be handled here, so start parsing here :/
msg = IRCMessage(line, self.encoding)
if msg.cmd in self.hookscmd:
self.hookscmd[msg.cmd](msg)
yield msg
def parse(self, msg):
mes = msg.to_bot_message(self)
if mes is not None:
yield mes
def subparse(self, orig, cnt):
msg = IRCMessage(("@time=%s :%s!user@host.com PRIVMSG %s :%s" % (orig.date.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), orig.frm, ",".join(orig.to), cnt)).encode(self.encoding), self.encoding)
return msg.to_bot_message(self)
class IRC(_IRC, SocketServer):
pass
class IRC_secure(_IRC, SecureSocketServer):
pass

View file

@ -1,375 +0,0 @@
# Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2026 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from datetime import datetime
import shlex
import threading
import irc.bot
import irc.client
import irc.connection
import nemubot.message as message
from nemubot.server.threaded import ThreadedServer
class _IRCBotAdapter(irc.bot.SingleServerIRCBot):
"""Internal adapter that bridges the irc library event model to nemubot.
Subclasses SingleServerIRCBot to get automatic reconnection, PING/PONG,
and nick-collision handling for free.
"""
def __init__(self, server_name, push_fn, channels, on_connect_cmds,
nick, server_list, owner=None, realname="Nemubot",
encoding="utf-8", **connect_params):
super().__init__(server_list, nick, realname, **connect_params)
self._nemubot_name = server_name
self._push = push_fn
self._channels_to_join = channels
self._on_connect_cmds = on_connect_cmds or []
self.owner = owner
self.encoding = encoding
self._stop_event = threading.Event()
# Event loop control
def start(self):
"""Run the reactor loop until stop() is called."""
self._connect()
while not self._stop_event.is_set():
self.reactor.process_once(timeout=0.2)
def stop(self):
"""Signal the loop to exit and disconnect cleanly."""
self._stop_event.set()
try:
self.connection.disconnect("Goodbye")
except Exception:
pass
def on_disconnect(self, connection, event):
"""Reconnect automatically unless we are shutting down."""
if not self._stop_event.is_set():
self.jump_server()
# Connection lifecycle
def on_welcome(self, connection, event):
"""001 — run on_connect commands then join channels."""
for cmd in self._on_connect_cmds:
if callable(cmd):
for c in (cmd() or []):
connection.send_raw(c)
else:
connection.send_raw(cmd)
for ch in self._channels_to_join:
if isinstance(ch, tuple):
connection.join(ch[0], ch[1] if len(ch) > 1 else "")
elif hasattr(ch, 'name'):
connection.join(ch.name, getattr(ch, 'password', "") or "")
else:
connection.join(str(ch))
def on_invite(self, connection, event):
"""Auto-join on INVITE."""
if event.arguments:
connection.join(event.arguments[0])
# CTCP
def on_ctcp(self, connection, event):
"""Handle CTCP requests (irc library >= 19 dispatches all to on_ctcp)."""
nick = irc.client.NickMask(event.source).nick
ctcp_type = event.arguments[0].upper() if event.arguments else ""
ctcp_arg = event.arguments[1] if len(event.arguments) > 1 else ""
self._reply_ctcp(connection, nick, ctcp_type, ctcp_arg)
# Fallbacks for older irc library versions that dispatch per-type
def on_ctcpversion(self, connection, event):
import nemubot
nick = irc.client.NickMask(event.source).nick
connection.ctcp_reply(nick, "VERSION nemubot v%s" % nemubot.__version__)
def on_ctcpping(self, connection, event):
nick = irc.client.NickMask(event.source).nick
arg = event.arguments[0] if event.arguments else ""
connection.ctcp_reply(nick, "PING %s" % arg)
def _reply_ctcp(self, connection, nick, ctcp_type, ctcp_arg):
import nemubot
responses = {
"ACTION": None, # handled as on_action
"CLIENTINFO": "CLIENTINFO ACTION CLIENTINFO FINGER PING SOURCE TIME USERINFO VERSION",
"FINGER": "FINGER nemubot v%s" % nemubot.__version__,
"PING": "PING %s" % ctcp_arg,
"SOURCE": "SOURCE https://github.com/nemunaire/nemubot",
"TIME": "TIME %s" % datetime.now(),
"USERINFO": "USERINFO Nemubot",
"VERSION": "VERSION nemubot v%s" % nemubot.__version__,
}
if ctcp_type in responses and responses[ctcp_type] is not None:
connection.ctcp_reply(nick, responses[ctcp_type])
# Incoming messages
def _decode(self, text):
if isinstance(text, bytes):
try:
return text.decode("utf-8")
except UnicodeDecodeError:
return text.decode(self.encoding, "replace")
return text
def _make_message(self, connection, source, target, text):
"""Convert raw IRC event data into a nemubot bot message."""
nick = irc.client.NickMask(source).nick
text = self._decode(text)
bot_nick = connection.get_nickname()
is_channel = irc.client.is_channel(target)
to = [target] if is_channel else [nick]
to_response = [target] if is_channel else [nick]
common = dict(
server=self._nemubot_name,
to=to,
to_response=to_response,
frm=nick,
frm_owner=(nick == self.owner),
)
# "botname: text" or "botname, text"
if (text.startswith(bot_nick + ":") or
text.startswith(bot_nick + ",")):
inner = text[len(bot_nick) + 1:].strip()
return message.DirectAsk(designated=bot_nick, message=inner,
**common)
# "!command [args]"
if len(text) > 1 and text[0] == '!':
inner = text[1:].strip()
try:
args = shlex.split(inner)
except ValueError:
args = inner.split()
if args:
# Extract @key=value named arguments (same logic as IRC.py)
kwargs = {}
while len(args) > 1:
arg = args[1]
if len(arg) > 2 and arg[0:2] == '\\@':
args[1] = arg[1:]
elif len(arg) > 1 and arg[0] == '@':
arsp = arg[1:].split("=", 1)
kwargs[arsp[0]] = arsp[1] if len(arsp) == 2 else None
args.pop(1)
continue
break
return message.Command(cmd=args[0], args=args[1:],
kwargs=kwargs, **common)
return message.Text(message=text, **common)
def on_pubmsg(self, connection, event):
msg = self._make_message(
connection, event.source, event.target,
event.arguments[0] if event.arguments else "",
)
if msg:
self._push(msg)
def on_privmsg(self, connection, event):
nick = irc.client.NickMask(event.source).nick
msg = self._make_message(
connection, event.source, nick,
event.arguments[0] if event.arguments else "",
)
if msg:
self._push(msg)
def on_action(self, connection, event):
"""CTCP ACTION (/me) — delivered as a plain Text message."""
nick = irc.client.NickMask(event.source).nick
text = "/me %s" % (event.arguments[0] if event.arguments else "")
is_channel = irc.client.is_channel(event.target)
to = [event.target] if is_channel else [nick]
self._push(message.Text(
message=text,
server=self._nemubot_name,
to=to, to_response=to,
frm=nick, frm_owner=(nick == self.owner),
))
class IRCLib(ThreadedServer):
"""IRC server using the irc Python library (jaraco).
Compared to the hand-rolled IRC.py implementation, this gets:
- Automatic exponential-backoff reconnection
- PING/PONG handled transparently
- Nick-collision suffix logic built-in
"""
def __init__(self, host="localhost", port=6667, nick="nemubot",
username=None, password=None, realname="Nemubot",
encoding="utf-8", owner=None, channels=None,
on_connect=None, ssl=False, **kwargs):
"""Prepare a connection to an IRC server.
Keyword arguments:
host -- IRC server hostname
port -- IRC server port (default 6667)
nick -- bot's nickname
username -- username for USER command (defaults to nick)
password -- server password (sent as PASS)
realname -- bot's real name
encoding -- fallback encoding for non-UTF-8 servers
owner -- nick of the bot's owner (sets frm_owner on messages)
channels -- list of channel names / (name, key) tuples to join
on_connect -- list of raw IRC commands (or a callable returning one)
to send after receiving 001
ssl -- wrap the connection in TLS
"""
name = (username or nick) + "@" + host + ":" + str(port)
super().__init__(name=name)
self._host = host
self._port = int(port)
self._nick = nick
self._username = username or nick
self._password = password
self._realname = realname
self._encoding = encoding
self.owner = owner
self._channels = channels or []
self._on_connect_cmds = on_connect
self._ssl = ssl
self._bot = None
self._thread = None
# ThreadedServer hooks
def _start(self):
server_list = [irc.bot.ServerSpec(self._host, self._port,
self._password)]
connect_params = {"username": self._username}
if self._ssl:
import ssl as ssl_mod
ctx = ssl_mod.create_default_context()
host = self._host # capture for closure
connect_params["connect_factory"] = irc.connection.Factory(
wrapper=lambda sock: ctx.wrap_socket(sock,
server_hostname=host)
)
self._bot = _IRCBotAdapter(
server_name=self.name,
push_fn=self._push_message,
channels=self._channels,
on_connect_cmds=self._on_connect_cmds,
nick=self._nick,
server_list=server_list,
owner=self.owner,
realname=self._realname,
encoding=self._encoding,
**connect_params,
)
self._thread = threading.Thread(
target=self._bot.start,
daemon=True,
name="nemubot.IRC/" + self.name,
)
self._thread.start()
def _stop(self):
if self._bot:
self._bot.stop()
if self._thread:
self._thread.join(timeout=5)
# Outgoing messages
def send_response(self, response):
if response is None:
return
if isinstance(response, list):
for r in response:
self.send_response(r)
return
if not self._bot:
return
from nemubot.message.printer.IRCLib import IRCLib as IRCLibPrinter
printer = IRCLibPrinter(self._bot.connection)
response.accept(printer)
# subparse: re-parse a plain string in the context of an existing message
# (used by alias, rnd, grep, cat, smmry, sms modules)
def subparse(self, orig, cnt):
bot_nick = (self._bot.connection.get_nickname()
if self._bot else self._nick)
common = dict(
server=self.name,
to=orig.to,
to_response=orig.to_response,
frm=orig.frm,
frm_owner=orig.frm_owner,
date=orig.date,
)
text = cnt
if (text.startswith(bot_nick + ":") or
text.startswith(bot_nick + ",")):
inner = text[len(bot_nick) + 1:].strip()
return message.DirectAsk(designated=bot_nick, message=inner,
**common)
if len(text) > 1 and text[0] == '!':
inner = text[1:].strip()
try:
args = shlex.split(inner)
except ValueError:
args = inner.split()
if args:
kwargs = {}
while len(args) > 1:
arg = args[1]
if len(arg) > 2 and arg[0:2] == '\\@':
args[1] = arg[1:]
elif len(arg) > 1 and arg[0] == '@':
arsp = arg[1:].split("=", 1)
kwargs[arsp[0]] = arsp[1] if len(arsp) == 2 else None
args.pop(1)
continue
break
return message.Command(cmd=args[0], args=args[1:],
kwargs=kwargs, **common)
return message.Text(message=text, **common)

View file

@ -1,200 +0,0 @@
# Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2026 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import asyncio
import shlex
import threading
import nemubot.message as message
from nemubot.server.threaded import ThreadedServer
class Matrix(ThreadedServer):
"""Matrix server implementation using matrix-nio's AsyncClient.
Runs an asyncio event loop in a daemon thread. Incoming room messages are
converted to nemubot bot messages and pushed through the pipe; outgoing
responses are sent via the async client from the same event loop.
"""
def __init__(self, homeserver, user_id, password=None, access_token=None,
owner=None, nick=None, channels=None, **kwargs):
"""Prepare a connection to a Matrix homeserver.
Keyword arguments:
homeserver -- base URL of the homeserver, e.g. "https://matrix.org"
user_id -- full MXID (@user:server) or bare localpart
password -- login password (required if no access_token)
access_token -- pre-obtained access token (alternative to password)
owner -- MXID of the bot owner (marks frm_owner on messages)
nick -- display name / prefix for DirectAsk detection
channels -- list of room IDs / aliases to join on connect
"""
# Ensure fully-qualified MXID
if not user_id.startswith("@"):
host = homeserver.split("//")[-1].rstrip("/")
user_id = "@%s:%s" % (user_id, host)
super().__init__(name=user_id)
self.homeserver = homeserver
self.user_id = user_id
self.password = password
self.access_token = access_token
self.owner = owner
self.nick = nick or user_id
self._initial_rooms = channels or []
self._client = None
self._loop = None
self._thread = None
# Open/close
def _start(self):
self._thread = threading.Thread(
target=self._run_loop,
daemon=True,
name="nemubot.Matrix/" + self._name,
)
self._thread.start()
def _stop(self):
if self._client and self._loop and not self._loop.is_closed():
try:
asyncio.run_coroutine_threadsafe(
self._client.close(), self._loop
).result(timeout=5)
except Exception:
self._logger.exception("Error while closing Matrix client")
if self._thread:
self._thread.join(timeout=5)
# Asyncio thread
def _run_loop(self):
self._loop = asyncio.new_event_loop()
asyncio.set_event_loop(self._loop)
try:
self._loop.run_until_complete(self._async_main())
except Exception:
self._logger.exception("Unhandled exception in Matrix event loop")
finally:
self._loop.close()
async def _async_main(self):
from nio import AsyncClient, LoginError, RoomMessageText
self._client = AsyncClient(self.homeserver, self.user_id)
if self.access_token:
self._client.access_token = self.access_token
self._logger.info("Using provided access token for %s", self.user_id)
elif self.password:
resp = await self._client.login(self.password)
if isinstance(resp, LoginError):
self._logger.error("Matrix login failed: %s", resp.message)
return
self._logger.info("Logged in to Matrix as %s", self.user_id)
else:
self._logger.error("Need either password or access_token to connect")
return
self._client.add_event_callback(self._on_room_message, RoomMessageText)
for room in self._initial_rooms:
await self._client.join(room)
self._logger.info("Joined room %s", room)
await self._client.sync_forever(timeout=30000, full_state=True)
# Incoming messages
async def _on_room_message(self, room, event):
"""Callback invoked by matrix-nio for each m.room.message event."""
if event.sender == self.user_id:
return # ignore own messages
text = event.body
room_id = room.room_id
frm = event.sender
common_args = {
"server": self.name,
"to": [room_id],
"to_response": [room_id],
"frm": frm,
"frm_owner": frm == self.owner,
}
if len(text) > 1 and text[0] == '!':
text = text[1:].strip()
try:
args = shlex.split(text)
except ValueError:
args = text.split(' ')
msg = message.Command(cmd=args[0], args=args[1:], **common_args)
elif (text.lower().startswith(self.nick.lower() + ":")
or text.lower().startswith(self.nick.lower() + ",")):
text = text[len(self.nick) + 1:].strip()
msg = message.DirectAsk(designated=self.nick, message=text,
**common_args)
else:
msg = message.Text(message=text, **common_args)
self._push_message(msg)
# Outgoing messages
def send_response(self, response):
if response is None:
return
if isinstance(response, list):
for r in response:
self.send_response(r)
return
from nemubot.message.printer.Matrix import Matrix as MatrixPrinter
printer = MatrixPrinter(self._send_text)
response.accept(printer)
def _send_text(self, room_id, text):
"""Thread-safe: schedule a Matrix room_send on the asyncio loop."""
if not self._client or not self._loop or self._loop.is_closed():
self._logger.warning("Cannot send: Matrix client not ready")
return
future = asyncio.run_coroutine_threadsafe(
self._client.room_send(
room_id=room_id,
message_type="m.room.message",
content={"msgtype": "m.text", "body": text},
ignore_unverified_devices=True,
),
self._loop,
)
future.add_done_callback(
lambda f: self._logger.warning("Matrix send error: %s", f.exception())
if not f.cancelled() and f.exception() else None
)

View file

@ -22,15 +22,24 @@ def factory(uri, ssl=False, **init_args):
srv = None srv = None
if o.scheme == "irc" or o.scheme == "ircs": if o.scheme == "irc" or o.scheme == "ircs":
# https://www.w3.org/Addressing/draft-mirashi-url-irc-01.txt # http://www.w3.org/Addressing/draft-mirashi-url-irc-01.txt
# https://www-archive.mozilla.org/projects/rt-messaging/chatzilla/irc-urls.html # http://www-archive.mozilla.org/projects/rt-messaging/chatzilla/irc-urls.html
args = dict(init_args) args = init_args
if o.scheme == "ircs": ssl = True if o.scheme == "ircs": ssl = True
if o.hostname is not None: args["host"] = o.hostname if o.hostname is not None: args["host"] = o.hostname
if o.port is not None: args["port"] = o.port if o.port is not None: args["port"] = o.port
if o.username is not None: args["username"] = o.username if o.username is not None: args["username"] = o.username
if o.password is not None: args["password"] = unquote(o.password) if o.password is not None: args["password"] = o.password
if ssl:
try:
from ssl import create_default_context
args["_context"] = create_default_context()
except ImportError:
# Python 3.3 compat
from ssl import SSLContext, PROTOCOL_TLSv1
args["_context"] = SSLContext(PROTOCOL_TLSv1)
modifiers = o.path.split(",") modifiers = o.path.split(",")
target = unquote(modifiers.pop(0)[1:]) target = unquote(modifiers.pop(0)[1:])
@ -41,58 +50,28 @@ def factory(uri, ssl=False, **init_args):
if "msg" in params: if "msg" in params:
if "on_connect" not in args: if "on_connect" not in args:
args["on_connect"] = [] args["on_connect"] = []
args["on_connect"].append("PRIVMSG %s :%s" % (target, params["msg"][0])) args["on_connect"].append("PRIVMSG %s :%s" % (target, params["msg"]))
if "key" in params: if "key" in params:
if "channels" not in args: if "channels" not in args:
args["channels"] = [] args["channels"] = []
args["channels"].append((target, params["key"][0])) args["channels"].append((target, params["key"]))
if "pass" in params: if "pass" in params:
args["password"] = params["pass"][0] args["password"] = params["pass"]
if "charset" in params: if "charset" in params:
args["encoding"] = params["charset"][0] args["encoding"] = params["charset"]
#
if "channels" not in args and "isnick" not in modifiers: if "channels" not in args and "isnick" not in modifiers:
args["channels"] = [target] args["channels"] = [ target ]
args["ssl"] = ssl if ssl:
from nemubot.server.IRC import IRC_secure as SecureIRCServer
from nemubot.server.IRCLib import IRCLib as IRCServer srv = SecureIRCServer(**args)
srv = IRCServer(**args) else:
from nemubot.server.IRC import IRC as IRCServer
elif o.scheme == "matrix": srv = IRCServer(**args)
# matrix://localpart:password@homeserver.tld/!room:homeserver.tld
# matrix://localpart:password@homeserver.tld/%23alias:homeserver.tld
# Use matrixs:// for https (default) vs http
args = dict(init_args)
homeserver = "https://" + o.hostname
if o.port is not None:
homeserver += ":%d" % o.port
args["homeserver"] = homeserver
if o.username is not None:
args["user_id"] = o.username
if o.password is not None:
args["password"] = unquote(o.password)
# Parse rooms from path (comma-separated, URL-encoded)
if o.path and o.path != "/":
rooms = [unquote(r) for r in o.path.lstrip("/").split(",") if r]
if rooms:
args.setdefault("channels", []).extend(rooms)
params = parse_qs(o.query)
if "token" in params:
args["access_token"] = params["token"][0]
if "nick" in params:
args["nick"] = params["nick"][0]
if "owner" in params:
args["owner"] = params["owner"][0]
from nemubot.server.Matrix import Matrix as MatrixServer
srv = MatrixServer(**args)
return srv return srv

View file

@ -16,7 +16,6 @@
import logging import logging
import queue import queue
import traceback
from nemubot.bot import sync_act from nemubot.bot import sync_act
@ -25,18 +24,18 @@ class AbstractServer:
"""An abstract server: handle communication with an IM server""" """An abstract server: handle communication with an IM server"""
def __init__(self, name, fdClass, **kwargs): def __init__(self, name=None, **kwargs):
"""Initialize an abstract server """Initialize an abstract server
Keyword argument: Keyword argument:
name -- Identifier of the socket, for convinience name -- Identifier of the socket, for convinience
fdClass -- Class to instantiate as support file
""" """
self._name = name self._name = name
self._fd = fdClass(**kwargs)
self._logger = logging.getLogger("nemubot.server." + str(self.name)) super().__init__(**kwargs)
self.logger = logging.getLogger("nemubot.server." + str(self.name))
self._readbuffer = b'' self._readbuffer = b''
self._sending_queue = queue.Queue() self._sending_queue = queue.Queue()
@ -46,7 +45,7 @@ class AbstractServer:
if self._name is not None: if self._name is not None:
return self._name return self._name
else: else:
return self._fd.fileno() return self.fileno()
# Open/close # Open/close
@ -54,25 +53,25 @@ class AbstractServer:
def connect(self, *args, **kwargs): def connect(self, *args, **kwargs):
"""Register the server in _poll""" """Register the server in _poll"""
self._logger.info("Opening connection") self.logger.info("Opening connection")
self._fd.connect(*args, **kwargs) super().connect(*args, **kwargs)
self._on_connect() self._on_connect()
def _on_connect(self): def _on_connect(self):
sync_act("sckt", "register", self._fd.fileno()) sync_act("sckt", "register", self.fileno())
def close(self, *args, **kwargs): def close(self, *args, **kwargs):
"""Unregister the server from _poll""" """Unregister the server from _poll"""
self._logger.info("Closing connection") self.logger.info("Closing connection")
if self._fd.fileno() > 0: if self.fileno() > 0:
sync_act("sckt", "unregister", self._fd.fileno()) sync_act("sckt", "unregister", self.fileno())
self._fd.close(*args, **kwargs) super().close(*args, **kwargs)
# Writes # Writes
@ -85,15 +84,15 @@ class AbstractServer:
""" """
self._sending_queue.put(self.format(message)) self._sending_queue.put(self.format(message))
self._logger.debug("Message '%s' appended to write queue coming from %s:%d in %s", message, *traceback.extract_stack(limit=3)[0][:3]) self.logger.debug("Message '%s' appended to write queue", message)
sync_act("sckt", "write", self._fd.fileno()) sync_act("sckt", "write", self.fileno())
def async_write(self): def async_write(self):
"""Internal function used when the file descriptor is writable""" """Internal function used when the file descriptor is writable"""
try: try:
sync_act("sckt", "unwrite", self._fd.fileno()) sync_act("sckt", "unwrite", self.fileno())
while not self._sending_queue.empty(): while not self._sending_queue.empty():
self._write(self._sending_queue.get_nowait()) self._write(self._sending_queue.get_nowait())
self._sending_queue.task_done() self._sending_queue.task_done()
@ -159,9 +158,4 @@ class AbstractServer:
def exception(self, flags): def exception(self, flags):
"""Exception occurs on fd""" """Exception occurs on fd"""
self._fd.close() self.close()
# Proxy
def fileno(self):
return self._fd.fileno()

View file

@ -0,0 +1,59 @@
# 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 socket
import unittest
from nemubot.server import factory
class TestFactory(unittest.TestCase):
def test_IRC1(self):
from nemubot.server.IRC import IRC as IRCServer
from nemubot.server.IRC import IRC_secure as IRCSServer
# <host>: If omitted, the client must connect to a prespecified default IRC server.
server = factory("irc:///")
self.assertIsInstance(server, IRCServer)
self.assertEqual(server._sockaddr,
socket.getaddrinfo("localhost", 6667, proto=socket.IPPROTO_TCP)[0][4])
server = factory("ircs:///")
self.assertIsInstance(server, IRCSServer)
self.assertEqual(server._sockaddr,
socket.getaddrinfo("localhost", 6667, proto=socket.IPPROTO_TCP)[0][4])
server = factory("irc://freenode.net")
self.assertIsInstance(server, IRCServer)
self.assertEqual(server._sockaddr,
socket.getaddrinfo("freenode.net", 6667, proto=socket.IPPROTO_TCP)[0][4])
server = factory("irc://freenode.org:1234")
self.assertIsInstance(server, IRCServer)
self.assertEqual(server._sockaddr,
socket.getaddrinfo("freenode.org", 1234, proto=socket.IPPROTO_TCP)[0][4])
server = factory("ircs://nemunai.re:194/")
self.assertIsInstance(server, IRCSServer)
self.assertEqual(server._sockaddr,
socket.getaddrinfo("nemunai.re", 194, proto=socket.IPPROTO_TCP)[0][4])
with self.assertRaises(socket.gaierror):
factory("irc://_nonexistent.nemunai.re")
if __name__ == '__main__':
unittest.main()

View file

@ -0,0 +1,210 @@
# 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 datetime import datetime, timezone
import re
import shlex
import nemubot.message as message
from nemubot.server.message.abstract import Abstract
mgx = re.compile(b'''^(?:@(?P<tags>[^ ]+)\ )?
(?::(?P<prefix>
(?P<nick>[^!@ ]+)
(?: !(?P<user>[^@ ]+))?
(?:@(?P<host>[^ ]*))?
)\ )?
(?P<command>(?:[a-zA-Z]+|[0-9]{3}))
(?P<params>(?:\ [^:][^ ]*)*)(?:\ :(?P<trailing>.*))?
$''', re.X)
class IRC(Abstract):
"""Class responsible for parsing IRC messages"""
def __init__(self, raw, encoding="utf-8"):
self.encoding = encoding
self.tags = { 'time': datetime.now(timezone.utc) }
self.params = list()
p = mgx.match(raw.rstrip())
if p is None:
raise Exception("Not a valid IRC message: %s" % raw)
# Parse tags if exists: @aaa=bbb;ccc;example.com/ddd=eee
if p.group("tags"):
for tgs in self.decode(p.group("tags")).split(';'):
tag = tgs.split('=')
if len(tag) > 1:
self.add_tag(tag[0], tag[1])
else:
self.add_tag(tag[0])
# Parse prefix if exists: :nick!user@host.com
self.prefix = self.decode(p.group("prefix"))
self.nick = self.decode(p.group("nick"))
self.user = self.decode(p.group("user"))
self.host = self.decode(p.group("host"))
# Parse command
self.cmd = self.decode(p.group("command"))
# Parse params
if p.group("params") is not None and p.group("params") != b'':
for param in p.group("params").strip().split(b' '):
self.params.append(param)
if p.group("trailing") is not None:
self.params.append(p.group("trailing"))
def add_tag(self, key, value=None):
"""Add an IRCv3.2 Message Tags
Arguments:
key -- tag identifier (unique for the message)
value -- optional value for the tag
"""
# Treat special tags
if key == "time" and value is not None:
import calendar, time
value = datetime.fromtimestamp(calendar.timegm(time.strptime(value, "%Y-%m-%dT%H:%M:%S.%fZ")), timezone.utc)
# Store tag
self.tags[key] = value
@property
def is_ctcp(self):
"""Analyze a message, to determine if this is a CTCP one"""
return self.cmd == "PRIVMSG" and len(self.params) == 2 and len(self.params[1]) > 1 and (self.params[1][0] == 0x01 or self.params[1][1] == 0x01)
def decode(self, s):
"""Decode the content string usign a specific encoding
Argument:
s -- string to decode
"""
if isinstance(s, bytes):
try:
s = s.decode()
except UnicodeDecodeError:
s = s.decode(self.encoding, 'replace')
return s
def to_server_string(self, client=True):
"""Pretty print the message to close to original input string
Keyword argument:
client -- export as a client-side string if true
"""
res = ";".join(["@%s=%s" % (k, v if not isinstance(v, datetime) else v.strftime("%Y-%m-%dT%H:%M:%S.%fZ")) for k, v in self.tags.items()])
if not client:
res += " :%s!%s@%s" % (self.nick, self.user, self.host)
res += " " + self.cmd
if len(self.params) > 0:
if len(self.params) > 1:
res += " " + self.decode(b" ".join(self.params[:-1]))
res += " :" + self.decode(self.params[-1])
return res
def to_bot_message(self, srv):
"""Convert to one of concrete implementation of AbstractMessage
Argument:
srv -- the server from the message was received
"""
if self.cmd == "PRIVMSG" or self.cmd == "NOTICE":
receivers = self.decode(self.params[0]).split(',')
common_args = {
"server": srv.name,
"date": self.tags["time"],
"to": receivers,
"to_response": [r if r != srv.nick else self.nick for r in receivers],
"frm": self.nick,
"frm_owner": self.nick == srv.owner
}
# If CTCP, remove 0x01
if self.is_ctcp:
text = self.decode(self.params[1][1:len(self.params[1])-1])
else:
text = self.decode(self.params[1])
if text.find(srv.nick) == 0 and len(text) > len(srv.nick) + 2 and text[len(srv.nick)] == ":":
designated = srv.nick
text = text[len(srv.nick) + 1:].strip()
else:
designated = None
# Is this a command?
if len(text) > 1 and text[0] == '!':
text = text[1:].strip()
# Split content by words
try:
args = shlex.split(text)
except ValueError:
args = text.split(' ')
# Extract explicit named arguments: @key=value or just @key, only at begening
kwargs = {}
while len(args) > 1:
arg = args[1]
if len(arg) > 2:
if arg[0:2] == '\\@':
args[1] = arg[1:]
elif arg[0] == '@':
arsp = arg[1:].split("=", 1)
if len(arsp) == 2:
kwargs[arsp[0]] = arsp[1]
else:
kwargs[arg[1:]] = None
args.pop(1)
continue
# Futher argument are considered as normal argument (this helps for subcommand treatment)
break
return message.Command(cmd=args[0],
args=args[1:],
kwargs=kwargs,
**common_args)
# Is this an ask for this bot?
elif designated is not None:
return message.DirectAsk(designated=designated, message=text, **common_args)
# Normal message
else:
return message.Text(message=text, **common_args)
return None

View file

@ -0,0 +1,15 @@
# 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/>.

View file

@ -0,0 +1,33 @@
# 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 to_bot_message(self, srv):
"""Convert to one of concrete implementation of AbstractMessage
Argument:
srv -- the server from the message was received
"""
raise NotImplemented
def to_server_string(self, **kwargs):
"""Pretty print the message to close to original input string
"""
raise NotImplemented

View file

@ -16,6 +16,7 @@
import os import os
import socket import socket
import ssl
import nemubot.message as message import nemubot.message as message
from nemubot.message.printer.socket import Socket as SocketPrinter from nemubot.message.printer.socket import Socket as SocketPrinter
@ -39,7 +40,7 @@ class _Socket(AbstractServer):
# Write # Write
def _write(self, cnt): def _write(self, cnt):
self._fd.sendall(cnt) self.sendall(cnt)
def format(self, txt): def format(self, txt):
@ -51,8 +52,8 @@ class _Socket(AbstractServer):
# Read # Read
def read(self, bufsize=1024, *args, **kwargs): def recv(self, n=1024):
return self._fd.recv(bufsize, *args, **kwargs) return super().recv(n)
def parse(self, line): def parse(self, line):
@ -66,7 +67,7 @@ class _Socket(AbstractServer):
args = line.split(' ') args = line.split(' ')
if len(args): if len(args):
yield message.Command(cmd=args[0], args=args[1:], server=self._fd.fileno(), to=["you"], frm="you") yield message.Command(cmd=args[0], args=args[1:], server=self.fileno(), to=["you"], frm="you")
def subparse(self, orig, cnt): def subparse(self, orig, cnt):
@ -77,43 +78,53 @@ class _Socket(AbstractServer):
yield m yield m
class SocketServer(_Socket): class _SocketServer(_Socket):
def __init__(self, host, port, bind=None, trynb=0, **kwargs): def __init__(self, host, port, bind=None, **kwargs):
destlist = socket.getaddrinfo(host, port, proto=socket.IPPROTO_TCP) (family, type, proto, canonname, sockaddr) = socket.getaddrinfo(host, port, proto=socket.IPPROTO_TCP)[0]
(family, type, proto, canonname, self._sockaddr) = destlist[trynb%len(destlist)]
super().__init__(fdClass=socket.socket, family=family, type=type, proto=proto, **kwargs) if isinstance(self, ssl.SSLSocket) and "server_hostname" not in kwargs:
kwargs["server_hostname"] = host
super().__init__(family=family, type=type, proto=proto, **kwargs)
self._sockaddr = sockaddr
self._bind = bind self._bind = bind
def connect(self): def connect(self):
self._logger.info("Connecting to %s:%d", *self._sockaddr[:2]) self.logger.info("Connection to %s:%d", *self._sockaddr[:2])
super().connect(self._sockaddr) super().connect(self._sockaddr)
self._logger.info("Connected to %s:%d", *self._sockaddr[:2])
if self._bind: if self._bind:
self._fd.bind(self._bind) super().bind(self._bind)
class SocketServer(_SocketServer, socket.socket):
pass
class SecureSocketServer(_SocketServer, ssl.SSLSocket):
pass
class UnixSocket: class UnixSocket:
def __init__(self, location, **kwargs): def __init__(self, location, **kwargs):
super().__init__(fdClass=socket.socket, family=socket.AF_UNIX, **kwargs) super().__init__(family=socket.AF_UNIX, **kwargs)
self._socket_path = location self._socket_path = location
def connect(self): def connect(self):
self._logger.info("Connection to unix://%s", self._socket_path) self.logger.info("Connection to unix://%s", self._socket_path)
self.connect(self._socket_path) super().connect(self._socket_path)
class SocketClient(_Socket): class SocketClient(_Socket, socket.socket):
def __init__(self, **kwargs): def read(self):
super().__init__(fdClass=socket.socket, **kwargs) return self.recv()
class _Listener: class _Listener:
@ -126,9 +137,9 @@ class _Listener:
def read(self): def read(self):
conn, addr = self._fd.accept() conn, addr = self.accept()
fileno = conn.fileno() fileno = conn.fileno()
self._logger.info("Accept new connection from %s (fd=%d)", addr, fileno) self.logger.info("Accept new connection from %s (fd=%d)", addr, fileno)
ss = self._instanciate(name=self.name + "#" + str(fileno), fileno=conn.detach()) ss = self._instanciate(name=self.name + "#" + str(fileno), fileno=conn.detach())
ss.connect = ss._on_connect ss.connect = ss._on_connect
@ -137,19 +148,23 @@ class _Listener:
return b'' return b''
class UnixSocketListener(_Listener, UnixSocket, _Socket): class UnixSocketListener(_Listener, UnixSocket, _Socket, socket.socket):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def connect(self): def connect(self):
self._logger.info("Creating Unix socket at unix://%s", self._socket_path) self.logger.info("Creating Unix socket at unix://%s", self._socket_path)
try: try:
os.remove(self._socket_path) os.remove(self._socket_path)
except FileNotFoundError: except FileNotFoundError:
pass pass
self._fd.bind(self._socket_path) self.bind(self._socket_path)
self._fd.listen(5) self.listen(5)
self._logger.info("Socket ready for accepting new connections") self.logger.info("Socket ready for accepting new connections")
self._on_connect() self._on_connect()
@ -159,7 +174,7 @@ class UnixSocketListener(_Listener, UnixSocket, _Socket):
import socket import socket
try: try:
self._fd.shutdown(socket.SHUT_RDWR) self.shutdown(socket.SHUT_RDWR)
except socket.error: except socket.error:
pass pass

View file

@ -0,0 +1,50 @@
import unittest
import nemubot.server.IRC as IRC
class TestIRCMessage(unittest.TestCase):
def setUp(self):
self.msg = IRC.IRCMessage(b":toto!titi@RZ-3je16g.re PRIVMSG #the-channel :Can you parse this message?")
def test_parsing(self):
self.assertEqual(self.msg.prefix, "toto!titi@RZ-3je16g.re")
self.assertEqual(self.msg.nick, "toto")
self.assertEqual(self.msg.user, "titi")
self.assertEqual(self.msg.host, "RZ-3je16g.re")
self.assertEqual(len(self.msg.params), 2)
self.assertEqual(self.msg.params[0], b"#the-channel")
self.assertEqual(self.msg.params[1], b"Can you parse this message?")
def test_prettyprint(self):
bst1 = self.msg.to_server_string(False)
msg2 = IRC.IRCMessage(bst1.encode())
bst2 = msg2.to_server_string(False)
msg3 = IRC.IRCMessage(bst2.encode())
bst3 = msg3.to_server_string(False)
self.assertEqual(bst2, bst3)
def test_tags(self):
self.assertEqual(len(self.msg.tags), 1)
self.assertIn("time", self.msg.tags)
self.msg.add_tag("time")
self.assertEqual(len(self.msg.tags), 1)
self.msg.add_tag("toto")
self.assertEqual(len(self.msg.tags), 2)
self.assertIn("toto", self.msg.tags)
if __name__ == '__main__':
unittest.main()

View file

@ -1,132 +0,0 @@
# Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2026 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
import os
import queue
from nemubot.bot import sync_act
class ThreadedServer:
"""A server backed by a library running in its own thread.
Uses an os.pipe() as a fake file descriptor to integrate with the bot's
select.poll() main loop without requiring direct socket access.
When the library thread has a message ready, it calls _push_message(),
which writes a wakeup byte to the pipe's write end. The bot's poll loop
sees the read end become readable, calls async_read(), which drains the
message queue and yields already-parsed bot-level messages.
This abstraction lets any IM library (IRC via python-irc, Matrix via
matrix-nio, ) plug into nemubot without touching bot.py.
"""
def __init__(self, name):
self._name = name
self._logger = logging.getLogger("nemubot.server." + name)
self._queue = queue.Queue()
self._pipe_r, self._pipe_w = os.pipe()
@property
def name(self):
return self._name
def fileno(self):
return self._pipe_r
# Open/close
def connect(self):
"""Start the library and register the pipe read-end with the poll loop."""
self._logger.info("Starting connection")
self._start()
sync_act("sckt", "register", self._pipe_r)
def _start(self):
"""Override: start the library's connection (e.g. launch a thread)."""
raise NotImplementedError
def close(self):
"""Unregister from poll, stop the library, and close the pipe."""
self._logger.info("Closing connection")
sync_act("sckt", "unregister", self._pipe_r)
self._stop()
for fd in (self._pipe_w, self._pipe_r):
try:
os.close(fd)
except OSError:
pass
def _stop(self):
"""Override: stop the library thread gracefully."""
pass
# Writes
def send_response(self, response):
"""Override: send a response via the underlying library."""
raise NotImplementedError
def async_write(self):
"""No-op: writes go directly through the library, not via poll."""
pass
# Read
def _push_message(self, msg):
"""Called from the library thread to enqueue a bot-level message.
Writes a wakeup byte to the pipe so the main loop wakes up and
calls async_read().
"""
self._queue.put(msg)
try:
os.write(self._pipe_w, b'\x00')
except OSError:
pass # pipe closed during shutdown
def async_read(self):
"""Called by the bot when the pipe is readable.
Drains the wakeup bytes and yields all queued bot messages.
"""
try:
os.read(self._pipe_r, 256)
except OSError:
return
while not self._queue.empty():
try:
yield self._queue.get_nowait()
except queue.Empty:
break
def parse(self, msg):
"""Messages pushed via _push_message are already bot-level — pass through."""
yield msg
# Exceptions
def exception(self, flags):
"""Called by the bot on POLLERR/POLLHUP/POLLNVAL."""
self._logger.warning("Exception on server %s: flags=0x%x", self._name, flags)

View file

@ -82,16 +82,11 @@ class RSSEntry:
else: else:
self.summary = None self.summary = None
if len(node.getElementsByTagName("link")) > 0: if len(node.getElementsByTagName("link")) > 0 and node.getElementsByTagName("link")[0].hasAttribute("href"):
self.link = node.getElementsByTagName("link")[0].firstChild.nodeValue self.link = node.getElementsByTagName("link")[0].getAttribute("href")
else: else:
self.link = None self.link = None
if len(node.getElementsByTagName("enclosure")) > 0 and node.getElementsByTagName("enclosure")[0].hasAttribute("url"):
self.enclosure = node.getElementsByTagName("enclosure")[0].getAttribute("url")
else:
self.enclosure = None
def __repr__(self): def __repr__(self):
return "<RSSEntry title='%s' updated='%s'>" % (self.title, self.pubDate) return "<RSSEntry title='%s' updated='%s'>" % (self.title, self.pubDate)
@ -110,13 +105,13 @@ class Feed:
self.updated = None self.updated = None
self.entries = list() self.entries = list()
if self.feed.tagName == "rdf:RDF" or self.feed.tagName == "rss": if self.feed.tagName == "rss":
self._parse_rss_feed() self._parse_rss_feed()
elif self.feed.tagName == "feed": elif self.feed.tagName == "feed":
self._parse_atom_feed() self._parse_atom_feed()
else: else:
from nemubot.exception import IMException from nemubot.exception import IMException
raise IMException("This is not a valid Atom or RSS feed.") raise IMException("This is not a valid Atom or RSS feed")
def _parse_atom_feed(self): def _parse_atom_feed(self):

View file

@ -1,6 +1,5 @@
import unittest import unittest
import io
import xml.parsers.expat import xml.parsers.expat
from nemubot.tools.xmlparser import XMLParser from nemubot.tools.xmlparser import XMLParser
@ -13,11 +12,6 @@ class StringNode():
def characters(self, content): def characters(self, content):
self.string += content self.string += content
def saveElement(self, store, tag="string"):
store.startElement(tag, {})
store.characters(self.string)
store.endElement(tag)
class TestNode(): class TestNode():
def __init__(self, option=None): def __init__(self, option=None):
@ -28,15 +22,6 @@ class TestNode():
self.mystr = child.string self.mystr = child.string
return True return True
def saveElement(self, store, tag="test"):
store.startElement(tag, {"option": self.option})
strNode = StringNode()
strNode.string = self.mystr
strNode.saveElement(store)
store.endElement(tag)
class Test2Node(): class Test2Node():
def __init__(self, option=None): def __init__(self, option=None):
@ -48,15 +33,6 @@ class Test2Node():
self.mystrs.append(attrs["value"]) self.mystrs.append(attrs["value"])
return True return True
def saveElement(self, store, tag="test"):
store.startElement(tag, {"option": self.option} if self.option is not None else {})
for mystr in self.mystrs:
store.startElement("string", {"value": mystr})
store.endElement("string")
store.endElement(tag)
class TestXMLParser(unittest.TestCase): class TestXMLParser(unittest.TestCase):
@ -68,11 +44,9 @@ class TestXMLParser(unittest.TestCase):
p.CharacterDataHandler = mod.characters p.CharacterDataHandler = mod.characters
p.EndElementHandler = mod.endElement p.EndElementHandler = mod.endElement
inputstr = "<string>toto</string>" p.Parse("<string>toto</string>", 1)
p.Parse(inputstr, 1)
self.assertEqual(mod.root.string, "toto") self.assertEqual(mod.root.string, "toto")
self.assertEqual(mod.saveDocument(header=False).getvalue(), inputstr)
def test_parser2(self): def test_parser2(self):
@ -83,12 +57,10 @@ class TestXMLParser(unittest.TestCase):
p.CharacterDataHandler = mod.characters p.CharacterDataHandler = mod.characters
p.EndElementHandler = mod.endElement p.EndElementHandler = mod.endElement
inputstr = '<test option="123"><string>toto</string></test>' p.Parse("<test option='123'><string>toto</string></test>", 1)
p.Parse(inputstr, 1)
self.assertEqual(mod.root.option, "123") self.assertEqual(mod.root.option, "123")
self.assertEqual(mod.root.mystr, "toto") self.assertEqual(mod.root.mystr, "toto")
self.assertEqual(mod.saveDocument(header=False).getvalue(), inputstr)
def test_parser3(self): def test_parser3(self):
@ -99,14 +71,12 @@ class TestXMLParser(unittest.TestCase):
p.CharacterDataHandler = mod.characters p.CharacterDataHandler = mod.characters
p.EndElementHandler = mod.endElement p.EndElementHandler = mod.endElement
inputstr = '<test><string value="toto"/><string value="toto2"/></test>' p.Parse("<test><string value='toto' /><string value='toto2' /></test>", 1)
p.Parse(inputstr, 1)
self.assertEqual(mod.root.option, None) self.assertEqual(mod.root.option, None)
self.assertEqual(len(mod.root.mystrs), 2) self.assertEqual(len(mod.root.mystrs), 2)
self.assertEqual(mod.root.mystrs[0], "toto") self.assertEqual(mod.root.mystrs[0], "toto")
self.assertEqual(mod.root.mystrs[1], "toto2") self.assertEqual(mod.root.mystrs[1], "toto2")
self.assertEqual(mod.saveDocument(header=False, short_empty_elements=True).getvalue(), inputstr)
if __name__ == '__main__': if __name__ == '__main__':

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