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:
parent
78487edd56
commit
e1fbf0e6ef
65
bot.py
65
bot.py
|
@ -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' )
|
||||
|
||||
|
|
|
@ -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' )
|
||||
|
||||
|
|
77
protocol.py
77
protocol.py
|
@ -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 ) )
|
||||
|
|
Loading…
Reference in New Issue