def _exec(irc, source, args, locals_dict=None): """<code> Admin-only. Executes <code> in the current PyLink instance. This command performs backslash escaping of characters, so things like \\n and \\ will work. \x02**WARNING: THIS CAN BE DANGEROUS IF USED IMPROPERLY!**\x02""" permissions.checkPermissions(irc, source, ['exec.exec']) # Allow using \n in the code, while escaping backslashes correctly otherwise. args = bytes(' '.join(args), 'utf-8').decode("unicode_escape") if not args.strip(): irc.reply('No code entered!') return log.info('(%s) Executing %r for %s', irc.name, args, irc.getHostmask(source)) if locals_dict is None: locals_dict = locals() else: # Add irc, source, and args to the given locals_dict, to allow basic things like irc.reply() # to still work. locals_dict['irc'] = irc locals_dict['source'] = source locals_dict['args'] = args exec(args, globals(), locals_dict) irc.reply("Done.")
def _shutdown(irc=None): """Shuts down the Pylink daemon.""" global tried_shutdown if tried_shutdown: # We froze on shutdown last time, so immediately abort. _print_remaining_threads() raise KeyboardInterrupt("Forcing shutdown.") tried_shutdown = True # HACK: run the _kill_plugins trigger with the current IRC object. XXX: We should really consider removing this # argument, since no plugins actually use it to do anything. atexit.unregister(_kill_plugins) _kill_plugins(irc) # Remove our main PyLink bot as well. utils.unregisterService('pylink') for ircobj in world.networkobjects.copy().values(): # Disconnect all our networks. remove_network(ircobj) log.info( "Waiting for remaining threads to stop; this may take a few seconds. If PyLink freezes " "at this stage, press Ctrl-C to force a shutdown.") _print_remaining_threads()
def _login(irc, source, username): """Internal function to process logins.""" # Mangle case before we start checking for login data. accounts = {k.lower(): v for k, v in conf.conf['login'].get('accounts', {}).items()} logindata = accounts.get(username.lower(), {}) 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.getHostmask(source), ', '.join(network_filter), irc.name) return elif require_oper and not irc.isOper(source, allowAuthed=False): irc.error("You must be opered to log in to %r." % username) log.warning("(%s) Failed login to %r from %s (needs oper)", irc.name, username, irc.getHostmask(source)) return elif hosts_filter and not any(irc.matchHost(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.getHostmask(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.getHostmask(source))
def load(irc, source, args): """<plugin name>. Loads a plugin from the plugin folder.""" irc.checkAuthenticated(source, allowOper=False) 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.getHostmask(source)) try: world.plugins[name] = pl = utils.loadPlugin(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.reply("Loaded plugin %r." % name)
def delacc(irc, source, args): """<channel/chanpair> <mask> Removes the Automode entry for the given mask on the given channel, if one exists. """ try: chanpair, mask = args except ValueError: error(irc, "Invalid arguments given. Needs 2: channel, mask") return else: ircobj, channel = getChannelPair(irc, source, chanpair, perm='manage') dbentry = db.get(ircobj.name+channel) if dbentry is None: error(irc, "No Automode access entries exist for \x02%s\x02." % channel) return if mask in dbentry: del dbentry[mask] log.info('(%s) %s removed modes for %s on %s', ircobj.name, irc.getHostmask(source), mask, channel) reply(irc, "Done. Removed the Automode access entry for \x02%s\x02 in \x02%s\x02." % (mask, channel)) else: error(irc, "No Automode access entry for \x02%s\x02 exists in \x02%s\x02." % (mask, channel)) # Remove channels if no more entries are left. if not dbentry: log.debug("Automode: purging empty channel pair %s/%s", ircobj.name, channel) del db[ircobj.name+channel]
def handle_partquit(irc, source, command, args): """Antispam part/quit message filter.""" text = args.get('text') pq_settings = irc.get_service_option('antispam', 'partquit', PARTQUIT_DEFAULTS) if not text: return # No text to match against elif command == 'QUIT' and not pq_settings.get('watch_quits', True): return # Not enabled elif command == 'PART' and not pq_settings.get('watch_parts', True): return # Merge together global and local partquit filter lists. pq_globs = set(conf.conf.get('antispam', {}).get('partquit_globs', [])) | \ set(irc.serverdata.get('antispam_partquit_globs', [])) if not pq_globs: return for filterglob in pq_globs: if utils.match_text(filterglob, text): # For parts, also log the affected channels if command == 'PART': filtered_message = pq_settings.get('part_filter_message', PARTQUIT_DEFAULTS['part_filter_message']) log.info('(%s) antispam: filtered part message from %s on %s due to part/quit filter glob %s', irc.name, irc.get_hostmask(source), ','.join(args['channels']), filterglob) else: filtered_message = pq_settings.get('quit_filter_message', PARTQUIT_DEFAULTS['quit_filter_message']) log.info('(%s) antispam: filtered quit message from %s due to part/quit filter glob %s', irc.name, args['userdata'].nick, filterglob) args['text'] = filtered_message break
def identify(irc, source, args): """<username> <password> Logs in to PyLink using the configured administrator account.""" if utils.isChannel(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 # Usernames are case-insensitive, passwords are NOT. if username.lower() == irc.conf['login']['user'].lower( ) and password == irc.conf['login']['password']: realuser = irc.conf['login']['user'] irc.users[source].identified = realuser irc.reply('Successfully logged in as %s.' % realuser) log.info("(%s) Successful login to %r by %s", irc.name, username, irc.getHostmask(source)) else: irc.reply('Error: Incorrect credentials.') u = irc.users[source] log.warning("(%s) Failed login to %r from %s", irc.name, username, irc.getHostmask(source))
def load(irc, source, args): """<plugin name>. Loads a plugin from the plugin folder.""" # Note: reload capability is acceptable here, because all it actually does is call # load after unload. permissions.checkPermissions(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.getHostmask(source)) try: world.plugins[name] = pl = utils.loadPlugin(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)
def disconnect(irc, source, args): """<network> Disconnects the network <network>. When all networks are disconnected, PyLink will automatically exit. To reconnect a network disconnected using this command, use REHASH to reload the networks list.""" permissions.check_permissions(irc, source, ['networks.disconnect']) try: netname = args[0] network = world.networkobjects[netname] except IndexError: # No argument given. irc.error( 'Not enough arguments (needs 1: network name (case sensitive)).') return except KeyError: # Unknown network. irc.error('No such network "%s" (case sensitive).' % netname) return if network.has_cap('virtual-server'): irc.error( '"%s" is a virtual server and cannot be directly disconnected.' % netname) return log.info('Disconnecting network %r per %s', netname, irc.get_hostmask(source)) control.remove_network(network) irc.reply( "Done. If you want to reconnect this network, use the 'rehash' command." )
def handle_stats(irc, source, command, args): """/STATS handler. Currently supports the following: c - link blocks o - oper blocks (accounts) u - shows uptime """ stats_type = args['stats_type'][0].lower( ) # stats_type shouldn't be more than 1 char anyways perms = ['stats.%s' % stats_type] if stats_type == 'u': perms.append('stats.uptime') # Consistency try: permissions.check_permissions(irc, source, perms) except utils.NotAuthorizedError as e: # Note, no irc.error() because this is not a command, but a handler irc.msg(source, 'Error: %s' % e, notice=True) return log.info('(%s) /STATS %s requested by %s', irc.name, stats_type, irc.get_hostmask(source)) def _num(num, text): irc.numeric(args['target'], num, source, text) if stats_type == 'c': # 213/RPL_STATSCLINE: "C <host> * <name> <port> <class>" for netname, serverdata in sorted(conf.conf['servers'].items()): # We're cramming as much as we can into the class field... _num( 213, "C %s * %s %s [%s:%s:%s]" % (serverdata.get('ip', '0.0.0.0'), netname, serverdata.get('port', 0), serverdata['protocol'], 'ssl' if serverdata.get('ssl') else 'no-ssl', serverdata.get('encoding', 'utf-8'))) elif stats_type == 'o': # 243/RPL_STATSOLINE: "O <hostmask> * <nick> [:<info>]" # New style accounts only! for accountname, accountdata in conf.conf['login'].get('accounts', {}).items(): networks = accountdata.get('networks', []) if irc.name in networks or not networks: hosts = ' '.join(accountdata.get('hosts', ['*@*'])) needoper = 'needoper' if accountdata.get( 'require_oper') else '' _num(243, "O %s * %s :%s" % (hosts, accountname, needoper)) elif stats_type == 'u': # 242/RPL_STATSUPTIME: ":Server Up <days> days <hours>:<minutes>:<seconds>" _num(242, ':Server Up %s' % timediff(world.start_ts, int(time.time()))) else: log.info('(%s) Unknown /STATS type %r requested by %s', irc.name, stats_type, irc.get_hostmask(source)) _num(219, "%s :End of /STATS report" % stats_type)
def on_member_add(self, event: events.GuildMemberAdd, *args, **kwargs): log.info('(%s) got GuildMemberAdd event for guild %s/%s: %s', self.protocol.name, event.guild.id, event.guild.name, event.member) try: pylink_netobj = self.protocol._children[event.guild.id] except KeyError: log.error("(%s) Could not burst user %s as the parent network object does not exist", self.protocol.name, event.member) return self._burst_new_client(event.guild, event.member, pylink_netobj)
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)
def on_server_update(self, event: events.GuildUpdate, *args, **kwargs): log.info('(%s) got GuildUpdate event for guild %s/%s', self.protocol.name, event.guild.id, event.guild.name) try: pylink_netobj = self.protocol._children[event.guild.id] except KeyError: log.error("(%s) Could not update guild %s/%s as the corresponding network object does not exist", self.protocol.name, event.guild.id, event.guild.name) return else: pylink_netobj._guild_name = event.guild.name
def rehash(): """Rehashes the PyLink daemon.""" log.info('Reloading PyLink configuration...') old_conf = conf.conf.copy() fname = conf.fname new_conf = conf.load_conf(fname, errors_fatal=False, logger=log) conf.conf = new_conf # Reset any file logger options. _stop_file_loggers() files = new_conf['logging'].get('files') if files: for filename, config in files.items(): _make_file_logger(filename, config.get('loglevel')) log.debug('rehash: updating console log level') world.console_handler.setLevel(_get_console_log_level()) login._make_cryptcontext() # refresh password hashing settings for network, ircobj in world.networkobjects.copy().items(): # Server was removed from the config file, disconnect them. log.debug('rehash: checking if %r is in new conf still.', network) if network not in new_conf['servers']: log.debug( 'rehash: removing connection to %r (removed from config).', network) remove_network(ircobj) else: # XXX: we should really just add abstraction to Irc to update config settings... ircobj.serverdata = new_conf['servers'][network] ircobj.autoconnect_active_multiplier = 1 # Clear the IRC object's channel loggers and replace them with # new ones by re-running log_setup(). while ircobj.loghandlers: log.removeHandler(ircobj.loghandlers.pop()) ircobj.log_setup() utils._reset_module_dirs() for network, sdata in new_conf['servers'].items(): # Connect any new networks or disconnected networks if they aren't already. if network not in world.networkobjects: try: proto = utils._get_protocol_module(sdata['protocol']) # API note: 2.0.x style of starting network connections world.networkobjects[network] = newirc = proto.Class(network) newirc.connect() except: log.exception( 'Failed to initialize network %r, skipping it...', network) log.info('Finished reloading PyLink configuration.')
def on_ready(self, event, *args, **kwargs): self.botuser = event.user.id log.info('(%s) got ready event, starting messaging thread', self.protocol.name) self._message_thread = threading.Thread( name="Messaging thread for %s" % self.name, target=self.protocol._message_builder, daemon=True) self._message_thread.start() self.protocol.connected.set()
def loadDB(): """Loads the Automode database, silently creating a new one if this fails.""" global db try: with open(dbname, "r") as f: db.update(json.load(f)) except (ValueError, IOError, OSError): log.info( "Automode: failed to load links database %s; creating a new one in " "memory.", dbname)
def _rehash(): """Rehashes the PyLink daemon.""" log.info('Reloading PyLink configuration...') old_conf = conf.conf.copy() fname = conf.fname new_conf = conf.loadConf(fname, errors_fatal=False, logger=log) conf.conf = new_conf # Reset any file logger options. stopFileLoggers() files = new_conf['logging'].get('files') if files: for filename, config in files.items(): makeFileLogger(filename, config.get('loglevel')) log.debug('rehash: updating console log level') world.console_handler.setLevel(getConsoleLogLevel()) # Reset permissions. log.debug('rehash: resetting permissions') permissions.resetPermissions() for network, ircobj in world.networkobjects.copy().items(): # Server was removed from the config file, disconnect them. log.debug('rehash: checking if %r is in new conf still.', network) if network not in new_conf['servers']: log.debug( 'rehash: removing connection to %r (removed from config).', network) remove_network(ircobj) else: # XXX: we should really just add abstraction to Irc to update config settings... ircobj.conf = new_conf ircobj.serverdata = new_conf['servers'][network] ircobj.botdata = new_conf['bot'] ircobj.autoconnect_active_multiplier = 1 # Clear the IRC object's channel loggers and replace them with # new ones by re-running logSetup(). while ircobj.loghandlers: log.removeHandler(ircobj.loghandlers.pop()) ircobj.logSetup() utils.resetModuleDirs() for network, sdata in new_conf['servers'].items(): # Connect any new networks or disconnected networks if they aren't already. if (network not in world.networkobjects) or ( not world.networkobjects[network].connection_thread.is_alive() ): proto = utils.getProtocolModule(sdata['protocol']) world.networkobjects[network] = classes.Irc( network, proto, new_conf) log.info('Finished reloading PyLink configuration.')
def hook_privmsg(irc, source, command, args): channel = args['target'] text = args['text'] # irc.pseudoclient stores the IrcUser object of the main PyLink client. # (i.e. the user defined in the bot: section of the config) if utils.isChannel(channel) and irc.pseudoclient.nick in text: irc.msg(channel, 'hi there!') # log.debug, log.info, log.warning, log.error, log.exception (within except: clauses) # and log.critical are supported here. log.info('%s said my name on channel %s (PRIVMSG hook caught)' % (source, channel))
def shutdown(irc, source, args): """takes no arguments. Exits PyLink by disconnecting all networks.""" irc.checkAuthenticated(source, allowOper=False) u = irc.users[source] log.info('(%s) SHUTDOWN requested by "%s!%s@%s", exiting...', irc.name, u.nick, u.ident, u.host) control._shutdown(irc)
def _burst_guild(self, guild): log.info('(%s) bursting guild %s/%s', self.protocol.name, guild.id, guild.name) pylink_netobj = self.protocol._create_child(guild.id, guild.name) pylink_netobj.uplink = None for member in guild.members.values(): self._burst_new_client(guild, member, pylink_netobj) pylink_netobj.connected.set() pylink_netobj.call_hooks([None, 'ENDBURST', {}])
def check_nick_collision(self, nick): """ Nick collision checker. """ uid = self.irc.nickToUid(nick) # If there is a nick collision, we simply alert plugins. Relay will purposely try to # lose fights and tag nicks instead, while other plugins can choose how to handle this. if uid: log.info( '(%s) Nick collision on %s/%s, forwarding this to plugins', self.irc.name, uid, nick) self.irc.callHooks([self.irc.sid, 'SAVE', {'target': uid}])
def on_member_remove(self, event: events.GuildMemberRemove, *args, **kwargs): log.info('(%s) got GuildMemberRemove event for guild %s: %s', self.protocol.name, event.guild_id, event.user) try: pylink_netobj = self.protocol._children[event.guild_id] except KeyError: log.debug("(%s) Could not remove user %s as the parent network object does not exist", self.protocol.name, event.user) return if event.user.id in pylink_netobj.users: pylink_netobj._remove_client(event.user.id) # XXX: make the message configurable pylink_netobj.call_hooks([event.user.id, 'QUIT', {'text': 'User left the guild'}])
def shutdown(irc, source, args): """takes no arguments. Exits PyLink by disconnecting all networks.""" permissions.checkPermissions(irc, source, ['core.shutdown']) u = irc.users[source] log.info('(%s) SHUTDOWN requested by "%s!%s@%s", exiting...', irc.name, u.nick, u.ident, u.host) control._shutdown(irc)
def delacc(irc, source, args): """<channel/chanpair> <mask or range string> Removes the Automode entry for the given mask or range string, if they exist. Range strings are indices (entry numbers) or ranges of them joined together with commas: e.g. "1", "2-10", "1,3,5-8". Entry numbers are shown by LISTACC. """ try: chanpair, mask = args except ValueError: error(irc, "Invalid arguments given. Needs 2: channel, mask") return else: ircobj, channel = _get_channel_pair(irc, source, chanpair, perm='manage') dbentry = db.get(ircobj.name+channel) if dbentry is None: error(irc, "No Automode access entries exist for \x02%s\x02." % channel) return if mask in dbentry: del dbentry[mask] log.info('(%s) %s removed modes for %s on %s', ircobj.name, irc.get_hostmask(source), mask, channel) reply(irc, "Done. Removed the Automode access entry for \x02%s\x02 in \x02%s\x02." % (mask, channel)) else: # Treat the mask as a range string. try: new_keys = utils.remove_range(mask, sorted(dbentry.keys())) except ValueError: error(irc, "No Automode access entry for \x02%s\x02 exists in \x02%s\x02." % (mask, channel)) return # XXX: Automode entries are actually unordered: what we're actually doing is sorting the keys # by name into a list, running remove_range on that, and removing the difference. removed = [] source_host = irc.get_hostmask(source) for mask_entry in dbentry.copy(): if mask_entry not in new_keys: del dbentry[mask_entry] log.info('(%s) %s removed modes for %s on %s', ircobj.name, source_host, mask_entry, channel) removed.append(mask_entry) reply(irc, 'Done. Removed \x02%d\x02 entries on \x02%s\x02: %s' % (len(removed), channel, ', '.join(removed))) # Remove channels if no more entries are left. if not dbentry: log.debug("Automode: purging empty channel pair %s/%s", ircobj.name, channel) del db[ircobj.name+channel] modebot.remove_persistent_channel(ircobj, 'automode', channel)
def squishids(ids): if ids == []: return "0" else: found = [[ids[0]]] log.info("Found: %s" % found) for previous, next in itertools.zip_longest(ids, ids[1:]): if next != previous + 1: if previous != found[-1][0]: found[-1].append(previous) if next is not None: found.append([next]) return ", ".join("-".join(str(r) for r in f) for f in found)
def _remove_pid(): pidfile = "%s.pid" % conf.confname if world._should_remove_pid: # Remove our pid file. log.info("Removing PID file %r.", pidfile) try: os.remove(pidfile) except OSError: log.exception("Failed to remove PID file %r, ignoring..." % pidfile) else: log.debug( 'Not removing PID file %s as world._should_remove_pid is False.' % pidfile)
def _kill_plugins(irc=None): log.info("Shutting down plugins.") for name, plugin in world.plugins.items(): # Before closing connections, tell all plugins to shutdown cleanly first. if hasattr(plugin, 'die'): log.debug( 'coremods.control: Running die() on plugin %s due to shutdown.', name) try: plugin.die(irc) except: # But don't allow it to crash the server. log.exception( 'coremods.control: Error occurred in die() of plugin %s, skipping...', name)
def _eval(irc, source, args): """<Python expression> Admin-only. Evaluates the given Python expression and returns the result. \x02**WARNING: THIS CAN BE DANGEROUS IF USED IMPROPERLY!**\x02""" irc.checkAuthenticated(source, allowOper=False) args = ' '.join(args) if not args.strip(): irc.reply('No code entered!') return log.info('(%s) Evaluating %r for %s', irc.name, args, irc.getHostmask(source)) irc.reply(eval(args))
def inject(irc, source, args): """<text> Admin-only. Injects raw text into the running PyLink protocol module, replying with the hook data returned. \x02**WARNING: THIS CAN BREAK YOUR NETWORK IF USED IMPROPERLY!**\x02""" irc.checkAuthenticated(source, allowOper=False) args = ' '.join(args) if not args.strip(): irc.reply('No text entered!') return log.info('(%s) Injecting raw text %r into protocol module for %s', irc.name, args, irc.getHostmask(source)) irc.reply(irc.runline(args))
def _exec(irc, source, args): """<code> Admin-only. Executes <code> in the current PyLink instance. This command performs backslash escaping of characters, so things like \\n and \\ will work. \x02**WARNING: THIS CAN BE DANGEROUS IF USED IMPROPERLY!**\x02""" irc.checkAuthenticated(source, allowOper=False) # Allow using \n in the code, while escaping backslashes correctly otherwise. args = bytes(' '.join(args), 'utf-8').decode("unicode_escape") if not args.strip(): irc.reply('No code entered!') return log.info('(%s) Executing %r for %s', irc.name, args, irc.getHostmask(source)) exec(args, globals(), locals())
def _submit_dronebl(irc, ip, apikey, nickuserhost=None): reason = irc.get_service_option('badchans', 'dnsbl_reason', DEFAULT_DNSBL_REASON) request = '<add ip="%s" type="%s" comment="%s" />' % (ip, DRONEBL_TYPE, reason) xml_data = '<?xml version="1.0"?><request key="%s">%s</request>' % (apikey, request) headers = {'Content-Type': 'text/xml'} log.debug('(%s) badchans: posting to dronebl: %s', irc.name, xml_data) # Expecting this to block r = requests.post('https://dronebl.org/RPC2', data=xml_data, headers=headers) dronebl_response = r.text log.debug('(%s) badchans: got response from dronebl: %s', irc.name, dronebl_response) if '<success' in dronebl_response: log.info('(%s) badchans: got success for DroneBL on %s (%s)', irc.name, ip, nickuserhost or 'some n!u@h') else: log.warning('(%s) badchans: dronebl submission error: %s', irc.name, dronebl_response)
def _check_connection(irc, args): ip = args['ip'] try: result = sshbl.scan(ip) except: log.exception("SSHBL scan errored:") return threshold = irc.get_service_option('sshbl', 'threshold', default=0) reason = irc.get_service_option('sshbl', 'reason', "Your host runs an SSH daemon commonly used by spammer IPs. Consider upgrading your machines " "or contacting network staff for an exemption.") if result: _, port, blacklisted, score = result if not blacklisted: return log.info("sshbl: caught IP %s:%s (%s/%s) with score %s", ip, port, irc.name, args['nick'], score) if args['uid'] in irc.users: irc.kill(irc.pseudoclient.uid, args['uid'], reason)
def handle_join(irc, source, command, args): """ killonjoin JOIN listener. """ # Ignore our own clients and other Ulines if irc.is_privileged_service(source) or irc.is_internal_server(source): return badchans = irc.serverdata.get('badchans') if not badchans: return elif not isinstance(badchans, list): log.error("(%s) badchans: the 'badchans' option must be a list of strings, not a %s", irc.name, type(badchans)) return use_kline = irc.get_service_option('badchans', 'use_kline', False) kline_duration = irc.get_service_option('badchans', 'kline_duration', DEFAULT_BAN_DURATION) try: kline_duration = utils.parse_duration(kline_duration) except ValueError: log.warning('(%s) badchans: invalid kline duration %s', irc.name, kline_duration, exc_info=True) kline_duration = DEFAULT_BAN_DURATION channel = args['channel'] for badchan in badchans: if irc.match_text(badchan, channel): asm_uid = None # Try to kill from the antispam service if available if 'antispam' in world.services: asm_uid = world.services['antispam'].uids.get(irc.name) for user in args['users']: try: ip = irc.users[user].ip ipa = ipaddress.ip_address(ip) except (KeyError, ValueError): log.error("(%s) badchans: could not obtain IP of user %s", irc.name, user) continue nuh = irc.get_hostmask(user) exempt_hosts = set(conf.conf.get('badchans', {}).get('exempt_hosts', [])) | \ set(irc.serverdata.get('badchans_exempt_hosts', [])) if not ipa.is_global: irc.msg(user, "Warning: %s kills unopered users, but non-public addresses are exempt." % channel, notice=True, source=asm_uid or irc.pseudoclient.uid) continue if exempt_hosts: skip = False for glob in exempt_hosts: if irc.match_host(glob, user): log.info("(%s) badchans: ignoring exempt user %s on %s (%s)", irc.name, nuh, channel, ip) irc.msg(user, "Warning: %s kills unopered users, but your host is exempt." % channel, notice=True, source=asm_uid or irc.pseudoclient.uid) skip = True break if skip: continue if irc.is_oper(user): irc.msg(user, "Warning: %s kills unopered users!" % channel, notice=True, source=asm_uid or irc.pseudoclient.uid) else: log.info('(%s) badchans: punishing user %s (server: %s) for joining channel %s', irc.name, nuh, irc.get_friendly_name(irc.get_server(user)), channel) if use_kline: irc.set_server_ban(asm_uid or irc.sid, kline_duration, host=ip, reason=REASON) else: irc.kill(asm_uid or irc.sid, user, REASON) if ip not in seen_ips: dronebl_key = irc.get_service_option('badchans', 'dronebl_key') if dronebl_key: log.info('(%s) badchans: submitting IP %s (%s) to DroneBL', irc.name, ip, nuh) pool.submit(_submit_dronebl, irc, ip, dronebl_key, nuh) dnsblim_key = irc.get_service_option('badchans', 'dnsblim_key') if dnsblim_key: log.info('(%s) badchans: submitting IP %s (%s) to DNSBL.im', irc.name, ip, nuh) pool.submit(_submit_dnsblim, irc, ip, dnsblim_key, nuh) seen_ips[ip] = time.time() else: log.debug('(%s) badchans: ignoring already submitted IP %s', irc.name, ip)