From 7d11f8c7e0cb2fe5cf0a5c6c37b95e3655a1af6d Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 14 May 2016 09:55:46 -0700 Subject: [PATCH] Begin work on service bot abstraction (#216) - State-keeping is done by coreplugin - utils.registerService() introduced - new PYLINK_NEW_SERVICE hook introduced --- coreplugin.py | 49 ++++++++++++++++++++++++++++++++++++++++ utils.py | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++ world.py | 5 +++-- 3 files changed, 114 insertions(+), 2 deletions(-) diff --git a/coreplugin.py b/coreplugin.py index ffcfed2..781341e 100644 --- a/coreplugin.py +++ b/coreplugin.py @@ -186,6 +186,55 @@ def handle_version(irc, source, command, args): irc.proto.numeric(irc.sid, 351, source, fullversion) utils.add_hook(handle_version, 'VERSION') +def handle_newservice(irc, source, command, args): + """Handles new service bot introductions.""" + + if not irc.connected.is_set(): + return + + name = args['name'] + ident = irc.botdata.get('ident') or 'pylink' + host = irc.serverdata["hostname"] + modes = [] + for mode in ('oper', 'hideoper', 'hidechans'): + mode = irc.cmodes.get(mode) + if mode: + modes.append((mode, None)) + + # Track the service's UIDs on each network. + service = world.services[name] + service.uids[irc.name] = u = irc.proto.spawnClient(name, name, + irc.serverdata['hostname'], modes=modes, opertype="PyLink Service") + + # TODO: channels should be tracked in a central database, not hardcoded + # in conf. + for chan in irc.serverdata['channels']: + irc.proto.join(u.uid, chan) + +utils.add_hook(handle_newservice, 'PYLINK_NEW_SERVICE') + +def handle_disconnect(irc, source, command, args): + """Handles network disconnections.""" + for name, sbot in world.services.items(): + try: + del sbot.uids[irc.name] + log.debug("coreplugin: removing uids[%s] from service bot %s", irc.name, sbot.name) + except KeyError: + continue + +utils.add_hook(handle_disconnect, 'PYLINK_DISCONNECT') + +def handle_endburst(irc, source, command, args): + """Handles network bursts.""" + if source == irc.uplink: + log.debug('(%s): spawning service bots now.') + + # We just connected. Burst all our registered services. + for name, sbot in world.services.items(): + handle_newservice(irc, source, command, {'name': name}) + +utils.add_hook(handle_endburst, 'ENDBURST') + # Essential, core commands go here so that the "commands" plugin with less-important, # but still generic functions can be reloaded. diff --git a/utils.py b/utils.py index b923ab7..daa872b 100644 --- a/utils.py +++ b/utils.py @@ -9,6 +9,7 @@ import string import re import importlib import os +import collections from log import log import world @@ -141,3 +142,64 @@ def getDatabaseName(dbname): dbname += '-%s' % conf.confname dbname += '.db' return dbname + +class ServiceBot(): + def __init__(self, name, default_help=True, default_request=True, default_list=True): + self.name = name + # We make the command definitions a dict of lists of functions. Multiple + # plugins are actually allowed to bind to one function name; this just causes + # them to be called in the order that they are bound. + self.commands = collections.defaultdict(list) + + # This tracks the UIDs of the service bot on different networks, as they are + # spawned. + self.uids = {} + + if default_help: + self.add_cmd(self.help) + + if default_request: + self.add_cmd(self.request) + self.add_cmd(self.remove) + + if default_list: + self.add_cmd(self.listcommands, 'list') + + def spawn(self, irc=None): + # Spawn the new service by calling the PYLINK_NEW_SERVICE hook, + # which is handled by coreplugin. + if irc is None: + for irc in world.networkobjects.values(): + irc.callHooks([None, 'PYLINK_NEW_SERVICE', {'name': self.name}]) + else: + raise NotImplementedError("Network specific plugins not supported yet.") + + + def add_cmd(self, func, name=None): + """Binds an IRC command function to the given command name.""" + if name is None: + name = func.__name__ + name = name.lower() + + self.commands[name].append(func) + return func + + def help(self, irc, source, args): + irc.reply("Help command stub called.") + + def request(self, irc, source, args): + irc.reply("Request command stub called.") + + def remove(self, irc, source, args): + irc.reply("Remove command stub called.") + + def listcommands(self, irc, source, args): + irc.reply("List command stub called.") + +def registerService(name, *args, **kwargs): + name = name.lower() + if name in world.services: + raise ValueError("Service name %s is already bound!" % name) + + world.services[name] = sbot = ServiceBot(name, *args, **kwargs) + sbot.spawn() diff --git a/world.py b/world.py index 9a44bcf..43c8f0f 100644 --- a/world.py +++ b/world.py @@ -14,13 +14,14 @@ testing = True # Sets the default protocol module to use with tests. testing_ircd = 'inspircd' -global commands, hooks -# This should be a mapping of command names to functions + commands = defaultdict(list) hooks = defaultdict(list) networkobjects = {} plugins = {} whois_handlers = [] +services = {} + started = threading.Event() plugins_folder = os.path.join(os.getcwd(), 'plugins')