Compare commits

...

17 commits

Author SHA1 Message Date
d84bf36ca0 WIP 2017-08-20 20:46:01 +02:00
4e8504bd1d xmlparser: Implement update method, as in dict 2017-08-20 20:46:01 +02:00
6b4a9a2e4a start working on NNTP module 2017-08-20 20:46:01 +02:00
db1e4e9266 disas: new module, aim to disassemble binary code. Closing #67 2017-08-11 12:30:08 +02:00
709128b7aa freetarifs: new module 2017-08-11 12:30:08 +02:00
55a8f74900 suivi: support USPS 2017-08-10 18:10:35 +02:00
92702f3995 suivi: support DHL 2017-08-10 18:10:35 +02:00
990599551c suivi: fix error handling of fedex parcel 2017-08-10 18:10:35 +02:00
7a52748849 pkgs: new module to display quick information about common softwares 2017-08-10 18:10:35 +02:00
ef4a6e9af5 Fix module unloading 2017-07-30 17:41:16 +02:00
9d0ab88c12 Store module into weakref 2017-07-30 17:41:16 +02:00
76bea2bc15 datastore/xml: handle entire file save and be closer with new nemubot XML API 2017-07-30 17:40:36 +02:00
c8afa65dcb tools/xmlparser: implement writer 2017-07-30 16:45:45 +02:00
7eac685a0a openroute: new module providing geocode and direction instructions
Closing issue #46
2017-07-30 12:28:37 +02:00
bc183bcfa0 tools/web: new option decode_error to decode non-200 page content (useful on REST API) 2017-07-26 10:57:09 +02:00
0d52fff64a tools/web: display socket timeout 2017-07-26 10:56:55 +02:00
2938287869 cve: update and clean module, following NIST website changes 2017-07-26 10:56:27 +02:00
15 changed files with 784 additions and 52 deletions

View file

@ -10,7 +10,7 @@ from nemubot.tools.web import getURLContent, striphtml
from more import Response from more import Response
BASEURL_NIST = 'https://web.nvd.nist.gov/view/vuln/detail?vulnId=' BASEURL_NIST = 'https://nvd.nist.gov/vuln/detail/'
# MODULE CORE ######################################################### # MODULE CORE #########################################################
@ -19,15 +19,40 @@ 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]
return [ return {
"Base score: " + cvss.findAll('div')[0].findAll('a')[0].text.strip(), "description": soup.body.find(attrs={"data-testid":"vuln-description"}).text.strip(),
vuln.findAll('p')[0].text, # description "published": soup.body.find(attrs={"data-testid":"vuln-published-on"}).text.strip(),
striphtml(vuln.findAll('div')[0].text).strip(), # publication date "last_modified": soup.body.find(attrs={"data-testid":"vuln-last-modified-on"}).text.strip(),
striphtml(vuln.findAll('div')[1].text).strip(), # last revised "source": soup.body.find(attrs={"data-testid":"vuln-source"}).text.strip(),
]
"base_score": float(soup.body.find(attrs={"data-testid":"vuln-cvssv3-base-score-link"}).text.strip()),
"severity": soup.body.find(attrs={"data-testid":"vuln-cvssv3-base-score-severity"}).text.strip(),
"impact_score": float(soup.body.find(attrs={"data-testid":"vuln-cvssv3-impact-score"}).text.strip()),
"exploitability_score": float(soup.body.find(attrs={"data-testid":"vuln-cvssv3-exploitability-score"}).text.strip()),
"av": soup.body.find(attrs={"data-testid":"vuln-cvssv3-av"}).text.strip(),
"ac": soup.body.find(attrs={"data-testid":"vuln-cvssv3-ac"}).text.strip(),
"pr": soup.body.find(attrs={"data-testid":"vuln-cvssv3-pr"}).text.strip(),
"ui": soup.body.find(attrs={"data-testid":"vuln-cvssv3-ui"}).text.strip(),
"s": soup.body.find(attrs={"data-testid":"vuln-cvssv3-s"}).text.strip(),
"c": soup.body.find(attrs={"data-testid":"vuln-cvssv3-c"}).text.strip(),
"i": soup.body.find(attrs={"data-testid":"vuln-cvssv3-i"}).text.strip(),
"a": soup.body.find(attrs={"data-testid":"vuln-cvssv3-a"}).text.strip(),
}
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 ####################################################
@ -42,6 +67,10 @@ 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
res.append_message(get_cve(cve_id)) cve = get_cve(cve_id)
metrics = display_metrics(**cve)
res.append_message("{cveid}: Base score: \x02{base_score} {severity}\x0F (impact: \x02{impact_score}\x0F, exploitability: \x02{exploitability_score}\x0F; {metrics}), from \x02{source}\x0F, last modified on \x02{last_modified}\x0F. {description}".format(cveid=cve_id, metrics=metrics, **cve))
return res return res
print(get_cve("CVE-2017-11108"))

85
modules/disas.py Normal file
View file

@ -0,0 +1,85 @@
"""The Ultimate Disassembler Module"""
# PYTHON STUFFS #######################################################
import capstone
from nemubot.exception import IMException
from nemubot.hooks import hook
from 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 "architecture" in msg.kwargs:
if msg.kwargs["architecture"] not in ARCHITECTURES:
raise IMException("unknown architectures '%s'" % msg.kwargs["architecture"])
architecture = ARCHITECTURES[msg.kwargs["architecture"]]
else:
architecture = capstone.CS_ARCH_X86
# Determine hardware modes
if "modes" in msg.kwargs:
modes = 0
for mode in msg.kwargs["modes"].split(','):
if mode not in MODES:
raise IMException("unknown mode '%s'" % mode)
modes += MODES[mode]
else:
modes = capstone.CS_MODE_32
# 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

64
modules/freetarifs.py Normal file
View file

@ -0,0 +1,64 @@
"""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 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

209
modules/nntp.py Normal file
View file

@ -0,0 +1,209 @@
"""The NNTP module"""
# PYTHON STUFFS #######################################################
import email
from email.utils import mktime_tz, parseaddr, parsedate_tz
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 more import Response
# LOADING #############################################################
def load(context):
for wn in context.data.getNodes("watched_newsgroup"):
watch(**wn)
# 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))
def whatsnew(date_last_check, group="*", **server):
with NNTP(**server) as srv:
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:
response, info = srv.article(msg_id)
yield email.message_from_bytes(b"\r\n".join(info.lines))
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):
return "{user}:{password}@{host}:{port}".format(**kwargs)
def _newevt(*args):
context.add_event(ModuleEvent(call=_fini, call_data=args, interval=42))
def _fini(lastcheck, server):
_newevt(datetime.now(), server)
n = 0
for art in whatsnew(lastcheck, 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="*", lastcheck=None, **server):
idsrv = _indexServer(**server)
if lastcheck is None:
lastcheck = datetime.now()
if idsrv not in watches:
wnnode = ModuleState("watched_newsgroup")
wnnode.setIndex("group")
wnnode["id"] = idsrv
wnnode.update(server)
context.data.addChild(wnnode)
_newevt(lastcheck, server)
else:
wnnode = context.data.index[idsrv]
if group not in wnnode:
ngnode = ModuleState("notify_group")
ngnode["group"] = group
wnnode.addChild(ngnode)
else:
ngnode = wnnode.index[group]
# Ensure this watch is not already registered
watches[idsrv][group].append((to_server, to_channel))
# 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")
print(type(msg))
if not msg.frm_owner:
raise IMException("sorry, this command is currently limited to the owner")
wnnode = ModuleState("watched_newsgroup")
context.data.addChild(wnnode)
watch(msg.server, msg.channel, msg.args[0] if len(msg.args) > 0 else "*", **msg.kwargs)
return Response("Ok ok, I watch this newsgroup!", channel=msg.channel)

158
modules/openroute.py Normal file
View file

@ -0,0 +1,158 @@
"""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 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

68
modules/pkgs.py Normal file
View file

@ -0,0 +1,68 @@
"""Get information about common software"""
# PYTHON STUFFS #######################################################
import portage
from nemubot import context
from nemubot.exception import IMException
from nemubot.hooks import hook
from 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

@ -126,6 +126,24 @@ 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(class_="tracking_history")
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({
@ -156,11 +174,22 @@ 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"]) len(fedex_data["TrackPackagesResponse"]["packageList"]) and
not fedex_data["TrackPackagesResponse"]["errorList"][0]["code"] 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):
@ -195,6 +224,13 @@ 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 is {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_colissimo(tracknum): def handle_colissimo(tracknum):
info = get_colissimo_info(tracknum) info = get_colissimo_info(tracknum)
if info: if info:
@ -229,6 +265,12 @@ 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,
@ -237,6 +279,8 @@ 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,
} }

View file

