Connection instances now track names and which channels they can be seen in. Added modules command to list loaded modules. List command can filter by module. Added admin-only connections command to list current server connections.

This commit is contained in:
Josh K 2018-09-02 19:24:37 -04:00
parent 78487edd56
commit e1fbf0e6ef
3 changed files with 126 additions and 20 deletions

65
bot.py
View File

@ -14,6 +14,7 @@ from protocol import IrcProtocol
import modules
# global module map, maps registered functions for each loaded module
# persists across reloads
try: _MOD_MAP
except NameError:
_MOD_MAP = {}
@ -54,13 +55,14 @@ class Bot:
# create and connect all servers in config
for s in self.cfg['server']:
ncon = IrcProtocol( self._loop, self.cfg['user'] )
# add message callback
# add message callback to the connections
ncon.add_message_callback( self._on_irc_message )
self._con.append( ncon )
self._loop.create_task( ncon.do_connect( s ) )
# called when a connected connection receives a full message
# passes the connection that sent and raw string message
# TODO: use a privmsg callback from protocol instead
def _on_irc_message( self, con, raw_msg ):
words = raw_msg.split()
if words[1] == 'PRIVMSG':
@ -71,7 +73,7 @@ class Bot:
# check for command-like message
# message split into words, first word is command rest are parms list
pfx = self.cfg['bot']['prefix']
strp_msg = msg.strip()
strp_msg = msg.lstrip()
if strp_msg[0 : len( pfx )] == pfx:
msg_words = strp_msg.split()
cmd = msg_words[0][len( pfx ):].lower()
@ -79,7 +81,7 @@ class Bot:
# check if command is in a registered module
for k, m in _MOD_MAP.items():
if cmd in m.keys():
con.log( 'CMD \'{}\' run in ({}) by <{}> with parms: {}'.format(
con.log( '::CMD:: \'{}\' run in ({}) by <{}> with parms: {}'.format(
cmd, chan, nick, parms ) )
# if msg was sent as pm,
# change dst to nick so say_to's can respond in pm
@ -123,7 +125,7 @@ async def cmd_help( b, p, c ):
dst = c['dst']
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\'.' )
helpstr = ''
@ -138,14 +140,37 @@ async def cmd_help( b, p, c ):
# list commands
@command( 'list' )
async def cmd_list( bot, p, ctx ):
""" Lists all available commands. TODO: module filter """
async def cmd_list( b, p, c ):
""" Lists available commands. Specify a module name from 'modules' command to filter. """
con = ctx['con']
cn = c['con']
cmdlist = [list( c.keys() ) for c in _MOD_MAP.values()]
cmdlist = []
for k,v in _MOD_MAP.items():
if p:
if p[0].lower() == k.lower():
cmdlist = list(v.keys())
break
else:
cmdlist.append( list(v.keys()) )
con.say_to( ctx['dst'], '> {}'.format( cmdlist ) )
cn.say_to( c['dst'], '> {}'.format( (cmdlist and cmdlist or 'Module not found.') ) )
# list loaded modules
@command( 'modules' )
async def cmd_modules( b, p, c ):
""" List all currently loaded modules. List commands within using 'list <module>' """
cn = c['con']
modlist = [m for m in _MOD_MAP.keys()]
cn.say_to( c['dst'], '> {}'.format( modlist ) )
# list active connections
@command( 'connections' )
async def cmd_connections( b, p, c ):
""" List currently connected connections """
if c['nick'] != c['con'].cfg['usr']['owner']: return
conlist = [c.cfg['sv']['name'] for c in b._con]
c['con'].say_to( c['dst'], '> {}'.format( conlist ) )
# reload a specified module
@command( 'reload' )
@ -186,6 +211,7 @@ async def cmd_connect( b, p, c ):
ncon.add_message_callback( b._on_irc_message )
b._con.append( ncon )
b._loop.create_task( ncon.do_connect( new_sv_cfg ) )
con.say_to( c['dst'], 'OMW...' )
# disconnect from currently connected server
@command( 'disconnect' )
@ -197,8 +223,21 @@ async def cmd_discon( b, p, c ):
con.send( 'QUIT :POOF' )
b._con.remove( con )
# custom modules
importlib.import_module( '.qdb', 'modules' )
importlib.import_module( '.misc', 'modules' )
#importlib.import_module( '.kai', 'modules' )
# list of names on chan
@command( 'names' )
async def cmd_names( b, p, c ):
cn = c['con']
if c['nick'] != cn.cfg['usr']['owner']: return
nmstr = ''
for k,v in cn.names.items():
if c['dst'] in v:
nmstr += str(k)+'0|0'
cn.say_to( c['dst'], '> {}'.format( nmstr ) )
cn.log( str(cn.names) )
# custom modules
importlib.import_module( '.misc', 'modules' )
importlib.import_module( '.qdb', 'modules' )

View File

@ -52,7 +52,3 @@ async def cmd_rfk( b, p, c ):
).rstrip().decode()
c['con'].say_to( c['dst'], '{}: {}'.format( c['nick'], nki ) )
@command( 'owo' )
async def cmd_owo( b, p, c ):
c['con'].say_to( c['dst'], 'nou' )

