2018-09-09 17:33:42 +00:00
""" The NNTP module """
# PYTHON STUFFS #######################################################
import email
2019-09-10 13:50:17 +00:00
import email . policy
2018-09-09 17:33:42 +00:00
from email . utils import mktime_tz , parseaddr , parsedate_tz
2018-12-29 23:42:21 +00:00
from functools import partial
2018-09-09 17:33:42 +00:00
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 )
2019-09-10 13:50:17 +00:00
return email . message_from_bytes ( b " \r \n " . join ( info . lines ) , policy = email . policy . SMTPUTF8 )
2018-09-09 17:33:42 +00:00
2019-11-09 13:46:32 +00:00
servers_lastcheck = dict ( )
2019-11-29 14:54:01 +00:00
servers_lastseen = dict ( )
2019-11-09 13:46:32 +00:00
def whatsnew ( group = " * " , * * server ) :
2018-09-09 17:33:42 +00:00
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 " ]
2019-11-09 13:46:32 +00:00
idx = _indexServer ( * * server )
if idx in servers_lastcheck and servers_lastcheck [ idx ] is not None :
date_last_check = servers_lastcheck [ idx ]
else :
date_last_check = datetime . now ( )
2019-11-29 14:54:01 +00:00
if idx not in servers_lastseen :
servers_lastseen [ idx ] = [ ]
2018-09-09 17:33:42 +00:00
with NNTP ( * * fill ) as srv :
2019-11-09 13:46:32 +00:00
response , servers_lastcheck [ idx ] = srv . date ( )
2018-09-09 17:33:42 +00:00
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 :
2019-11-29 14:54:01 +00:00
if msg_id not in servers_lastseen [ idx ] :
servers_lastseen [ idx ] . append ( msg_id )
response , info = srv . article ( msg_id )
yield email . message_from_bytes ( b " \r \n " . join ( info . lines ) )
# Clean huge lists
if len ( servers_lastseen [ idx ] ) > 42 :
servers_lastseen [ idx ] = servers_lastseen [ idx ] [ 23 : ]
2018-09-09 17:33:42 +00:00
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 = " \x03 14In \x0F \x03 {0:02d} {Newsgroups} \x0F \x03 14: on \x0F {Date} \x03 14 by \x0F \x03 {0:02d} { X-FromName} \x0F \x02 {Subject} \x0F "
else :
title = " \x03 14In \x0F \x03 {0:02d} {Newsgroups} \x0F \x03 14: 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 ) :
2019-11-09 13:46:32 +00:00
context . add_event ( ModuleEvent ( call = partial ( _ticker , * * args ) , interval = 42 ) )
2018-09-09 17:33:42 +00:00
2019-11-09 13:46:32 +00:00
def _ticker ( to_server , to_channel , group , server ) :
_newevt ( to_server = to_server , to_channel = to_channel , group = group , server = server )
2018-09-09 17:33:42 +00:00
n = 0
2019-11-09 13:46:32 +00:00
for art in whatsnew ( group , * * server ) :
2018-09-09 17:33:42 +00:00
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 ) )
2019-11-09 13:46:32 +00:00
def watch ( to_server , to_channel , group = " * " , * * server ) :
_newevt ( to_server = to_server , to_channel = to_channel , group = group , server = server )
2018-09-09 17:33:42 +00:00
# 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 : \x03 14 { 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 )