1
0
Fork 0

nntp: Here it is!

This commit is contained in:
nemunaire 2018-09-09 19:33:42 +02:00
parent 31abcc97cf
commit 2fd20d9002
1 changed files with 209 additions and 0 deletions

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