View File

@ -27,7 +27,7 @@ class IrcProtocol( asyncio.Protocol ):
Accepts a user config on creation and a server config when connecting
Can add message callbacks that get called when connection
receives a new message
receives messages
Attempts to reconnect if connection lost.
@ -43,7 +43,8 @@ class IrcProtocol( asyncio.Protocol ):
self._trans = None # protocol's transport
self._stop = False # True if doing a manual stop and dont try to reconnect
self.cfg = {'usr':user_cfg,'sv':{}} # server config set on first connect
self._msg_cb = [] # list of message callbacks
self._msg_cb = [] # list of raw message callbacks
self.names = {} # map of names on server to list of channels they're visible in
def connection_made( self, transport ):
self.log( 'Connection made! {}'.format( transport ) )
@ -53,6 +54,7 @@ class IrcProtocol( asyncio.Protocol ):
def connection_lost( self, exc ):
self._trans.close()
self.names.clear() # clear names
if exc is None: exc = 'EOF'
self.warning( 'Connection lost to server \'{}\'! ({})'.format( self.cfg['sv']['host'], exc ) )
# if was a manual stop, dont attempt reconnect
@ -78,12 +80,80 @@ class IrcProtocol( asyncio.Protocol ):
for c in self.cfg['sv']['channels']:
self.send( 'JOIN {}'.format( c ) )
else: self.cfg['sv']['channels'] = []
# on privmsg
elif words[1] == 'PRIVMSG': pass
# on names
elif words[1] == '353':
#self.log( '::NAMES:: {}'.format( raw_msg.decode() ) )
names_chan = words[4]
for n in words[5:]:
if n[0] == ':': n = n[1:] # strip first name
# strip user mode chars
for c in ['+','~','%','&','@']:
if n[0] == c: n = n[1:]
self.join_name( n, names_chan )
# update names (add callbacks?)
elif 'JOIN' in words[1]:
nick = words[0][1:words[0].find('!')]
if nick == self.cfg['usr']['nick']:
self.log( '::JOIN:: {} has joined {}.'.format( nick, words[2][1:] ) )
self.join_name( nick, words[2][1:] )
elif 'PART' in words[1]:
nick = words[0][1:words[0].find('!')]
if nick == self.cfg['usr']['nick']:
self.log( '::PART:: {} has left {}.'.format( nick, words[2] ) )
self.part_name( nick, words[2] )
elif 'QUIT' in words[1]:
nick = words[0][1:words[0].find('!')]
if nick == self.cfg['usr']['nick']:
self.log( '::QUIT:: {} has quit the server.'.format( nick ) )
self.quit_name( nick )
elif 'NICK' in words[1]:
nick = words[0][1:words[0].find('!')]
#self.log( '::NICK:: {} is now known as {}.'.format( nick, words[2][1:] ) )
self.change_name( nick, words[2][1:] )
elif 'KICK' in words[1]:
if words[3] == self.cfg['usr']['nick']:
self.log( '::KICK:: {} has been kicked from {}.'.format( words[3], words[2] ) )
self.part_name( words[3], words[2] )
# send raw message to all added callbacks
for c in self._msg_cb: c( self, raw_msg.decode() )
# manual stop
#self._stop = True
#self.send('QUIT :stopped' )
# names management
def join_name( self, name, chan ):
""" Adds a chan belonging to name """
if name not in self.names.keys():
self.names[name] = []
if chan not in self.names[name]:
self.names[name].append( chan )
def part_name( self, name, chan ):
""" Removes chan from name """
self.names[name].remove( chan )
# if chans are empty, remove name completely
if not self.names[name]: self.quit_name( name )
# when self leaves a chan, remove chan from all existing names
if name == self.cfg['usr']['nick']:
nms = []
for k,v in self.names.items():
if chan in v:
nms.append( k )
for n in nms:
#self.names[n].remove( chan )
self.part_name( n, chan )
def quit_name( self, name ):
""" Removes name from connection """
self.names.pop( name )
def change_name( self, name, new ):
""" Renames an existing name """
self.names[new] = self.names.pop( name )
# callbacks
def add_message_callback( self, cb ):
"""
Adds a function to callback when a raw message is received.
@ -120,7 +190,8 @@ class IrcProtocol( asyncio.Protocol ):
self.log( 'Connecting to server \'{}\'...'.format( self.cfg['sv']['host'] ) )
try: await asyncio.wait_for( self._loop.create_connection(
lambda: self, self.cfg['sv']['host'], self.cfg['sv']['port'], ssl=SSL_CTX ), 10, loop=self._loop )
lambda: self, self.cfg['sv']['host'], self.cfg['sv']['port'], ssl=SSL_CTX ),
10, loop=self._loop )
except Exception as e:
if e is None: e = 'EOF'
self.warning( 'Connection exception! {}'.format( e ) )