Compare commits
17 commits
master
...
module/nnt
| Author | SHA1 | Date | |
|---|---|---|---|
| d84bf36ca0 | |||
| 4e8504bd1d | |||
| 6b4a9a2e4a | |||
| db1e4e9266 | |||
| 709128b7aa | |||
| 55a8f74900 | |||
| 92702f3995 | |||
| 990599551c | |||
| 7a52748849 | |||
| ef4a6e9af5 | |||
| 9d0ab88c12 | |||
| 76bea2bc15 | |||
| c8afa65dcb | |||
| 7eac685a0a | |||
| bc183bcfa0 | |||
| 0d52fff64a | |||
| 2938287869 |
15 changed files with 784 additions and 52 deletions
|
|
@ -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
85
modules/disas.py
Normal 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
64
modules/freetarifs.py
Normal 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
209
modules/nntp.py
Normal 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
158
modules/openroute.py
Normal 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
68
modules/pkgs.py
Normal 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
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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...")
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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__':
|
||||||
|
|
|
||||||
|
|
@ -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]))
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue