Added a parser function decorator which allows module functions to be registered as a PRIVMSG callback. Added !botlist and !rollcall for tildeverse's RFC on standardized bot behavior.

This commit is contained in:
Josh K 2018-09-12 22:16:03 -04:00
parent 84f11b9ddf
commit 8ae7a6970c
3 changed files with 51 additions and 14 deletions

53
bot.py
View File

@ -13,12 +13,26 @@ from protocol import IrcProtocol
import modules import modules
# global module map, maps registered commands for each loaded module # global module map, maps registered commands and parsers for each loaded module
# persists across reloads # persists across reloads
try: _MOD_MAP try: _MOD_MAP
except NameError: except NameError:
_MOD_MAP = {} _MOD_MAP = {}
def add_module( name ):
""" adds a new named empty module definition to the global module map
'fixes' passed module name by removing top-level package name
returns newly created empty module object """
mod_name = name[name.find( '.' ) + 1: ]
if mod_name not in _MOD_MAP.keys():
logging.info( 'Registering \'%s\' module...', # \'%s\' (%s)...',
mod_name )
_MOD_MAP[mod_name] = {'name':mod_name,'cmds':{},'parsers':[]}
return _MOD_MAP[mod_name]
def command( name, admin=False ): def command( name, admin=False ):
""" """
Command registering decorator. Command registering decorator.
@ -29,27 +43,32 @@ def command( name, admin=False ):
containing the nick that sent, channel it was sent in, and reference to connection object. containing the nick that sent, channel it was sent in, and reference to connection object.
""" """
def __deco__( func ): def __deco__( func ):
# "fixed" module name. removes top-level 'module' package name mod = add_module( func.__module__ )
mod_name = func.__module__[func.__module__.find( '.' ) + 1: ]
if mod_name not in _MOD_MAP.keys():
logging.info( 'Registering \'%s\' module commands...', # \'%s\' (%s)...',
mod_name ) #, name, func.__name__ )
# set admin flag if needed # set admin flag if needed
if admin: func.__dict__['admin'] = True if admin: func.__dict__['admin'] = True
if not func.__doc__: func.__doc__ = 'No help available for command' if not func.__doc__: func.__doc__ = 'No help available for command'
func.__doc__ += ' {}(Module: {})'.format( admin and '(Admin-Only) ' or '', mod_name ) func.__doc__ += ' {}(Module: {})'.format( admin and '(Admin-Only) ' or '', mod['name'] )
if mod_name not in _MOD_MAP.keys(): mod['cmds'][name.lower()] = func
_MOD_MAP[mod_name] = {'cmds':{}}
_MOD_MAP[mod_name]['cmds'][name.lower()] = func
return func #__inner__ # func return func #__inner__ # func
return __deco__ return __deco__
def parser():
"""
message parser decorator.
decorated function gets passed con, dst, nick, msg on each privmsg
"""
def __deco__( func ):
mod = add_module( func.__module__ )
mod['parsers'].append( func )
return func
return __deco__
class Bot: class Bot:
""" Bot class """ """ Bot class """
def __init__( self, loop, cfg ): def __init__( self, loop, cfg ):
@ -94,6 +113,13 @@ class Bot:
self, parms, {'con':con,'dst':dst,'nick':nick} ) ) self, parms, {'con':con,'dst':dst,'nick':nick} ) )
break break
# callback registered parsers, not for self messages though
if nick != self.cfg['user']['nick']:
# call each registered parser
for v in _MOD_MAP.values():
for p in v['parsers']:
p( con, dst, nick, msg )
# core commands # core commands
# join / part # join / part
@ -127,6 +153,7 @@ async def cmd_help( b, p, c ):
if not p: if not p:
con.say_to( dst, 'No command specified. See list of commands with \'list\'.' ) con.say_to( dst, 'No command specified. See list of commands with \'list\'.' )
return
helpstr = '' helpstr = ''
@ -162,7 +189,7 @@ async def cmd_list( b, p, c ):
async def cmd_modules( b, p, c ): async def cmd_modules( b, p, c ):
""" List all currently loaded modules. List commands within using 'list <module>' """ """ List all currently loaded modules. List commands within using 'list <module>' """
cn = c['con'] cn = c['con']
modlist = [m for m in _MOD_MAP.keys()] modlist = list(_MOD_MAP.keys())
cn.say_to( c['dst'], '> {}'.format( modlist ) ) cn.say_to( c['dst'], '> {}'.format( modlist ) )
# list active connections # list active connections
@ -206,7 +233,7 @@ async def cmd_connect( b, p, c ):
new_sv_cfg = {'host':p[1],'port':6697,'name':p[0]} new_sv_cfg = {'host':p[1],'port':6697,'name':p[0]}
ncon = IrcProtocol( b._loop, con.cfg['usr'] ) ncon = IrcProtocol( b._loop, con.cfg['usr'] )
# add message callback # add message callback
ncon.add_message_callback( b._on_irc_message ) ncon.add_message_callback( b._on_priv_message )
b._con.append( ncon ) b._con.append( ncon )
b._loop.create_task( ncon.do_connect( new_sv_cfg ) ) b._loop.create_task( ncon.do_connect( new_sv_cfg ) )
con.say_to( c['dst'], 'OMW...' ) con.say_to( c['dst'], 'OMW...' )

View File

@ -10,7 +10,7 @@ import gzip
import requests import requests
from bot import command from bot import command, parser
# public python GAE api url. thanks sopel # public python GAE api url. thanks sopel
PY_APP_URL = 'https://tumbolia-sopel.appspot.com/py/' PY_APP_URL = 'https://tumbolia-sopel.appspot.com/py/'
@ -42,6 +42,15 @@ async def cmd_eval( b, p, c ):
c['con'].say_to( c['dst'], '> {}'.format( rstr ) ) c['con'].say_to( c['dst'], '> {}'.format( rstr ) )
# parser test
@parser()
def parse_misc( c, d, n, m ):
if m.split()[0].lower() == '!botlist' or m.split()[0].lower() == '!rollcall':
pfx = ';' # HAX
c.say_to( d, ('> dustbot | Owner: {} | '
'Source: https://tildegit.org/slipyx/dustbot | Prefix: \'{}\'. | '
'Commands: See \'{}list\'.').format( c.cfg['usr']['owner'],pfx,pfx ) )
@command( 'rfk' ) @command( 'rfk' )
async def cmd_rfk( b, p, c ): async def cmd_rfk( b, p, c ):
""" Try to find kitten! """ """ Try to find kitten! """

View File

@ -177,6 +177,7 @@ class IrcProtocol( asyncio.Protocol ):
# helper func for sending a privmsg to specified destination # helper func for sending a privmsg to specified destination
def say_to( self, dst, msg ): def say_to( self, dst, msg ):
msg = msg.replace( '\n', ' ' ).replace( '\r', '' ) msg = msg.replace( '\n', ' ' ).replace( '\r', '' )
self.log( '{} >> {}'.format( dst, msg ) )
self.send( 'PRIVMSG {} :{}'.format( dst, msg ) ) self.send( 'PRIVMSG {} :{}'.format( dst, msg ) )
# create connection task # create connection task