@ -20,6 +20,7 @@ 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
@ -99,15 +100,15 @@ class Bot(threading.Thread):
from 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 msg.args[0] in self.modules: if msg.args[0] in self.modules and self.modules[msg.args[0]]() is not None:
if hasattr(self.modules[msg.args[0]], "help_full"): if hasattr(self.modules[msg.args[0]](), "help_full"):
hlp = self.modules[msg.args[0]].help_full() hlp = self.modules[msg.args[0]]().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[msg.args[0]].__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:])):
@ -137,7 +138,7 @@ class Bot(threading.Thread):
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].__doc__]) 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__])
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")
@ -518,18 +519,20 @@ class Bot(threading.Thread):
raise raise
# Save a reference to the module # Save a reference to the module
self.modules[module_name] = module self.modules[module_name] = weakref.ref(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: if name in self.modules and self.modules[name]() is not None:
self.modules[name].print("Unloading module %s" % name) module = self.modules[name]()
module.print("Unloading module %s" % name)
# Call the user defined unload method # Call the user defined unload method
if hasattr(self.modules[name], "unload"): if hasattr(module, "unload"):
self.modules[name].unload(self) module.unload(self)
self.modules[name].__nemubot_context__.unload() module.__nemubot_context__.unload()
# Remove from the nemubot dict # Remove from the nemubot dict
del self.modules[name] del self.modules[name]
@ -566,7 +569,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 self.modules.items(): for mod in [m for m in self.modules.keys()]:
self.unload_module(mod) self.unload_module(mod)
logger.info("Close all servers connection...") logger.info("Close all servers connection...")

View file

@ -143,4 +143,15 @@ class XML(Abstract):
if self.rotate: if self.rotate:
self._rotate(path) self._rotate(path)
return data.save(path) import tempfile
_, tmpath = tempfile.mkstemp()
with open(tmpath, "w") as f:
import xml.sax.saxutils
gen = xml.sax.saxutils.XMLGenerator(f, "utf-8")
gen.startDocument()
data.saveElement(gen)
gen.endDocument()
# Atomic save
import shutil
shutil.move(tmpath, path)

View file

@ -1,5 +1,6 @@
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
@ -12,6 +13,11 @@ 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):
@ -22,6 +28,15 @@ 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):
@ -33,6 +48,15 @@ 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):
@ -44,9 +68,11 @@ class TestXMLParser(unittest.TestCase):
p.CharacterDataHandler = mod.characters p.CharacterDataHandler = mod.characters
p.EndElementHandler = mod.endElement p.EndElementHandler = mod.endElement
p.Parse("<string>toto</string>", 1) inputstr = "<string>toto</string>"
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):
@ -57,10 +83,12 @@ class TestXMLParser(unittest.TestCase):
p.CharacterDataHandler = mod.characters p.CharacterDataHandler = mod.characters
p.EndElementHandler = mod.endElement p.EndElementHandler = mod.endElement
p.Parse("<test option='123'><string>toto</string></test>", 1) inputstr = '<test option="123"><string>toto</string></test>'
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):
@ -71,12 +99,14 @@ class TestXMLParser(unittest.TestCase):
p.CharacterDataHandler = mod.characters p.CharacterDataHandler = mod.characters
p.EndElementHandler = mod.endElement p.EndElementHandler = mod.endElement
p.Parse("<test><string value='toto' /><string value='toto2' /></test>", 1) inputstr = '<test><string value="toto"/><string value="toto2"/></test>'
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__':

View file

@ -15,6 +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 urllib.parse import urljoin, urlparse, urlsplit, urlunsplit from urllib.parse import urljoin, urlparse, urlsplit, urlunsplit
import socket
from nemubot.exception import IMException from nemubot.exception import IMException
@ -67,13 +68,14 @@ def getPassword(url):
# Get real pages # Get real pages
def getURLContent(url, body=None, timeout=7, header=None): def getURLContent(url, body=None, timeout=7, header=None, decode_error=False):
"""Return page content corresponding to URL or None if any error occurs """Return page content corresponding to URL or None if any error occurs
Arguments: Arguments:
url -- the URL to get url -- the URL to get
body -- Data to send as POST content body -- Data to send as POST content
timeout -- maximum number of seconds to wait before returning an exception timeout -- maximum number of seconds to wait before returning an exception
decode_error -- raise exception on non-200 pages or ignore it
""" """
o = urlparse(_getNormalizedURL(url), "http") o = urlparse(_getNormalizedURL(url), "http")
@ -123,6 +125,8 @@ def getURLContent(url, body=None, timeout=7, header=None):
o.path, o.path,
body, body,
header) header)
except socket.timeout as e:
raise IMException(e)
except OSError as e: except OSError as e:
raise IMException(e.strerror) raise IMException(e.strerror)
@ -163,7 +167,10 @@ def getURLContent(url, body=None, timeout=7, header=None):
urljoin(url, res.getheader("Location")), urljoin(url, res.getheader("Location")),
body=body, body=body,
timeout=timeout, timeout=timeout,
header=header) header=header,
decode_error=decode_error)
elif decode_error:
return data.decode(charset).strip()
else: else:
raise IMException("A HTTP error occurs: %d - %s" % raise IMException("A HTTP error occurs: %d - %s" %
(res.status, http.client.responses[res.status])) (res.status, http.client.responses[res.status]))

