""" corecommands.py - Implements core PyLink commands. """ import gc import sys import importlib from . import control, login, permissions from pylinkirc import utils, world, conf from pylinkirc.log import log # Essential, core commands go here so that the "commands" plugin with less-important, # but still generic functions can be reloaded. def _login(irc, source, username): """Internal function to process logins.""" if irc.is_internal_client(source): irc.error("Cannot use 'identify' via a command proxy.") return logindata = conf.conf['login'].get('accounts', {}).get(username, {}) network_filter = logindata.get('networks') require_oper = logindata.get('require_oper', False) hosts_filter = logindata.get('hosts', []) if network_filter and irc.name not in network_filter: irc.error("You are not authorized to log in to %r on this network." % username) log.warning("(%s) Failed login to %r from %s (wrong network: networks filter says %r but we got %r)", irc.name, username, irc.get_hostmask(source), ', '.join(network_filter), irc.name) return elif require_oper and not irc.is_oper(source, allowAuthed=False): irc.error("You must be opered up to log in to %r." % username) log.warning("(%s) Failed login to %r from %s (needs oper)", irc.name, username, irc.get_hostmask(source)) return elif hosts_filter and not any(irc.match_host(host, source) for host in hosts_filter): irc.error("Failed to log in to %r: hostname mismatch." % username) log.warning("(%s) Failed login to %r from %s (hostname mismatch)", irc.name, username, irc.get_hostmask(source)) return irc.users[source].account = username irc.reply('Successfully logged in as %s.' % username) log.info("(%s) Successful login to %r by %s", irc.name, username, irc.get_hostmask(source)) def _loginfail(irc, source, username): """Internal function to process login failures.""" irc.error('Incorrect credentials.') log.warning("(%s) Failed login to %r from %s", irc.name, username, irc.get_hostmask(source)) def identify(irc, source, args): """ Logs in to PyLink using the configured administrator account.""" if irc.is_channel(irc.called_in): irc.reply('Error: This command must be sent in private. ' '(Would you really type a password inside a channel?)') return try: username, password = args[0], args[1] except IndexError: irc.reply('Error: Not enough arguments.') return # Process new-style accounts. if login.checkLogin(username, password): _login(irc, source, username) return # Process legacy logins (login:user). if username.lower() == conf.conf['login'].get('user', '').lower() and password == conf.conf['login'].get('password'): realuser = conf.conf['login']['user'] _login(irc, source, realuser) else: # Username not found. _loginfail(irc, source, username) utils.add_cmd(identify, aliases=('login', 'id')) @utils.add_cmd def shutdown(irc, source, args): """takes no arguments. Exits PyLink by disconnecting all networks.""" permissions.check_permissions(irc, source, ['core.shutdown']) log.info('(%s) SHUTDOWN requested by %s, exiting...', irc.name, irc.get_hostmask(source)) control.shutdown(irc=irc) @utils.add_cmd def load(irc, source, args): """. Loads a plugin from the plugin folder.""" # Note: reload capability is acceptable here, because all it actually does is call # load after unload. permissions.check_permissions(irc, source, ['core.load', 'core.reload']) try: name = args[0] except IndexError: irc.reply("Error: Not enough arguments. Needs 1: plugin name.") return if name in world.plugins: irc.reply("Error: %r is already loaded." % name) return log.info('(%s) Loading plugin %r for %s', irc.name, name, irc.get_hostmask(source)) try: world.plugins[name] = pl = utils._load_plugin(name) except ImportError as e: if str(e) == ('No module named %r' % name): log.exception('Failed to load plugin %r: The plugin could not be found.', name) else: log.exception('Failed to load plugin %r: ImportError.', name) raise else: if hasattr(pl, 'main'): log.debug('Calling main() function of plugin %r', pl) pl.main(irc=irc) irc.reply("Loaded plugin %r." % name) @utils.add_cmd def unload(irc, source, args): """. Unloads a currently loaded plugin.""" permissions.check_permissions(irc, source, ['core.unload', 'core.reload']) try: name = args[0] except IndexError: irc.reply("Error: Not enough arguments. Needs 1: plugin name.") return # Since we're using absolute imports in 0.9.x+, the module name differs from the actual plugin # name. modulename = utils.PLUGIN_PREFIX + name if name in world.plugins: log.info('(%s) Unloading plugin %r for %s', irc.name, name, irc.get_hostmask(source)) pl = world.plugins[name] log.debug('sys.getrefcount of plugin %s is %s', pl, sys.getrefcount(pl)) # Remove any command functions defined by the plugin. for cmdname, cmdfuncs in world.services['pylink'].commands.copy().items(): log.debug('cmdname=%s, cmdfuncs=%s', cmdname, cmdfuncs) for cmdfunc in cmdfuncs: log.debug('__module__ of cmdfunc %s is %s', cmdfunc, cmdfunc.__module__) if cmdfunc.__module__ == modulename: log.debug("Removing %s from world.services['pylink'].commands[%s]", cmdfunc, cmdname) world.services['pylink'].commands[cmdname].remove(cmdfunc) # If the cmdfunc list is empty, remove it. if not cmdfuncs: log.debug("Removing world.services['pylink'].commands[%s] (it's empty now)", cmdname) del world.services['pylink'].commands[cmdname] # Remove any command hooks set by the plugin. for hookname, hookpairs in world.hooks.copy().items(): for hookpair in hookpairs: hookfunc = hookpair[1] if hookfunc.__module__ == modulename: log.debug('Trying to remove hook func %s (%s) from plugin %s', hookfunc, hookname, modulename) world.hooks[hookname].remove(hookpair) # If the hookfuncs list is empty, remove it. if not hookpairs: del world.hooks[hookname] # Call the die() function in the plugin, if present. if hasattr(pl, 'die'): try: pl.die(irc=irc) except: # But don't allow it to crash the server. log.exception('(%s) Error occurred in die() of plugin %s, skipping...', irc.name, pl) # Delete it from memory (hopefully). del world.plugins[name] for n in (name, modulename): if n in sys.modules: del sys.modules[n] if n in globals(): del globals()[n] # Garbage collect. gc.collect() irc.reply("Unloaded plugin %r." % name) return True # We succeeded, make it clear (this status is used by reload() below) else: irc.reply("Unknown plugin %r." % name) @utils.add_cmd def reload(irc, source, args): """. Loads a plugin from the plugin folder.""" try: name = args[0] except IndexError: irc.reply("Error: Not enough arguments. Needs 1: plugin name.") return # Note: these functions do permission checks, so there are none needed here. if unload(irc, source, args): load(irc, source, args) @utils.add_cmd def rehash(irc, source, args): """takes no arguments. Reloads the configuration file for PyLink, (dis)connecting added/removed networks. Note: plugins must be manually reloaded.""" permissions.check_permissions(irc, source, ['core.rehash']) try: control.rehash() except Exception as e: # Something went wrong, abort. irc.reply("Error loading configuration file: %s: %s" % (type(e).__name__, e)) return else: irc.reply("Done.") @utils.add_cmd def clearqueue(irc, source, args): """takes no arguments. Clears the outgoing text queue for the current connection.""" permissions.check_permissions(irc, source, ['core.clearqueue']) irc._queue.queue.clear()