View file

@ -134,6 +134,21 @@ class XMLParser:
return return
raise TypeError(name + " tag not expected in " + self.display_stack()) raise TypeError(name + " tag not expected in " + self.display_stack())
def saveDocument(self, f=None, header=True, short_empty_elements=False):
if f is None:
import io
f = io.StringIO()
import xml.sax.saxutils
gen = xml.sax.saxutils.XMLGenerator(f, "utf-8", short_empty_elements=short_empty_elements)
if header:
gen.startDocument()
self.root.saveElement(gen)
if header:
gen.endDocument()
return f
def parse_file(filename): def parse_file(filename):
p = xml.parsers.expat.ParserCreate() p = xml.parsers.expat.ParserCreate()

View file

@ -44,6 +44,13 @@ class ListNode:
return self.items.__repr__() return self.items.__repr__()
def saveElement(self, store, tag="list"):
store.startElement(tag, {})
for i in self.items:
i.saveElement(store)
store.endElement(tag)
class DictNode: class DictNode:
"""XML node representing a Python dictionnnary """XML node representing a Python dictionnnary
@ -106,3 +113,10 @@ class DictNode:
def __repr__(self): def __repr__(self):
return self.items.__repr__() return self.items.__repr__()
def saveElement(self, store, tag="dict"):
store.startElement(tag, {})
for k, v in self.items.items():
v.saveElement(store)
store.endElement(tag)

View file

@ -53,6 +53,14 @@ class ParsingNode:
return item in self.attrs return item in self.attrs
def saveElement(self, store, tag=None):
store.startElement(tag if tag is not None else self.tag, self.attrs)
for child in self.children:
child.saveElement(store)
store.characters(self.content)
store.endElement(tag if tag is not None else self.tag)
class GenericNode(ParsingNode): class GenericNode(ParsingNode):
"""Consider all subtags as dictionnary """Consider all subtags as dictionnary

View file

@ -124,9 +124,12 @@ class ModuleState:
def setIndex(self, fieldname="name", tagname=None): def setIndex(self, fieldname="name", tagname=None):
"""Defines an hash table to accelerate childs search. """Defines an hash table to accelerate childs search.
You have just to define a common attribute""" You have just to define a common attribute"""
self.index = self.tmpIndex(fieldname, tagname)
self.index_fieldname = fieldname self.index_fieldname = fieldname
self.index_tagname = tagname self.index_tagname = tagname
self._updateIndex()
def _updateIndex(self):
self.index = self.tmpIndex(self.index_fieldname, self.index_tagname)
def __contains__(self, i): def __contains__(self, i):
"""Return true if i is found in the index""" """Return true if i is found in the index"""
@ -135,6 +138,10 @@ class ModuleState:
else: else:
return self.hasAttribute(i) return self.hasAttribute(i)
def update(self, *args, **kwargs):
self.attributes.update(*args, **kwargs)
self._updateIndex()
def hasAttribute(self, name): def hasAttribute(self, name):
"""DOM like method""" """DOM like method"""
return (name in self.attributes) return (name in self.attributes)
@ -196,7 +203,7 @@ class ModuleState:
if self.index_fieldname is not None: if self.index_fieldname is not None:
self.setIndex(self.index_fieldname, self.index_tagname) self.setIndex(self.index_fieldname, self.index_tagname)
def save_node(self, gen): def saveElement(self, gen):
"""Serialize this node as a XML node""" """Serialize this node as a XML node"""
from datetime import datetime from datetime import datetime
attribs = {} attribs = {}
@ -215,29 +222,9 @@ class ModuleState:
gen.startElement(self.name, attrs) gen.startElement(self.name, attrs)
for child in self.childs: for child in self.childs:
child.save_node(gen) child.saveElement(gen)
gen.endElement(self.name) gen.endElement(self.name)
except: except:
logger.exception("Error occured when saving the following " logger.exception("Error occured when saving the following "
"XML node: %s with %s", self.name, attrs) "XML node: %s with %s", self.name, attrs)
def save(self, filename):
"""Save the current node as root node in a XML file
Argument:
filename -- location of the file to create/erase
"""
import tempfile
_, tmpath = tempfile.mkstemp()
with open(tmpath, "w") as f:
import xml.sax.saxutils
gen = xml.sax.saxutils.XMLGenerator(f, "utf-8")
gen.startDocument()
self.save_node(gen)
gen.endDocument()
# Atomic save
import shutil
shutil.move(tmpath, filename)