diff --git a/classes.py b/classes.py index 0c033b4..7409ee8 100644 --- a/classes.py +++ b/classes.py @@ -14,8 +14,6 @@ import ssl import hashlib from copy import deepcopy import inspect -import re -from collections import defaultdict import ipaddress import queue @@ -29,15 +27,16 @@ from .log import * ### Exceptions +# XXX: is this the right place to put this class? class ProtocolError(RuntimeError): pass ### Internal classes (users, servers, channels) -class Irc(utils.DeprecatedAttributesObject): +class PyLinkNetworkCore(utils.DeprecatedAttributesObject, utils.CamelCaseToSnakeCase): """Base IRC object for PyLink.""" - def __init__(self, netname, proto, conf): + def __init__(self, netname): """ Initializes an IRC object. This takes 3 variables: the network name (a string), the name of the protocol module to use for this connection, @@ -50,41 +49,39 @@ class Irc(utils.DeprecatedAttributesObject): self.loghandlers = [] self.name = netname - self.conf = conf + self.conf = conf.conf self.sid = None - self.serverdata = conf['servers'][netname] - self.botdata = conf['bot'] - self.protoname = proto.__name__.split('.')[-1] # Remove leading pylinkirc.protocols. - self.proto = proto.Class(self) + self.serverdata = conf.conf['servers'][netname] + self.botdata = conf.conf['bot'] + self.protoname = self.__class__.__module__.split('.')[-1] # Remove leading pylinkirc.protocols. + self.proto = self.irc = self # Backwards compat + + # Protocol stuff + self.casemapping = 'rfc1459' + self.hook_map = {} + + # Lists required conf keys for the server block. + self.conf_keys = {'ip', 'port', 'hostname', 'sid', 'sidrange', 'protocol', 'sendpass', + 'recvpass'} + + # Defines a set of PyLink protocol capabilities + self.protocol_caps = set() # These options depend on self.serverdata from above to be set. self.encoding = None - self.pingfreq = self.serverdata.get('pingfreq') or 90 - self.pingtimeout = self.pingfreq * 2 - - self.queue = None self.connected = threading.Event() self.aborted = threading.Event() self.reply_lock = threading.RLock() - self.pingTimer = None - # Sets the multiplier for autoconnect delay (grows with time). self.autoconnect_active_multiplier = 1 - self.initVars() + self.was_successful = False - if world.testing: - # HACK: Don't thread if we're running tests. - self.connect() - else: - self.connection_thread = threading.Thread(target=self.connect, - name="Listener for %s" % - self.name) - self.connection_thread.start() + self.init_vars() - def logSetup(self): + def log_setup(self): """ Initializes any channel loggers defined for the current network. """ @@ -109,20 +106,15 @@ class Irc(utils.DeprecatedAttributesObject): self.loghandlers.append(handler) log.addHandler(handler) - def initVars(self): + def init_vars(self): """ (Re)sets an IRC object to its default state. This should be called when an IRC object is first created, and on every reconnection to a network. """ self.encoding = self.serverdata.get('encoding') or 'utf-8' - self.pingfreq = self.serverdata.get('pingfreq') or 90 - self.pingtimeout = self.pingfreq * 3 + # Tracks the main PyLink client's UID. self.pseudoclient = None - self.lastping = time.time() - - self.maxsendq = self.serverdata.get('maxsendq', 4096) - self.queue = queue.Queue(self.maxsendq) # Internal variable to set the place and caller of the last command (in PM # or in a channel), used by fantasy command support. @@ -134,7 +126,7 @@ class Irc(utils.DeprecatedAttributesObject): # now. self.servers = {} self.users = {} - self.channels = structures.KeyedDefaultdict(IrcChannel) + self.channels = structures.KeyedDefaultdict(Channel) # This sets the list of supported channel and user modes: the default # RFC1459 modes are implied. Named modes are used here to make @@ -176,37 +168,989 @@ class Irc(utils.DeprecatedAttributesObject): self.start_ts = int(time.time()) # Set up channel logging for the network - self.logSetup() + self.log_setup() - def processQueue(self): - """Loop to process outgoing queue data.""" - while True: - throttle_time = self.serverdata.get('throttle_time', 0.005) - if not self.aborted.wait(throttle_time): - data = self.queue.get() - if data is None: - log.debug('(%s) Stopping queue thread due to getting None as item', self.name) - break - elif data: - self._send(data) + def __repr__(self): + return "<%s object for network %r>" % (self.__class__.name, self.name) + + ### General utility functions + def call_hooks(self, hook_args): + """Calls a hook function with the given hook args.""" + numeric, command, parsed_args = hook_args + # Always make sure TS is sent. + if 'ts' not in parsed_args: + parsed_args['ts'] = int(time.time()) + hook_cmd = command + hook_map = self.hook_map + + # If the hook name is present in the protocol module's hook_map, then we + # should set the hook name to the name that points to instead. + # For example, plugins will read SETHOST as CHGHOST, EOS (end of sync) + # as ENDBURST, etc. + if command in hook_map: + hook_cmd = hook_map[command] + + # However, individual handlers can also return a 'parse_as' key to send + # their payload to a different hook. An example of this is "/join 0" + # being interpreted as leaving all channels (PART). + hook_cmd = parsed_args.get('parse_as') or hook_cmd + + log.debug('(%s) Raw hook data: [%r, %r, %r] received from %s handler ' + '(calling hook %s)', self.name, numeric, hook_cmd, parsed_args, + command, hook_cmd) + + # Iterate over registered hook functions, catching errors accordingly. + for hook_func in world.hooks[hook_cmd]: + try: + log.debug('(%s) Calling hook function %s from plugin "%s"', self.name, + hook_func, hook_func.__module__) + hook_func(self, numeric, command, parsed_args) + except Exception: + # We don't want plugins to crash our servers... + log.exception('(%s) Unhandled exception caught in hook %r from plugin "%s"', + self.name, hook_func, hook_func.__module__) + log.error('(%s) The offending hook data was: %s', self.name, + hook_args) + continue + + def call_command(self, source, text): + """ + Calls a PyLink bot command. source is the caller's UID, and text is the + full, unparsed text of the message. + """ + world.services['pylink'].call_cmd(self, source, text) + + def msg(self, target, text, notice=None, source=None, loopback=True): + """Handy function to send messages/notices to clients. Source + is optional, and defaults to the main PyLink client if not specified.""" + if not text: + return + + if not (source or self.pseudoclient): + # No explicit source set and our main client wasn't available; abort. + return + source = source or self.pseudoclient.uid + + if notice: + self.notice(source, target, text) + cmd = 'PYLINK_SELF_NOTICE' + else: + self.message(source, target, text) + cmd = 'PYLINK_SELF_PRIVMSG' + + if loopback: + # Determines whether we should send a hook for this msg(), to relay things like services + # replies across relay. + self.call_hooks([source, cmd, {'target': target, 'text': text}]) + + def _reply(self, text, notice=None, source=None, private=None, force_privmsg_in_private=False, + loopback=True): + """ + Core of the reply() function - replies to the last caller in the right context + (channel or PM). + """ + if private is None: + # Allow using private replies as the default, if no explicit setting was given. + private = conf.conf['bot'].get("prefer_private_replies") + + # Private reply is enabled, or the caller was originally a PM + if private or (self.called_in in self.users): + if not force_privmsg_in_private: + # For private replies, the default is to override the notice=True/False argument, + # and send replies as notices regardless. This is standard behaviour for most + # IRC services, but can be disabled if force_privmsg_in_private is given. + notice = True + target = self.called_by + else: + target = self.called_in + + self.msg(target, text, notice=notice, source=source, loopback=loopback) + + def reply(self, *args, **kwargs): + """ + Replies to the last caller in the right context (channel or PM). + + This function wraps around _reply() and can be monkey-patched in a thread-safe manner + to temporarily redirect plugin output to another target. + """ + with self.reply_lock: + self._reply(*args, **kwargs) + + def error(self, text, **kwargs): + """Replies with an error to the last caller in the right context (channel or PM).""" + # This is a stub to alias error to reply + self.reply("Error: %s" % text, **kwargs) + + ### Configuration-based lookup functions. + def version(self): + """ + Returns a detailed version string including the PyLink daemon version, + the protocol module in use, and the server hostname. + """ + fullversion = 'PyLink-%s. %s :[protocol:%s, encoding:%s]' % (__version__, self.hostname(), self.protoname, self.encoding) + return fullversion + + def hostname(self): + """ + Returns the server hostname used by PyLink on the given server. + """ + return self.serverdata.get('hostname', world.fallback_hostname) + + def get_full_network_name(self): + """ + Returns the full network name (as defined by the "netname" option), or the + short network name if that isn't defined. + """ + return self.serverdata.get('netname', self.name) + + def parse_protocol_command(self, line): + """Sends a command to the protocol module.""" + log.debug("(%s) <- %s", self.name, line) + try: + hook_args = self.handle_events(line) + except Exception: + log.exception('(%s) Caught error in handle_events, disconnecting!', self.name) + log.error('(%s) The offending line was: <- %s', self.name, line) + self.aborted.set() + return + # Only call our hooks if there's data to process. Handlers that support + # hooks will return a dict of parsed arguments, which can be passed on + # to plugins and the like. For example, the JOIN handler will return + # something like: {'channel': '#whatever', 'users': ['UID1', 'UID2', + # 'UID3']}, etc. + if hook_args is not None: + self.call_hooks(hook_args) + + return hook_args + runline = parse_protocol_command + + def _pre_connect(self): + self.aborted.clear() + self.init_vars() + + try: + self.validateServerConf() + except AssertionError as e: + log.exception("(%s) Configuration error: %s", self.name, e) + raise + + def _run_autoconnect(self): + """Blocks for the autoconnect time and returns True if autoconnect is enabled.""" + autoconnect = self.serverdata.get('autoconnect') + + # Sets the autoconnect growth multiplier (e.g. a value of 2 multiplies the autoconnect + # time by 2 on every failure, etc.) + autoconnect_multiplier = self.serverdata.get('autoconnect_multiplier', 2) + autoconnect_max = self.serverdata.get('autoconnect_max', 1800) + # These values must at least be 1. + autoconnect_multiplier = max(autoconnect_multiplier, 1) + autoconnect_max = max(autoconnect_max, 1) + + log.debug('(%s) _run_autoconnect: Autoconnect delay set to %s seconds.', self.name, autoconnect) + if autoconnect is not None and autoconnect >= 1: + log.debug('(%s) _run_autoconnect: Multiplying autoconnect delay %s by %s.', self.name, autoconnect, self.autoconnect_active_multiplier) + autoconnect *= self.autoconnect_active_multiplier + # Add a cap on the max. autoconnect delay, so that we don't go on forever... + autoconnect = min(autoconnect, autoconnect_max) + + log.info('(%s) _run_autoconnect: Going to auto-reconnect in %s seconds.', self.name, autoconnect) + # Continue when either self.aborted is set or the autoconnect time passes. + # Compared to time.sleep(), this allows us to stop connections quicker if we + # break while while for autoconnect. + self.aborted.clear() + self.aborted.wait(autoconnect) + + # Store in the local state what the autoconnect multiplier currently is. + self.autoconnect_active_multiplier *= autoconnect_multiplier + + if self not in world.networkobjects.values(): + log.debug('(%s) _run_autoconnect: Stopping stale connect loop', self.name) + return + return True + + else: + log.info('(%s) _run_autoconnect: Stopping connect loop (autoconnect value %r is < 1).', self.name, autoconnect) + return + + def _pre_disconnect(self): + self.was_successful = self.connected.is_set() + log.debug('(%s) _pre_disconnect: got %s for was_successful state', self.name, self.was_successful) + + log.debug('(%s) _pre_disconnect: Clearing self.connected state.', self.name) + self.connected.clear() + + log.debug('(%s) _pre_disconnect: Removing channel logging handlers due to disconnect.', self.name) + while self.loghandlers: + log.removeHandler(self.loghandlers.pop()) + + def _post_disconnect(self): + log.debug('(%s) _post_disconnect: Setting self.aborted to True.', self.name) + self.aborted.set() + + # Internal hook signifying that a network has disconnected. + self.call_hooks([None, 'PYLINK_DISCONNECT', {'was_successful': self.was_successful}]) + + log.debug('(%s) _post_disconnect: Clearing state via init_vars().', self.name) + self.init_vars() + + def validate_server_conf(self): + return + + def has_cap(self, capab): + """ + Returns whether this protocol module instance has the requested capability. + """ + return capab.lower() in self.protocol_caps + + def _remove_client(self, numeric): + """Internal function to remove a client from our internal state.""" + for c, v in self.channels.copy().items(): + v.remove_user(numeric) + # Clear empty non-permanent channels. + if not (self.channels[c].users or ((self.cmodes.get('permanent'), None) in self.channels[c].modes)): + del self.channels[c] + + sid = self.get_server(numeric) + log.debug('Removing client %s from self.users', numeric) + del self.users[numeric] + log.debug('Removing client %s from self.servers[%s].users', numeric, sid) + self.servers[sid].users.discard(numeric) + + ### State checking functions + def nick_to_uid(self, nick): + """Looks up the UID of a user with the given nick, if one is present.""" + nick = self.to_lower(nick) + for k, v in self.users.copy().items(): + if self.to_lower(v.nick) == nick: + return k + + def is_internal_client(self, numeric): + """ + Returns whether the given client numeric (UID) is a PyLink client. + """ + sid = self.get_server(numeric) + if sid and self.servers[sid].internal: + return True + return False + + def is_internal_server(self, sid): + """Returns whether the given SID is an internal PyLink server.""" + return (sid in self.servers and self.servers[sid].internal) + + def get_server(self, numeric): + """Finds the SID of the server a user is on.""" + userobj = self.users.get(numeric) + if userobj: + return userobj.server + + def is_manipulatable_client(self, uid): + """ + Returns whether the given user is marked as an internal, manipulatable + client. Usually, automatically spawned services clients should have this + set True to prevent interactions with opers (like mode changes) from + causing desyncs. + """ + return self.is_internal_client(uid) and self.users[uid].manipulatable + + def get_service_bot(self, uid): + """ + Checks whether the given UID is a registered service bot. If True, + returns the cooresponding ServiceBot object. + """ + userobj = self.users.get(uid) + if not userobj: + return False + + # Look for the "service" attribute in the User object, if one exists. + try: + sname = userobj.service + # Warn if the service name we fetched isn't a registered service. + if sname not in world.services.keys(): + log.warning("(%s) User %s / %s had a service bot record to a service that doesn't " + "exist (%s)!", self.name, uid, userobj.nick, sname) + return world.services.get(sname) + except AttributeError: + return False + +class PyLinkNetworkCoreWithUtils(PyLinkNetworkCore): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Lock for updateTS to make sure only one thread can change the channel TS at one time. + self._ts_lock = threading.Lock() + + def to_lower(self, text): + """Returns a lowercase representation of text based on the IRC object's + casemapping (rfc1459 or ascii).""" + if self.casemapping == 'rfc1459': + text = text.replace('{', '[') + text = text.replace('}', ']') + text = text.replace('|', '\\') + text = text.replace('~', '^') + # Encode the text as bytes first, and then lowercase it so that only ASCII characters are + # changed. Unicode in channel names, etc. is case sensitive because IRC is just that old of + # a protocol!!! + return text.encode().lower().decode() + + def parse_modes(self, target, args): + """Parses a modestring list into a list of (mode, argument) tuples. + ['+mitl-o', '3', 'person'] => [('+m', None), ('+i', None), ('+t', None), ('+l', '3'), ('-o', 'person')] + """ + # http://www.irc.org/tech_docs/005.html + # A = Mode that adds or removes a nick or address to a list. Always has a parameter. + # B = Mode that changes a setting and always has a parameter. + # C = Mode that changes a setting and only has a parameter when set. + # D = Mode that changes a setting and never has a parameter. + + if type(args) == str: + # If the modestring was given as a string, split it into a list. + args = args.split() + + assert args, 'No valid modes were supplied!' + usermodes = not utils.isChannel(target) + prefix = '' + modestring = args[0] + args = args[1:] + if usermodes: + log.debug('(%s) Using self.umodes for this query: %s', self.name, self.umodes) + + if target not in self.users: + log.debug('(%s) Possible desync! Mode target %s is not in the users index.', self.name, target) + return [] # Return an empty mode list + + supported_modes = self.umodes + oldmodes = self.users[target].modes + else: + log.debug('(%s) Using self.cmodes for this query: %s', self.name, self.cmodes) + + supported_modes = self.cmodes + oldmodes = self.channels[target].modes + res = [] + for mode in modestring: + if mode in '+-': + prefix = mode else: - break + if not prefix: + prefix = '+' + arg = None + log.debug('Current mode: %s%s; args left: %s', prefix, mode, args) + try: + if mode in self.prefixmodes and not usermodes: + # We're setting a prefix mode on someone (e.g. +o user1) + log.debug('Mode %s: This mode is a prefix mode.', mode) + arg = args.pop(0) + # Convert nicks to UIDs implicitly; most IRCds will want + # this already. + arg = self.nick_to_uid(arg) or arg + if arg not in self.users: # Target doesn't exist, skip it. + log.debug('(%s) Skipping setting mode "%s %s"; the ' + 'target doesn\'t seem to exist!', self.name, + mode, arg) + continue + elif mode in (supported_modes['*A'] + supported_modes['*B']): + # Must have parameter. + log.debug('Mode %s: This mode must have parameter.', mode) + arg = args.pop(0) + if prefix == '-': + if mode in supported_modes['*B'] and arg == '*': + # Charybdis allows unsetting +k without actually + # knowing the key by faking the argument when unsetting + # as a single "*". + # We'd need to know the real argument of +k for us to + # be able to unset the mode. + oldarg = dict(oldmodes).get(mode) + if oldarg: + # Set the arg to the old one on the channel. + arg = oldarg + log.debug("Mode %s: coersing argument of '*' to %r.", mode, arg) - def connect(self): + log.debug('(%s) parse_modes: checking if +%s %s is in old modes list: %s', self.name, mode, arg, oldmodes) + + if (mode, arg) not in oldmodes: + # Ignore attempts to unset bans that don't exist. + log.debug("(%s) parse_modes(): ignoring removal of non-existent list mode +%s %s", self.name, mode, arg) + continue + + elif prefix == '+' and mode in supported_modes['*C']: + # Only has parameter when setting. + log.debug('Mode %s: Only has parameter when setting.', mode) + arg = args.pop(0) + except IndexError: + log.warning('(%s/%s) Error while parsing mode %r: mode requires an ' + 'argument but none was found. (modestring: %r)', + self.name, target, mode, modestring) + continue # Skip this mode; don't error out completely. + res.append((prefix + mode, arg)) + return res + + def apply_modes(self, target, changedmodes): + """Takes a list of parsed IRC modes, and applies them on the given target. + + The target can be either a channel or a user; this is handled automatically.""" + usermodes = not utils.isChannel(target) + + try: + if usermodes: + old_modelist = self.users[target].modes + supported_modes = self.umodes + else: + old_modelist = self.channels[target].modes + supported_modes = self.cmodes + except KeyError: + log.warning('(%s) Possible desync? Mode target %s is unknown.', self.name, target) + return + + modelist = set(old_modelist) + log.debug('(%s) Applying modes %r on %s (initial modelist: %s)', self.name, changedmodes, target, modelist) + for mode in changedmodes: + # Chop off the +/- part that parse_modes gives; it's meaningless for a mode list. + try: + real_mode = (mode[0][1], mode[1]) + except IndexError: + real_mode = mode + + if not usermodes: + # We only handle +qaohv for now. Iterate over every supported mode: + # if the IRCd supports this mode and it is the one being set, add/remove + # the person from the corresponding prefix mode list (e.g. c.prefixmodes['op'] + # for ops). + for pmode, pmodelist in self.channels[target].prefixmodes.items(): + if pmode in self.cmodes and real_mode[0] == self.cmodes[pmode]: + log.debug('(%s) Initial prefixmodes list: %s', self.name, pmodelist) + if mode[0][0] == '+': + pmodelist.add(mode[1]) + else: + pmodelist.discard(mode[1]) + + log.debug('(%s) Final prefixmodes list: %s', self.name, pmodelist) + + if real_mode[0] in self.prefixmodes: + # Don't add prefix modes to Channel.modes; they belong in the + # prefixmodes mapping handled above. + log.debug('(%s) Not adding mode %s to Channel.modes because ' + 'it\'s a prefix mode.', self.name, str(mode)) + continue + + if mode[0][0] != '-': + # We're adding a mode + existing = [m for m in modelist if m[0] == real_mode[0] and m[1] != real_mode[1]] + if existing and real_mode[1] and real_mode[0] not in self.cmodes['*A']: + # The mode we're setting takes a parameter, but is not a list mode (like +beI). + # Therefore, only one version of it can exist at a time, and we must remove + # any old modepairs using the same letter. Otherwise, we'll get duplicates when, + # for example, someone sets mode "+l 30" on a channel already set "+l 25". + log.debug('(%s) Old modes for mode %r exist on %s, removing them: %s', + self.name, real_mode, target, str(existing)) + [modelist.discard(oldmode) for oldmode in existing] + modelist.add(real_mode) + log.debug('(%s) Adding mode %r on %s', self.name, real_mode, target) + else: + log.debug('(%s) Removing mode %r on %s', self.name, real_mode, target) + # We're removing a mode + if real_mode[1] is None: + # We're removing a mode that only takes arguments when setting. + # Remove all mode entries that use the same letter as the one + # we're unsetting. + for oldmode in modelist.copy(): + if oldmode[0] == real_mode[0]: + modelist.discard(oldmode) + else: + modelist.discard(real_mode) + log.debug('(%s) Final modelist: %s', self.name, modelist) + try: + if usermodes: + self.users[target].modes = modelist + else: + self.channels[target].modes = modelist + except KeyError: + log.warning("(%s) Invalid MODE target %s (usermodes=%s)", self.name, target, usermodes) + + @staticmethod + def _flip(mode): + """Flips a mode character.""" + # Make it a list first, strings don't support item assignment + mode = list(mode) + if mode[0] == '-': # Query is something like "-n" + mode[0] = '+' # Change it to "+n" + elif mode[0] == '+': + mode[0] = '-' + else: # No prefix given, assume + + mode.insert(0, '-') + return ''.join(mode) + + def reverse_modes(self, target, modes, oldobj=None): + """Reverses/Inverts the mode string or mode list given. + + Optionally, an oldobj argument can be given to look at an earlier state of + a channel/user object, e.g. for checking the op status of a mode setter + before their modes are processed and added to the channel state. + + This function allows both mode strings or mode lists. Example uses: + "+mi-lk test => "-mi+lk test" + "mi-k test => "-mi+k test" + [('+m', None), ('+r', None), ('+l', '3'), ('-o', 'person') + => {('-m', None), ('-r', None), ('-l', None), ('+o', 'person')}) + {('s', None), ('+o', 'whoever') => {('-s', None), ('-o', 'whoever')}) + """ + origtype = type(modes) + # If the query is a string, we have to parse it first. + if origtype == str: + modes = self.parse_modes(target, modes.split(" ")) + # Get the current mode list first. + if utils.isChannel(target): + c = oldobj or self.channels[target] + oldmodes = c.modes.copy() + possible_modes = self.cmodes.copy() + # For channels, this also includes the list of prefix modes. + possible_modes['*A'] += ''.join(self.prefixmodes) + for name, userlist in c.prefixmodes.items(): + try: + oldmodes.update([(self.cmodes[name], u) for u in userlist]) + except KeyError: + continue + else: + oldmodes = self.users[target].modes + possible_modes = self.umodes + newmodes = [] + log.debug('(%s) reverse_modes: old/current mode list for %s is: %s', self.name, + target, oldmodes) + for char, arg in modes: + # Mode types: + # A = Mode that adds or removes a nick or address to a list. Always has a parameter. + # B = Mode that changes a setting and always has a parameter. + # C = Mode that changes a setting and only has a parameter when set. + # D = Mode that changes a setting and never has a parameter. + mchar = char[-1] + if mchar in possible_modes['*B'] + possible_modes['*C']: + # We need to find the current mode list, so we can reset arguments + # for modes that have arguments. For example, setting +l 30 on a channel + # that had +l 50 set should give "+l 30", not "-l". + oldarg = [m for m in oldmodes if m[0] == mchar] + if oldarg: # Old mode argument for this mode existed, use that. + oldarg = oldarg[0] + mpair = ('+%s' % oldarg[0], oldarg[1]) + else: # Not found, flip the mode then. + # Mode takes no arguments when unsetting. + if mchar in possible_modes['*C'] and char[0] != '-': + arg = None + mpair = (self._flip(char), arg) + else: + mpair = (self._flip(char), arg) + if char[0] != '-' and (mchar, arg) in oldmodes: + # Mode is already set. + log.debug("(%s) reverse_modes: skipping reversing '%s %s' with %s since we're " + "setting a mode that's already set.", self.name, char, arg, mpair) + continue + elif char[0] == '-' and (mchar, arg) not in oldmodes and mchar in possible_modes['*A']: + # We're unsetting a prefixmode that was never set - don't set it in response! + # Charybdis lacks verification for this server-side. + log.debug("(%s) reverse_modes: skipping reversing '%s %s' with %s since it " + "wasn't previously set.", self.name, char, arg, mpair) + continue + newmodes.append(mpair) + + log.debug('(%s) reverse_modes: new modes: %s', self.name, newmodes) + if origtype == str: + # If the original query is a string, send it back as a string. + return self.join_modes(newmodes) + else: + return set(newmodes) + + @staticmethod + def join_modes(modes, sort=False): + """Takes a list of (mode, arg) tuples in parse_modes() format, and + joins them into a string. + + See testJoinModes in tests/test_utils.py for some examples.""" + prefix = '+' # Assume we're adding modes unless told otherwise + modelist = '' + args = [] + + # Sort modes alphabetically like a conventional IRCd. + if sort: + modes = sorted(modes) + + for modepair in modes: + mode, arg = modepair + assert len(mode) in (1, 2), "Incorrect length of a mode (received %r)" % mode + try: + # If the mode has a prefix, use that. + curr_prefix, mode = mode + except ValueError: + # If not, the current prefix stays the same; move on to the next + # modepair. + pass + else: + # If the prefix of this mode isn't the same as the last one, add + # the prefix to the modestring. This prevents '+nt-lk' from turning + # into '+n+t-l-k' or '+ntlk'. + if prefix != curr_prefix: + modelist += curr_prefix + prefix = curr_prefix + modelist += mode + if arg is not None: + args.append(arg) + if not modelist.startswith(('+', '-')): + # Our starting mode didn't have a prefix with it. Assume '+'. + modelist = '+' + modelist + if args: + # Add the args if there are any. + modelist += ' %s' % ' '.join(args) + return modelist + + @classmethod + def wrap_modes(cls, modes, limit, max_modes_per_msg=0): + """ + Takes a list of modes and wraps it across multiple lines. + """ + strings = [] + + # This process is slightly trickier than just wrapping arguments, because modes create + # positional arguments that can't be separated from its character. + queued_modes = [] + total_length = 0 + + last_prefix = '+' + orig_modes = modes.copy() + modes = list(modes) + while modes: + # PyLink mode lists come in the form [('+t', None), ('-b', '*!*@someone'), ('+l', 3)] + # The +/- part is optional depending on context, and should either: + # 1) The prefix of the last mode. + # 2) + (adding modes), if no prefix was ever given + next_mode = modes.pop(0) + + modechar, arg = next_mode + prefix = modechar[0] + if prefix not in '+-': + prefix = last_prefix + # Explicitly add the prefix to the mode character to prevent + # ambiguity when passing it to join_modes(). + modechar = prefix + modechar + # XXX: because tuples are immutable, we have to replace the entire modepair.. + next_mode = (modechar, arg) + + # Figure out the length that the next mode will add to the buffer. If we're changing + # from + to - (setting to removing modes) or vice versa, we'll need two characters + # ("+" or "-") plus the mode char itself. + next_length = 1 + if prefix != last_prefix: + next_length += 1 + + # Replace the last_prefix with the current one for the next iteration. + last_prefix = prefix + + if arg: + # This mode has an argument, so add the length of that and a space. + next_length += 1 + next_length += len(arg) + + assert next_length <= limit, \ + "wrap_modes: Mode %s is too long for the given length %s" % (next_mode, limit) + + # Check both message length and max. modes per msg if enabled. + if (next_length + total_length) <= limit and ((not max_modes_per_msg) or len(queued_modes) < max_modes_per_msg): + # We can fit this mode in the next message; add it. + total_length += next_length + log.debug('wrap_modes: Adding mode %s to queued modes', str(next_mode)) + queued_modes.append(next_mode) + log.debug('wrap_modes: queued modes: %s', queued_modes) + else: + # Otherwise, create a new message by joining the previous queue. + # Then, add our current mode. + strings.append(cls.join_modes(queued_modes)) + queued_modes.clear() + + log.debug('wrap_modes: cleared queue (length %s) and now adding %s', limit, str(next_mode)) + queued_modes.append(next_mode) + total_length = next_length + else: + # Everything fit in one line, so just use that. + strings.append(cls.join_modes(queued_modes)) + + log.debug('wrap_modes: returning %s for %s', strings, orig_modes) + return strings + + def get_hostmask(self, user, realhost=False, ip=False): + """ + Returns the hostmask of the given user, if present. If the realhost option + is given, return the real host of the user instead of the displayed host. + If the ip option is given, return the IP address of the user (this overrides + realhost).""" + userobj = self.users.get(user) + + try: + nick = userobj.nick + except AttributeError: + nick = '' + + try: + ident = userobj.ident + except AttributeError: + ident = '' + + try: + if ip: + host = userobj.ip + elif realhost: + host = userobj.realhost + else: + host = userobj.host + except AttributeError: + host = '' + + return '%s!%s@%s' % (nick, ident, host) + + def get_friendly_name(self, entityid): + """ + Returns the friendly name of a SID or UID (server name for SIDs, nick for UID). + """ + if entityid in self.servers: + return self.servers[entityid].name + elif entityid in self.users: + return self.users[entityid].nick + else: + raise KeyError("Unknown UID/SID %s" % entityid) + + def is_oper(self, uid, allowAuthed=True, allowOper=True): + """ + Returns whether the given user has operator status on PyLink. This can be achieved + by either identifying to PyLink as admin (if allowAuthed is True), + or having user mode +o set (if allowOper is True). At least one of + allowAuthed or allowOper must be True for this to give any meaningful + results. + """ + if uid in self.users: + if allowOper and ("o", None) in self.users[uid].modes: + return True + elif allowAuthed and self.users[uid].account: + return True + return False + + def check_authenticated(self, uid, allowAuthed=True, allowOper=True): + """ + Checks whether the given user has operator status on PyLink, raising + NotAuthorizedError and logging the access denial if not. + """ + log.warning("(%s) check_authenticated() is deprecated as of PyLink 1.2 and may be " + "removed in a future relase. Consider migrating to the PyLink Permissions API.", + self.name) + lastfunc = inspect.stack()[1][3] + if not self.is_oper(uid, allowAuthed=allowAuthed, allowOper=allowOper): + log.warning('(%s) Access denied for %s calling %r', self.name, + self.get_hostmask(uid), lastfunc) + raise utils.NotAuthorizedError("You are not authenticated!") + return True + + def match_host(self, glob, target, ip=True, realhost=True): + """ + Checks whether the given host, or given UID's hostmask matches the given nick!user@host + glob. + + If the target given is a UID, and the 'ip' or 'realhost' options are True, this will also + match against the target's IP address and real host, respectively. + + This function respects IRC casemappings (rfc1459 and ascii). If the given target is a UID, + and the 'ip' option is enabled, the host portion of the glob is also matched as a CIDR + range. + """ + # Get the corresponding casemapping value used by ircmatch. + if self.casemapping == 'rfc1459': + casemapping = 0 + else: + casemapping = 1 + + # Try to convert target into a UID. If this fails, it's probably a hostname. + target = self.nick_to_uid(target) or target + + # Allow queries like !$exttarget to invert the given match. + invert = glob.startswith('!') + if invert: + glob = glob.lstrip('!') + + def match_host_core(): + """ + Core processor for match_host(), minus the inversion check. + """ + # Work with variables in the match_host() scope, from + # http://stackoverflow.com/a/8178808 + nonlocal glob + + # Prepare a list of hosts to check against. + if target in self.users: + if glob.startswith('$'): + # Exttargets start with $. Skip regular ban matching and find the matching ban handler. + glob = glob.lstrip('$') + exttargetname = glob.split(':', 1)[0] + handler = world.exttarget_handlers.get(exttargetname) + + if handler: + # Handler exists. Return what it finds. + result = handler(self, glob, target) + log.debug('(%s) Got %s from exttarget %s in match_host() glob $%s for target %s', + self.name, result, exttargetname, glob, target) + return result + else: + log.debug('(%s) Unknown exttarget %s in match_host() glob $%s', self.name, + exttargetname, glob) + return False + + hosts = {self.get_hostmask(target)} + + if ip: + hosts.add(self.get_hostmask(target, ip=True)) + + # HACK: support CIDR hosts in the hosts portion + try: + header, cidrtarget = glob.split('@', 1) + log.debug('(%s) Processing CIDRs for %s (full host: %s)', self.name, + cidrtarget, glob) + # Try to parse the host portion as a CIDR range + network = ipaddress.ip_network(cidrtarget) + + log.debug('(%s) Found CIDR for %s, replacing target host with IP %s', self.name, + realhost, target) + real_ip = self.users[target].ip + if ipaddress.ip_address(real_ip) in network: + # If the CIDR matches, hack around the host matcher by pretending that + # the lookup target was the IP and not the CIDR range! + glob = '@'.join((header, real_ip)) + except ValueError: + pass + + if realhost: + hosts.add(self.get_hostmask(target, realhost=True)) + + else: # We were given a host, use that. + hosts = [target] + + # Iterate over the hosts to match using ircmatch. + for host in hosts: + if ircmatch.match(casemapping, glob, host): + return True + + return False + + result = match_host_core() + if invert: + result = not result + return result + + def updateTS(self, sender, channel, their_ts, modes=None): + """ + Merges modes of a channel given the remote TS and a list of modes. + """ + + # Okay, so the situation is that we have 6 possible TS/sender combinations: + + # | our TS lower | TS equal | their TS lower + # mode origin is us | OVERWRITE | MERGE | IGNORE + # mode origin is uplink | IGNORE | MERGE | OVERWRITE + + if modes is None: + modes = [] + + def _clear(): + log.debug("(%s) Clearing local modes from channel %s due to TS change", self.name, + channel) + self.channels[channel].modes.clear() + for p in self.channels[channel].prefixmodes.values(): + for user in p.copy(): + if not self.is_internal_client(user): + p.discard(user) + + def _apply(): + if modes: + log.debug("(%s) Applying modes on channel %s (TS ok)", self.name, + channel) + self.apply_modes(channel, modes) + + # Use a lock so only one thread can change a channel's TS at once: this prevents race + # conditions that would otherwise desync channel modes. + with self._ts_lock: + our_ts = self.channels[channel].ts + assert type(our_ts) == int, "Wrong type for our_ts (expected int, got %s)" % type(our_ts) + assert type(their_ts) == int, "Wrong type for their_ts (expected int, got %s)" % type(their_ts) + + # Check if we're the mode sender based on the UID / SID given. + our_mode = self.is_internal_client(sender) or self.is_internal_server(sender) + + log.debug("(%s/%s) our_ts: %s; their_ts: %s; is the mode origin us? %s", self.name, + channel, our_ts, their_ts, our_mode) + + if their_ts == our_ts: + log.debug("(%s/%s) remote TS of %s is equal to our %s; mode query %s", + self.name, channel, their_ts, our_ts, modes) + # Their TS is equal to ours. Merge modes. + _apply() + + elif (their_ts < our_ts): + if their_ts < 750000: + log.warning('(%s) Possible desync? Not setting bogus TS %s on channel %s', self.name, their_ts, channel) + else: + log.debug('(%s) Resetting channel TS of %s from %s to %s (remote has lower TS)', + self.name, channel, our_ts, their_ts) + self.channels[channel].ts = their_ts + + # Remote TS was lower and we're receiving modes. Clear the modelist and apply theirs. + + _clear() + _apply() + + # TODO: these wrappers really need to be standardized + def _get_SID(self, sname): + """Returns the SID of a server with the given name, if present.""" + name = sname.lower() + for k, v in self.servers.items(): + if v.name.lower() == name: + return k + else: + return sname # Fall back to given text instead of None + _getSid = _get_SID + + def _get_UID(self, target): + """Converts a nick argument to its matching UID. This differs from irc.nick_to_uid() + in that it returns the original text instead of None, if no matching nick is found.""" + target = self.nick_to_uid(target) or target + return target + _getUid = _get_UID + +class IRCNetwork(PyLinkNetworkCoreWithUtils): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.connection_thread = None + self.queue = None + self.pingTimer = None + + def init_vars(self, *args, **kwargs): + super().init_vars(*args, **kwargs) + + # Set IRC specific variables for ping checking and queuing + self.lastping = time.time() + self.pingfreq = self.serverdata.get('pingfreq') or 90 + self.pingtimeout = self.pingfreq * 3 + + self.maxsendq = self.serverdata.get('maxsendq', 4096) + self.queue = queue.Queue(self.maxsendq) + + def _schedule_ping(self): + """Schedules periodic pings in a loop.""" + self.ping() + + self.pingTimer = threading.Timer(self.pingfreq, self._schedule_ping) + self.pingTimer.daemon = True + self.pingTimer.name = 'Ping timer loop for %s' % self.name + self.pingTimer.start() + + log.debug('(%s) Ping scheduled at %s', self.name, time.time()) + + def _connect(self): """ Runs the connect loop for the IRC object. This is usually called by __init__ in a separate thread to allow multiple concurrent connections. """ while True: - - self.aborted.clear() - self.initVars() - - try: - self.proto.validateServerConf() - except AssertionError as e: - log.exception("(%s) Configuration error: %s", self.name, e) - return + self._pre_connect() ip = self.serverdata["ip"] port = self.serverdata["port"] @@ -307,32 +1251,32 @@ class Irc(utils.DeprecatedAttributesObject): if checks_ok: self.queue_thread = threading.Thread(name="Queue thread for %s" % self.name, - target=self.processQueue, daemon=True) + target=self._process_queue, daemon=True) self.queue_thread.start() self.sid = self.serverdata.get("sid") # All our checks passed, get the protocol module to connect and run the listen # loop. This also updates any SID values should the protocol module do so. - self.proto.connect() + self.post_connect() log.info('(%s) Enumerating our own SID %s', self.name, self.sid) host = self.hostname() - self.servers[self.sid] = IrcServer(None, host, internal=True, - desc=self.serverdata.get('serverdesc') - or conf.conf['bot']['serverdesc']) + self.servers[self.sid] = Server(None, host, internal=True, + desc=self.serverdata.get('serverdesc') + or conf.conf['bot']['serverdesc']) log.info('(%s) Starting ping schedulers....', self.name) - self.schedulePing() + self._schedule_ping() log.info('(%s) Server ready; listening for data.', self.name) self.autoconnect_active_multiplier = 1 # Reset any extra autoconnect delays - self.run() + self._run_irc() else: # Configuration error :( log.error('(%s) A configuration error was encountered ' 'trying to set up this connection. Please check' ' your configuration file and try again.', self.name) - # self.run() or the protocol module it called raised an exception, meaning we've disconnected! + # _run_irc() or the protocol module it called raised an exception, meaning we've disconnected! # Note: socket.error, ConnectionError, IOError, etc. are included in OSError since Python 3.3, # so we don't need to explicitly catch them here. # We also catch SystemExit here as a way to abort out connection threads properly, and stop the @@ -341,55 +1285,26 @@ class Irc(utils.DeprecatedAttributesObject): log.exception('(%s) Disconnected from IRC:', self.name) self.disconnect() - - # If autoconnect is enabled, loop back to the start. Otherwise, - # return and stop. - autoconnect = self.serverdata.get('autoconnect') - - # Sets the autoconnect growth multiplier (e.g. a value of 2 multiplies the autoconnect - # time by 2 on every failure, etc.) - autoconnect_multiplier = self.serverdata.get('autoconnect_multiplier', 2) - autoconnect_max = self.serverdata.get('autoconnect_max', 1800) - # These values must at least be 1. - autoconnect_multiplier = max(autoconnect_multiplier, 1) - autoconnect_max = max(autoconnect_max, 1) - - log.debug('(%s) Autoconnect delay set to %s seconds.', self.name, autoconnect) - if autoconnect is not None and autoconnect >= 1: - log.debug('(%s) Multiplying autoconnect delay %s by %s.', self.name, autoconnect, self.autoconnect_active_multiplier) - autoconnect *= self.autoconnect_active_multiplier - # Add a cap on the max. autoconnect delay, so that we don't go on forever... - autoconnect = min(autoconnect, autoconnect_max) - - log.info('(%s) Going to auto-reconnect in %s seconds.', self.name, autoconnect) - # Continue when either self.aborted is set or the autoconnect time passes. - # Compared to time.sleep(), this allows us to stop connections quicker if we - # break while while for autoconnect. - self.aborted.clear() - self.aborted.wait(autoconnect) - - # Store in the local state what the autoconnect multiplier currently is. - self.autoconnect_active_multiplier *= autoconnect_multiplier - - if self not in world.networkobjects.values(): - log.debug('Stopping stale connect loop for old connection %r', self.name) - return - - else: - log.info('(%s) Stopping connect loop (autoconnect value %r is < 1).', self.name, autoconnect) + if not self._run_autoconnect(): return + def connect(self): + log.debug('(%s) calling _connect() (world.testing=%s)', self.name, world.testing) + if world.testing: + # HACK: Don't thread if we're running tests. + self._connect() + else: + if self.connection_thread and self.connection_thread.is_alive(): + raise RuntimeError("Refusing to start multiple connection threads for network %r!" % self.name) + + self.connection_thread = threading.Thread(target=self._connect, + name="Listener for %s" % + self.name) + self.connection_thread.start() + def disconnect(self): """Handle disconnects from the remote server.""" - was_successful = self.connected.is_set() - log.debug('(%s) disconnect: got %s for was_successful state', self.name, was_successful) - - log.debug('(%s) disconnect: Clearing self.connected state.', self.name) - self.connected.clear() - - log.debug('(%s) Removing channel logging handlers due to disconnect.', self.name) - while self.loghandlers: - log.removeHandler(self.loghandlers.pop()) + self._pre_disconnect() try: log.debug('(%s) disconnect: Shutting down socket.', self.name) @@ -408,17 +1323,9 @@ class Irc(utils.DeprecatedAttributesObject): if self.pingTimer: log.debug('(%s) Canceling pingTimer at %s due to disconnect() call', self.name, time.time()) self.pingTimer.cancel() + self._post_disconnect() - log.debug('(%s) disconnect: Setting self.aborted to True.', self.name) - self.aborted.set() - - # Internal hook signifying that a network has disconnected. - self.callHooks([None, 'PYLINK_DISCONNECT', {'was_successful': was_successful}]) - - log.debug('(%s) disconnect: Clearing state via initVars().', self.name) - self.initVars() - - def run(self): + def _run_irc(self): """Main IRC loop which listens for messages.""" buf = b"" data = b"" @@ -447,65 +1354,6 @@ class Irc(utils.DeprecatedAttributesObject): line = line.decode(self.encoding, "replace") self.runline(line) - def runline(self, line): - """Sends a command to the protocol module.""" - log.debug("(%s) <- %s", self.name, line) - try: - hook_args = self.proto.handle_events(line) - except Exception: - log.exception('(%s) Caught error in handle_events, disconnecting!', self.name) - log.error('(%s) The offending line was: <- %s', self.name, line) - self.aborted.set() - return - # Only call our hooks if there's data to process. Handlers that support - # hooks will return a dict of parsed arguments, which can be passed on - # to plugins and the like. For example, the JOIN handler will return - # something like: {'channel': '#whatever', 'users': ['UID1', 'UID2', - # 'UID3']}, etc. - if hook_args is not None: - self.callHooks(hook_args) - - return hook_args - - def callHooks(self, hook_args): - """Calls a hook function with the given hook args.""" - numeric, command, parsed_args = hook_args - # Always make sure TS is sent. - if 'ts' not in parsed_args: - parsed_args['ts'] = int(time.time()) - hook_cmd = command - hook_map = self.proto.hook_map - - # If the hook name is present in the protocol module's hook_map, then we - # should set the hook name to the name that points to instead. - # For example, plugins will read SETHOST as CHGHOST, EOS (end of sync) - # as ENDBURST, etc. - if command in hook_map: - hook_cmd = hook_map[command] - - # However, individual handlers can also return a 'parse_as' key to send - # their payload to a different hook. An example of this is "/join 0" - # being interpreted as leaving all channels (PART). - hook_cmd = parsed_args.get('parse_as') or hook_cmd - - log.debug('(%s) Raw hook data: [%r, %r, %r] received from %s handler ' - '(calling hook %s)', self.name, numeric, hook_cmd, parsed_args, - command, hook_cmd) - - # Iterate over registered hook functions, catching errors accordingly. - for hook_func in world.hooks[hook_cmd]: - try: - log.debug('(%s) Calling hook function %s from plugin "%s"', self.name, - hook_func, hook_func.__module__) - hook_func(self, numeric, command, parsed_args) - except Exception: - # We don't want plugins to crash our servers... - log.exception('(%s) Unhandled exception caught in hook %r from plugin "%s"', - self.name, hook_func, hook_func.__module__) - log.error('(%s) The offending hook data was: %s', self.name, - hook_args) - continue - def _send(self, data): """Sends raw text to the uplink server.""" # Safeguard against newlines in input!! Otherwise, each line gets @@ -532,725 +1380,23 @@ class Irc(utils.DeprecatedAttributesObject): else: self._send(data) - def schedulePing(self): - """Schedules periodic pings in a loop.""" - self.proto.ping() - - self.pingTimer = threading.Timer(self.pingfreq, self.schedulePing) - self.pingTimer.daemon = True - self.pingTimer.name = 'Ping timer loop for %s' % self.name - self.pingTimer.start() - - log.debug('(%s) Ping scheduled at %s', self.name, time.time()) - - def __repr__(self): - return "" % self.name - - ### General utility functions - def callCommand(self, source, text): - """ - Calls a PyLink bot command. source is the caller's UID, and text is the - full, unparsed text of the message. - """ - world.services['pylink'].call_cmd(self, source, text) - - def msg(self, target, text, notice=None, source=None, loopback=True): - """Handy function to send messages/notices to clients. Source - is optional, and defaults to the main PyLink client if not specified.""" - if not text: - return - - if not (source or self.pseudoclient): - # No explicit source set and our main client wasn't available; abort. - return - source = source or self.pseudoclient.uid - - if notice: - self.proto.notice(source, target, text) - cmd = 'PYLINK_SELF_NOTICE' - else: - self.proto.message(source, target, text) - cmd = 'PYLINK_SELF_PRIVMSG' - - if loopback: - # Determines whether we should send a hook for this msg(), to relay things like services - # replies across relay. - self.callHooks([source, cmd, {'target': target, 'text': text}]) - - def _reply(self, text, notice=None, source=None, private=None, force_privmsg_in_private=False, - loopback=True): - """ - Core of the reply() function - replies to the last caller in the right context - (channel or PM). - """ - if private is None: - # Allow using private replies as the default, if no explicit setting was given. - private = conf.conf['bot'].get("prefer_private_replies") - - # Private reply is enabled, or the caller was originally a PM - if private or (self.called_in in self.users): - if not force_privmsg_in_private: - # For private replies, the default is to override the notice=True/False argument, - # and send replies as notices regardless. This is standard behaviour for most - # IRC services, but can be disabled if force_privmsg_in_private is given. - notice = True - target = self.called_by - else: - target = self.called_in - - self.msg(target, text, notice=notice, source=source, loopback=loopback) - - def reply(self, *args, **kwargs): - """ - Replies to the last caller in the right context (channel or PM). - - This function wraps around _reply() and can be monkey-patched in a thread-safe manner - to temporarily redirect plugin output to another target. - """ - with self.reply_lock: - self._reply(*args, **kwargs) - - def error(self, text, **kwargs): - """Replies with an error to the last caller in the right context (channel or PM).""" - # This is a stub to alias error to reply - self.reply("Error: %s" % text, **kwargs) - - def toLower(self, text): - """Returns a lowercase representation of text based on the IRC object's - casemapping (rfc1459 or ascii).""" - if self.proto.casemapping == 'rfc1459': - text = text.replace('{', '[') - text = text.replace('}', ']') - text = text.replace('|', '\\') - text = text.replace('~', '^') - # Encode the text as bytes first, and then lowercase it so that only ASCII characters are - # changed. Unicode in channel names, etc. is case sensitive because IRC is just that old of - # a protocol!!! - return text.encode().lower().decode() - - def parseModes(self, target, args): - """Parses a modestring list into a list of (mode, argument) tuples. - ['+mitl-o', '3', 'person'] => [('+m', None), ('+i', None), ('+t', None), ('+l', '3'), ('-o', 'person')] - """ - # http://www.irc.org/tech_docs/005.html - # A = Mode that adds or removes a nick or address to a list. Always has a parameter. - # B = Mode that changes a setting and always has a parameter. - # C = Mode that changes a setting and only has a parameter when set. - # D = Mode that changes a setting and never has a parameter. - - if type(args) == str: - # If the modestring was given as a string, split it into a list. - args = args.split() - - assert args, 'No valid modes were supplied!' - usermodes = not utils.isChannel(target) - prefix = '' - modestring = args[0] - args = args[1:] - if usermodes: - log.debug('(%s) Using self.umodes for this query: %s', self.name, self.umodes) - - if target not in self.users: - log.debug('(%s) Possible desync! Mode target %s is not in the users index.', self.name, target) - return [] # Return an empty mode list - - supported_modes = self.umodes - oldmodes = self.users[target].modes - else: - log.debug('(%s) Using self.cmodes for this query: %s', self.name, self.cmodes) - - supported_modes = self.cmodes - oldmodes = self.channels[target].modes - res = [] - for mode in modestring: - if mode in '+-': - prefix = mode + def _process_queue(self): + """Loop to process outgoing queue data.""" + while True: + throttle_time = self.serverdata.get('throttle_time', 0.005) + if not self.aborted.wait(throttle_time): + data = self.queue.get() + if data is None: + log.debug('(%s) Stopping queue thread due to getting None as item', self.name) + break + elif data: + self._send(data) else: - if not prefix: - prefix = '+' - arg = None - log.debug('Current mode: %s%s; args left: %s', prefix, mode, args) - try: - if mode in self.prefixmodes and not usermodes: - # We're setting a prefix mode on someone (e.g. +o user1) - log.debug('Mode %s: This mode is a prefix mode.', mode) - arg = args.pop(0) - # Convert nicks to UIDs implicitly; most IRCds will want - # this already. - arg = self.nickToUid(arg) or arg - if arg not in self.users: # Target doesn't exist, skip it. - log.debug('(%s) Skipping setting mode "%s %s"; the ' - 'target doesn\'t seem to exist!', self.name, - mode, arg) - continue - elif mode in (supported_modes['*A'] + supported_modes['*B']): - # Must have parameter. - log.debug('Mode %s: This mode must have parameter.', mode) - arg = args.pop(0) - if prefix == '-': - if mode in supported_modes['*B'] and arg == '*': - # Charybdis allows unsetting +k without actually - # knowing the key by faking the argument when unsetting - # as a single "*". - # We'd need to know the real argument of +k for us to - # be able to unset the mode. - oldarg = dict(oldmodes).get(mode) - if oldarg: - # Set the arg to the old one on the channel. - arg = oldarg - log.debug("Mode %s: coersing argument of '*' to %r.", mode, arg) + break - log.debug('(%s) parseModes: checking if +%s %s is in old modes list: %s', self.name, mode, arg, oldmodes) +Irc = IRCNetwork - if (mode, arg) not in oldmodes: - # Ignore attempts to unset bans that don't exist. - log.debug("(%s) parseModes(): ignoring removal of non-existent list mode +%s %s", self.name, mode, arg) - continue - - elif prefix == '+' and mode in supported_modes['*C']: - # Only has parameter when setting. - log.debug('Mode %s: Only has parameter when setting.', mode) - arg = args.pop(0) - except IndexError: - log.warning('(%s/%s) Error while parsing mode %r: mode requires an ' - 'argument but none was found. (modestring: %r)', - self.name, target, mode, modestring) - continue # Skip this mode; don't error out completely. - res.append((prefix + mode, arg)) - return res - - def applyModes(self, target, changedmodes): - """Takes a list of parsed IRC modes, and applies them on the given target. - - The target can be either a channel or a user; this is handled automatically.""" - usermodes = not utils.isChannel(target) - log.debug('(%s) Using usermodes for this query? %s', self.name, usermodes) - - try: - if usermodes: - old_modelist = self.users[target].modes - supported_modes = self.umodes - else: - old_modelist = self.channels[target].modes - supported_modes = self.cmodes - except KeyError: - log.warning('(%s) Possible desync? Mode target %s is unknown.', self.name, target) - return - - modelist = set(old_modelist) - log.debug('(%s) Applying modes %r on %s (initial modelist: %s)', self.name, changedmodes, target, modelist) - for mode in changedmodes: - # Chop off the +/- part that parseModes gives; it's meaningless for a mode list. - try: - real_mode = (mode[0][1], mode[1]) - except IndexError: - real_mode = mode - - if not usermodes: - # We only handle +qaohv for now. Iterate over every supported mode: - # if the IRCd supports this mode and it is the one being set, add/remove - # the person from the corresponding prefix mode list (e.g. c.prefixmodes['op'] - # for ops). - for pmode, pmodelist in self.channels[target].prefixmodes.items(): - if pmode in self.cmodes and real_mode[0] == self.cmodes[pmode]: - log.debug('(%s) Initial prefixmodes list: %s', self.name, pmodelist) - if mode[0][0] == '+': - pmodelist.add(mode[1]) - else: - pmodelist.discard(mode[1]) - - log.debug('(%s) Final prefixmodes list: %s', self.name, pmodelist) - - if real_mode[0] in self.prefixmodes: - # Don't add prefix modes to IrcChannel.modes; they belong in the - # prefixmodes mapping handled above. - log.debug('(%s) Not adding mode %s to IrcChannel.modes because ' - 'it\'s a prefix mode.', self.name, str(mode)) - continue - - if mode[0][0] != '-': - # We're adding a mode - existing = [m for m in modelist if m[0] == real_mode[0] and m[1] != real_mode[1]] - if existing and real_mode[1] and real_mode[0] not in self.cmodes['*A']: - # The mode we're setting takes a parameter, but is not a list mode (like +beI). - # Therefore, only one version of it can exist at a time, and we must remove - # any old modepairs using the same letter. Otherwise, we'll get duplicates when, - # for example, someone sets mode "+l 30" on a channel already set "+l 25". - log.debug('(%s) Old modes for mode %r exist on %s, removing them: %s', - self.name, real_mode, target, str(existing)) - [modelist.discard(oldmode) for oldmode in existing] - modelist.add(real_mode) - log.debug('(%s) Adding mode %r on %s', self.name, real_mode, target) - else: - log.debug('(%s) Removing mode %r on %s', self.name, real_mode, target) - # We're removing a mode - if real_mode[1] is None: - # We're removing a mode that only takes arguments when setting. - # Remove all mode entries that use the same letter as the one - # we're unsetting. - for oldmode in modelist.copy(): - if oldmode[0] == real_mode[0]: - modelist.discard(oldmode) - else: - modelist.discard(real_mode) - log.debug('(%s) Final modelist: %s', self.name, modelist) - try: - if usermodes: - self.users[target].modes = modelist - else: - self.channels[target].modes = modelist - except KeyError: - log.warning("(%s) Invalid MODE target %s (usermodes=%s)", self.name, target, usermodes) - - @staticmethod - def _flip(mode): - """Flips a mode character.""" - # Make it a list first, strings don't support item assignment - mode = list(mode) - if mode[0] == '-': # Query is something like "-n" - mode[0] = '+' # Change it to "+n" - elif mode[0] == '+': - mode[0] = '-' - else: # No prefix given, assume + - mode.insert(0, '-') - return ''.join(mode) - - def reverseModes(self, target, modes, oldobj=None): - """Reverses/Inverts the mode string or mode list given. - - Optionally, an oldobj argument can be given to look at an earlier state of - a channel/user object, e.g. for checking the op status of a mode setter - before their modes are processed and added to the channel state. - - This function allows both mode strings or mode lists. Example uses: - "+mi-lk test => "-mi+lk test" - "mi-k test => "-mi+k test" - [('+m', None), ('+r', None), ('+l', '3'), ('-o', 'person') - => {('-m', None), ('-r', None), ('-l', None), ('+o', 'person')}) - {('s', None), ('+o', 'whoever') => {('-s', None), ('-o', 'whoever')}) - """ - origtype = type(modes) - # If the query is a string, we have to parse it first. - if origtype == str: - modes = self.parseModes(target, modes.split(" ")) - # Get the current mode list first. - if utils.isChannel(target): - c = oldobj or self.channels[target] - oldmodes = c.modes.copy() - possible_modes = self.cmodes.copy() - # For channels, this also includes the list of prefix modes. - possible_modes['*A'] += ''.join(self.prefixmodes) - for name, userlist in c.prefixmodes.items(): - try: - oldmodes.update([(self.cmodes[name], u) for u in userlist]) - except KeyError: - continue - else: - oldmodes = self.users[target].modes - possible_modes = self.umodes - newmodes = [] - log.debug('(%s) reverseModes: old/current mode list for %s is: %s', self.name, - target, oldmodes) - for char, arg in modes: - # Mode types: - # A = Mode that adds or removes a nick or address to a list. Always has a parameter. - # B = Mode that changes a setting and always has a parameter. - # C = Mode that changes a setting and only has a parameter when set. - # D = Mode that changes a setting and never has a parameter. - mchar = char[-1] - if mchar in possible_modes['*B'] + possible_modes['*C']: - # We need to find the current mode list, so we can reset arguments - # for modes that have arguments. For example, setting +l 30 on a channel - # that had +l 50 set should give "+l 30", not "-l". - oldarg = [m for m in oldmodes if m[0] == mchar] - if oldarg: # Old mode argument for this mode existed, use that. - oldarg = oldarg[0] - mpair = ('+%s' % oldarg[0], oldarg[1]) - else: # Not found, flip the mode then. - # Mode takes no arguments when unsetting. - if mchar in possible_modes['*C'] and char[0] != '-': - arg = None - mpair = (self._flip(char), arg) - else: - mpair = (self._flip(char), arg) - if char[0] != '-' and (mchar, arg) in oldmodes: - # Mode is already set. - log.debug("(%s) reverseModes: skipping reversing '%s %s' with %s since we're " - "setting a mode that's already set.", self.name, char, arg, mpair) - continue - elif char[0] == '-' and (mchar, arg) not in oldmodes and mchar in possible_modes['*A']: - # We're unsetting a prefixmode that was never set - don't set it in response! - # Charybdis lacks verification for this server-side. - log.debug("(%s) reverseModes: skipping reversing '%s %s' with %s since it " - "wasn't previously set.", self.name, char, arg, mpair) - continue - newmodes.append(mpair) - - log.debug('(%s) reverseModes: new modes: %s', self.name, newmodes) - if origtype == str: - # If the original query is a string, send it back as a string. - return self.joinModes(newmodes) - else: - return set(newmodes) - - @staticmethod - def joinModes(modes, sort=False): - """Takes a list of (mode, arg) tuples in parseModes() format, and - joins them into a string. - - See testJoinModes in tests/test_utils.py for some examples.""" - prefix = '+' # Assume we're adding modes unless told otherwise - modelist = '' - args = [] - - # Sort modes alphabetically like a conventional IRCd. - if sort: - modes = sorted(modes) - - for modepair in modes: - mode, arg = modepair - assert len(mode) in (1, 2), "Incorrect length of a mode (received %r)" % mode - try: - # If the mode has a prefix, use that. - curr_prefix, mode = mode - except ValueError: - # If not, the current prefix stays the same; move on to the next - # modepair. - pass - else: - # If the prefix of this mode isn't the same as the last one, add - # the prefix to the modestring. This prevents '+nt-lk' from turning - # into '+n+t-l-k' or '+ntlk'. - if prefix != curr_prefix: - modelist += curr_prefix - prefix = curr_prefix - modelist += mode - if arg is not None: - args.append(arg) - if not modelist.startswith(('+', '-')): - # Our starting mode didn't have a prefix with it. Assume '+'. - modelist = '+' + modelist - if args: - # Add the args if there are any. - modelist += ' %s' % ' '.join(args) - return modelist - - @classmethod - def wrapModes(cls, modes, limit, max_modes_per_msg=0): - """ - Takes a list of modes and wraps it across multiple lines. - """ - strings = [] - - # This process is slightly trickier than just wrapping arguments, because modes create - # positional arguments that can't be separated from its character. - queued_modes = [] - total_length = 0 - - last_prefix = '+' - orig_modes = modes.copy() - modes = list(modes) - while modes: - # PyLink mode lists come in the form [('+t', None), ('-b', '*!*@someone'), ('+l', 3)] - # The +/- part is optional depending on context, and should either: - # 1) The prefix of the last mode. - # 2) + (adding modes), if no prefix was ever given - next_mode = modes.pop(0) - - modechar, arg = next_mode - prefix = modechar[0] - if prefix not in '+-': - prefix = last_prefix - # Explicitly add the prefix to the mode character to prevent - # ambiguity when passing it to joinModes(). - modechar = prefix + modechar - # XXX: because tuples are immutable, we have to replace the entire modepair.. - next_mode = (modechar, arg) - - # Figure out the length that the next mode will add to the buffer. If we're changing - # from + to - (setting to removing modes) or vice versa, we'll need two characters - # ("+" or "-") plus the mode char itself. - next_length = 1 - if prefix != last_prefix: - next_length += 1 - - # Replace the last_prefix with the current one for the next iteration. - last_prefix = prefix - - if arg: - # This mode has an argument, so add the length of that and a space. - next_length += 1 - next_length += len(arg) - - assert next_length <= limit, \ - "wrapModes: Mode %s is too long for the given length %s" % (next_mode, limit) - - # Check both message length and max. modes per msg if enabled. - if (next_length + total_length) <= limit and ((not max_modes_per_msg) or len(queued_modes) < max_modes_per_msg): - # We can fit this mode in the next message; add it. - total_length += next_length - log.debug('wrapModes: Adding mode %s to queued modes', str(next_mode)) - queued_modes.append(next_mode) - log.debug('wrapModes: queued modes: %s', queued_modes) - else: - # Otherwise, create a new message by joining the previous queue. - # Then, add our current mode. - strings.append(cls.joinModes(queued_modes)) - queued_modes.clear() - - log.debug('wrapModes: cleared queue (length %s) and now adding %s', limit, str(next_mode)) - queued_modes.append(next_mode) - total_length = next_length - else: - # Everything fit in one line, so just use that. - strings.append(cls.joinModes(queued_modes)) - - log.debug('wrapModes: returning %s for %s', strings, orig_modes) - return strings - - def version(self): - """ - Returns a detailed version string including the PyLink daemon version, - the protocol module in use, and the server hostname. - """ - fullversion = 'PyLink-%s. %s :[protocol:%s, encoding:%s]' % (__version__, self.hostname(), self.protoname, self.encoding) - return fullversion - - def hostname(self): - """ - Returns the server hostname used by PyLink on the given server. - """ - return self.serverdata.get('hostname', world.fallback_hostname) - - ### State checking functions - def nickToUid(self, nick): - """Looks up the UID of a user with the given nick, if one is present.""" - nick = self.toLower(nick) - for k, v in self.users.copy().items(): - if self.toLower(v.nick) == nick: - return k - - def isInternalClient(self, numeric): - """ - Returns whether the given client numeric (UID) is a PyLink client. - """ - sid = self.getServer(numeric) - if sid and self.servers[sid].internal: - return True - return False - - def isInternalServer(self, sid): - """Returns whether the given SID is an internal PyLink server.""" - return (sid in self.servers and self.servers[sid].internal) - - def getServer(self, numeric): - """Finds the SID of the server a user is on.""" - userobj = self.users.get(numeric) - if userobj: - return userobj.server - - def isManipulatableClient(self, uid): - """ - Returns whether the given user is marked as an internal, manipulatable - client. Usually, automatically spawned services clients should have this - set True to prevent interactions with opers (like mode changes) from - causing desyncs. - """ - return self.isInternalClient(uid) and self.users[uid].manipulatable - - def getServiceBot(self, uid): - """ - Checks whether the given UID is a registered service bot. If True, - returns the cooresponding ServiceBot object. - """ - userobj = self.users.get(uid) - if not userobj: - return False - - # Look for the "service" attribute in the IrcUser object, if one exists. - try: - sname = userobj.service - # Warn if the service name we fetched isn't a registered service. - if sname not in world.services.keys(): - log.warning("(%s) User %s / %s had a service bot record to a service that doesn't " - "exist (%s)!", self.name, uid, userobj.nick, sname) - return world.services.get(sname) - except AttributeError: - return False - - def getHostmask(self, user, realhost=False, ip=False): - """ - Returns the hostmask of the given user, if present. If the realhost option - is given, return the real host of the user instead of the displayed host. - If the ip option is given, return the IP address of the user (this overrides - realhost).""" - userobj = self.users.get(user) - - try: - nick = userobj.nick - except AttributeError: - nick = '' - - try: - ident = userobj.ident - except AttributeError: - ident = '' - - try: - if ip: - host = userobj.ip - elif realhost: - host = userobj.realhost - else: - host = userobj.host - except AttributeError: - host = '' - - return '%s!%s@%s' % (nick, ident, host) - - def getFriendlyName(self, entityid): - """ - Returns the friendly name of a SID or UID (server name for SIDs, nick for UID). - """ - if entityid in self.servers: - return self.servers[entityid].name - elif entityid in self.users: - return self.users[entityid].nick - else: - raise KeyError("Unknown UID/SID %s" % entityid) - - def getFullNetworkName(self): - """ - Returns the full network name (as defined by the "netname" option), or the - short network name if that isn't defined. - """ - return self.serverdata.get('netname', self.name) - - def isOper(self, uid, allowAuthed=True, allowOper=True): - """ - Returns whether the given user has operator status on PyLink. This can be achieved - by either identifying to PyLink as admin (if allowAuthed is True), - or having user mode +o set (if allowOper is True). At least one of - allowAuthed or allowOper must be True for this to give any meaningful - results. - """ - if uid in self.users: - if allowOper and ("o", None) in self.users[uid].modes: - return True - elif allowAuthed and self.users[uid].account: - return True - return False - - def checkAuthenticated(self, uid, allowAuthed=True, allowOper=True): - """ - Checks whether the given user has operator status on PyLink, raising - NotAuthorizedError and logging the access denial if not. - """ - log.warning("(%s) Irc.checkAuthenticated() is deprecated as of PyLink 1.2 and may be " - "removed in a future relase. Consider migrating to the PyLink Permissions API.", - self.name) - lastfunc = inspect.stack()[1][3] - if not self.isOper(uid, allowAuthed=allowAuthed, allowOper=allowOper): - log.warning('(%s) Access denied for %s calling %r', self.name, - self.getHostmask(uid), lastfunc) - raise utils.NotAuthorizedError("You are not authenticated!") - return True - - def matchHost(self, glob, target, ip=True, realhost=True): - """ - Checks whether the given host, or given UID's hostmask matches the given nick!user@host - glob. - - If the target given is a UID, and the 'ip' or 'realhost' options are True, this will also - match against the target's IP address and real host, respectively. - - This function respects IRC casemappings (rfc1459 and ascii). If the given target is a UID, - and the 'ip' option is enabled, the host portion of the glob is also matched as a CIDR - range. - """ - # Get the corresponding casemapping value used by ircmatch. - if self.proto.casemapping == 'rfc1459': - casemapping = 0 - else: - casemapping = 1 - - # Try to convert target into a UID. If this fails, it's probably a hostname. - target = self.nickToUid(target) or target - - # Allow queries like !$exttarget to invert the given match. - invert = glob.startswith('!') - if invert: - glob = glob.lstrip('!') - - def match_host_core(): - """ - Core processor for matchHost(), minus the inversion check. - """ - # Work with variables in the matchHost() scope, from - # http://stackoverflow.com/a/8178808 - nonlocal glob - - # Prepare a list of hosts to check against. - if target in self.users: - if glob.startswith('$'): - # Exttargets start with $. Skip regular ban matching and find the matching ban handler. - glob = glob.lstrip('$') - exttargetname = glob.split(':', 1)[0] - handler = world.exttarget_handlers.get(exttargetname) - - if handler: - # Handler exists. Return what it finds. - result = handler(self, glob, target) - log.debug('(%s) Got %s from exttarget %s in matchHost() glob $%s for target %s', - self.name, result, exttargetname, glob, target) - return result - else: - log.debug('(%s) Unknown exttarget %s in matchHost() glob $%s', self.name, - exttargetname, glob) - return False - - hosts = {self.getHostmask(target)} - - if ip: - hosts.add(self.getHostmask(target, ip=True)) - - # HACK: support CIDR hosts in the hosts portion - try: - header, cidrtarget = glob.split('@', 1) - log.debug('(%s) Processing CIDRs for %s (full host: %s)', self.name, - cidrtarget, glob) - # Try to parse the host portion as a CIDR range - network = ipaddress.ip_network(cidrtarget) - - log.debug('(%s) Found CIDR for %s, replacing target host with IP %s', self.name, - realhost, target) - real_ip = self.users[target].ip - if ipaddress.ip_address(real_ip) in network: - # If the CIDR matches, hack around the host matcher by pretending that - # the lookup target was the IP and not the CIDR range! - glob = '@'.join((header, real_ip)) - except ValueError: - pass - - if realhost: - hosts.add(self.getHostmask(target, realhost=True)) - - else: # We were given a host, use that. - hosts = [target] - - # Iterate over the hosts to match using ircmatch. - for host in hosts: - if ircmatch.match(casemapping, glob, host): - return True - - return False - - result = match_host_core() - if invert: - result = not result - return result - -class IrcUser(): +class User(): """PyLink IRC user class.""" def __init__(self, nick, ts, uid, server, ident='null', host='null', realname='PyLink dummy client', realhost='null', @@ -1288,15 +1434,16 @@ class IrcUser(): self.manipulatable = manipulatable def __repr__(self): - return 'IrcUser(%s/%s)' % (self.uid, self.nick) + return 'User(%s/%s)' % (self.uid, self.nick) +IrcUser = User -class IrcServer(): +class Server(): """PyLink IRC server class. - uplink: The SID of this IrcServer instance's uplink. This is set to None - for the main PyLink PseudoServer! + uplink: The SID of this PyLinkServer instance's uplink. This is set to None + for the main PyLink server. name: The name of the server. - internal: Whether the server is an internal PyLink PseudoServer. + internal: Whether the server is an internal PyLink server. """ def __init__(self, uplink, name, internal=False, desc="(None given)"): @@ -1307,10 +1454,12 @@ class IrcServer(): self.desc = desc def __repr__(self): - return 'IrcServer(%s)' % self.name + return 'Server(%s)' % self.name +IrcServer = Server -class IrcChannel(): +class Channel(utils.DeprecatedAttributesObject, utils.CamelCaseToSnakeCase): """PyLink IRC channel class.""" + def __init__(self, name=None): # Initialize variables, such as the topic, user list, TS, who's opped, etc. self.users = set() @@ -1327,52 +1476,55 @@ class IrcChannel(): # Saves the channel name (may be useful to plugins, etc.) self.name = name - def __repr__(self): - return 'IrcChannel(%s)' % self.name + self.deprecated_attributes = {'removeuser': 'Deprecated in 2.0; use remove_user() instead!'} - def removeuser(self, target): + def __repr__(self): + return 'Channel(%s)' % self.name + + def remove_user(self, target): """Removes a user from a channel.""" for s in self.prefixmodes.values(): s.discard(target) self.users.discard(target) + removeuser = remove_user def deepcopy(self): """Returns a deep copy of the channel object.""" return deepcopy(self) - def isVoice(self, uid): + def is_voice(self, uid): """Returns whether the given user is voice in the channel.""" return uid in self.prefixmodes['voice'] - def isHalfop(self, uid): + def is_halfop(self, uid): """Returns whether the given user is halfop in the channel.""" return uid in self.prefixmodes['halfop'] - def isOp(self, uid): + def is_op(self, uid): """Returns whether the given user is op in the channel.""" return uid in self.prefixmodes['op'] - def isAdmin(self, uid): + def is_admin(self, uid): """Returns whether the given user is admin (&) in the channel.""" return uid in self.prefixmodes['admin'] - def isOwner(self, uid): + def is_owner(self, uid): """Returns whether the given user is owner (~) in the channel.""" return uid in self.prefixmodes['owner'] - def isVoicePlus(self, uid): + def is_voice_plus(self, uid): """Returns whether the given user is voice or above in the channel.""" # If the user has any prefix mode, it has to be voice or greater. return bool(self.getPrefixModes(uid)) - def isHalfopPlus(self, uid): + def is_halfop_plus(self, uid): """Returns whether the given user is halfop or above in the channel.""" for mode in ('halfop', 'op', 'admin', 'owner'): if uid in self.prefixmodes[mode]: return True return False - def isOpPlus(self, uid): + def is_op_plus(self, uid): """Returns whether the given user is op or above in the channel.""" for mode in ('op', 'admin', 'owner'): if uid in self.prefixmodes[mode]: @@ -1380,7 +1532,7 @@ class IrcChannel(): return False @staticmethod - def sortPrefixes(key): + def sort_prefixes(key): """ Implements a sorted()-compatible sorter for prefix modes, giving each one a numeric value. @@ -1391,7 +1543,7 @@ class IrcChannel(): # support them. return values.get(key, 1000) - def getPrefixModes(self, uid, prefixmodes=None): + def get_prefix_modes(self, uid, prefixmodes=None): """Returns a list of all named prefix modes the given user has in the channel. Optionally, a prefixmodes argument can be given to look at an earlier state of @@ -1409,246 +1561,5 @@ class IrcChannel(): if uid in modelist: result.append(mode) - return sorted(result, key=self.sortPrefixes) - -class Protocol(): - """Base Protocol module class for PyLink.""" - def __init__(self, irc): - self.irc = irc - self.casemapping = 'rfc1459' - self.hook_map = {} - - # Lock for updateTS to make sure only one thread can change the channel TS at one time. - self.ts_lock = threading.Lock() - - # Lists required conf keys for the server block. - self.conf_keys = {'ip', 'port', 'hostname', 'sid', 'sidrange', 'protocol', 'sendpass', - 'recvpass'} - - # Defines a set of PyLink protocol capabilities - self.protocol_caps = set() - - def validateServerConf(self): - """Validates that the server block given contains the required keys.""" - for k in self.conf_keys: - assert k in self.irc.serverdata, "Missing option %r in server block for network %s." % (k, self.irc.name) - - port = self.irc.serverdata['port'] - assert type(port) == int and 0 < port < 65535, "Invalid port %r for network %s" % (port, self.irc.name) - - @staticmethod - def parseArgs(args): - """ - Parses a string or list of of RFC1459-style arguments, where ":" may - be used for multi-word arguments that last until the end of a line. - """ - if isinstance(args, str): - args = args.split(' ') - - real_args = [] - for idx, arg in enumerate(args): - if arg.startswith(':') and idx != 0: - # ":" is used to begin multi-word arguments that last until the end of the message. - # Use list splicing here to join them into one argument, and then add it to our list of args. - joined_arg = ' '.join(args[idx:])[1:] # Cut off the leading : as well - real_args.append(joined_arg) - break - real_args.append(arg) - - return real_args - - def hasCap(self, capab): - """ - Returns whether this protocol module instance has the requested capability. - """ - return capab.lower() in self.protocol_caps - - def removeClient(self, numeric): - """Internal function to remove a client from our internal state.""" - for c, v in self.irc.channels.copy().items(): - v.removeuser(numeric) - # Clear empty non-permanent channels. - if not (self.irc.channels[c].users or ((self.irc.cmodes.get('permanent'), None) in self.irc.channels[c].modes)): - del self.irc.channels[c] - assert numeric not in v.users, "IrcChannel's removeuser() is broken!" - - sid = self.irc.getServer(numeric) - log.debug('Removing client %s from self.irc.users', numeric) - del self.irc.users[numeric] - log.debug('Removing client %s from self.irc.servers[%s].users', numeric, sid) - self.irc.servers[sid].users.discard(numeric) - - def updateTS(self, sender, channel, their_ts, modes=None): - """ - Merges modes of a channel given the remote TS and a list of modes. - """ - - # Okay, so the situation is that we have 6 possible TS/sender combinations: - - # | our TS lower | TS equal | their TS lower - # mode origin is us | OVERWRITE | MERGE | IGNORE - # mode origin is uplink | IGNORE | MERGE | OVERWRITE - - if modes is None: - modes = [] - - def _clear(): - log.debug("(%s) Clearing local modes from channel %s due to TS change", self.irc.name, - channel) - self.irc.channels[channel].modes.clear() - for p in self.irc.channels[channel].prefixmodes.values(): - for user in p.copy(): - if not self.irc.isInternalClient(user): - p.discard(user) - - def _apply(): - if modes: - log.debug("(%s) Applying modes on channel %s (TS ok)", self.irc.name, - channel) - self.irc.applyModes(channel, modes) - - # Use a lock so only one thread can change a channel's TS at once: this prevents race - # conditions from desyncing the channel list. - with self.ts_lock: - our_ts = self.irc.channels[channel].ts - assert type(our_ts) == int, "Wrong type for our_ts (expected int, got %s)" % type(our_ts) - assert type(their_ts) == int, "Wrong type for their_ts (expected int, got %s)" % type(their_ts) - - # Check if we're the mode sender based on the UID / SID given. - our_mode = self.irc.isInternalClient(sender) or self.irc.isInternalServer(sender) - - log.debug("(%s/%s) our_ts: %s; their_ts: %s; is the mode origin us? %s", self.irc.name, - channel, our_ts, their_ts, our_mode) - - if their_ts == our_ts: - log.debug("(%s/%s) remote TS of %s is equal to our %s; mode query %s", - self.irc.name, channel, their_ts, our_ts, modes) - # Their TS is equal to ours. Merge modes. - _apply() - - elif (their_ts < our_ts): - if their_ts < 750000: - log.warning('(%s) Possible desync? Not setting bogus TS %s on channel %s', self.irc.name, their_ts, channel) - else: - log.debug('(%s) Resetting channel TS of %s from %s to %s (remote has lower TS)', - self.irc.name, channel, our_ts, their_ts) - self.irc.channels[channel].ts = their_ts - - # Remote TS was lower and we're receiving modes. Clear the modelist and apply theirs. - - _clear() - _apply() - - def _getSid(self, sname): - """Returns the SID of a server with the given name, if present.""" - name = sname.lower() - for k, v in self.irc.servers.items(): - if v.name.lower() == name: - return k - else: - return sname # Fall back to given text instead of None - - def _getUid(self, target): - """Converts a nick argument to its matching UID. This differs from irc.nickToUid() - in that it returns the original text instead of None, if no matching nick is found.""" - target = self.irc.nickToUid(target) or target - return target - - @classmethod - def parsePrefixedArgs(cls, args): - """Similar to parseArgs(), but stripping leading colons from the first argument - of a line (usually the sender field).""" - args = cls.parseArgs(args) - args[0] = args[0].split(':', 1)[1] - return args - - def _squit(self, numeric, command, args): - """Handles incoming SQUITs.""" - - split_server = self._getSid(args[0]) - - # Normally we'd only need to check for our SID as the SQUIT target, but Nefarious - # actually uses the uplink server as the SQUIT target. - # <- ABAAE SQ nefarious.midnight.vpn 0 :test - if split_server in (self.irc.sid, self.irc.uplink): - raise ProtocolError('SQUIT received: (reason: %s)' % args[-1]) - - affected_users = [] - affected_nicks = defaultdict(list) - log.debug('(%s) Splitting server %s (reason: %s)', self.irc.name, split_server, args[-1]) - - if split_server not in self.irc.servers: - log.warning("(%s) Tried to split a server (%s) that didn't exist!", self.irc.name, split_server) - return - - # Prevent RuntimeError: dictionary changed size during iteration - old_servers = self.irc.servers.copy() - old_channels = self.irc.channels.copy() - - # Cycle through our list of servers. If any server's uplink is the one that is being SQUIT, - # remove them and all their users too. - for sid, data in old_servers.items(): - if data.uplink == split_server: - log.debug('Server %s also hosts server %s, removing those users too...', split_server, sid) - # Recursively run SQUIT on any other hubs this server may have been connected to. - args = self._squit(sid, 'SQUIT', [sid, "0", - "PyLink: Automatically splitting leaf servers of %s" % sid]) - affected_users += args['users'] - - for user in self.irc.servers[split_server].users.copy(): - affected_users.append(user) - nick = self.irc.users[user].nick - - # Nicks affected is channel specific for SQUIT:. This makes Clientbot's SQUIT relaying - # much easier to implement. - for name, cdata in old_channels.items(): - if user in cdata.users: - affected_nicks[name].append(nick) - - log.debug('Removing client %s (%s)', user, nick) - self.removeClient(user) - - serverdata = self.irc.servers[split_server] - sname = serverdata.name - uplink = serverdata.uplink - - del self.irc.servers[split_server] - log.debug('(%s) Netsplit affected users: %s', self.irc.name, affected_users) - - return {'target': split_server, 'users': affected_users, 'name': sname, - 'uplink': uplink, 'nicks': affected_nicks, 'serverdata': serverdata, - 'channeldata': old_channels} - - @staticmethod - def parseCapabilities(args, fallback=''): - """ - Parses a string of capabilities in the 005 / RPL_ISUPPORT format. - """ - - if type(args) == str: - args = args.split(' ') - - caps = {} - for cap in args: - try: - # Try to split it as a KEY=VALUE pair. - key, value = cap.split('=', 1) - except ValueError: - key = cap - value = fallback - caps[key] = value - - return caps - - @staticmethod - def parsePrefixes(args): - """ - Separates prefixes field like "(qaohv)~&@%+" into a dict mapping mode characters to mode - prefixes. - """ - prefixsearch = re.search(r'\(([A-Za-z]+)\)(.*)', args) - return dict(zip(prefixsearch.group(1), prefixsearch.group(2))) - - def handle_error(self, numeric, command, args): - """Handles ERROR messages - these mean that our uplink has disconnected us!""" - raise ProtocolError('Received an ERROR, disconnecting!') + return sorted(result, key=self.sort_prefixes) +IrcChannel = Channel diff --git a/coremods/control.py b/coremods/control.py index 3d273ce..8e2c778 100644 --- a/coremods/control.py +++ b/coremods/control.py @@ -130,7 +130,11 @@ def _rehash(): # 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) + + # API note: 2.0.x style of starting network connections + world.networkobjects[network] = newirc = proto.Class(network) + newirc.connect() + log.info('Finished reloading PyLink configuration.') if os.name == 'posix': diff --git a/coremods/corecommands.py b/coremods/corecommands.py index 9b5bb5c..68df8e3 100644 --- a/coremods/corecommands.py +++ b/coremods/corecommands.py @@ -18,12 +18,12 @@ def _login(irc, source, username): 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)) + 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.getHostmask(source)) + log.warning("(%s) Failed login to %r from %s", irc.name, username, irc.get_hostmask(source)) @utils.add_cmd def identify(irc, source, args): @@ -86,7 +86,7 @@ def load(irc, source, args): 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)) + log.info('(%s) Loading plugin %r for %s', irc.name, name, irc.get_hostmask(source)) try: world.plugins[name] = pl = utils.loadPlugin(name) except ImportError as e: @@ -119,7 +119,7 @@ def unload(irc, source, args): modulename = utils.PLUGIN_PREFIX + name if name in world.plugins: - log.info('(%s) Unloading plugin %r for %s', irc.name, name, irc.getHostmask(source)) + 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)) diff --git a/coremods/exttargets.py b/coremods/exttargets.py index 60a6c76..6f90914 100644 --- a/coremods/exttargets.py +++ b/coremods/exttargets.py @@ -41,10 +41,10 @@ def account(irc, host, uid): homenet, realuid) return False - slogin = irc.toLower(userobj.services_account) + slogin = irc.to_lower(userobj.services_account) # Split the given exttarget host into parts, so we know how many to look for. - groups = list(map(irc.toLower, host.split(':'))) + groups = list(map(irc.to_lower, host.split(':'))) log.debug('(%s) exttargets.account: groups to match: %s', irc.name, groups) if len(groups) == 1: @@ -74,10 +74,10 @@ def ircop(irc, host, uid): if len(groups) == 1: # 1st scenario. - return irc.isOper(uid, allowAuthed=False) + return irc.is_oper(uid, allowAuthed=False) else: - # 2nd scenario. Use matchHost (ircmatch) to match the opertype glob to the opertype. - return irc.matchHost(groups[1], irc.users[uid].opertype) + # 2nd scenario. Use match_host (ircmatch) to match the opertype glob to the opertype. + return irc.match_host(groups[1], irc.users[uid].opertype) @bind def server(irc, host, uid): @@ -93,10 +93,10 @@ def server(irc, host, uid): log.debug('(%s) exttargets.server: groups to match: %s', irc.name, groups) if len(groups) >= 2: - sid = irc.getServer(uid) + sid = irc.get_server(uid) query = groups[1] # Return True if the SID matches the query or the server's name glob matches it. - return sid == query or irc.matchHost(query, irc.getFriendlyName(sid)) + return sid == query or irc.match_host(query, irc.get_friendly_name(sid)) # $server alone is invalid. Don't match anything. return False @@ -123,7 +123,7 @@ def channel(irc, host, uid): return uid in irc.channels[channel].users elif len(groups) >= 3: # For things like #channel:op, check if the query is in the user's prefix modes. - return (uid in irc.channels[channel].users) and (groups[2].lower() in irc.channels[channel].getPrefixModes(uid)) + return (uid in irc.channels[channel].users) and (groups[2].lower() in irc.channels[channel].get_prefix_modes(uid)) @bind def pylinkacc(irc, host, uid): @@ -134,8 +134,8 @@ def pylinkacc(irc, host, uid): $pylinkacc -> Returns True if the target is logged in to PyLink. $pylinkacc:accountname -> Returns True if the target's PyLink login matches the one given. """ - login = irc.toLower(irc.users[uid].account) - groups = list(map(irc.toLower, host.split(':'))) + login = irc.to_lower(irc.users[uid].account) + groups = list(map(irc.to_lower, host.split(':'))) log.debug('(%s) exttargets.pylinkacc: groups to match: %s', irc.name, groups) if len(groups) == 1: @@ -187,6 +187,6 @@ def exttarget_and(irc, host, uid): targets = targets[1:-1] targets = list(filter(None, targets.split('+'))) log.debug('exttargets_and: using raw subtargets list %r (original query=%r)', targets, host) - # Wrap every subtarget into irc.matchHost and return True if all subtargets return True. - return all(map(lambda sub_exttarget: irc.matchHost(sub_exttarget, uid), targets)) + # Wrap every subtarget into irc.match_host and return True if all subtargets return True. + return all(map(lambda sub_exttarget: irc.match_host(sub_exttarget, uid), targets)) world.exttarget_handlers['and'] = exttarget_and diff --git a/coremods/handlers.py b/coremods/handlers.py index 2ff9208..c3d0fc1 100644 --- a/coremods/handlers.py +++ b/coremods/handlers.py @@ -11,10 +11,10 @@ def handle_whois(irc, source, command, args): target = args['target'] user = irc.users.get(target) - f = lambda num, source, text: irc.proto.numeric(irc.sid, num, source, text) + f = lambda num, source, text: irc.numeric(irc.sid, num, source, text) # Get the server that the target is on. - server = irc.getServer(target) + server = irc.get_server(target) if user is None: # User doesn't exist # <- :42X 401 7PYAAAAAB GL- :No such nick/channel @@ -22,7 +22,7 @@ def handle_whois(irc, source, command, args): f(401, source, "%s :No such nick/channel" % nick) else: nick = user.nick - sourceisOper = ('o', None) in irc.users[source].modes + sourceis_oper = ('o', None) in irc.users[source].modes sourceisBot = (irc.umodes.get('bot'), None) in irc.users[source].modes # Get the full network name. @@ -35,7 +35,7 @@ def handle_whois(irc, source, command, args): # 319: RPL_WHOISCHANNELS; Show public channels of the target, respecting # hidechans umodes for non-oper callers. isHideChans = (irc.umodes.get('hidechans'), None) in user.modes - if (not isHideChans) or (isHideChans and sourceisOper): + if (not isHideChans) or (isHideChans and sourceis_oper): public_chans = [] for chan in user.channels: c = irc.channels[chan] @@ -44,11 +44,11 @@ def handle_whois(irc, source, command, args): if ((irc.cmodes.get('secret'), None) in c.modes or \ (irc.cmodes.get('private'), None) in c.modes) \ - and not (sourceisOper or source in c.users): + and not (sourceis_oper or source in c.users): continue # Show the highest prefix mode like a regular IRCd does, if there are any. - prefixes = c.getPrefixModes(target) + prefixes = c.get_prefix_modes(target) if prefixes: highest = prefixes[-1] @@ -74,7 +74,7 @@ def handle_whois(irc, source, command, args): # 2) +H is set, but the caller is oper # 3) +H is set, but whois_use_hideoper is disabled in config isHideOper = (irc.umodes.get('hideoper'), None) in user.modes - if (not isHideOper) or (isHideOper and sourceisOper) or \ + if (not isHideOper) or (isHideOper and sourceis_oper) or \ (isHideOper and not conf.conf['bot'].get('whois_use_hideoper', True)): # Let's be gramatically correct. (If the opertype starts with a vowel, # write "an Operator" instead of "a Operator") @@ -84,9 +84,9 @@ def handle_whois(irc, source, command, args): # 379: RPL_WHOISMODES, used by UnrealIRCd and InspIRCd to show user modes. # Only show this to opers! - if sourceisOper: + if sourceis_oper: f(378, source, "%s :is connecting from %s@%s %s" % (nick, user.ident, user.realhost, user.ip)) - f(379, source, '%s :is using modes %s' % (nick, irc.joinModes(user.modes, sort=True))) + f(379, source, '%s :is using modes %s' % (nick, irc.join_modes(user.modes, sort=True))) # 301: used to show away information if present away_text = user.away @@ -101,7 +101,7 @@ def handle_whois(irc, source, command, args): # Call custom WHOIS handlers via the PYLINK_CUSTOM_WHOIS hook, unless the # caller is marked a bot and the whois_show_extensions_to_bots option is False if (sourceisBot and conf.conf['bot'].get('whois_show_extensions_to_bots')) or (not sourceisBot): - irc.callHooks([source, 'PYLINK_CUSTOM_WHOIS', {'target': target, 'server': server}]) + irc.call_hooks([source, 'PYLINK_CUSTOM_WHOIS', {'target': target, 'server': server}]) else: log.debug('(%s) coremods.handlers.handle_whois: skipping custom whois handlers because ' 'caller %s is marked as a bot', irc.name, source) @@ -116,15 +116,15 @@ def handle_mode(irc, source, command, args): modes = args['modes'] # If the sender is not a PyLink client, and the target IS a protected # client, revert any forced deoper attempts. - if irc.isInternalClient(target) and not irc.isInternalClient(source): - if ('-o', None) in modes and (target == irc.pseudoclient.uid or not irc.isManipulatableClient(target)): - irc.proto.mode(irc.sid, target, {('+o', None)}) + if irc.is_internal_client(target) and not irc.is_internal_client(source): + if ('-o', None) in modes and (target == irc.pseudoclient.uid or not irc.is_manipulatable_client(target)): + irc.mode(irc.sid, target, {('+o', None)}) utils.add_hook(handle_mode, 'MODE') def handle_operup(irc, source, command, args): """Logs successful oper-ups on networks.""" otype = args.get('text', 'IRC Operator') - log.debug("(%s) Successful oper-up (opertype %r) from %s", irc.name, otype, irc.getHostmask(source)) + log.debug("(%s) Successful oper-up (opertype %r) from %s", irc.name, otype, irc.get_hostmask(source)) irc.users[source].opertype = otype utils.add_hook(handle_operup, 'CLIENT_OPERED') @@ -143,11 +143,11 @@ def handle_version(irc, source, command, args): """Handles requests for the PyLink server version.""" # 351 syntax is usually ". : fullversion = irc.version() - irc.proto.numeric(irc.sid, 351, source, fullversion) + irc.numeric(irc.sid, 351, source, fullversion) utils.add_hook(handle_version, 'VERSION') def handle_time(irc, source, command, args): """Handles requests for the PyLink server time.""" timestring = time.ctime() - irc.proto.numeric(irc.sid, 391, source, '%s :%s' % (irc.hostname(), timestring)) + irc.numeric(irc.sid, 391, source, '%s :%s' % (irc.hostname(), timestring)) utils.add_hook(handle_time, 'TIME') diff --git a/coremods/permissions.py b/coremods/permissions.py index 66ab886..c5998ea 100644 --- a/coremods/permissions.py +++ b/coremods/permissions.py @@ -60,22 +60,22 @@ def checkPermissions(irc, uid, perms, also_show=[]): """ # For old (< 1.1 login blocks): # If the user is logged in, they automatically have all permissions. - if irc.matchHost('$pylinkacc', uid) and conf.conf['login'].get('user'): + if irc.match_host('$pylinkacc', uid) and conf.conf['login'].get('user'): log.debug('permissions: overriding permissions check for old-style admin user %s', - irc.getHostmask(uid)) + irc.get_hostmask(uid)) return True # Iterate over all hostmask->permission list mappings. for host, permlist in permissions.copy().items(): log.debug('permissions: permlist for %s: %s', host, permlist) - if irc.matchHost(host, uid): + if irc.match_host(host, uid): # Now, iterate over all the perms we are looking for. for perm in permlist: - # Use irc.matchHost to expand globs in an IRC-case insensitive and wildcard + # Use irc.match_host to expand globs in an IRC-case insensitive and wildcard # friendly way. e.g. 'xyz.*.#Channel\' will match 'xyz.manage.#channel|' on IRCds # using the RFC1459 casemapping. log.debug('permissions: checking if %s glob matches anything in %s', perm, permlist) - if any(irc.matchHost(perm, p) for p in perms): + if any(irc.match_host(perm, p) for p in perms): return True raise utils.NotAuthorizedError("You are missing one of the following permissions: %s" % (', '.join(perms+also_show))) diff --git a/coremods/service_support.py b/coremods/service_support.py index 248f195..c352283 100644 --- a/coremods/service_support.py +++ b/coremods/service_support.py @@ -14,7 +14,7 @@ def spawn_service(irc, source, command, args): # Service name name = args['name'] - if name != 'pylink' and not irc.proto.hasCap('can-spawn-clients'): + if name != 'pylink' and not irc.has_cap('can-spawn-clients'): log.debug("(%s) Not spawning service %s because the server doesn't support spawning clients", irc.name, name) return @@ -49,16 +49,16 @@ def spawn_service(irc, source, command, args): # Track the service's UIDs on each network. log.debug('(%s) spawn_service: Using nick %s for service %s', irc.name, nick, name) - u = irc.nickToUid(nick) - if u and irc.isInternalClient(u): # If an internal client exists, reuse it. + u = irc.nick_to_uid(nick) + if u and irc.is_internal_client(u): # If an internal client exists, reuse it. log.debug('(%s) spawn_service: Using existing client %s/%s', irc.name, u, nick) userobj = irc.users[u] else: log.debug('(%s) spawn_service: Spawning new client %s', irc.name, nick) - userobj = irc.proto.spawnClient(nick, ident, host, modes=modes, opertype="PyLink Service", + userobj = irc.spawn_client(nick, ident, host, modes=modes, opertype="PyLink Service", manipulatable=sbot.manipulatable) - # Store the service name in the IrcUser object for easier access. + # Store the service name in the User object for easier access. userobj.service = name sbot.uids[irc.name] = u = userobj.uid @@ -101,7 +101,7 @@ def handle_kill(irc, source, command, args): """Handle KILLs to PyLink service bots, respawning them as needed.""" target = args['target'] userdata = args.get('userdata') - sbot = irc.getServiceBot(target) + sbot = irc.get_service_bot(target) servicename = None if userdata and hasattr(userdata, 'service'): # Look for the target's service name attribute @@ -118,7 +118,7 @@ def handle_kick(irc, source, command, args): """Handle KICKs to the PyLink service bots, rejoining channels as needed.""" kicked = args['target'] channel = args['channel'] - sbot = irc.getServiceBot(kicked) + sbot = irc.get_service_bot(kicked) if sbot: sbot.join(irc, channel) utils.add_hook(handle_kick, 'KICK') @@ -128,7 +128,7 @@ def handle_commands(irc, source, command, args): target = args['target'] text = args['text'] - sbot = irc.getServiceBot(target) + sbot = irc.get_service_bot(target) if sbot: sbot.call_cmd(irc, source, text) diff --git a/docs/technical/hooks-reference.md b/docs/technical/hooks-reference.md index 9038fe7..6b8306b 100644 --- a/docs/technical/hooks-reference.md +++ b/docs/technical/hooks-reference.md @@ -163,7 +163,7 @@ Some hooks do not map directly to IRC commands, but to events that protocol modu - **PYLINK_CUSTOM_WHOIS**: `{'target': UID1, 'server': SID1}` - This hook is called by `coremods/handlers.py` during its WHOIS handling process, to allow plugins to provide custom WHOIS information. The `target` field represents the target UID, while the `server` field represents the SID that should be replying to the WHOIS request. The source of the payload is the user using `/whois`. - - Plugins wishing to implement this should use the standard WHOIS numerics, using `irc.proto.numeric()` to reply to the source from the given server. + - Plugins wishing to implement this should use the standard WHOIS numerics, using `irc.numeric()` to reply to the source from the given server. - This hook replaces the pre-0.8.x fashion of defining custom WHOIS handlers, which was non-standard and poorly documented. ## Commands handled WITHOUT hooks diff --git a/docs/technical/pmodule-spec.md b/docs/technical/pmodule-spec.md index 85fd6cc..407f850 100644 --- a/docs/technical/pmodule-spec.md +++ b/docs/technical/pmodule-spec.md @@ -77,7 +77,7 @@ internals](https://github.com/GLolol/PyLink/blob/1.0-beta1/classes.py#L474-L483) - **`sjoin`**`(self, server, channel, users, ts=None, modes=set())` - Sends an SJOIN for a group of users to a channel. The sender should always be a Server ID (SID). TS is optional, and defaults to the one we've stored in the channel state if not given. `users` is a list of `(prefix mode, UID)` pairs. Example uses: - `sjoin('100', '#test', [('', '100AAABBC'), ('qo', 100AAABBB'), ('h', '100AAADDD')])` - - `sjoin(self.irc.sid, '#test', [('o', self.irc.pseudoclient.uid)])` + - `sjoin(self.sid, '#test', [('o', self.pseudoclient.uid)])` - **`spawnServer`**`(self, name, sid=None, uplink=None, desc=None)` - Spawns a server off another PyLink server. `desc` (server description) defaults to the one in the config. `uplink` defaults to the main PyLink server, and `sid` (the server ID) is automatically generated if not given. Sanity checks for server name and SID validity ARE done by the protocol module here. @@ -105,7 +105,7 @@ A protocol module should also set the following variables in their protocol clas A protocol module manipulates the following attributes in the IRC object it is attached to: -- `self.irc.cmodes` / `self.irc.umodes`: These are mappings of named IRC modes (e.g. `inviteonly` or `moderated`) to a string list of mode letters, that should be either set during link negotiation or hardcoded into the protocol module. There are also special keys: `*A`, `*B`, `*C`, and `*D`, which **must** be set properly with a list of mode characters for that type of mode. +- `self.cmodes` / `self.umodes`: These are mappings of named IRC modes (e.g. `inviteonly` or `moderated`) to a string list of mode letters, that should be either set during link negotiation or hardcoded into the protocol module. There are also special keys: `*A`, `*B`, `*C`, and `*D`, which **must** be set properly with a list of mode characters for that type of mode. - Types of modes are defined as follows (from http://www.irc.org/tech_docs/005.html): - A = Mode that adds or removes a nick or address to a list. Always has a parameter. - B = Mode that changes a setting and always has a parameter. @@ -114,8 +114,8 @@ A protocol module manipulates the following attributes in the IRC object it is a - If not defined, these will default to modes defined by RFC 1459: https://github.com/GLolol/PyLink/blob/1.0-beta1/classes.py#L127-L152 - An example of mode mapping hardcoding can be found here: https://github.com/GLolol/PyLink/blob/1.0-beta1/protocols/ts6.py#L259-L311 - You can find a list of supported (named) channel modes [here](channel-modes.csv), and a list of user modes [here](user-modes.csv). -- `self.irc.prefixmodes`: This defines a mapping of prefix modes (+o, +v, etc.) to their respective mode prefix. This will default to `{'o': '@', 'v': '+'}` (the standard op and voice) if not defined. - - Example: `self.irc.prefixmodes = {'o': '@', 'h': '%', 'v': '+'}` +- `self.prefixmodes`: This defines a mapping of prefix modes (+o, +v, etc.) to their respective mode prefix. This will default to `{'o': '@', 'v': '+'}` (the standard op and voice) if not defined. + - Example: `self.prefixmodes = {'o': '@', 'h': '%', 'v': '+'}` ### Topics @@ -167,7 +167,7 @@ As an example, one protocol module that tweaks this is [`Clientbot`](https://git ## Changes * 2017-03-15 (1.2-dev) - - Corrected the location of `self.irc.cmodes/umodes/prefixmodes` attributes + - Corrected the location of `self.cmodes/umodes/prefixmodes` attributes - Mention `self.conf_keys` as a special variable for completeness * 2017-01-29 (1.2-dev) - NOTICE can now be sent from servers. diff --git a/docs/technical/writing-plugins.md b/docs/technical/writing-plugins.md index 649fb0e..de68fea 100644 --- a/docs/technical/writing-plugins.md +++ b/docs/technical/writing-plugins.md @@ -46,7 +46,7 @@ Command handlers do not return anything and can raise exceptions, which are caug Plugins receive data from the underlying protocol module, and communicate back using outgoing [command functions](pmodule-spec.md) implemented by the protocol module. They should *never* send raw data directly back to IRC, because that wouldn't be portable across different IRCds. -These functions are usually called in this fashion: `irc.proto.command(arg1, arg2, ...)`. For example, the command `irc.proto.join('10XAAAAAB', '#bots')` would join a PyLink client with UID `10XAAAAAB` to channel `#bots`. +These functions are usually called in this fashion: `irc.command(arg1, arg2, ...)`. For example, the command `irc.join('10XAAAAAB', '#bots')` would join a PyLink client with UID `10XAAAAAB` to channel `#bots`. For sending messages (e.g. replies to commands), simpler forms of: diff --git a/plugins/automode.py b/plugins/automode.py index d129bd4..a8a1c61 100644 --- a/plugins/automode.py +++ b/plugins/automode.py @@ -58,7 +58,7 @@ def checkAccess(irc, uid, channel, command): # - automode..#channel: ability to automode on the given channel. # - automode.savedb: ability to save the automode DB. log.debug('(%s) Automode: checking access for %s/%s for %s capability on %s', irc.name, uid, - irc.getHostmask(uid), command, channel) + irc.get_hostmask(uid), command, channel) baseperm = 'automode.%s' % command try: @@ -99,7 +99,7 @@ def match(irc, channel, uids=None): for mask, modes in dbentry.items(): for uid in uids: - if irc.matchHost(mask, uid): + if irc.match_host(mask, uid): # User matched a mask. Filter the mode list given to only those that are valid # prefix mode characters. outgoing_modes += [('+'+mode, uid) for mode in modes if mode in irc.prefixmodes] @@ -113,10 +113,10 @@ def match(irc, channel, uids=None): log.debug("(%s) automode: sending modes from modebot_uid %s", irc.name, modebot_uid) - irc.proto.mode(modebot_uid, channel, outgoing_modes) + irc.mode(modebot_uid, channel, outgoing_modes) # Create a hook payload to support plugins like relay. - irc.callHooks([modebot_uid, 'AUTOMODE_MODE', + irc.call_hooks([modebot_uid, 'AUTOMODE_MODE', {'target': channel, 'modes': outgoing_modes, 'parse_as': 'MODE'}]) def handle_join(irc, source, command, args): @@ -124,7 +124,7 @@ def handle_join(irc, source, command, args): Automode JOIN listener. This sets modes accordingly if the person joining matches a mask in the ACL. """ - channel = irc.toLower(args['channel']) + channel = irc.to_lower(args['channel']) match(irc, channel, args['users']) utils.add_hook(handle_join, 'JOIN') @@ -153,7 +153,7 @@ def getChannelPair(irc, source, chanpair, perm=None): except ValueError: raise ValueError("Invalid channel pair %r" % chanpair) channel = '#' + channel - channel = irc.toLower(channel) + channel = irc.to_lower(channel) assert utils.isChannel(channel), "Invalid channel name %s." % channel @@ -202,7 +202,7 @@ def setacc(irc, source, args): modes = modes.lstrip('+') # remove extraneous leading +'s dbentry[mask] = modes - log.info('(%s) %s set modes +%s for %s on %s', ircobj.name, irc.getHostmask(source), modes, mask, channel) + log.info('(%s) %s set modes +%s for %s on %s', ircobj.name, irc.get_hostmask(source), modes, mask, channel) reply(irc, "Done. \x02%s\x02 now has modes \x02+%s\x02 in \x02%s\x02." % (mask, modes, channel)) # Join the Automode bot to the channel if not explicitly told to. @@ -233,7 +233,7 @@ def delacc(irc, source, args): if mask in dbentry: del dbentry[mask] - log.info('(%s) %s removed modes for %s on %s', ircobj.name, irc.getHostmask(source), mask, channel) + 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: error(irc, "No Automode access entry for \x02%s\x02 exists in \x02%s\x02." % (mask, channel)) @@ -299,7 +299,7 @@ def syncacc(irc, source, args): else: ircobj, channel = getChannelPair(irc, source, chanpair, perm='sync') - log.info('(%s) %s synced modes on %s', ircobj.name, irc.getHostmask(source), channel) + log.info('(%s) %s synced modes on %s', ircobj.name, irc.get_hostmask(source), channel) match(ircobj, channel) reply(irc, 'Done.') @@ -324,7 +324,7 @@ def clearacc(irc, source, args): if db.get(ircobj.name+channel): del db[ircobj.name+channel] - log.info('(%s) %s cleared modes on %s', ircobj.name, irc.getHostmask(source), channel) + log.info('(%s) %s cleared modes on %s', ircobj.name, irc.get_hostmask(source), channel) reply(irc, "Done. Removed all Automode access entries for \x02%s\x02." % channel) else: error(irc, "No Automode access entries exist for \x02%s\x02." % channel) diff --git a/plugins/bots.py b/plugins/bots.py index 7f764d7..cf2af36 100644 --- a/plugins/bots.py +++ b/plugins/bots.py @@ -18,7 +18,7 @@ def spawnclient(irc, source, args): except ValueError: irc.error("Not enough arguments. Needs 3: nick, user, host.") return - irc.proto.spawnClient(nick, ident, host, manipulatable=True) + irc.spawn_client(nick, ident, host, manipulatable=True) irc.reply("Done.") @utils.add_cmd @@ -33,21 +33,21 @@ def quit(irc, source, args): except IndexError: irc.error("Not enough arguments. Needs 1-2: nick, reason (optional).") return - if irc.pseudoclient.uid == irc.nickToUid(nick): + if irc.pseudoclient.uid == irc.nick_to_uid(nick): irc.error("Cannot quit the main PyLink client!") return - u = irc.nickToUid(nick) + u = irc.nick_to_uid(nick) quitmsg = ' '.join(args[1:]) or 'Client Quit' - if not irc.isManipulatableClient(u): + if not irc.is_manipulatable_client(u): irc.error("Cannot force quit a protected PyLink services client.") return - irc.proto.quit(u, quitmsg) + irc.quit(u, quitmsg) irc.reply("Done.") - irc.callHooks([u, 'PYLINK_BOTSPLUGIN_QUIT', {'text': quitmsg, 'parse_as': 'QUIT'}]) + irc.call_hooks([u, 'PYLINK_BOTSPLUGIN_QUIT', {'text': quitmsg, 'parse_as': 'QUIT'}]) def joinclient(irc, source, args): """[] [,,,...] @@ -63,9 +63,9 @@ def joinclient(irc, source, args): try: # Check if the first argument is an existing PyLink client. If it is not, # then assume that the first argument was actually the channels being joined. - u = irc.nickToUid(args[0]) + u = irc.nick_to_uid(args[0]) - if not irc.isInternalClient(u): # First argument isn't one of our clients + if not irc.is_internal_client(u): # First argument isn't one of our clients raise IndexError clist = args[1] @@ -82,7 +82,7 @@ def joinclient(irc, source, args): irc.error("No valid channels given.") return - if not (irc.isManipulatableClient(u) or irc.getServiceBot(u)): + if not (irc.is_manipulatable_client(u) or irc.get_service_bot(u)): irc.error("Cannot force join a protected PyLink services client.") return @@ -99,12 +99,12 @@ def joinclient(irc, source, args): # join() doesn't support prefixes. if prefixes: - irc.proto.sjoin(irc.sid, real_channel, [(joinmodes, u)]) + irc.sjoin(irc.sid, real_channel, [(joinmodes, u)]) else: - irc.proto.join(u, real_channel) + irc.join(u, real_channel) # Call a join hook manually so other plugins like relay can understand it. - irc.callHooks([u, 'PYLINK_BOTSPLUGIN_JOIN', {'channel': real_channel, 'users': [u], + irc.call_hooks([u, 'PYLINK_BOTSPLUGIN_JOIN', {'channel': real_channel, 'users': [u], 'modes': irc.channels[real_channel].modes, 'parse_as': 'JOIN'}]) irc.reply("Done.") @@ -128,7 +128,7 @@ def nick(irc, source, args): except IndexError: irc.error("Not enough arguments. Needs 1-2: nick (optional), newnick.") return - u = irc.nickToUid(nick) + u = irc.nick_to_uid(nick) if newnick in ('0', u): # Allow /nick 0 to work newnick = u @@ -137,14 +137,14 @@ def nick(irc, source, args): irc.error('Invalid nickname %r.' % newnick) return - elif not (irc.isManipulatableClient(u) or irc.getServiceBot(u)): + elif not (irc.is_manipulatable_client(u) or irc.get_service_bot(u)): irc.error("Cannot force nick changes for a protected PyLink services client.") return - irc.proto.nick(u, newnick) + irc.nick(u, newnick) irc.reply("Done.") # Ditto above: manually send a NICK change hook payload to other plugins. - irc.callHooks([u, 'PYLINK_BOTSPLUGIN_NICK', {'newnick': newnick, 'oldnick': nick, 'parse_as': 'NICK'}]) + irc.call_hooks([u, 'PYLINK_BOTSPLUGIN_NICK', {'newnick': newnick, 'oldnick': nick, 'parse_as': 'NICK'}]) @utils.add_cmd def part(irc, source, args): @@ -161,8 +161,8 @@ def part(irc, source, args): # First, check if the first argument is an existing PyLink client. If it is not, # then assume that the first argument was actually the channels being parted. - u = irc.nickToUid(nick) - if not irc.isInternalClient(u): # First argument isn't one of our clients + u = irc.nick_to_uid(nick) + if not irc.is_internal_client(u): # First argument isn't one of our clients raise IndexError except IndexError: # No nick was given; shift arguments one to the left. @@ -180,7 +180,7 @@ def part(irc, source, args): irc.error("No valid channels given.") return - if not (irc.isManipulatableClient(u) or irc.getServiceBot(u)): + if not (irc.is_manipulatable_client(u) or irc.get_service_bot(u)): irc.error("Cannot force part a protected PyLink services client.") return @@ -188,10 +188,10 @@ def part(irc, source, args): if not utils.isChannel(channel): irc.error("Invalid channel name %r." % channel) return - irc.proto.part(u, channel, reason) + irc.part(u, channel, reason) irc.reply("Done.") - irc.callHooks([u, 'PYLINK_BOTSPLUGIN_PART', {'channels': clist, 'text': reason, 'parse_as': 'PART'}]) + irc.call_hooks([u, 'PYLINK_BOTSPLUGIN_PART', {'channels': clist, 'text': reason, 'parse_as': 'PART'}]) @utils.add_cmd def msg(irc, source, args): @@ -208,8 +208,8 @@ def msg(irc, source, args): # First, check if the first argument is an existing PyLink client. If it is not, # then assume that the first argument was actually the message TARGET. - sourceuid = irc.nickToUid(msgsource) - if not irc.isInternalClient(sourceuid): # First argument isn't one of our clients + sourceuid = irc.nick_to_uid(msgsource) + if not irc.is_internal_client(sourceuid): # First argument isn't one of our clients raise IndexError if not text: @@ -229,14 +229,14 @@ def msg(irc, source, args): if not utils.isChannel(target): # Convert nick of the message target to a UID, if the target isn't a channel - real_target = irc.nickToUid(target) + real_target = irc.nick_to_uid(target) if real_target is None: # Unknown target user, if target isn't a valid channel name irc.error('Unknown user %r.' % target) return else: real_target = target - irc.proto.message(sourceuid, real_target, text) + irc.message(sourceuid, real_target, text) irc.reply("Done.") - irc.callHooks([sourceuid, 'PYLINK_BOTSPLUGIN_MSG', {'target': real_target, 'text': text, 'parse_as': 'PRIVMSG'}]) + irc.call_hooks([sourceuid, 'PYLINK_BOTSPLUGIN_MSG', {'target': real_target, 'text': text, 'parse_as': 'PRIVMSG'}]) utils.add_cmd(msg, 'say') diff --git a/plugins/changehost.py b/plugins/changehost.py index d40dbfc..40e82c1 100644 --- a/plugins/changehost.py +++ b/plugins/changehost.py @@ -19,7 +19,7 @@ def _changehost(irc, target, args): if target not in irc.users: return - elif irc.isInternalClient(target): + elif irc.is_internal_client(target): log.debug('(%s) Skipping changehost on internal client %s', irc.name, target) return @@ -48,7 +48,7 @@ def _changehost(irc, target, args): for host_glob, host_template in changehost_hosts.items(): log.debug('(%s) Changehost: checking mask %s', irc.name, host_glob) - if irc.matchHost(host_glob, target, ip=match_ip, realhost=match_realhosts): + if irc.match_host(host_glob, target, ip=match_ip, realhost=match_realhosts): log.debug('(%s) Changehost matched mask %s', irc.name, host_glob) # This uses template strings for simple substitution: # https://docs.python.org/3/library/string.html#template-strings @@ -78,7 +78,7 @@ def _changehost(irc, target, args): if char not in allowed_chars: new_host = new_host.replace(char, '-') - irc.proto.updateClient(target, 'HOST', new_host) + irc.update_client(target, 'HOST', new_host) # Only operate on the first match. break @@ -103,13 +103,13 @@ def handle_chghost(irc, sender, command, args): target = args['target'] - if (not irc.isInternalClient(sender)) and (not irc.isInternalServer(sender)): + if (not irc.is_internal_client(sender)) and (not irc.is_internal_server(sender)): if irc.name in changehost_conf.get('enforced_nets', []): log.debug('(%s) Enforce for network is on, re-checking host for target %s/%s', - irc.name, target, irc.getFriendlyName(target)) + irc.name, target, irc.get_friendly_name(target)) for ex in changehost_conf.get("enforce_exceptions", []): - if irc.matchHost(ex, target): + if irc.match_host(ex, target): log.debug('(%s) Skipping host change for target %s; they are exempted by mask %s', irc.name, target, ex) return diff --git a/plugins/commands.py b/plugins/commands.py index c358e8e..ffd2463 100644 --- a/plugins/commands.py +++ b/plugins/commands.py @@ -29,7 +29,7 @@ def status(irc, source, args): irc.reply('You are identified as \x02%s\x02.' % identified) else: irc.reply('You are not identified as anyone.') - irc.reply('Operator access: \x02%s\x02' % bool(irc.isOper(source))) + irc.reply('Operator access: \x02%s\x02' % bool(irc.is_oper(source))) _none = '\x1D(none)\x1D' @utils.add_cmd @@ -43,10 +43,10 @@ def showuser(irc, source, args): except IndexError: irc.error("Not enough arguments. Needs 1: nick.") return - u = irc.nickToUid(target) or target + u = irc.nick_to_uid(target) or target # Only show private info if the person is calling 'showuser' on themselves, # or is an oper. - verbose = irc.isOper(source) or u == source + verbose = irc.is_oper(source) or u == source if u not in irc.users: irc.error('Unknown user %r.' % target) return @@ -57,7 +57,7 @@ def showuser(irc, source, args): f('Showing information on user \x02%s\x02 (%s@%s): %s' % (userobj.nick, userobj.ident, userobj.host, userobj.realname)) - sid = irc.getServer(u) + sid = irc.get_server(u) serverobj = irc.servers[sid] ts = userobj.ts @@ -67,7 +67,7 @@ def showuser(irc, source, args): if verbose: # Oper only data: user modes, channels on, account info, etc. - f('\x02User modes\x02: %s' % irc.joinModes(userobj.modes, sort=True)) + f('\x02User modes\x02: %s' % irc.join_modes(userobj.modes, sort=True)) f('\x02Protocol UID\x02: %s; \x02Real host\x02: %s; \x02IP\x02: %s' % \ (u, userobj.realhost, userobj.ip)) channels = sorted(userobj.channels) @@ -83,7 +83,7 @@ def showchan(irc, source, args): Shows information about .""" permissions.checkPermissions(irc, source, ['commands.showchan']) try: - channel = irc.toLower(args[0]) + channel = irc.to_lower(args[0]) except IndexError: irc.error("Not enough arguments. Needs 1: channel.") return @@ -95,7 +95,7 @@ def showchan(irc, source, args): c = irc.channels[channel] # Only show verbose info if caller is oper or is in the target channel. - verbose = source in c.users or irc.isOper(source) + verbose = source in c.users or irc.is_oper(source) secret = ('s', None) in c.modes if secret and not verbose: # Hide secret channels from normal users. @@ -110,17 +110,17 @@ def showchan(irc, source, args): # Mark TS values as untrusted on Clientbot and others (where TS is read-only or not trackable) f('\x02Channel creation time\x02: %s (%s)%s' % (ctime(c.ts), c.ts, - ' [UNTRUSTED]' if not irc.proto.hasCap('has-ts') else '')) + ' [UNTRUSTED]' if not irc.has_cap('has-ts') else '')) # Show only modes that aren't list-style modes. - modes = irc.joinModes([m for m in c.modes if m[0] not in irc.cmodes['*A']], sort=True) + modes = irc.join_modes([m for m in c.modes if m[0] not in irc.cmodes['*A']], sort=True) f('\x02Channel modes\x02: %s' % modes) if verbose: nicklist = [] # Iterate over the user list, sorted by nick. for user, nick in sorted(zip(c.users, nicks), key=lambda userpair: userpair[1].lower()): - for pmode in c.getPrefixModes(user): + for pmode in c.get_prefix_modes(user): # Show prefix modes in order from highest to lowest. nick = irc.prefixmodes.get(irc.cmodes.get(pmode, ''), '') + nick nicklist.append(nick) @@ -179,7 +179,7 @@ def logout(irc, source, args): irc.error("You are not logged in!") return else: - otheruid = irc.nickToUid(othernick) + otheruid = irc.nick_to_uid(othernick) if not otheruid: irc.error("Unknown user %s." % othernick) return diff --git a/plugins/example.py b/plugins/example.py index b994db0..2709e6b 100644 --- a/plugins/example.py +++ b/plugins/example.py @@ -18,7 +18,7 @@ def hook_privmsg(irc, source, command, args): channel = args['target'] text = args['text'] - # irc.pseudoclient stores the IrcUser object of the main PyLink client. + # irc.pseudoclient stores the User 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!') diff --git a/plugins/exec.py b/plugins/exec.py index 5a93752..c2ea60e 100644 --- a/plugins/exec.py +++ b/plugins/exec.py @@ -34,7 +34,7 @@ def _exec(irc, source, args, locals_dict=None): return log.info('(%s) Executing %r for %s', irc.name, args, - irc.getHostmask(source)) + irc.get_hostmask(source)) if locals_dict is None: locals_dict = locals() else: @@ -86,7 +86,7 @@ def _eval(irc, source, args, locals_dict=None, pretty_print=False): locals_dict['args'] = args log.info('(%s) Evaluating %r for %s', irc.name, args, - irc.getHostmask(source)) + irc.get_hostmask(source)) result = eval(args, globals(), locals_dict) @@ -150,7 +150,7 @@ def raw(irc, source, args): return log.debug('(%s) Sending raw text %r to IRC for %s', irc.name, args, - irc.getHostmask(source)) + irc.get_hostmask(source)) irc.send(args) irc.reply("Done.") @@ -170,5 +170,5 @@ def inject(irc, source, args): return log.info('(%s) Injecting raw text %r into protocol module for %s', irc.name, - args, irc.getHostmask(source)) + args, irc.get_hostmask(source)) irc.reply(irc.runline(args)) diff --git a/plugins/fantasy.py b/plugins/fantasy.py index fdd96f6..93b519b 100644 --- a/plugins/fantasy.py +++ b/plugins/fantasy.py @@ -12,7 +12,7 @@ def handle_fantasy(irc, source, command, args): channel = args['target'] orig_text = args['text'] - if utils.isChannel(channel) and not irc.isInternalClient(source): + if utils.isChannel(channel) and not irc.is_internal_client(source): # The following conditions must be met for an incoming message for # fantasy to trigger: # 1) The message target is a channel. @@ -42,7 +42,7 @@ def handle_fantasy(irc, source, command, args): # If responding to nick is enabled, add variations of the current nick # to the prefix list: "," and ":" - nick = irc.toLower(irc.users[servuid].nick) + nick = irc.to_lower(irc.users[servuid].nick) nick_prefixes = [nick+',', nick+':'] if respondtonick: @@ -52,7 +52,7 @@ def handle_fantasy(irc, source, command, args): # No prefixes were set, so skip. continue - lowered_text = irc.toLower(orig_text) + lowered_text = irc.to_lower(orig_text) for prefix in filter(None, prefixes): # Cycle through the prefixes list we finished with. if lowered_text.startswith(prefix): diff --git a/plugins/global.py b/plugins/global.py index edebbdc..7817dc8 100644 --- a/plugins/global.py +++ b/plugins/global.py @@ -20,12 +20,12 @@ def g(irc, source, args): for name, ircd in world.networkobjects.items(): if ircd.connected.is_set(): # Only attempt to send to connected networks for channel in ircd.pseudoclient.channels: - subst = {'sender': irc.getFriendlyName(source), + subst = {'sender': irc.get_friendly_name(source), 'network': irc.name, - 'fullnetwork': irc.getFullNetworkName(), + 'fullnetwork': irc.get_full_network_name(), 'current_channel': channel, 'current_network': ircd.name, - 'current_fullnetwork': ircd.getFullNetworkName(), + 'current_fullnetwork': ircd.get_full_network_name(), 'text': message} # Disable relaying or other plugins handling the global message. diff --git a/plugins/opercmds.py b/plugins/opercmds.py index 98eba02..3ec3cf8 100644 --- a/plugins/opercmds.py +++ b/plugins/opercmds.py @@ -27,10 +27,10 @@ def checkban(irc, source, args): results = 0 for uid, userobj in irc.users.copy().items(): - if irc.matchHost(banmask, uid): + if irc.match_host(banmask, uid): if results < 50: # XXX rather arbitrary limit s = "\x02%s\x02 (%s@%s) [%s] {\x02%s\x02}" % (userobj.nick, userobj.ident, - userobj.host, userobj.realname, irc.getFriendlyName(irc.getServer(uid))) + userobj.host, userobj.realname, irc.get_friendly_name(irc.get_server(uid))) # Always reply in private to prevent information leaks. irc.reply(s, private=True) @@ -42,9 +42,9 @@ def checkban(irc, source, args): else: irc.msg(source, "No results found.", notice=True) else: - # Target can be both a nick (of an online user) or a hostmask. irc.matchHost() handles this + # Target can be both a nick (of an online user) or a hostmask. irc.match_host() handles this # automatically. - if irc.matchHost(banmask, targetmask): + if irc.match_host(banmask, targetmask): irc.reply('Yes, \x02%s\x02 matches \x02%s\x02.' % (targetmask, banmask)) else: irc.reply('No, \x02%s\x02 does not match \x02%s\x02.' % (targetmask, banmask)) @@ -61,7 +61,7 @@ def jupe(irc, source, args): try: servername = args[0] reason = ' '.join(args[1:]) or "No reason given" - desc = "Juped by %s: [%s]" % (irc.getHostmask(source), reason) + desc = "Juped by %s: [%s]" % (irc.get_hostmask(source), reason) except IndexError: irc.error('Not enough arguments. Needs 1-2: servername, reason (optional).') return @@ -70,9 +70,9 @@ def jupe(irc, source, args): irc.error("Invalid server name '%s'." % servername) return - sid = irc.proto.spawnServer(servername, desc=desc) + sid = irc.spawn_server(servername, desc=desc) - irc.callHooks([irc.pseudoclient.uid, 'OPERCMDS_SPAWNSERVER', + irc.call_hooks([irc.pseudoclient.uid, 'OPERCMDS_SPAWNSERVER', {'name': servername, 'sid': sid, 'text': desc}]) irc.reply("Done.") @@ -85,14 +85,14 @@ def kick(irc, source, args): Admin only. Kicks from the specified channel.""" permissions.checkPermissions(irc, source, ['opercmds.kick']) try: - channel = irc.toLower(args[0]) + channel = irc.to_lower(args[0]) target = args[1] reason = ' '.join(args[2:]) except IndexError: irc.error("Not enough arguments. Needs 2-3: channel, target, reason (optional).") return - targetu = irc.nickToUid(target) + targetu = irc.nick_to_uid(target) if channel not in irc.channels: # KICK only works on channels that exist. irc.error("Unknown channel %r." % channel) @@ -104,9 +104,9 @@ def kick(irc, source, args): return sender = irc.pseudoclient.uid - irc.proto.kick(sender, channel, targetu, reason) + irc.kick(sender, channel, targetu, reason) irc.reply("Done.") - irc.callHooks([sender, 'CHANCMDS_KICK', {'channel': channel, 'target': targetu, + irc.call_hooks([sender, 'CHANCMDS_KICK', {'channel': channel, 'target': targetu, 'text': reason, 'parse_as': 'KICK'}]) @utils.add_cmd @@ -124,7 +124,7 @@ def kill(irc, source, args): # Convert the source and target nicks to UIDs. sender = irc.pseudoclient.uid - targetu = irc.nickToUid(target) + targetu = irc.nick_to_uid(target) userdata = irc.users.get(targetu) if targetu not in irc.users: @@ -132,13 +132,13 @@ def kill(irc, source, args): irc.error("No such nick '%s'." % target) return - irc.proto.kill(sender, targetu, reason) + irc.kill(sender, targetu, reason) # Format the kill reason properly in hooks. - reason = "Killed (%s (%s))" % (irc.getFriendlyName(sender), reason) + reason = "Killed (%s (%s))" % (irc.get_friendly_name(sender), reason) irc.reply("Done.") - irc.callHooks([sender, 'CHANCMDS_KILL', {'target': targetu, 'text': reason, + irc.call_hooks([sender, 'CHANCMDS_KILL', {'target': targetu, 'text': reason, 'userdata': userdata, 'parse_as': 'KILL'}]) @utils.add_cmd @@ -164,7 +164,7 @@ def mode(irc, source, args): irc.error("No valid modes were given.") return - parsedmodes = irc.parseModes(target, modes) + parsedmodes = irc.parse_modes(target, modes) if not parsedmodes: # Modes were given but they failed to parse into anything meaningful. @@ -173,10 +173,10 @@ def mode(irc, source, args): irc.error("No valid modes were given.") return - irc.proto.mode(irc.pseudoclient.uid, target, parsedmodes) + irc.mode(irc.pseudoclient.uid, target, parsedmodes) # Call the appropriate hooks for plugins like relay. - irc.callHooks([irc.pseudoclient.uid, 'OPERCMDS_MODEOVERRIDE', + irc.call_hooks([irc.pseudoclient.uid, 'OPERCMDS_MODEOVERRIDE', {'target': target, 'modes': parsedmodes, 'parse_as': 'MODE'}]) irc.reply("Done.") @@ -198,9 +198,9 @@ def topic(irc, source, args): irc.error("Unknown channel %r." % channel) return - irc.proto.topic(irc.pseudoclient.uid, channel, topic) + irc.topic(irc.pseudoclient.uid, channel, topic) irc.reply("Done.") - irc.callHooks([irc.pseudoclient.uid, 'CHANCMDS_TOPIC', + irc.call_hooks([irc.pseudoclient.uid, 'CHANCMDS_TOPIC', {'channel': channel, 'text': topic, 'setter': source, 'parse_as': 'TOPIC'}]) diff --git a/plugins/relay.py b/plugins/relay.py index e0ebf0d..3286424 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -106,7 +106,7 @@ def normalize_nick(irc, netname, nick, times_tagged=0, uid=''): forcetag_nicks = conf.conf.get('relay', {}).get('forcetag_nicks', []) log.debug('(%s) relay.normalize_nick: checking if globs %s match %s.', irc.name, forcetag_nicks, nick) for glob in forcetag_nicks: - if irc.matchHost(glob, nick): + if irc.match_host(glob, nick): # User matched a nick to force tag nicks for. Tag them. times_tagged = 1 break @@ -118,7 +118,7 @@ def normalize_nick(irc, netname, nick, times_tagged=0, uid=''): # Charybdis, IRCu, etc. don't allow / in nicks, and will SQUIT with a protocol # violation if it sees one. Or it might just ignore the client introduction and # cause bad desyncs. - protocol_allows_slashes = irc.proto.hasCap('slash-in-nicks') or \ + protocol_allows_slashes = irc.has_cap('slash-in-nicks') or \ irc.serverdata.get('relay_force_slashes') if '/' not in separator or not protocol_allows_slashes: @@ -154,7 +154,7 @@ def normalize_nick(irc, netname, nick, times_tagged=0, uid=''): if char not in allowed_chars: nick = nick.replace(char, fallback_separator) - while irc.nickToUid(nick) and irc.nickToUid(nick) != uid: + while irc.nick_to_uid(nick) and irc.nick_to_uid(nick) != uid: # The nick we want exists: Increase the separator length by 1 if the user was already # tagged, but couldn't be created due to a nick conflict. This can happen when someone # steals a relay user's nick. @@ -177,11 +177,11 @@ def normalize_host(irc, host): log.debug('(%s) relay.normalize_host: IRCd=%s, host=%s', irc.name, irc.protoname, host) allowed_chars = string.ascii_letters + string.digits + '-.:' - if irc.proto.hasCap('slash-in-hosts'): + if irc.has_cap('slash-in-hosts'): # UnrealIRCd and IRCd-Hybrid don't allow slashes in hostnames allowed_chars += '/' - if irc.proto.hasCap('underscore-in-hosts'): + if irc.has_cap('underscore-in-hosts'): # Most IRCds allow _ in hostnames, but hybrid/charybdis/ratbox IRCds do not. allowed_chars += '_' @@ -207,7 +207,7 @@ def get_prefix_modes(irc, remoteirc, channel, user, mlist=None): # Note: reverse the order so prefix modes are bursted in their traditional order # (e.g. owner before op before halfop). TODO: SJOIN modes should probably be # consistently sorted IRCd-side. - for pmode in reversed(irc.channels[channel].getPrefixModes(user, prefixmodes=mlist)): + for pmode in reversed(irc.channels[channel].get_prefix_modes(user, prefixmodes=mlist)): if pmode in remoteirc.cmodes: modes += remoteirc.cmodes[pmode] return modes @@ -220,9 +220,9 @@ def spawn_relay_server(irc, remoteirc): suffix = conf.conf.get('relay', {}).get('server_suffix', 'relay') # Strip any leading or trailing .'s suffix = suffix.strip('.') - sid = irc.proto.spawnServer('%s.%s' % (remoteirc.name, suffix), + sid = irc.spawn_server('%s.%s' % (remoteirc.name, suffix), desc="PyLink Relay network - %s" % - (remoteirc.getFullNetworkName()), endburst_delay=3) + (remoteirc.get_full_network_name()), endburst_delay=3) except (RuntimeError, ValueError): # Network not initialized yet, or a server name conflict. log.exception('(%s) Failed to spawn server for %r (possible jupe?):', irc.name, remoteirc.name) @@ -298,7 +298,7 @@ def spawn_relay_user(irc, remoteirc, user, times_tagged=0): else: opertype = 'IRC Operator' - opertype += ' (on %s)' % irc.getFullNetworkName() + opertype += ' (on %s)' % irc.get_full_network_name() # Set hideoper on remote opers, to prevent inflating # /lusers and various /stats @@ -328,14 +328,14 @@ def spawn_relay_user(irc, remoteirc, user, times_tagged=0): realhost = None ip = '0.0.0.0' - u = remoteirc.proto.spawnClient(nick, ident=ident, host=host, realname=realname, modes=modes, + u = remoteirc.spawn_client(nick, ident=ident, host=host, realname=realname, modes=modes, opertype=opertype, server=rsid, ip=ip, realhost=realhost).uid try: remoteirc.users[u].remote = (irc.name, user) remoteirc.users[u].opertype = opertype away = userobj.away if away: - remoteirc.proto.away(u, away) + remoteirc.away(u, away) except KeyError: # User got killed somehow while we were setting options on it. # This is probably being done by the uplink, due to something like an @@ -353,7 +353,7 @@ def get_remote_user(irc, remoteirc, user, spawn_if_missing=True, times_tagged=0) # Wait until the network is working before trying to spawn anything. if irc.connected.wait(TCONDITION_TIMEOUT): # Don't spawn clones for registered service bots. - sbot = irc.getServiceBot(user) + sbot = irc.get_service_bot(user) if sbot: return sbot.uids.get(remoteirc.name) @@ -480,10 +480,9 @@ def initialize_channel(irc, channel): # Only update the topic if it's different from what we already have, # and topic bursting is complete. if remoteirc.channels[remotechan].topicset and topic != irc.channels[channel].topic: - irc.proto.topicBurst(irc.sid, channel, topic) + irc.topic_burst(irc.sid, channel, topic) # Send our users and channel modes to the other nets - log.debug('(%s) relay.initialize_channel: joining our (%s) users: %s', irc.name, remotenet, irc.channels[channel].users) relay_joins(irc, channel, irc.channels[channel].users, irc.channels[channel].ts) if 'pylink' in world.services: @@ -497,7 +496,7 @@ def remove_channel(irc, channel): if channel not in map(str.lower, irc.serverdata.get('channels', [])): world.services['pylink'].extra_channels[irc.name].discard(channel) if irc.pseudoclient: - irc.proto.part(irc.pseudoclient.uid, channel, 'Channel delinked.') + irc.part(irc.pseudoclient.uid, channel, 'Channel delinked.') relay = get_relay((irc.name, channel)) if relay: @@ -509,12 +508,12 @@ def remove_channel(irc, channel): if user == irc.pseudoclient.uid and channel in \ irc.serverdata.get('channels', []): continue - irc.proto.part(user, channel, 'Channel delinked.') + irc.part(user, channel, 'Channel delinked.') # Don't ever quit it either... if user != irc.pseudoclient.uid and not irc.users[user].channels: remoteuser = get_orig_user(irc, user) del relayusers[remoteuser][irc.name] - irc.proto.quit(user, 'Left all shared channels.') + irc.quit(user, 'Left all shared channels.') def check_claim(irc, channel, sender, chanobj=None): """ @@ -541,8 +540,8 @@ def check_claim(irc, channel, sender, chanobj=None): return (not relay) or irc.name == relay[0] or not db[relay]['claim'] or \ irc.name in db[relay]['claim'] or \ any([mode in sender_modes for mode in ('y', 'q', 'a', 'o', 'h')]) \ - or irc.isInternalClient(sender) or \ - irc.isInternalServer(sender) + or irc.is_internal_client(sender) or \ + irc.is_internal_server(sender) def get_supported_umodes(irc, remoteirc, modes): """Given a list of user modes, filters out all of those not supported by the @@ -651,7 +650,7 @@ def relay_joins(irc, channel, users, ts, burst=True): # Fetch the known channel TS and all the prefix modes for each user. This ensures # the different sides of the relay are merged properly. - if not irc.proto.hasCap('has-ts'): + if not irc.has_cap('has-ts'): # Special hack for clientbot: just use the remote's modes so mode changes # take precendence. (TS is always outside the clientbot's control) ts = remoteirc.channels[remotechan].ts @@ -671,16 +670,16 @@ def relay_joins(irc, channel, users, ts, burst=True): modes = get_supported_cmodes(irc, remoteirc, channel, irc.channels[channel].modes) rsid = get_remote_sid(remoteirc, irc) if rsid: - remoteirc.proto.sjoin(rsid, remotechan, queued_users, ts=ts, modes=modes) + remoteirc.sjoin(rsid, remotechan, queued_users, ts=ts, modes=modes) else: # A regular JOIN only needs the user and the channel. TS, source SID, etc., can all be omitted. - remoteirc.proto.join(queued_users[0][1], remotechan) + remoteirc.join(queued_users[0][1], remotechan) joined_nets[remoteirc] = {'channel': remotechan, 'users': [u[-1] for u in queued_users]} for remoteirc, hookdata in joined_nets.items(): # HACK: Announce this JOIN as a special hook on each network, for plugins like Automode. - remoteirc.callHooks([remoteirc.sid, 'PYLINK_RELAY_JOIN', hookdata]) + remoteirc.call_hooks([remoteirc.sid, 'PYLINK_RELAY_JOIN', hookdata]) def relay_part(irc, channel, user): """ @@ -704,11 +703,11 @@ def relay_part(irc, channel, user): continue # Part the relay client with the channel delinked message. - remoteirc.proto.part(remoteuser, remotechan, 'Channel delinked.') + remoteirc.part(remoteuser, remotechan, 'Channel delinked.') # If the relay client no longer has any channels, quit them to prevent inflating /lusers. if isRelayClient(remoteirc, remoteuser) and not remoteirc.users[remoteuser].channels: - remoteirc.proto.quit(remoteuser, 'Left all shared channels.') + remoteirc.quit(remoteuser, 'Left all shared channels.') del relayusers[(irc.name, user)][remoteirc.name] @@ -786,7 +785,7 @@ def get_supported_cmodes(irc, remoteirc, channel, modes): "for network %r.", irc.name, modechar, arg, remoteirc.name) - if (not irc.proto.hasCap('can-spawn-clients')) and irc.pseudoclient and arg == irc.pseudoclient.uid: + if (not irc.has_cap('can-spawn-clients')) and irc.pseudoclient and arg == irc.pseudoclient.uid: # Skip modesync on the main PyLink client. log.debug("(%s) relay.get_supported_cmodes: filtering prefix change (%r, %r) on Clientbot relayer", irc.name, name, arg) @@ -851,7 +850,7 @@ def handle_relay_whois(irc, source, command, args): """Convenience wrapper to return WHOIS replies.""" # WHOIS replies are by convention prefixed with the target user's nick. text = '%s %s' % (targetuser.nick, text) - irc.proto.numeric(server, num, source, text) + irc.numeric(server, num, source, text) def checkSendKey(infoline): """ @@ -860,7 +859,7 @@ def handle_relay_whois(irc, source, command, args): setting = conf.conf.get('relay', {}).get(infoline, '').lower() if setting == 'all': return True - elif setting == 'opers' and irc.isOper(source, allowAuthed=False): + elif setting == 'opers' and irc.is_oper(source, allowAuthed=False): return True return False @@ -870,7 +869,7 @@ def handle_relay_whois(irc, source, command, args): homenet, uid = origuser realirc = world.networkobjects[homenet] realuser = realirc.users[uid] - netname = realirc.getFullNetworkName() + netname = realirc.get_full_network_name() wreply(320, ":is a remote user connected via PyLink Relay. Home network: %s; " "Home nick: %s" % (netname, realuser.nick)) @@ -879,9 +878,9 @@ def handle_relay_whois(irc, source, command, args): # Send account information if told to and the target is logged in. wreply(330, "%s :is logged in (on %s) as" % (realuser.services_account, netname)) - if checkSendKey('whois_show_server') and realirc.proto.hasCap('can-track-servers'): + if checkSendKey('whois_show_server') and realirc.has_cap('can-track-servers'): wreply(320, ":is actually connected via the following server:") - realserver = realirc.getServer(uid) + realserver = realirc.get_server(uid) realserver = realirc.servers[realserver] wreply(312, "%s :%s" % (realserver.name, realserver.desc)) @@ -891,7 +890,7 @@ def handle_operup(irc, numeric, command, args): """ Handles setting oper types on relay clients during oper up. """ - newtype = '%s (on %s)' % (args['text'], irc.getFullNetworkName()) + newtype = '%s (on %s)' % (args['text'], irc.get_full_network_name()) for netname, user in relayusers[(irc.name, numeric)].items(): log.debug('(%s) relay.handle_opertype: setting OPERTYPE of %s/%s to %s', irc.name, user, netname, newtype) @@ -924,11 +923,11 @@ def handle_join(irc, numeric, command, args): # XXX: Find the diff of the new and old mode lists of the channel. Not pretty, but I'd # rather not change the 'users' format of SJOIN just for this. -GL try: - oldmodes = set(chandata.getPrefixModes(user)) + oldmodes = set(chandata.get_prefix_modes(user)) except KeyError: # User was never in channel. Treat their mode list as empty. oldmodes = set() - newmodes = set(current_chandata.getPrefixModes(user)) + newmodes = set(current_chandata.get_prefix_modes(user)) modediff = newmodes - oldmodes log.debug('(%s) relay.handle_join: mode diff for %s on %s: %s oldmodes=%s newmodes=%s', irc.name, user, channel, modediff, oldmodes, newmodes) @@ -938,8 +937,8 @@ def handle_join(irc, numeric, command, args): modes.append(('-%s' % modechar, user)) if modes: - log.debug('(%s) relay.handle_join: reverting modes on BURST: %s', irc.name, irc.joinModes(modes)) - irc.proto.mode(irc.sid, channel, modes) + log.debug('(%s) relay.handle_join: reverting modes on BURST: %s', irc.name, irc.join_modes(modes)) + irc.mode(irc.sid, channel, modes) relay_joins(irc, channel, users, ts, burst=False) utils.add_hook(handle_join, 'JOIN') @@ -954,7 +953,7 @@ def handle_quit(irc, numeric, command, args): for netname, user in relayusers[(irc.name, numeric)].copy().items(): remoteirc = world.networkobjects[netname] try: # Try to quit the client. If this fails because they're missing, bail. - remoteirc.proto.quit(user, args['text']) + remoteirc.quit(user, args['text']) except LookupError: pass del relayusers[(irc.name, numeric)] @@ -1011,7 +1010,7 @@ def handle_nick(irc, numeric, command, args): remoteirc = world.networkobjects[netname] newnick = normalize_nick(remoteirc, irc.name, args['newnick'], uid=user) if remoteirc.users[user].nick != newnick: - remoteirc.proto.nick(user, newnick) + remoteirc.nick(user, newnick) utils.add_hook(handle_nick, 'NICK') def handle_part(irc, numeric, command, args): @@ -1021,14 +1020,14 @@ def handle_part(irc, numeric, command, args): if numeric == irc.pseudoclient.uid: # For clientbot: treat forced parts to the bot as clearchan, and attempt to rejoin only # if it affected a relay. - if not irc.proto.hasCap('can-spawn-clients'): + if not irc.has_cap('can-spawn-clients'): for channel in [c for c in channels if get_relay((irc.name, c))]: for user in irc.channels[channel].users: - if (not irc.isInternalClient(user)) and (not isRelayClient(irc, user)): - irc.callHooks([irc.sid, 'CLIENTBOT_SERVICE_KICKED', {'channel': channel, 'target': user, + if (not irc.is_internal_client(user)) and (not isRelayClient(irc, user)): + irc.call_hooks([irc.sid, 'CLIENTBOT_SERVICE_KICKED', {'channel': channel, 'target': user, 'text': 'Clientbot was force parted (Reason: %s)' % text or 'None', 'parse_as': 'KICK'}]) - irc.proto.join(irc.pseudoclient.uid, channel) + irc.join(irc.pseudoclient.uid, channel) return return @@ -1039,9 +1038,9 @@ def handle_part(irc, numeric, command, args): remotechan = get_remote_channel(irc, remoteirc, channel) if remotechan is None: continue - remoteirc.proto.part(user, remotechan, text) + remoteirc.part(user, remotechan, text) if not remoteirc.users[user].channels: - remoteirc.proto.quit(user, 'Left all shared channels.') + remoteirc.quit(user, 'Left all shared channels.') del relayusers[(irc.name, numeric)][remoteirc.name] utils.add_hook(handle_part, 'PART') @@ -1049,7 +1048,7 @@ def handle_messages(irc, numeric, command, args): notice = (command in ('NOTICE', 'PYLINK_SELF_NOTICE')) target = args['target'] text = args['text'] - if irc.isInternalClient(numeric) and irc.isInternalClient(target): + if irc.is_internal_client(numeric) and irc.is_internal_client(target): # Drop attempted PMs between internal clients (this shouldn't happen, # but whatever). return @@ -1093,8 +1092,8 @@ def handle_messages(irc, numeric, command, args): # Skip "from:" formatting for servers; it's messy with longer hostnames. # Also skip this formatting for servicebot relaying. - if numeric not in irc.servers and not irc.getServiceBot(numeric): - displayedname = irc.getFriendlyName(numeric) + if numeric not in irc.servers and not irc.get_service_bot(numeric): + displayedname = irc.get_friendly_name(numeric) real_text = '<%s/%s> %s' % (displayedname, irc.name, text) else: real_text = text @@ -1124,9 +1123,9 @@ def handle_messages(irc, numeric, command, args): try: if notice: - remoteirc.proto.notice(user, real_target, real_text) + remoteirc.notice(user, real_target, real_text) else: - remoteirc.proto.message(user, real_target, real_text) + remoteirc.message(user, real_target, real_text) except LookupError: # Our relay clone disappeared while we were trying to send the message. # This is normally due to a nick conflict with the IRCd. @@ -1153,7 +1152,7 @@ def handle_messages(irc, numeric, command, args): return remoteirc = world.networkobjects[homenet] - if (not remoteirc.proto.hasCap('can-spawn-clients')) and not conf.conf.get('relay', {}).get('allow_clientbot_pms'): + if (not remoteirc.has_cap('can-spawn-clients')) and not conf.conf.get('relay', {}).get('allow_clientbot_pms'): irc.msg(numeric, 'Private messages to users connected via Clientbot have ' 'been administratively disabled.', notice=True) return @@ -1162,9 +1161,9 @@ def handle_messages(irc, numeric, command, args): try: if notice: - remoteirc.proto.notice(user, real_target, text) + remoteirc.notice(user, real_target, text) else: - remoteirc.proto.message(user, real_target, text) + remoteirc.message(user, real_target, text) except LookupError: # Our relay clone disappeared while we were trying to send the message. # This is normally due to a nick conflict with the IRCd. @@ -1184,17 +1183,17 @@ def handle_kick(irc, source, command, args): relay = get_relay((irc.name, channel)) # Special case for clientbot: treat kicks to the PyLink service bot as channel clear. - if (not irc.proto.hasCap('can-spawn-clients')) and irc.pseudoclient and target == irc.pseudoclient.uid: + if (not irc.has_cap('can-spawn-clients')) and irc.pseudoclient and target == irc.pseudoclient.uid: for user in irc.channels[channel].users: - if (not irc.isInternalClient(user)) and (not isRelayClient(irc, user)): - reason = "Clientbot kicked by %s (Reason: %s)" % (irc.getFriendlyName(source), text) - irc.callHooks([irc.sid, 'CLIENTBOT_SERVICE_KICKED', {'channel': channel, 'target': user, + if (not irc.is_internal_client(user)) and (not isRelayClient(irc, user)): + reason = "Clientbot kicked by %s (Reason: %s)" % (irc.get_friendly_name(source), text) + irc.call_hooks([irc.sid, 'CLIENTBOT_SERVICE_KICKED', {'channel': channel, 'target': user, 'text': reason, 'parse_as': 'KICK'}]) return # Don't relay kicks to protected service bots. - if relay is None or irc.getServiceBot(target): + if relay is None or irc.get_service_bot(target): return origuser = get_orig_user(irc, target) @@ -1226,7 +1225,7 @@ def handle_kick(irc, source, command, args): # kick ops, admins can't kick owners, etc. modes = get_prefix_modes(remoteirc, irc, remotechan, real_target) # Join the kicked client back with its respective modes. - irc.proto.sjoin(irc.sid, channel, [(modes, target)]) + irc.sjoin(irc.sid, channel, [(modes, target)]) if kicker in irc.users: log.info('(%s) relay: Blocked KICK (reason %r) from %s/%s to relay client %s on %s.', irc.name, args['text'], irc.users[source].nick, irc.name, @@ -1245,13 +1244,13 @@ def handle_kick(irc, source, command, args): # Propogate the kick! if real_kicker: log.debug('(%s) relay.handle_kick: Kicking %s from channel %s via %s on behalf of %s/%s', irc.name, real_target, remotechan,real_kicker, kicker, irc.name) - remoteirc.proto.kick(real_kicker, remotechan, real_target, args['text']) + remoteirc.kick(real_kicker, remotechan, real_target, args['text']) else: # Kick originated from a server, or the kicker isn't in any # common channels with the target relay network. rsid = get_remote_sid(remoteirc, irc) log.debug('(%s) relay.handle_kick: Kicking %s from channel %s via %s on behalf of %s/%s', irc.name, real_target, remotechan, rsid, kicker, irc.name) - if not irc.proto.hasCap('can-spawn-clients'): + if not irc.has_cap('can-spawn-clients'): # Special case for clientbot: no kick prefixes are needed. text = args['text'] else: @@ -1264,16 +1263,16 @@ def handle_kick(irc, source, command, args): except AttributeError: text = "(@%s) %s" % (irc.name, args['text']) rsid = rsid or remoteirc.sid # Fall back to the main PyLink SID if get_remote_sid() fails - remoteirc.proto.kick(rsid, remotechan, real_target, text) + remoteirc.kick(rsid, remotechan, real_target, text) # If the target isn't on any channels, quit them. if remoteirc != irc and (not remoteirc.users[real_target].channels) and not origuser: del relayusers[(irc.name, target)][remoteirc.name] - remoteirc.proto.quit(real_target, 'Left all shared channels.') + remoteirc.quit(real_target, 'Left all shared channels.') if origuser and not irc.users[target].channels: del relayusers[origuser][irc.name] - irc.proto.quit(target, 'Left all shared channels.') + irc.quit(target, 'Left all shared channels.') utils.add_hook(handle_kick, 'KICK') @@ -1296,7 +1295,7 @@ def handle_chgclient(irc, source, command, args): newtext = normalize_host(remoteirc, text) else: # Don't overwrite the original text variable on every iteration. newtext = text - remoteirc.proto.updateClient(user, field, newtext) + remoteirc.update_client(user, field, newtext) except NotImplementedError: # IRCd doesn't support changing the field we want log.debug('(%s) relay.handle_chgclient: Ignoring changing field %r of %s on %s (for %s/%s);' ' remote IRCd doesn\'t support it', irc.name, field, @@ -1326,17 +1325,17 @@ def handle_mode(irc, numeric, command, args): # from the corresponding server. u = get_remote_user(irc, remoteirc, numeric, spawn_if_missing=False) if u: - remoteirc.proto.mode(u, remotechan, supported_modes) + remoteirc.mode(u, remotechan, supported_modes) else: rsid = get_remote_sid(remoteirc, irc) rsid = rsid or remoteirc.sid - remoteirc.proto.mode(rsid, remotechan, supported_modes) + remoteirc.mode(rsid, remotechan, supported_modes) else: # Mode change blocked by CLAIM. - reversed_modes = irc.reverseModes(target, modes, oldobj=oldchan) + reversed_modes = irc.reverse_modes(target, modes, oldobj=oldchan) log.debug('(%s) relay.handle_mode: Reversing mode changes of %r with %r (CLAIM).', irc.name, modes, reversed_modes) if reversed_modes: - irc.proto.mode(irc.sid, target, reversed_modes) + irc.mode(irc.sid, target, reversed_modes) break else: @@ -1354,7 +1353,7 @@ def handle_mode(irc, numeric, command, args): remoteuser = get_remote_user(irc, remoteirc, target, spawn_if_missing=False) if remoteuser and modes: - remoteirc.proto.mode(remoteuser, remoteuser, modes) + remoteirc.mode(remoteuser, remoteuser, modes) utils.add_hook(handle_mode, 'MODE') @@ -1374,12 +1373,12 @@ def handle_topic(irc, numeric, command, args): # This might originate from a server too. remoteuser = get_remote_user(irc, remoteirc, numeric, spawn_if_missing=False) if remoteuser: - remoteirc.proto.topic(remoteuser, remotechan, topic) + remoteirc.topic(remoteuser, remotechan, topic) else: rsid = get_remote_sid(remoteirc, irc) - remoteirc.proto.topicBurst(rsid, remotechan, topic) + remoteirc.topic_burst(rsid, remotechan, topic) elif oldtopic: # Topic change blocked by claim. - irc.proto.topicBurst(irc.sid, channel, oldtopic) + irc.topic_burst(irc.sid, channel, oldtopic) utils.add_hook(handle_topic, 'TOPIC') @@ -1407,7 +1406,7 @@ def handle_kill(irc, numeric, command, args): modes = get_prefix_modes(remoteirc, irc, remotechan, realuser[1]) log.debug('(%s) relay.handle_kill: userpair: %s, %s', irc.name, modes, realuser) client = get_remote_user(remoteirc, irc, realuser[1], times_tagged=1) - irc.proto.sjoin(get_remote_sid(irc, remoteirc), localchan, [(modes, client)]) + irc.sjoin(get_remote_sid(irc, remoteirc), localchan, [(modes, client)]) if userdata and numeric in irc.users: log.info('(%s) relay.handle_kill: Blocked KILL (reason %r) from %s to relay client %s/%s.', @@ -1436,7 +1435,7 @@ utils.add_hook(handle_kill, 'KILL') def handle_away(irc, numeric, command, args): for netname, user in relayusers[(irc.name, numeric)].items(): remoteirc = world.networkobjects[netname] - remoteirc.proto.away(user, args['text']) + remoteirc.away(user, args['text']) utils.add_hook(handle_away, 'AWAY') def handle_invite(irc, source, command, args): @@ -1457,7 +1456,7 @@ def handle_invite(irc, source, command, args): 'channel not on their network!', notice=True) else: - remoteirc.proto.invite(remotesource, remoteuser, + remoteirc.invite(remotesource, remoteuser, remotechan) utils.add_hook(handle_invite, 'INVITE') @@ -1472,7 +1471,7 @@ def handle_services_login(irc, numeric, command, args): """ for netname, user in relayusers[(irc.name, numeric)].items(): remoteirc = world.networkobjects[netname] - remoteirc.callHooks([user, 'PYLINK_RELAY_SERVICES_LOGIN', args]) + remoteirc.call_hooks([user, 'PYLINK_RELAY_SERVICES_LOGIN', args]) utils.add_hook(handle_services_login, 'CLIENT_SERVICES_LOGIN') def handle_disconnect(irc, numeric, command, args): @@ -1546,7 +1545,7 @@ def nick_collide(irc, target): newnick = normalize_nick(irc, remotenet, nick, times_tagged=1) log.debug('(%s) relay.nick_collide: Fixing nick of relay client %r (%s) to %s', irc.name, target, nick, newnick) - irc.proto.nick(target, newnick) + irc.nick(target, newnick) def handle_save(irc, numeric, command, args): target = args['target'] @@ -1581,14 +1580,14 @@ def create(irc, source, args): Opens up the given channel over PyLink Relay.""" try: - channel = irc.toLower(args[0]) + channel = irc.to_lower(args[0]) except IndexError: irc.error("Not enough arguments. Needs 1: channel.") return if not utils.isChannel(channel): irc.error('Invalid channel %r.' % channel) return - if not irc.proto.hasCap('can-host-relay'): + if not irc.has_cap('can-host-relay'): irc.error('Clientbot networks cannot be used to host a relay.') return if source not in irc.channels[channel].users: @@ -1604,7 +1603,7 @@ def create(irc, source, args): irc.error('Channel %r is already part of a relay.' % channel) return - creator = irc.getHostmask(source) + creator = irc.get_hostmask(source) # Create the relay database entry with the (network name, channel name) # pair - this is just a dict with various keys. db[(irc.name, channel)] = {'claim': [irc.name], 'links': set(), @@ -1628,11 +1627,11 @@ def destroy(irc, source, args): Removes the given channel from the PyLink Relay, delinking all networks linked to it. If the home network is given and you are logged in as admin, this can also remove relay channels from other networks.""" try: # Two args were given: first one is network name, second is channel. - channel = irc.toLower(args[1]) + channel = irc.to_lower(args[1]) network = args[0] except IndexError: try: # One argument was given; assume it's just the channel. - channel = irc.toLower(args[0]) + channel = irc.to_lower(args[0]) network = irc.name except IndexError: irc.error("Not enough arguments. Needs 1-2: channel, network (optional).") @@ -1655,7 +1654,7 @@ def destroy(irc, source, args): del db[entry] log.info('(%s) relay: Channel %s destroyed by %s.', irc.name, - channel, irc.getHostmask(source)) + channel, irc.get_hostmask(source)) irc.reply('Done.') else: irc.error("No such channel %r exists. If you're trying to delink a channel from " @@ -1709,8 +1708,8 @@ def link(irc, source, args): args = link_parser.parse_args(args) # Normalize channel case - channel = irc.toLower(args.channel) - localchan = irc.toLower(args.localchannel or args.channel) + channel = irc.to_lower(args.channel) + localchan = irc.to_lower(args.localchannel or args.channel) remotenet = args.remotenet for c in (channel, localchan): @@ -1729,7 +1728,7 @@ def link(irc, source, args): # Special case for Clientbot: join the requested channel first, then # require that the caller be opped. if localchan not in irc.pseudoclient.channels: - irc.proto.join(irc.pseudoclient.uid, localchan) + irc.join(irc.pseudoclient.uid, localchan) irc.reply('Joining %r now to check for op status; please run this command again after I join.' % localchan) return elif not irc.channels[localchan].isOpPlus(source): @@ -1780,7 +1779,7 @@ def link(irc, source, args): our_ts = irc.channels[localchan].ts their_ts = world.networkobjects[remotenet].channels[channel].ts - if (our_ts < their_ts) and irc.proto.hasCap('has-ts'): + if (our_ts < their_ts) and irc.has_cap('has-ts'): log.debug('(%s) relay: Blocking link request %s%s -> %s%s due to bad TS (%s < %s)', irc.name, irc.name, localchan, remotenet, args.channel, our_ts, their_ts) irc.error("The channel creation date (TS) on %s (%s) is lower than the target " @@ -1791,7 +1790,7 @@ def link(irc, source, args): entry['links'].add((irc.name, localchan)) log.info('(%s) relay: Channel %s linked to %s%s by %s.', irc.name, - localchan, remotenet, args.channel, irc.getHostmask(source)) + localchan, remotenet, args.channel, irc.get_hostmask(source)) initialize_channel(irc, localchan) irc.reply('Done.') link = utils.add_cmd(link, featured=True) @@ -1802,7 +1801,7 @@ def delink(irc, source, args): Delinks the given channel from PyLink Relay. \x02network\x02 must and can only be specified if you are on the host network for the channel given, and allows you to pick which network to delink. To remove a relay channel entirely, use the 'destroy' command instead.""" try: - channel = irc.toLower(args[0]) + channel = irc.to_lower(args[0]) except IndexError: irc.error("Not enough arguments. Needs 1-2: channel, remote netname (optional).") return @@ -1835,7 +1834,7 @@ def delink(irc, source, args): db[entry]['links'].remove((irc.name, channel)) irc.reply('Done.') log.info('(%s) relay: Channel %s delinked from %s%s by %s.', irc.name, - channel, entry[0], entry[1], irc.getHostmask(source)) + channel, entry[0], entry[1], irc.get_hostmask(source)) else: irc.error('No such relay %r.' % channel) delink = utils.add_cmd(delink, featured=True) @@ -1886,7 +1885,7 @@ def linked(irc, source, args): # Only show secret channels to opers or those in the channel, and tag them as # [secret]. localchan = get_remote_channel(remoteirc, irc, channel) - if irc.isOper(source) or (localchan and source in irc.channels[localchan].users): + if irc.is_oper(source) or (localchan and source in irc.channels[localchan].users): s += '\x02[secret]\x02 ' else: continue @@ -1902,7 +1901,7 @@ def linked(irc, source, args): irc.reply(s, private=True) - if irc.isOper(source): + if irc.is_oper(source): s = '' # If the caller is an oper, we can show the hostmasks of people @@ -1933,7 +1932,7 @@ def linkacl(irc, source, args): try: cmd = args[0].lower() - channel = irc.toLower(args[1]) + channel = irc.to_lower(args[1]) except IndexError: irc.error(missingargs) return @@ -1980,7 +1979,7 @@ def showuser(irc, source, args): # No errors here; showuser from the commands plugin already does this # for us. return - u = irc.nickToUid(target) + u = irc.nick_to_uid(target) if u: irc.reply("Showing relay information on user \x02%s\x02:" % irc.users[u].nick, private=True) try: @@ -2003,7 +2002,7 @@ def showuser(irc, source, args): relay = get_relay((irc.name, ch)) if relay: relaychannels.append(''.join(relay)) - if relaychannels and (irc.isOper(source) or u == source): + if relaychannels and (irc.is_oper(source) or u == source): irc.reply("\x02Relay channels\x02: %s" % ' '.join(relaychannels), private=True) @utils.add_cmd @@ -2012,7 +2011,7 @@ def showchan(irc, source, args): Shows relay data about the given channel. This supplements the 'showchan' command in the 'commands' plugin, which provides more general information.""" try: - channel = irc.toLower(args[0]) + channel = irc.to_lower(args[0]) except IndexError: return if channel not in irc.channels: @@ -2023,7 +2022,7 @@ def showchan(irc, source, args): c = irc.channels[channel] # Only show verbose info if caller is oper or is in the target channel. - verbose = source in c.users or irc.isOper(source) + verbose = source in c.users or irc.is_oper(source) secret = ('s', None) in c.modes if secret and not verbose: # Hide secret channels from normal users. @@ -2061,7 +2060,7 @@ def claim(irc, source, args): as well). """ try: - channel = irc.toLower(args[0]) + channel = irc.to_lower(args[0]) except IndexError: irc.error("Not enough arguments. Needs 1-2: channel, list of networks (optional).") return diff --git a/plugins/relay_clientbot.py b/plugins/relay_clientbot.py index ca632fa..ae96fe2 100644 --- a/plugins/relay_clientbot.py +++ b/plugins/relay_clientbot.py @@ -42,7 +42,7 @@ def cb_relay_core(irc, source, command, args): if irc.pseudoclient and relay: try: - sourcename = irc.getFriendlyName(source) + sourcename = irc.get_friendly_name(source) except KeyError: # User has left due to /quit sourcename = args['userdata'].nick @@ -84,7 +84,7 @@ def cb_relay_core(irc, source, command, args): text_template = string.Template(text_template) if text_template: - if irc.getServiceBot(source): + if irc.get_service_bot(source): # HACK: service bots are global and lack the relay state we look for. # just pretend the message comes from the current network. log.debug('(%s) relay_cb_core: Overriding network origin to local (source=%s)', irc.name, source) @@ -128,7 +128,7 @@ def cb_relay_core(irc, source, command, args): if source in irc.users: try: - identhost = irc.getHostmask(source).split('!')[-1] + identhost = irc.get_hostmask(source).split('!')[-1] except KeyError: # User got removed due to quit identhost = '%s@%s' % (args['olduser'].ident, args['olduser'].host) # This is specifically spaced so that ident@host is only shown for users that have @@ -139,7 +139,7 @@ def cb_relay_core(irc, source, command, args): # $target_nick: Convert the target for kicks, etc. from a UID to a nick if args.get("target") in irc.users: - args["target_nick"] = irc.getFriendlyName(args['target']) + args["target_nick"] = irc.get_friendly_name(args['target']) args.update({'netname': netname, 'sender': sourcename, 'sender_identhost': identhost, 'colored_sender': color_text(sourcename), 'colored_netname': color_text(netname)}) @@ -201,7 +201,7 @@ def rpm(irc, source, args): return relay = world.plugins.get('relay') - if irc.proto.hasCap('can-spawn-clients'): + if irc.has_cap('can-spawn-clients'): irc.error('This command is only supported on Clientbot networks. Try /msg %s ' % target) return elif relay is None: @@ -215,7 +215,7 @@ def rpm(irc, source, args): 'administratively disabled.') return - uid = irc.nickToUid(target) + uid = irc.nick_to_uid(target) if not uid: irc.error('Unknown user %s.' % target) return @@ -223,7 +223,7 @@ def rpm(irc, source, args): irc.error('%s is not a relay user.' % target) return else: - assert not irc.isInternalClient(source), "rpm is not allowed from PyLink bots" + assert not irc.is_internal_client(source), "rpm is not allowed from PyLink bots" # Send the message through relay by faking a hook for its handler. relay.handle_messages(irc, source, 'RELAY_CLIENTBOT_PRIVMSG', {'target': uid, 'text': text}) irc.reply('Message sent.') diff --git a/plugins/servermaps.py b/plugins/servermaps.py index 42fab69..8fbab0f 100644 --- a/plugins/servermaps.py +++ b/plugins/servermaps.py @@ -78,7 +78,7 @@ def _map(irc, source, args, show_relay=True): # This is a relay server - display the remote map of the network it represents relay_server = serverlist[leaf].remote remoteirc = world.networkobjects[relay_server] - if remoteirc.proto.hasCap('can-track-servers'): + if remoteirc.has_cap('can-track-servers'): # Only ever show relay subservers once - this prevents infinite loops. showall(remoteirc, remoteirc.sid, hops=hops, is_relay_server=True) diff --git a/plugins/servprotect.py b/plugins/servprotect.py index 12ca2a7..3400a73 100644 --- a/plugins/servprotect.py +++ b/plugins/servprotect.py @@ -18,7 +18,7 @@ def handle_kill(irc, numeric, command, args): automatically disconnects from the network. """ - if (args['userdata'] and irc.isInternalServer(args['userdata'].server)) or irc.isInternalClient(args['target']): + if (args['userdata'] and irc.is_internal_server(args['userdata'].server)) or irc.is_internal_client(args['target']): if killcache.setdefault(irc.name, 1) >= length: log.error('(%s) servprotect: Too many kills received, aborting!', irc.name) irc.disconnect() @@ -33,7 +33,7 @@ def handle_save(irc, numeric, command, args): Tracks SAVEs (nick collision) against PyLink clients. If too many are received, automatically disconnects from the network. """ - if irc.isInternalClient(args['target']): + if irc.is_internal_client(args['target']): if savecache.setdefault(irc.name, 0) >= length: log.error('(%s) servprotect: Too many nick collisions, aborting!', irc.name) irc.disconnect() diff --git a/protocols/clientbot.py b/protocols/clientbot.py index c16f3a3..897b7e6 100644 --- a/protocols/clientbot.py +++ b/protocols/clientbot.py @@ -4,15 +4,15 @@ import base64 from pylinkirc import utils, conf from pylinkirc.log import log -from pylinkirc.classes import Protocol, IrcUser, IrcServer, ProtocolError +from pylinkirc.classes import Protocol, User, Server, ProtocolError FALLBACK_REALNAME = 'PyLink Relay Mirror Client' COMMON_PREFIXMODES = [('h', 'halfop'), ('a', 'admin'), ('q', 'owner'), ('y', 'owner')] IRCV3_CAPABILITIES = {'multi-prefix', 'sasl'} -class ClientbotWrapperProtocol(Protocol): - def __init__(self, irc): - super().__init__(irc) +class ClientbotWrapperProtocol(IRCCommonProtocol): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.protocol_caps = {'clear-channels-on-leave', 'slash-in-nicks', 'slash-in-hosts', 'underscore-in-hosts'} @@ -45,24 +45,24 @@ class ClientbotWrapperProtocol(Protocol): """ Returns the real nick for the given PUID. """ - if uid in self.irc.users: - nick = self.irc.users[uid].nick - log.debug('(%s) Mangling target PUID %s to nick %s', self.irc.name, uid, nick) + if uid in self.users: + nick = self.users[uid].nick + log.debug('(%s) Mangling target PUID %s to nick %s', self.name, uid, nick) return nick return uid - def connect(self): + def post_connect(self): """Initializes a connection to a server.""" # (Re)initialize counter-based pseudo UID generators self.uidgen = utils.PUIDGenerator('PUID') self.sidgen = utils.PUIDGenerator('PSID') self.has_eob = False - ts = self.irc.start_ts - f = lambda text: self.irc.send(text, queue=False) + ts = self.start_ts + f = lambda text: self.send(text, queue=False) # Enumerate our own server - self.irc.sid = self.sidgen.next_sid() + self.sid = self.sidgen.next_sid() # Clear states from last connect self.who_received.clear() @@ -71,7 +71,7 @@ class ClientbotWrapperProtocol(Protocol): self.ircv3_caps.clear() self.ircv3_caps_available.clear() - sendpass = self.irc.serverdata.get("sendpass") + sendpass = self.serverdata.get("sendpass") if sendpass: f('PASS %s' % sendpass) @@ -79,129 +79,129 @@ class ClientbotWrapperProtocol(Protocol): # Start a timer to call CAP END if registration freezes (e.g. if AUTHENTICATE for SASL is # never replied to). - def capEnd(): + def _do_cap_end_wrapper(): log.info('(%s) Skipping SASL due to timeout; are the IRCd and services configured ' - 'properly?', self.irc.name) - self.capEnd() - self._cap_timer = threading.Timer(self.irc.serverdata.get('sasl_timeout') or 15, capEnd) + 'properly?', self.name) + self._do_cap_end() + self._cap_timer = threading.Timer(self.serverdata.get('sasl_timeout') or 15, _do_cap_end_wrapper) self._cap_timer.start() # This is a really gross hack to get the defined NICK/IDENT/HOST/GECOS. - # But this connection stuff is done before any of the spawnClient stuff in + # But this connection stuff is done before any of the spawn_client stuff in # services_support fires. - self.conf_nick = self.irc.serverdata.get('pylink_nick') or conf.conf["bot"].get("nick", "PyLink") + self.conf_nick = self.serverdata.get('pylink_nick') or conf.conf["bot"].get("nick", "PyLink") f('NICK %s' % (self.conf_nick)) - ident = self.irc.serverdata.get('pylink_ident') or conf.conf["bot"].get("ident", "pylink") + ident = self.serverdata.get('pylink_ident') or conf.conf["bot"].get("ident", "pylink") f('USER %s 8 * :%s' % (ident, # TODO: per net realnames or hostnames aren't implemented yet. conf.conf["bot"].get("realname", "PyLink Clientbot"))) # Note: clientbot clients are initialized with umode +i by default - def spawnClient(self, nick, ident='unknown', host='unknown.host', realhost=None, modes={('i', None)}, + def spawn_client(self, nick, ident='unknown', host='unknown.host', realhost=None, modes={('i', None)}, server=None, ip='0.0.0.0', realname='', ts=None, opertype=None, manipulatable=False): """ STUB: Pretends to spawn a new client with a subset of the given options. """ - server = server or self.irc.sid + server = server or self.sid uid = self.uidgen.next_uid(prefix=nick) ts = ts or int(time.time()) - log.debug('(%s) spawnClient stub called, saving nick %s as PUID %s', self.irc.name, nick, uid) - u = self.irc.users[uid] = IrcUser(nick, ts, uid, server, ident=ident, host=host, realname=realname, + log.debug('(%s) spawn_client stub called, saving nick %s as PUID %s', self.name, nick, uid) + u = self.users[uid] = User(nick, ts, uid, server, ident=ident, host=host, realname=realname, manipulatable=manipulatable, realhost=realhost, ip=ip) - self.irc.servers[server].users.add(uid) + self.servers[server].users.add(uid) - self.irc.applyModes(uid, modes) + self.apply_modes(uid, modes) return u - def spawnServer(self, name, sid=None, uplink=None, desc=None, endburst_delay=0, internal=True): + def spawn_server(self, name, sid=None, uplink=None, desc=None, endburst_delay=0, internal=True): """ STUB: Pretends to spawn a new server with a subset of the given options. """ name = name.lower() sid = self.sidgen.next_sid(prefix=name) - self.irc.servers[sid] = IrcServer(uplink, name, internal=internal) + self.servers[sid] = Server(uplink, name, internal=internal) return sid def away(self, source, text): """STUB: sets away messages for clients internally.""" - log.debug('(%s) away: target is %s, internal client? %s', self.irc.name, source, self.irc.isInternalClient(source)) + log.debug('(%s) away: target is %s, internal client? %s', self.name, source, self.is_internal_client(source)) - if self.irc.users[source].away != text: - if not self.irc.isInternalClient(source): - log.debug('(%s) away: sending AWAY hook from %s with text %r', self.irc.name, source, text) - self.irc.callHooks([source, 'AWAY', {'text': text}]) + if self.users[source].away != text: + if not self.is_internal_client(source): + log.debug('(%s) away: sending AWAY hook from %s with text %r', self.name, source, text) + self.call_hooks([source, 'AWAY', {'text': text}]) - self.irc.users[source].away = text + self.users[source].away = text def invite(self, client, target, channel): """Invites a user to a channel.""" - self.irc.send('INVITE %s %s' % (self.irc.getFriendlyName(target), channel)) + self.send('INVITE %s %s' % (self.get_friendly_name(target), channel)) def join(self, client, channel): """STUB: Joins a user to a channel.""" - channel = self.irc.toLower(channel) + channel = self.to_lower(channel) # Only joins for the main PyLink client are actually forwarded. Others are ignored. # Note: we do not automatically add our main client to the channel state, as we # rely on the /NAMES reply to sync it up properly. - if self.irc.pseudoclient and client == self.irc.pseudoclient.uid: - self.irc.send('JOIN %s' % channel) + if self.pseudoclient and client == self.pseudoclient.uid: + self.send('JOIN %s' % channel) # Send /names and /who requests right after - self.irc.send('MODE %s' % channel) - self.irc.send('NAMES %s' % channel) - self.irc.send('WHO %s' % channel) + self.send('MODE %s' % channel) + self.send('NAMES %s' % channel) + self.send('WHO %s' % channel) else: - self.irc.channels[channel].users.add(client) - self.irc.users[client].channels.add(channel) + self.channels[channel].users.add(client) + self.users[client].channels.add(channel) - log.debug('(%s) join: faking JOIN of client %s/%s to %s', self.irc.name, client, - self.irc.getFriendlyName(client), channel) - self.irc.callHooks([client, 'CLIENTBOT_JOIN', {'channel': channel}]) + log.debug('(%s) join: faking JOIN of client %s/%s to %s', self.name, client, + self.get_friendly_name(client), channel) + self.call_hooks([client, 'CLIENTBOT_JOIN', {'channel': channel}]) def kick(self, source, channel, target, reason=''): """Sends channel kicks.""" log.debug('(%s) kick: checking if target %s (nick: %s) is an internal client? %s', - self.irc.name, target, self.irc.getFriendlyName(target), - self.irc.isInternalClient(target)) - if self.irc.isInternalClient(target): + self.name, target, self.get_friendly_name(target), + self.is_internal_client(target)) + if self.is_internal_client(target): # Target was one of our virtual clients. Just remove them from the state. self.handle_part(target, 'KICK', [channel, reason]) # Send a KICK hook for message formatting. - self.irc.callHooks([source, 'CLIENTBOT_KICK', {'channel': channel, 'target': target, 'text': reason}]) + self.call_hooks([source, 'CLIENTBOT_KICK', {'channel': channel, 'target': target, 'text': reason}]) return - self.irc.send('KICK %s %s :<%s> %s' % (channel, self._expandPUID(target), - self.irc.getFriendlyName(source), reason)) + self.send('KICK %s %s :<%s> %s' % (channel, self._expandPUID(target), + self.get_friendly_name(source), reason)) # Don't update our state here: wait for the IRCd to send an acknowledgement instead. # There is essentially a 3 second wait to do this, as we send NAMES with a delay # to resync any users lost due to kicks being blocked, etc. if (channel not in self.kick_queue) or (not self.kick_queue[channel][1].is_alive()): # However, only do this if there isn't a NAMES request scheduled already. - t = threading.Timer(3, lambda: self.irc.send('NAMES %s' % channel)) - log.debug('(%s) kick: setting NAMES timer for %s on %s', self.irc.name, target, channel) + t = threading.Timer(3, lambda: self.send('NAMES %s' % channel)) + log.debug('(%s) kick: setting NAMES timer for %s on %s', self.name, target, channel) # Store the channel, target UID, and timer object in the internal kick queue. self.kick_queue[channel] = ({target}, t) t.start() else: - log.debug('(%s) kick: adding %s to kick queue for channel %s', self.irc.name, target, channel) + log.debug('(%s) kick: adding %s to kick queue for channel %s', self.name, target, channel) self.kick_queue[channel][0].add(target) def message(self, source, target, text, notice=False): """Sends messages to the target.""" command = 'NOTICE' if notice else 'PRIVMSG' - if self.irc.pseudoclient and self.irc.pseudoclient.uid == source: - self.irc.send('%s %s :%s' % (command, self._expandPUID(target), text)) + if self.pseudoclient and self.pseudoclient.uid == source: + self.send('%s %s :%s' % (command, self._expandPUID(target), text)) else: - self.irc.callHooks([source, 'CLIENTBOT_MESSAGE', {'target': target, 'is_notice': notice, 'text': text}]) + self.call_hooks([source, 'CLIENTBOT_MESSAGE', {'target': target, 'is_notice': notice, 'text': text}]) def mode(self, source, channel, modes, ts=None): """Sends channel MODE changes.""" @@ -211,35 +211,35 @@ class ClientbotWrapperProtocol(Protocol): # things that were never banned. This prevents the bot from getting caught in a loop # with IRCd MODE acknowledgements. # FIXME: More related safety checks should be added for this. - log.debug('(%s) mode: re-parsing modes %s', self.irc.name, modes) - joined_modes = self.irc.joinModes(modes) - for modepair in self.irc.parseModes(channel, joined_modes): - log.debug('(%s) mode: checking if %s a prefix mode: %s', self.irc.name, modepair, self.irc.prefixmodes) - if modepair[0][-1] in self.irc.prefixmodes: - if self.irc.isInternalClient(modepair[1]): + log.debug('(%s) mode: re-parsing modes %s', self.name, modes) + joined_modes = self.join_modes(modes) + for modepair in self.parse_modes(channel, joined_modes): + log.debug('(%s) mode: checking if %s a prefix mode: %s', self.name, modepair, self.prefixmodes) + if modepair[0][-1] in self.prefixmodes: + if self.is_internal_client(modepair[1]): # Ignore prefix modes for virtual internal clients. - log.debug('(%s) mode: skipping virtual client prefixmode change %s', self.irc.name, modepair) + log.debug('(%s) mode: skipping virtual client prefixmode change %s', self.name, modepair) continue else: # For other clients, change the mode argument to nick instead of PUID. - nick = self.irc.getFriendlyName(modepair[1]) - log.debug('(%s) mode: coersing mode %s argument to %s', self.irc.name, modepair, nick) + nick = self.get_friendly_name(modepair[1]) + log.debug('(%s) mode: coersing mode %s argument to %s', self.name, modepair, nick) modepair = (modepair[0], nick) extmodes.append(modepair) - log.debug('(%s) mode: filtered modes for %s: %s', self.irc.name, channel, extmodes) + log.debug('(%s) mode: filtered modes for %s: %s', self.name, channel, extmodes) if extmodes: - self.irc.send('MODE %s %s' % (channel, self.irc.joinModes(extmodes))) + self.send('MODE %s %s' % (channel, self.join_modes(extmodes))) # Don't update the state here: the IRCd sill respond with a MODE reply if successful. def nick(self, source, newnick): """STUB: Sends NICK changes.""" - if self.irc.pseudoclient and source == self.irc.pseudoclient.uid: - self.irc.send('NICK :%s' % newnick) + if self.pseudoclient and source == self.pseudoclient.uid: + self.send('NICK :%s' % newnick) # No state update here: the IRCd will respond with a NICK acknowledgement if the change succeeds. else: - self.irc.callHooks([source, 'CLIENTBOT_NICK', {'newnick': newnick}]) - self.irc.users[source].nick = newnick + self.call_hooks([source, 'CLIENTBOT_NICK', {'newnick': newnick}]) + self.users[source].nick = newnick def notice(self, source, target, text): """Sends notices to the target.""" @@ -250,29 +250,29 @@ class ClientbotWrapperProtocol(Protocol): """ Sends PING to the uplink. """ - if self.irc.uplink: - self.irc.send('PING %s' % self.irc.getFriendlyName(self.irc.uplink)) + if self.uplink: + self.send('PING %s' % self.get_friendly_name(self.uplink)) # Poll WHO periodically to figure out any ident/host/away status changes. - for channel in self.irc.pseudoclient.channels: - self.irc.send('WHO %s' % channel) + for channel in self.pseudoclient.channels: + self.send('WHO %s' % channel) def part(self, source, channel, reason=''): """STUB: Parts a user from a channel.""" - self.irc.channels[channel].removeuser(source) - self.irc.users[source].channels.discard(channel) + self.channels[channel].remove_user(source) + self.users[source].channels.discard(channel) # Only parts for the main PyLink client are actually forwarded. Others are ignored. - if self.irc.pseudoclient and source == self.irc.pseudoclient.uid: - self.irc.send('PART %s :%s' % (channel, reason)) + if self.pseudoclient and source == self.pseudoclient.uid: + self.send('PART %s :%s' % (channel, reason)) else: - self.irc.callHooks([source, 'CLIENTBOT_PART', {'channel': channel, 'text': reason}]) + self.call_hooks([source, 'CLIENTBOT_PART', {'channel': channel, 'text': reason}]) def quit(self, source, reason): """STUB: Quits a client.""" - userdata = self.irc.users[source] - self.removeClient(source) - self.irc.callHooks([source, 'CLIENTBOT_QUIT', {'text': reason, 'userdata': userdata}]) + userdata = self.users[source] + self._remove_client(source) + self.call_hooks([source, 'CLIENTBOT_QUIT', {'text': reason, 'userdata': userdata}]) def sjoin(self, server, channel, users, ts=None, modes=set()): """STUB: bursts joins from a server.""" @@ -280,16 +280,16 @@ class ClientbotWrapperProtocol(Protocol): # given. modes and TS are currently ignored. puids = {u[-1] for u in users} for user in puids: - if self.irc.pseudoclient and self.irc.pseudoclient.uid == user: + if self.pseudoclient and self.pseudoclient.uid == user: # If the SJOIN affects our main client, forward it as a regular JOIN. self.join(user, channel) else: # Otherwise, track the state for our virtual clients. - self.irc.users[user].channels.add(channel) + self.users[user].channels.add(channel) - self.irc.channels[channel].users |= puids - nicks = {self.irc.getFriendlyName(u) for u in puids} - self.irc.callHooks([server, 'CLIENTBOT_SJOIN', {'channel': channel, 'nicks': nicks}]) + self.channels[channel].users |= puids + nicks = {self.get_friendly_name(u) for u in puids} + self.call_hooks([server, 'CLIENTBOT_SJOIN', {'channel': channel, 'nicks': nicks}]) def squit(self, source, target, text): """STUB: SQUITs a server.""" @@ -298,65 +298,65 @@ class ClientbotWrapperProtocol(Protocol): squit_data = self._squit(source, 'CLIENTBOT_VIRTUAL_SQUIT', [target, text]) if squit_data.get('nicks'): - self.irc.callHooks([source, 'CLIENTBOT_SQUIT', squit_data]) + self.call_hooks([source, 'CLIENTBOT_SQUIT', squit_data]) def _stub(self, *args): """Stub outgoing command function (does nothing).""" return - kill = topic = topicBurst = knock = numeric = _stub + kill = topic = topic_burst = knock = numeric = _stub - def updateClient(self, target, field, text): + def update_client(self, target, field, text): """Updates the known ident, host, or realname of a client.""" - if target not in self.irc.users: - log.warning("(%s) Unknown target %s for updateClient()", self.irc.name, target) + if target not in self.users: + log.warning("(%s) Unknown target %s for update_client()", self.name, target) return - u = self.irc.users[target] + u = self.users[target] if field == 'IDENT' and u.ident != text: u.ident = text - if not self.irc.isInternalClient(target): + if not self.is_internal_client(target): # We're updating the host of an external client in our state, so send the appropriate # hook payloads. - self.irc.callHooks([self.irc.sid, 'CHGIDENT', + self.call_hooks([self.sid, 'CHGIDENT', {'target': target, 'newident': text}]) elif field == 'HOST' and u.host != text: u.host = text - if not self.irc.isInternalClient(target): - self.irc.callHooks([self.irc.sid, 'CHGHOST', + if not self.is_internal_client(target): + self.call_hooks([self.sid, 'CHGHOST', {'target': target, 'newhost': text}]) elif field in ('REALNAME', 'GECOS') and u.realname != text: u.realname = text - if not self.irc.isInternalClient(target): - self.irc.callHooks([self.irc.sid, 'CHGNAME', + if not self.is_internal_client(target): + self.call_hooks([self.sid, 'CHGNAME', {'target': target, 'newgecos': text}]) else: return # Nothing changed - def _getUid(self, nick, ident=None, host=None): + def _get_UID(self, nick, ident=None, host=None): """ Fetches the UID for the given nick, creating one if it does not already exist. Limited (internal) nick collision checking is done here to prevent Clientbot users from being confused with virtual clients, and vice versa.""" self._validateNick(nick) - idsource = self.irc.nickToUid(nick) - is_internal = self.irc.isInternalClient(idsource) + idsource = self.nick_to_uid(nick) + is_internal = self.is_internal_client(idsource) # If this sender isn't known or it is one of our virtual clients, spawn a new one. # This also takes care of any nick collisions caused by new, Clientbot users # taking the same nick as one of our virtual clients, and will force the virtual client to lose. - if (not idsource) or (is_internal and self.irc.pseudoclient and idsource != self.irc.pseudoclient.uid): + if (not idsource) or (is_internal and self.pseudoclient and idsource != self.pseudoclient.uid): if idsource: - log.debug('(%s) Nick-colliding virtual client %s/%s', self.irc.name, idsource, nick) - self.irc.callHooks([self.irc.sid, 'CLIENTBOT_NICKCOLLIDE', {'target': idsource, 'parse_as': 'SAVE'}]) + log.debug('(%s) Nick-colliding virtual client %s/%s', self.name, idsource, nick) + self.call_hooks([self.sid, 'CLIENTBOT_NICKCOLLIDE', {'target': idsource, 'parse_as': 'SAVE'}]) - idsource = self.spawnClient(nick, ident or 'unknown', host or 'unknown', - server=self.irc.uplink, realname=FALLBACK_REALNAME).uid + idsource = self.spawn_client(nick, ident or 'unknown', host or 'unknown', + server=self.uplink, realname=FALLBACK_REALNAME).uid return idsource - def parseMessageTags(self, data): + def parse_message_tags(self, data): """ Parses a message with IRC v3.2 message tags, as described at http://ircv3.net/specs/core/message-tags-3.2.html """ @@ -372,8 +372,8 @@ class ClientbotWrapperProtocol(Protocol): tag = tag.replace(r'\:', ';') tagdata[idx] = tag - results = self.parseCapabilities(tagdata, fallback=None) - log.debug('(%s) parsed message tags %s', self.irc.name, results) + results = self.parse_isupport(tagdata, fallback=None) + log.debug('(%s) parsed message tags %s', self.name, results) return results return {} @@ -381,21 +381,21 @@ class ClientbotWrapperProtocol(Protocol): """Event handler for the RFC1459/2812 (clientbot) protocol.""" data = data.split(" ") - tags = self.parseMessageTags(data) + tags = self.parse_message_tags(data) if tags: # If we have tags, split off the first argument. data = data[1:] try: - args = self.parsePrefixedArgs(data) + args = self.parse_prefixed_args(data) sender = args[0] command = args[1] args = args[2:] except IndexError: # Raw command without an explicit sender; assume it's being sent by our uplink. - args = self.parseArgs(data) - idsource = sender = self.irc.uplink + args = self.parse_args(data) + idsource = sender = self.uplink command = args[0] args = args[1:] else: @@ -404,9 +404,9 @@ class ClientbotWrapperProtocol(Protocol): # pseudo-uids and pseudo-sids as we see prefixes. if ('!' not in sender) and '.' in sender: # Sender is a server name. XXX: make this check more foolproof - idsource = self._getSid(sender) - if idsource not in self.irc.servers: - idsource = self.spawnServer(sender, internal=False) + idsource = self._get_SID(sender) + if idsource not in self.servers: + idsource = self.spawn_server(sender, internal=False) else: # Sender is a either a nick or a nick!user@host prefix. Split it into its relevant parts. try: @@ -414,7 +414,7 @@ class ClientbotWrapperProtocol(Protocol): except ValueError: ident = host = None # Set ident and host as null for now. nick = sender # Treat the sender prefix we received as a nick. - idsource = self._getUid(nick, ident, host) + idsource = self._get_UID(nick, ident, host) try: func = getattr(self, 'handle_'+command.lower()) @@ -426,57 +426,57 @@ class ClientbotWrapperProtocol(Protocol): parsed_args['tags'] = tags # Add message tags to this dict. return [idsource, command, parsed_args] - def capEnd(self): + def _do_cap_end(self): """ Abort SASL login by sending CAP END. """ - self.irc.send('CAP END') - log.debug("(%s) Stopping CAP END timer.", self.irc.name) + self.send('CAP END') + log.debug("(%s) Stopping CAP END timer.", self.name) self._cap_timer.cancel() - def saslAuth(self): + def _try_sasl_auth(self): """ Starts an authentication attempt via SASL. This returns True if SASL is enabled and correctly configured, and False otherwise. """ if 'sasl' not in self.ircv3_caps: - log.info("(%s) Skipping SASL auth since the IRCd doesn't support it.", self.irc.name) + log.info("(%s) Skipping SASL auth since the IRCd doesn't support it.", self.name) return - sasl_mech = self.irc.serverdata.get('sasl_mechanism') + sasl_mech = self.serverdata.get('sasl_mechanism') if sasl_mech: sasl_mech = sasl_mech.upper() - sasl_user = self.irc.serverdata.get('sasl_username') - sasl_pass = self.irc.serverdata.get('sasl_password') - ssl_cert = self.irc.serverdata.get('ssl_certfile') - ssl_key = self.irc.serverdata.get('ssl_keyfile') - ssl = self.irc.serverdata.get('ssl') + sasl_user = self.serverdata.get('sasl_username') + sasl_pass = self.serverdata.get('sasl_password') + ssl_cert = self.serverdata.get('ssl_certfile') + ssl_key = self.serverdata.get('ssl_keyfile') + ssl = self.serverdata.get('ssl') if sasl_mech == 'PLAIN': if not (sasl_user and sasl_pass): log.warning("(%s) Not attempting PLAIN authentication; sasl_username and/or " - "sasl_password aren't correctly set.", self.irc.name) + "sasl_password aren't correctly set.", self.name) return False elif sasl_mech == 'EXTERNAL': if not ssl: log.warning("(%s) Not attempting EXTERNAL authentication; SASL external requires " - "SSL, but it isn't enabled.", self.irc.name) + "SSL, but it isn't enabled.", self.name) return False elif not (ssl_cert and ssl_key): log.warning("(%s) Not attempting EXTERNAL authentication; ssl_certfile and/or " - "ssl_keyfile aren't correctly set.", self.irc.name) + "ssl_keyfile aren't correctly set.", self.name) return False else: - log.warning('(%s) Unsupported SASL mechanism %s; aborting SASL.', self.irc.name, sasl_mech) + log.warning('(%s) Unsupported SASL mechanism %s; aborting SASL.', self.name, sasl_mech) return False - self.irc.send('AUTHENTICATE %s' % sasl_mech, queue=False) + self.send('AUTHENTICATE %s' % sasl_mech, queue=False) return True return False - def sendAuthChunk(self, data): + def _send_auth_chunk(self, data): """Send Base64 encoded SASL authentication chunks.""" enc_data = base64.b64encode(data).decode() - self.irc.send('AUTHENTICATE %s' % enc_data, queue=False) + self.send('AUTHENTICATE %s' % enc_data, queue=False) def handle_authenticate(self, source, command, args): """ @@ -488,34 +488,34 @@ class ClientbotWrapperProtocol(Protocol): if not args: return if args[0] == '+': - sasl_mech = self.irc.serverdata['sasl_mechanism'].upper() + sasl_mech = self.serverdata['sasl_mechanism'].upper() if sasl_mech == 'PLAIN': - sasl_user = self.irc.serverdata['sasl_username'] - sasl_pass = self.irc.serverdata['sasl_password'] + sasl_user = self.serverdata['sasl_username'] + sasl_pass = self.serverdata['sasl_password'] authstring = '%s\0%s\0%s' % (sasl_user, sasl_user, sasl_pass) - self.sendAuthChunk(authstring.encode('utf-8')) + self._send_auth_chunk(authstring.encode('utf-8')) elif sasl_mech == 'EXTERNAL': - self.irc.send('AUTHENTICATE +') + self.send('AUTHENTICATE +') def handle_904(self, source, command, args): """ Handles SASL authentication status reports. """ logfunc = log.info if command == '903' else log.warning - logfunc('(%s) %s', self.irc.name, args[-1]) + logfunc('(%s) %s', self.name, args[-1]) if not self.has_eob: - self.capEnd() + self._do_cap_end() handle_903 = handle_902 = handle_905 = handle_906 = handle_907 = handle_904 - def requestNewCaps(self): + def _request_ircv3_caps(self): # Filter the capabilities we want by the ones actually supported by the server. available_caps = {cap for cap in IRCV3_CAPABILITIES if cap in self.ircv3_caps_available} # And by the ones we don't already have. caps_wanted = available_caps - self.ircv3_caps - log.debug('(%s) Requesting IRCv3 capabilities %s (available: %s)', self.irc.name, caps_wanted, available_caps) + log.debug('(%s) Requesting IRCv3 capabilities %s (available: %s)', self.name, caps_wanted, available_caps) if caps_wanted: - self.irc.send('CAP REQ :%s' % ' '.join(caps_wanted), queue=False) + self.send('CAP REQ :%s' % ' '.join(caps_wanted), queue=False) def handle_cap(self, source, command, args): """ @@ -527,45 +527,45 @@ class ClientbotWrapperProtocol(Protocol): # Server: CAP * LS * :multi-prefix extended-join account-notify batch invite-notify tls # Server: CAP * LS * :cap-notify server-time example.org/dummy-cap=dummyvalue example.org/second-dummy-cap # Server: CAP * LS :userhost-in-names sasl=EXTERNAL,DH-AES,DH-BLOWFISH,ECDSA-NIST256P-CHALLENGE,PLAIN - log.debug('(%s) Got new capabilities %s', self.irc.name, args[-1]) - self.ircv3_caps_available.update(self.parseCapabilities(args[-1], None)) + log.debug('(%s) Got new capabilities %s', self.name, args[-1]) + self.ircv3_caps_available.update(self.parse_isupport(args[-1], None)) if args[2] != '*': - self.requestNewCaps() + self._request_ircv3_caps() elif subcmd == 'ACK': # Server: CAP * ACK :multi-prefix sasl newcaps = set(args[-1].split()) - log.debug('(%s) Received ACK for IRCv3 capabilities %s', self.irc.name, newcaps) + log.debug('(%s) Received ACK for IRCv3 capabilities %s', self.name, newcaps) self.ircv3_caps |= newcaps # Only send CAP END immediately if SASL is disabled. Otherwise, wait for the 90x responses # to do so. - if not self.saslAuth(): + if not self._try_sasl_auth(): if not self.has_eob: - self.capEnd() + self._do_cap_end() elif subcmd == 'NAK': log.warning('(%s) Got NAK for IRCv3 capabilities %s, even though they were supposedly available', - self.irc.name, args[-1]) + self.name, args[-1]) if not self.has_eob: - self.capEnd() + self._do_cap_end() elif subcmd == 'NEW': # :irc.example.com CAP modernclient NEW :batch # :irc.example.com CAP tester NEW :away-notify extended-join # Note: CAP NEW allows capabilities with values (e.g. sasl=mech1,mech2), while CAP DEL # does not. - log.debug('(%s) Got new capabilities %s', self.irc.name, args[-1]) - newcaps = self.parseCapabilities(args[-1], None) + log.debug('(%s) Got new capabilities %s', self.name, args[-1]) + newcaps = self.parse_isupport(args[-1], None) self.ircv3_caps_available.update(newcaps) - self.requestNewCaps() + self._request_ircv3_caps() # Attempt SASL auth routines when sasl is added/removed, if doing so is enabled. - if 'sasl' in newcaps and self.irc.serverdata.get('sasl_reauth'): - log.debug('(%s) Attempting SASL reauth due to CAP NEW', self.irc.name) - self.saslAuth() + if 'sasl' in newcaps and self.serverdata.get('sasl_reauth'): + log.debug('(%s) Attempting SASL reauth due to CAP NEW', self.name) + self._try_sasl_auth() elif subcmd == 'DEL': # :irc.example.com CAP modernclient DEL :userhost-in-names multi-prefix away-notify - log.debug('(%s) Removing capabilities %s', self.irc.name, args[-1]) + log.debug('(%s) Removing capabilities %s', self.name, args[-1]) for cap in args[-1].split(): # Remove the capabilities from the list available, and return None (ignore) if any fail self.ircv3_caps_available.pop(cap, None) @@ -576,41 +576,41 @@ class ClientbotWrapperProtocol(Protocol): Handles 001 / RPL_WELCOME. """ # enumerate our uplink - self.irc.uplink = source + self.uplink = source def handle_005(self, source, command, args): """ Handles 005 / RPL_ISUPPORT. """ - self.caps.update(self.parseCapabilities(args[1:-1])) - log.debug('(%s) handle_005: self.caps is %s', self.irc.name, self.caps) + self.caps.update(self.parse_isupport(args[1:-1])) + log.debug('(%s) handle_005: self.caps is %s', self.name, self.caps) if 'CHANMODES' in self.caps: - self.irc.cmodes['*A'], self.irc.cmodes['*B'], self.irc.cmodes['*C'], self.irc.cmodes['*D'] = \ + self.cmodes['*A'], self.cmodes['*B'], self.cmodes['*C'], self.cmodes['*D'] = \ self.caps['CHANMODES'].split(',') - log.debug('(%s) handle_005: cmodes: %s', self.irc.name, self.irc.cmodes) + log.debug('(%s) handle_005: cmodes: %s', self.name, self.cmodes) if 'USERMODES' in self.caps: - self.irc.umodes['*A'], self.irc.umodes['*B'], self.irc.umodes['*C'], self.irc.umodes['*D'] = \ + self.umodes['*A'], self.umodes['*B'], self.umodes['*C'], self.umodes['*D'] = \ self.caps['USERMODES'].split(',') - log.debug('(%s) handle_005: umodes: %s', self.irc.name, self.irc.umodes) + log.debug('(%s) handle_005: umodes: %s', self.name, self.umodes) self.casemapping = self.caps.get('CASEMAPPING', self.casemapping) - log.debug('(%s) handle_005: casemapping set to %s', self.irc.name, self.casemapping) + log.debug('(%s) handle_005: casemapping set to %s', self.name, self.casemapping) if 'PREFIX' in self.caps: - self.irc.prefixmodes = prefixmodes = self.parsePrefixes(self.caps['PREFIX']) - log.debug('(%s) handle_005: prefix modes set to %s', self.irc.name, self.irc.prefixmodes) + self.prefixmodes = prefixmodes = self.parse_isupport_prefixes(self.caps['PREFIX']) + log.debug('(%s) handle_005: prefix modes set to %s', self.name, self.prefixmodes) # Autodetect common prefix mode names. for char, modename in COMMON_PREFIXMODES: # Don't overwrite existing named mode definitions. - if char in self.irc.prefixmodes and modename not in self.irc.cmodes: - self.irc.cmodes[modename] = char - log.debug('(%s) handle_005: autodetecting mode %s (%s) as %s', self.irc.name, - char, self.irc.prefixmodes[char], modename) + if char in self.prefixmodes and modename not in self.cmodes: + self.cmodes[modename] = char + log.debug('(%s) handle_005: autodetecting mode %s (%s) as %s', self.name, + char, self.prefixmodes[char], modename) - self.irc.connected.set() + self.connected.set() def handle_376(self, source, command, args): """ @@ -618,8 +618,8 @@ class ClientbotWrapperProtocol(Protocol): """ # Run autoperform commands. - for line in self.irc.serverdata.get("autoperform", []): - self.irc.send(line) + for line in self.serverdata.get("autoperform", []): + self.send(line) # Virtual endburst hook. if not self.has_eob: @@ -634,14 +634,14 @@ class ClientbotWrapperProtocol(Protocol): # <- :charybdis.midnight.vpn 353 ice = #test :ice @GL # Mark "@"-type channels as secret automatically, per RFC2812. - channel = self.irc.toLower(args[2]) + channel = self.to_lower(args[2]) if args[1] == '@': - self.irc.applyModes(channel, [('+s', None)]) + self.apply_modes(channel, [('+s', None)]) names = set() modes = set() - prefix_to_mode = {v:k for k, v in self.irc.prefixmodes.items()} - prefixes = ''.join(self.irc.prefixmodes.values()) + prefix_to_mode = {v:k for k, v in self.prefixmodes.items()} + prefixes = ''.join(self.prefixmodes.values()) for name in args[-1].split(): nick = name.lstrip(prefixes) @@ -649,45 +649,45 @@ class ClientbotWrapperProtocol(Protocol): # Get the PUID for the given nick. If one doesn't exist, spawn # a new virtual user. TODO: wait for WHO responses for each nick before # spawning in order to get a real ident/host. - idsource = self._getUid(nick) + idsource = self._get_UID(nick) # Queue these virtual users to be joined if they're not already in the channel, # or we're waiting for a kick acknowledgment for them. - if (idsource not in self.irc.channels[channel].users) or (idsource in \ + if (idsource not in self.channels[channel].users) or (idsource in \ self.kick_queue.get(channel, ([],))[0]): names.add(idsource) - self.irc.users[idsource].channels.add(channel) + self.users[idsource].channels.add(channel) # Process prefix modes for char in name: - if char in self.irc.prefixmodes.values(): + if char in self.prefixmodes.values(): modes.add(('+' + prefix_to_mode[char], idsource)) else: break # Statekeeping: make sure the channel's user list is updated! - self.irc.channels[channel].users |= names - self.irc.applyModes(channel, modes) + self.channels[channel].users |= names + self.apply_modes(channel, modes) - log.debug('(%s) handle_353: adding users %s to %s', self.irc.name, names, channel) - log.debug('(%s) handle_353: adding modes %s to %s', self.irc.name, modes, channel) + log.debug('(%s) handle_353: adding users %s to %s', self.name, names, channel) + log.debug('(%s) handle_353: adding modes %s to %s', self.name, modes, channel) # Unless /WHO has already been received for the given channel, we generally send the hook # for JOIN after /who data is received, to enumerate the ident, host, and real names of # users. - if names and hasattr(self.irc.channels[channel], 'who_received'): + if names and hasattr(self.channels[channel], 'who_received'): # /WHO *HAS* already been received. Send JOIN hooks here because we use this to keep # track of any failed KICK attempts sent by the relay bot. log.debug('(%s) handle_353: sending JOIN hook because /WHO was already received for %s', - self.irc.name, channel) - return {'channel': channel, 'users': names, 'modes': self.irc.channels[channel].modes, + self.name, channel) + return {'channel': channel, 'users': names, 'modes': self.channels[channel].modes, 'parse_as': "JOIN"} def _validateNick(self, nick): """ Checks to make sure a nick doesn't clash with a PUID. """ - if nick in self.irc.users or nick in self.irc.servers: + if nick in self.users or nick in self.servers: raise ProtocolError("Got bad nick %s from IRC which clashes with a PUID. Is someone trying to spoof users?" % nick) def handle_352(self, source, command, args): @@ -705,42 +705,42 @@ class ClientbotWrapperProtocol(Protocol): realname = args[-1].split(' ', 1)[-1] self._validateNick(nick) - uid = self.irc.nickToUid(nick) + uid = self.nick_to_uid(nick) if uid is None: - log.debug("(%s) Ignoring extraneous /WHO info for %s", self.irc.name, nick) + log.debug("(%s) Ignoring extraneous /WHO info for %s", self.name, nick) return - self.updateClient(uid, 'IDENT', ident) - self.updateClient(uid, 'HOST', host) - self.updateClient(uid, 'GECOS', realname) + self.update_client(uid, 'IDENT', ident) + self.update_client(uid, 'HOST', host) + self.update_client(uid, 'GECOS', realname) # The status given uses the following letters: [*][@|+] # H means here (not marked /away) # G means away is set (we'll have to fake a message because it's not given) # * means IRCop. # The rest are prefix modes. Multiple can be given by the IRCd if multiple are set - log.debug('(%s) handle_352: status string on user %s: %s', self.irc.name, nick, status) + log.debug('(%s) handle_352: status string on user %s: %s', self.name, nick, status) if status[0] == 'G': - log.debug('(%s) handle_352: calling away() with argument', self.irc.name) + log.debug('(%s) handle_352: calling away() with argument', self.name) self.away(uid, 'Away') elif status[0] == 'H': - log.debug('(%s) handle_352: calling away() without argument', self.irc.name) + log.debug('(%s) handle_352: calling away() without argument', self.name) self.away(uid, '') # Unmark away status else: - log.warning('(%s) handle_352: got wrong string %s for away status', self.irc.name, status[0]) + log.warning('(%s) handle_352: got wrong string %s for away status', self.name, status[0]) - if self.irc.serverdata.get('track_oper_statuses'): + if self.serverdata.get('track_oper_statuses'): if '*' in status: # Track IRCop status - if not self.irc.isOper(uid, allowAuthed=False): + if not self.is_oper(uid, allowAuthed=False): # Don't send duplicate oper ups if the target is already oper. - self.irc.applyModes(uid, [('+o', None)]) - self.irc.callHooks([uid, 'MODE', {'target': uid, 'modes': {('+o', None)}}]) - self.irc.callHooks([uid, 'CLIENT_OPERED', {'text': 'IRC Operator'}]) - elif self.irc.isOper(uid, allowAuthed=False) and not self.irc.isInternalClient(uid): + self.apply_modes(uid, [('+o', None)]) + self.call_hooks([uid, 'MODE', {'target': uid, 'modes': {('+o', None)}}]) + self.call_hooks([uid, 'CLIENT_OPERED', {'text': 'IRC Operator'}]) + elif self.is_oper(uid, allowAuthed=False) and not self.is_internal_client(uid): # Track deopers - self.irc.applyModes(uid, [('-o', None)]) - self.irc.callHooks([uid, 'MODE', {'target': uid, 'modes': {('-o', None)}}]) + self.apply_modes(uid, [('-o', None)]) + self.call_hooks([uid, 'MODE', {'target': uid, 'modes': {('-o', None)}}]) self.who_received.add(uid) @@ -753,22 +753,22 @@ class ClientbotWrapperProtocol(Protocol): users = self.who_received.copy() self.who_received.clear() - channel = self.irc.toLower(args[1]) - c = self.irc.channels[channel] + channel = self.to_lower(args[1]) + c = self.channels[channel] c.who_received = True modes = set(c.modes) for user in users: # Fill in prefix modes of everyone when doing mock SJOIN. try: - for mode in c.getPrefixModes(user): - modechar = self.irc.cmodes.get(mode) - log.debug('(%s) handle_315: adding mode %s +%s %s', self.irc.name, mode, modechar, user) + for mode in c.get_prefix_modes(user): + modechar = self.cmodes.get(mode) + log.debug('(%s) handle_315: adding mode %s +%s %s', self.name, mode, modechar, user) if modechar: modes.add((modechar, user)) except KeyError as e: log.debug("(%s) Ignoring KeyError (%s) from WHO response; it's probably someone we " - "don't share any channels with", self.irc.name, e) + "don't share any channels with", self.name, e) return {'channel': channel, 'users': users, 'modes': modes, 'parse_as': "JOIN"} @@ -779,8 +779,8 @@ class ClientbotWrapperProtocol(Protocol): # irc.pseudoclient doesn't exist as an attribute until we get run the ENDBURST stuff # in service_support (this is mapped to 005 here). self.conf_nick += '_' - self.irc.serverdata['pylink_nick'] = self.conf_nick - self.irc.send('NICK %s' % self.conf_nick) + self.serverdata['pylink_nick'] = self.conf_nick + self.send('NICK %s' % self.conf_nick) handle_432 = handle_437 = handle_433 def handle_join(self, source, command, args): @@ -788,18 +788,18 @@ class ClientbotWrapperProtocol(Protocol): Handles incoming JOINs. """ # <- :GL|!~GL@127.0.0.1 JOIN #whatever - channel = self.irc.toLower(args[0]) + channel = self.to_lower(args[0]) self.join(source, channel) - return {'channel': channel, 'users': [source], 'modes': self.irc.channels[channel].modes} + return {'channel': channel, 'users': [source], 'modes': self.channels[channel].modes} def handle_kick(self, source, command, args): """ Handles incoming KICKs. """ # <- :GL!~gl@127.0.0.1 KICK #whatever GL| :xd - channel = self.irc.toLower(args[0]) - target = self.irc.nickToUid(args[1]) + channel = self.to_lower(args[0]) + target = self.nick_to_uid(args[1]) try: reason = args[2] @@ -808,29 +808,29 @@ class ClientbotWrapperProtocol(Protocol): if channel in self.kick_queue: # Remove this client from the kick queue if present there. - log.debug('(%s) kick: removing %s from kick queue for channel %s', self.irc.name, target, channel) + log.debug('(%s) kick: removing %s from kick queue for channel %s', self.name, target, channel) self.kick_queue[channel][0].discard(target) if not self.kick_queue[channel][0]: - log.debug('(%s) kick: cancelling kick timer for channel %s (all kicks accounted for)', self.irc.name, channel) + log.debug('(%s) kick: cancelling kick timer for channel %s (all kicks accounted for)', self.name, channel) # There aren't any kicks that failed to be acknowledged. We can remove the timer now self.kick_queue[channel][1].cancel() del self.kick_queue[channel] # Statekeeping: remove the target from the channel they were previously in. - self.irc.channels[channel].removeuser(target) + self.channels[channel].remove_user(target) try: - self.irc.users[target].channels.remove(channel) + self.users[target].channels.remove(channel) except KeyError: pass - if (not self.irc.isInternalClient(source)) and not self.irc.isInternalServer(source): + if (not self.is_internal_client(source)) and not self.is_internal_server(source): # Don't repeat hooks if we're the kicker. - self.irc.callHooks([source, 'KICK', {'channel': channel, 'target': target, 'text': reason}]) + self.call_hooks([source, 'KICK', {'channel': channel, 'target': target, 'text': reason}]) # Delete channels that we were kicked from, for better state keeping. - if self.irc.pseudoclient and target == self.irc.pseudoclient.uid: - del self.irc.channels[channel] + if self.pseudoclient and target == self.pseudoclient.uid: + del self.channels[channel] def handle_mode(self, source, command, args): """Handles MODE changes.""" @@ -838,23 +838,23 @@ class ClientbotWrapperProtocol(Protocol): # <- :ice MODE ice :+Zi target = args[0] if utils.isChannel(target): - target = self.irc.toLower(target) - oldobj = self.irc.channels[target].deepcopy() + target = self.to_lower(target) + oldobj = self.channels[target].deepcopy() else: - target = self.irc.nickToUid(target) + target = self.nick_to_uid(target) oldobj = None modes = args[1:] - changedmodes = self.irc.parseModes(target, modes) - self.irc.applyModes(target, changedmodes) + changedmodes = self.parse_modes(target, modes) + self.apply_modes(target, changedmodes) - if self.irc.isInternalClient(target): - log.debug('(%s) Suppressing MODE change hook for internal client %s', self.irc.name, target) + if self.is_internal_client(target): + log.debug('(%s) Suppressing MODE change hook for internal client %s', self.name, target) return if changedmodes: # Prevent infinite loops: don't send MODE hooks if the sender is US. # Note: this is not the only check in Clientbot to prevent mode loops: if our nick # somehow gets desynced, this may not catch everything it's supposed to. - if (self.irc.pseudoclient and source != self.irc.pseudoclient.uid) or not self.irc.pseudoclient: + if (self.pseudoclient and source != self.pseudoclient.uid) or not self.pseudoclient: return {'target': target, 'modes': changedmodes, 'channeldata': oldobj} def handle_324(self, source, command, args): @@ -862,36 +862,36 @@ class ClientbotWrapperProtocol(Protocol): # -> MODE #test # <- :midnight.vpn 324 GL #test +nt # <- :midnight.vpn 329 GL #test 1491773459 - channel = self.irc.toLower(args[1]) + channel = self.to_lower(args[1]) modes = args[2:] - log.debug('(%s) Got RPL_CHANNELMODEIS (324) modes %s for %s', self.irc.name, modes, channel) - changedmodes = self.irc.parseModes(channel, modes) - self.irc.applyModes(channel, changedmodes) + log.debug('(%s) Got RPL_CHANNELMODEIS (324) modes %s for %s', self.name, modes, channel) + changedmodes = self.parse_modes(channel, modes) + self.apply_modes(channel, changedmodes) def handle_329(self, source, command, args): """Handles TS announcements via RPL_CREATIONTIME.""" - channel = self.irc.toLower(args[1]) + channel = self.to_lower(args[1]) ts = int(args[2]) - self.irc.channels[channel].ts = ts + self.channels[channel].ts = ts def handle_nick(self, source, command, args): """Handles NICK changes.""" # <- :GL|!~GL@127.0.0.1 NICK :GL_ - if not self.irc.pseudoclient: + if not self.pseudoclient: # We haven't properly logged on yet, so any initial NICK should be treated as a forced # nick change for US. For example, this clause is used to handle forced nick changes # sent by ZNC, when the login nick and the actual IRC nick of the bouncer differ. # HACK: change the nick config entry so services_support knows what our main # pseudoclient is called. - oldnick = self.irc.serverdata['pylink_nick'] - self.irc.serverdata['pylink_nick'] = self.conf_nick = args[0] - log.debug('(%s) Pre-auth FNC: Forcing configured nick to %s from %s', self.irc.name, args[0], oldnick) + oldnick = self.serverdata['pylink_nick'] + self.serverdata['pylink_nick'] = self.conf_nick = args[0] + log.debug('(%s) Pre-auth FNC: Forcing configured nick to %s from %s', self.name, args[0], oldnick) return - oldnick = self.irc.users[source].nick - self.irc.users[source].nick = args[0] + oldnick = self.users[source].nick + self.users[source].nick = args[0] return {'newnick': args[0], 'oldnick': oldnick} @@ -900,35 +900,35 @@ class ClientbotWrapperProtocol(Protocol): Handles incoming PARTs. """ # <- :GL|!~GL@127.0.0.1 PART #whatever - channels = list(map(self.irc.toLower, args[0].split(','))) + channels = list(map(self.to_lower, args[0].split(','))) try: reason = args[1] except IndexError: reason = '' for channel in channels: - self.irc.channels[channel].removeuser(source) - self.irc.users[source].channels -= set(channels) + self.channels[channel].remove_user(source) + self.users[source].channels -= set(channels) - self.irc.callHooks([source, 'PART', {'channels': channels, 'text': reason}]) + self.call_hooks([source, 'PART', {'channels': channels, 'text': reason}]) # Clear channels that are empty, or that we're parting. for channel in channels: - if (self.irc.pseudoclient and source == self.irc.pseudoclient.uid) or not self.irc.channels[channel].users: - del self.irc.channels[channel] + if (self.pseudoclient and source == self.pseudoclient.uid) or not self.channels[channel].users: + del self.channels[channel] def handle_ping(self, source, command, args): """ Handles incoming PING requests. """ - self.irc.send('PONG :%s' % args[0], queue=False) + self.send('PONG :%s' % args[0], queue=False) def handle_pong(self, source, command, args): """ Handles incoming PONG. """ - if source == self.irc.uplink: - self.irc.lastping = time.time() + if source == self.uplink: + self.lastping = time.time() def handle_privmsg(self, source, command, args): """Handles incoming PRIVMSG/NOTICE.""" @@ -936,22 +936,22 @@ class ClientbotWrapperProtocol(Protocol): # <- :sender NOTICE somenick :afasfsa target = args[0] - if self.irc.isInternalClient(source) or self.irc.isInternalServer(source): - log.warning('(%s) Received %s to %s being routed the wrong way!', self.irc.name, command, target) + if self.is_internal_client(source) or self.is_internal_server(source): + log.warning('(%s) Received %s to %s being routed the wrong way!', self.name, command, target) return # We use lowercase channels internally. if utils.isChannel(target): - target = self.irc.toLower(target) + target = self.to_lower(target) else: - target = self.irc.nickToUid(target) + target = self.nick_to_uid(target) if target: return {'target': target, 'text': args[1]} handle_notice = handle_privmsg def handle_quit(self, source, command, args): """Handles incoming QUITs.""" - if self.irc.pseudoclient and source == self.irc.pseudoclient.uid: + if self.pseudoclient and source == self.pseudoclient.uid: # Someone faked a quit from us? We should abort. raise ProtocolError("Received QUIT from uplink (%s)" % args[0]) diff --git a/protocols/hybrid.py b/protocols/hybrid.py index e22e85e..d2c5fdb 100644 --- a/protocols/hybrid.py +++ b/protocols/hybrid.py @@ -5,10 +5,10 @@ from pylinkirc.log import log from pylinkirc.classes import * from pylinkirc.protocols.ts6 import * +# This protocol module inherits from the TS6 protocol. class HybridProtocol(TS6Protocol): - def __init__(self, irc): - # This protocol module inherits from the TS6 protocol. - super().__init__(irc) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.casemapping = 'ascii' self.caps = {} @@ -16,11 +16,11 @@ class HybridProtocol(TS6Protocol): self.has_eob = False self.protocol_caps -= {'slash-in-hosts'} - def connect(self): + def post_connect(self): """Initializes a connection to a server.""" - ts = self.irc.start_ts + ts = self.start_ts self.has_eob = False - f = self.irc.send + f = self.send # https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L80 cmodes = { @@ -37,7 +37,7 @@ class HybridProtocol(TS6Protocol): '*A': 'beI', '*B': 'k', '*C': 'l', '*D': 'cimnprstCMORS' } - self.irc.cmodes = cmodes + self.cmodes = cmodes umodes = { 'oper': 'o', 'invisible': 'i', 'wallops': 'w', 'locops': 'l', @@ -52,13 +52,13 @@ class HybridProtocol(TS6Protocol): '*A': '', '*B': '', '*C': '', '*D': 'DFGHRSWabcdefgijklnopqrsuwxy' } - self.irc.umodes = umodes + self.umodes = umodes # halfops is mandatory on Hybrid - self.irc.prefixmodes = {'o': '@', 'h': '%', 'v': '+'} + self.prefixmodes = {'o': '@', 'h': '%', 'v': '+'} # https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L55 - f('PASS %s TS 6 %s' % (self.irc.serverdata["sendpass"], self.irc.sid)) + f('PASS %s TS 6 %s' % (self.serverdata["sendpass"], self.sid)) # We request the following capabilities (for hybrid): @@ -68,7 +68,7 @@ class HybridProtocol(TS6Protocol): # CHW: Allow sending messages to @#channel and the like. # KNOCK: Support for /knock # SVS: Deal with extended NICK/UID messages that contain service IDs/stamps - # TBURST: Topic Burst command; we send this in topicBurst + # TBURST: Topic Burst command; we send this in topic_burst # DLN: DLINE command # UNDLN: UNDLINE command # KLN: KLINE command @@ -79,13 +79,13 @@ class HybridProtocol(TS6Protocol): # EOB: Supports EOB (end of burst) command f('CAPAB :TBURST DLN KNOCK UNDLN UNKLN KLN ENCAP IE EX HOPS CHW SVS CLUSTER EOB QS') - f('SERVER %s 0 :%s' % (self.irc.serverdata["hostname"], - self.irc.serverdata.get('serverdesc') or conf.conf['bot']['serverdesc'])) + f('SERVER %s 0 :%s' % (self.serverdata["hostname"], + self.serverdata.get('serverdesc') or conf.conf['bot']['serverdesc'])) # send endburst now - self.irc.send(':%s EOB' % (self.irc.sid,)) + self.send(':%s EOB' % (self.sid,)) - def spawnClient(self, nick, ident='null', host='null', realhost=None, modes=set(), + def spawn_client(self, nick, ident='null', host='null', realhost=None, modes=set(), server=None, ip='0.0.0.0', realname=None, ts=None, opertype=None, manipulatable=False): """ @@ -95,8 +95,8 @@ class HybridProtocol(TS6Protocol): up to plugins to make sure they don't introduce anything invalid. """ - server = server or self.irc.sid - if not self.irc.isInternalServer(server): + server = server or self.sid + if not self.is_internal_server(server): raise ValueError('Server %r is not a PyLink server!' % server) uid = self.uidgen[server].next_uid() @@ -104,18 +104,18 @@ class HybridProtocol(TS6Protocol): ts = ts or int(time.time()) realname = realname or conf.conf['bot']['realname'] realhost = realhost or host - raw_modes = self.irc.joinModes(modes) - u = self.irc.users[uid] = IrcUser(nick, ts, uid, server, ident=ident, host=host, realname=realname, + raw_modes = self.join_modes(modes) + u = self.users[uid] = User(nick, ts, uid, server, ident=ident, host=host, realname=realname, realhost=realhost, ip=ip, manipulatable=manipulatable) - self.irc.applyModes(uid, modes) - self.irc.servers[server].users.add(uid) - self._send(server, "UID {nick} 1 {ts} {modes} {ident} {host} {ip} {uid} " + self.apply_modes(uid, modes) + self.servers[server].users.add(uid) + self._send_with_prefix(server, "UID {nick} 1 {ts} {modes} {ident} {host} {ip} {uid} " "* :{realname}".format(ts=ts, host=host, nick=nick, ident=ident, uid=uid, modes=raw_modes, ip=ip, realname=realname)) return u - def updateClient(self, target, field, text): + def update_client(self, target, field, text): """Updates the ident, host, or realname of a PyLink client.""" # https://github.com/ircd-hybrid/ircd-hybrid/blob/58323b8/modules/m_svsmode.c#L40-L103 # parv[0] = command @@ -125,28 +125,28 @@ class HybridProtocol(TS6Protocol): # parv[4] = optional argument (services account, vhost) field = field.upper() - ts = self.irc.users[target].ts + ts = self.users[target].ts if field == 'HOST': - self.irc.users[target].host = text + self.users[target].host = text # On Hybrid, it appears that host changing is actually just forcing umode # "+x " on the target. -GLolol - self._send(self.irc.sid, 'SVSMODE %s %s +x %s' % (target, ts, text)) + self._send_with_prefix(self.sid, 'SVSMODE %s %s +x %s' % (target, ts, text)) else: raise NotImplementedError("Changing field %r of a client is unsupported by this protocol." % field) - def topicBurst(self, numeric, target, text): + def topic_burst(self, numeric, target, text): """Sends a topic change from a PyLink server. This is usually used on burst.""" # <- :0UY TBURST 1459308205 #testchan 1459309379 dan!~d@localhost :sdf - if not self.irc.isInternalServer(numeric): + if not self.is_internal_server(numeric): raise LookupError('No such PyLink server exists.') - ts = self.irc.channels[target].ts - servername = self.irc.servers[numeric].name + ts = self.channels[target].ts + servername = self.servers[numeric].name - self._send(numeric, 'TBURST %s %s %s %s :%s' % (ts, target, int(time.time()), servername, text)) - self.irc.channels[target].topic = text - self.irc.channels[target].topicset = True + self._send_with_prefix(numeric, 'TBURST %s %s %s %s :%s' % (ts, target, int(time.time()), servername, text)) + self.channels[target].topic = text + self.channels[target].topicset = True # command handlers @@ -154,13 +154,13 @@ class HybridProtocol(TS6Protocol): # We only get a list of keywords here. Hybrid obviously assumes that # we know what modes it supports (indeed, this is a standard list). # <- CAPAB :UNDLN UNKLN KLN TBURST KNOCK ENCAP DLN IE EX HOPS CHW SVS CLUSTER EOB QS - self.irc.caps = caps = args[0].split() + self.caps = caps = args[0].split() for required_cap in ('EX', 'IE', 'SVS', 'EOB', 'HOPS', 'QS', 'TBURST', 'SVS'): if required_cap not in caps: raise ProtocolError('%s not found in TS6 capabilities list; this is required! (got %r)' % (required_cap, caps)) - log.debug('(%s) self.irc.connected set!', self.irc.name) - self.irc.connected.set() + log.debug('(%s) self.connected set!', self.name) + self.connected.set() def handle_uid(self, numeric, command, args): """ @@ -174,39 +174,39 @@ class HybridProtocol(TS6Protocol): if account == '*': account = None log.debug('(%s) handle_uid: got args nick=%s ts=%s uid=%s ident=%s ' - 'host=%s realname=%s ip=%s', self.irc.name, nick, ts, uid, + 'host=%s realname=%s ip=%s', self.name, nick, ts, uid, ident, host, realname, ip) - self.irc.users[uid] = IrcUser(nick, ts, uid, numeric, ident, host, realname, host, ip) + self.users[uid] = User(nick, ts, uid, numeric, ident, host, realname, host, ip) - parsedmodes = self.irc.parseModes(uid, [modes]) - log.debug('(%s) handle_uid: Applying modes %s for %s', self.irc.name, parsedmodes, uid) - self.irc.applyModes(uid, parsedmodes) - self.irc.servers[numeric].users.add(uid) + parsedmodes = self.parse_modes(uid, [modes]) + log.debug('(%s) handle_uid: Applying modes %s for %s', self.name, parsedmodes, uid) + self.apply_modes(uid, parsedmodes) + self.servers[numeric].users.add(uid) # Call the OPERED UP hook if +o is being added to the mode list. if ('+o', None) in parsedmodes: - self.irc.callHooks([uid, 'CLIENT_OPERED', {'text': 'IRC_Operator'}]) + self.call_hooks([uid, 'CLIENT_OPERED', {'text': 'IRC_Operator'}]) # Set the account name if present if account: - self.irc.callHooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': account}]) + self.call_hooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': account}]) return {'uid': uid, 'ts': ts, 'nick': nick, 'realname': realname, 'host': host, 'ident': ident, 'ip': ip} def handle_tburst(self, numeric, command, args): """Handles incoming topic burst (TBURST) commands.""" # <- :0UY TBURST 1459308205 #testchan 1459309379 dan!~d@localhost :sdf - channel = self.irc.toLower(args[1]) + channel = self.to_lower(args[1]) ts = args[2] setter = args[3] topic = args[-1] - self.irc.channels[channel].topic = topic - self.irc.channels[channel].topicset = True + self.channels[channel].topic = topic + self.channels[channel].topicset = True return {'channel': channel, 'setter': setter, 'ts': ts, 'text': topic} def handle_eob(self, numeric, command, args): - log.debug('(%s) end of burst received', self.irc.name) + log.debug('(%s) end of burst received', self.name) if not self.has_eob: # Only call ENDBURST hooks if we haven't already. return {} @@ -221,7 +221,7 @@ class HybridProtocol(TS6Protocol): target = args[0] ts = args[1] modes = args[2:] - parsedmodes = self.irc.parseModes(target, modes) + parsedmodes = self.parse_modes(target, modes) for modepair in parsedmodes: if modepair[0] == '+d': @@ -241,7 +241,7 @@ class HybridProtocol(TS6Protocol): # Send the login hook, and remove this mode from the mode # list, as it shouldn't be parsed literally. - self.irc.callHooks([target, 'CLIENT_SERVICES_LOGIN', {'text': account}]) + self.call_hooks([target, 'CLIENT_SERVICES_LOGIN', {'text': account}]) parsedmodes.remove(modepair) elif modepair[0] == '+x': @@ -250,16 +250,16 @@ class HybridProtocol(TS6Protocol): # to some.host, for example. host = args[-1] - self.irc.users[target].host = host + self.users[target].host = host # Propagate the hostmask change as a hook. - self.irc.callHooks([numeric, 'CHGHOST', + self.call_hooks([numeric, 'CHGHOST', {'target': target, 'newhost': host}]) parsedmodes.remove(modepair) if parsedmodes: - self.irc.applyModes(target, parsedmodes) + self.apply_modes(target, parsedmodes) return {'target': target, 'modes': parsedmodes} diff --git a/protocols/inspircd.py b/protocols/inspircd.py index 2525c56..684e60b 100644 --- a/protocols/inspircd.py +++ b/protocols/inspircd.py @@ -11,8 +11,8 @@ from pylinkirc.log import log from pylinkirc.protocols.ts6_common import * class InspIRCdProtocol(TS6BaseProtocol): - def __init__(self, irc): - super().__init__(irc) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.protocol_caps |= {'slash-in-nicks', 'slash-in-hosts', 'underscore-in-hosts'} @@ -33,7 +33,7 @@ class InspIRCdProtocol(TS6BaseProtocol): ### Outgoing commands - def spawnClient(self, nick, ident='null', host='null', realhost=None, modes=set(), + def spawn_client(self, nick, ident='null', host='null', realhost=None, modes=set(), server=None, ip='0.0.0.0', realname=None, ts=None, opertype='IRC Operator', manipulatable=False): """ @@ -42,9 +42,9 @@ class InspIRCdProtocol(TS6BaseProtocol): Note: No nick collision / valid nickname checks are done here; it is up to plugins to make sure they don't introduce anything invalid. """ - server = server or self.irc.sid + server = server or self.sid - if not self.irc.isInternalServer(server): + if not self.is_internal_server(server): raise ValueError('Server %r is not a PyLink server!' % server) uid = self.uidgen[server].next_uid() @@ -52,14 +52,14 @@ class InspIRCdProtocol(TS6BaseProtocol): ts = ts or int(time.time()) realname = realname or conf.conf['bot']['realname'] realhost = realhost or host - raw_modes = self.irc.joinModes(modes) - u = self.irc.users[uid] = IrcUser(nick, ts, uid, server, ident=ident, host=host, realname=realname, + raw_modes = self.join_modes(modes) + u = self.users[uid] = User(nick, ts, uid, server, ident=ident, host=host, realname=realname, realhost=realhost, ip=ip, manipulatable=manipulatable, opertype=opertype) - self.irc.applyModes(uid, modes) - self.irc.servers[server].users.add(uid) + self.apply_modes(uid, modes) + self.servers[server].users.add(uid) - self._send(server, "UID {uid} {ts} {nick} {realhost} {host} {ident} {ip}" + self._send_with_prefix(server, "UID {uid} {ts} {nick} {realhost} {host} {ident} {ip}" " {ts} {modes} + :{realname}".format(ts=ts, host=host, nick=nick, ident=ident, uid=uid, modes=raw_modes, ip=ip, realname=realname, @@ -73,20 +73,20 @@ class InspIRCdProtocol(TS6BaseProtocol): # InspIRCd doesn't distinguish between burst joins and regular joins, # so what we're actually doing here is sending FJOIN from the server, # on behalf of the clients that are joining. - channel = self.irc.toLower(channel) + channel = self.to_lower(channel) - server = self.irc.getServer(client) - if not self.irc.isInternalServer(server): - log.error('(%s) Error trying to join %r to %r (no such client exists)', self.irc.name, client, channel) + server = self.get_server(client) + if not self.is_internal_server(server): + log.error('(%s) Error trying to join %r to %r (no such client exists)', self.name, client, channel) raise LookupError('No such PyLink client exists.') # Strip out list-modes, they shouldn't be ever sent in FJOIN. - modes = [m for m in self.irc.channels[channel].modes if m[0] not in self.irc.cmodes['*A']] - self._send(server, "FJOIN {channel} {ts} {modes} :,{uid}".format( - ts=self.irc.channels[channel].ts, uid=client, channel=channel, - modes=self.irc.joinModes(modes))) - self.irc.channels[channel].users.add(client) - self.irc.users[client].channels.add(channel) + modes = [m for m in self.channels[channel].modes if m[0] not in self.cmodes['*A']] + self._send_with_prefix(server, "FJOIN {channel} {ts} {modes} :,{uid}".format( + ts=self.channels[channel].ts, uid=client, channel=channel, + modes=self.join_modes(modes))) + self.channels[channel].users.add(client) + self.users[client].channels.add(channel) def sjoin(self, server, channel, users, ts=None, modes=set()): """Sends an SJOIN for a group of users to a channel. @@ -97,29 +97,29 @@ class InspIRCdProtocol(TS6BaseProtocol): Example uses: sjoin('100', '#test', [('', '100AAABBC'), ('qo', 100AAABBB'), ('h', '100AAADDD')]) - sjoin(self.irc.sid, '#test', [('o', self.irc.pseudoclient.uid)]) + sjoin(self.sid, '#test', [('o', self.pseudoclient.uid)]) """ - channel = self.irc.toLower(channel) - server = server or self.irc.sid + channel = self.to_lower(channel) + server = server or self.sid assert users, "sjoin: No users sent?" - log.debug('(%s) sjoin: got %r for users', self.irc.name, users) + log.debug('(%s) sjoin: got %r for users', self.name, users) if not server: raise LookupError('No such PyLink client exists.') # Strip out list-modes, they shouldn't ever be sent in FJOIN (protocol rules). - modes = modes or self.irc.channels[channel].modes - orig_ts = self.irc.channels[channel].ts + modes = modes or self.channels[channel].modes + orig_ts = self.channels[channel].ts ts = ts or orig_ts banmodes = [] regularmodes = [] for mode in modes: modechar = mode[0][-1] - if modechar in self.irc.cmodes['*A']: + if modechar in self.cmodes['*A']: # Track bans separately (they are sent as a normal FMODE instead of in FJOIN. # However, don't reset bans that have already been set. - if (modechar, mode[1]) not in self.irc.channels[channel].modes: + if (modechar, mode[1]) not in self.channels[channel].modes: banmodes.append(mode) else: regularmodes.append(mode) @@ -137,21 +137,21 @@ class InspIRCdProtocol(TS6BaseProtocol): for m in prefixes: changedmodes.add(('+%s' % m, user)) try: - self.irc.users[user].channels.add(channel) + self.users[user].channels.add(channel) except KeyError: # Not initialized yet? - log.debug("(%s) sjoin: KeyError trying to add %r to %r's channel list?", self.irc.name, channel, user) + log.debug("(%s) sjoin: KeyError trying to add %r to %r's channel list?", self.name, channel, user) namelist = ' '.join(namelist) - self._send(server, "FJOIN {channel} {ts} {modes} :{users}".format( + self._send_with_prefix(server, "FJOIN {channel} {ts} {modes} :{users}".format( ts=ts, users=namelist, channel=channel, - modes=self.irc.joinModes(modes))) - self.irc.channels[channel].users.update(uids) + modes=self.join_modes(modes))) + self.channels[channel].users.update(uids) if banmodes: # Burst ban modes if there are any. # <- :1ML FMODE #test 1461201525 +bb *!*@bad.user *!*@rly.bad.user - self._send(server, "FMODE {channel} {ts} {modes} ".format( - ts=ts, channel=channel, modes=self.irc.joinModes(banmodes))) + self._send_with_prefix(server, "FMODE {channel} {ts} {modes} ".format( + ts=ts, channel=channel, modes=self.join_modes(banmodes))) self.updateTS(server, channel, ts, changedmodes) @@ -163,19 +163,19 @@ class InspIRCdProtocol(TS6BaseProtocol): recognize ANY non-burst oper ups. Plugins don't have to call this function themselves, but they can - set the opertype attribute of an IrcUser object (in self.irc.users), + set the opertype attribute of an User object (in self.users), and the change will be reflected here.""" - userobj = self.irc.users[target] + userobj = self.users[target] try: otype = opertype or userobj.opertype or 'IRC Operator' except AttributeError: log.debug('(%s) opertype field for %s (%s) isn\'t filled yet!', - self.irc.name, target, userobj.nick) + self.name, target, userobj.nick) # whatever, this is non-standard anyways. otype = 'IRC Operator' assert otype, "Tried to send an empty OPERTYPE!" log.debug('(%s) Sending OPERTYPE from %s to oper them up.', - self.irc.name, target) + self.name, target) userobj.opertype = otype # InspIRCd 2.x uses _ in OPERTYPE to denote spaces, while InspIRCd 3.x does not. This is not @@ -187,75 +187,75 @@ class InspIRCdProtocol(TS6BaseProtocol): else: otype = ':' + otype - self._send(target, 'OPERTYPE %s' % otype) + self._send_with_prefix(target, 'OPERTYPE %s' % otype) def mode(self, numeric, target, modes, ts=None): """Sends mode changes from a PyLink client/server.""" # -> :9PYAAAAAA FMODE #pylink 1433653951 +os 9PYAAAAAA # -> :9PYAAAAAA MODE 9PYAAAAAA -i+w - if (not self.irc.isInternalClient(numeric)) and \ - (not self.irc.isInternalServer(numeric)): + if (not self.is_internal_client(numeric)) and \ + (not self.is_internal_server(numeric)): raise LookupError('No such PyLink client/server exists.') - log.debug('(%s) inspircd._sendModes: received %r for mode list', self.irc.name, modes) + log.debug('(%s) inspircd._send_with_prefixModes: received %r for mode list', self.name, modes) if ('+o', None) in modes and not utils.isChannel(target): # https://github.com/inspircd/inspircd/blob/master/src/modules/m_spanningtree/opertype.cpp#L26-L28 # Servers need a special command to set umode +o on people. self._operUp(target) - self.irc.applyModes(target, modes) - joinedmodes = self.irc.joinModes(modes) + self.apply_modes(target, modes) + joinedmodes = self.join_modes(modes) if utils.isChannel(target): - ts = ts or self.irc.channels[self.irc.toLower(target)].ts - self._send(numeric, 'FMODE %s %s %s' % (target, ts, joinedmodes)) + ts = ts or self.channels[self.to_lower(target)].ts + self._send_with_prefix(numeric, 'FMODE %s %s %s' % (target, ts, joinedmodes)) else: - self._send(numeric, 'MODE %s %s' % (target, joinedmodes)) + self._send_with_prefix(numeric, 'MODE %s %s' % (target, joinedmodes)) def kill(self, numeric, target, reason): """Sends a kill from a PyLink client/server.""" - if (not self.irc.isInternalClient(numeric)) and \ - (not self.irc.isInternalServer(numeric)): + if (not self.is_internal_client(numeric)) and \ + (not self.is_internal_server(numeric)): raise LookupError('No such PyLink client/server exists.') # InspIRCd will show the raw kill message sent from our server as the quit message. # So, make the kill look actually like a kill instead of someone quitting with # an arbitrary message. - if numeric in self.irc.servers: - sourcenick = self.irc.servers[numeric].name + if numeric in self.servers: + sourcenick = self.servers[numeric].name else: - sourcenick = self.irc.users[numeric].nick + sourcenick = self.users[numeric].nick - self._send(numeric, 'KILL %s :Killed (%s (%s))' % (target, sourcenick, reason)) + self._send_with_prefix(numeric, 'KILL %s :Killed (%s (%s))' % (target, sourcenick, reason)) - # We only need to call removeClient here if the target is one of our + # We only need to call _remove_client here if the target is one of our # clients, since any remote servers will send a QUIT from # their target if the command succeeds. - if self.irc.isInternalClient(target): - self.removeClient(target) + if self.is_internal_client(target): + self._remove_client(target) - def topicBurst(self, numeric, target, text): + def topic_burst(self, numeric, target, text): """Sends a topic change from a PyLink server. This is usually used on burst.""" - if not self.irc.isInternalServer(numeric): + if not self.is_internal_server(numeric): raise LookupError('No such PyLink server exists.') ts = int(time.time()) - servername = self.irc.servers[numeric].name - self._send(numeric, 'FTOPIC %s %s %s :%s' % (target, ts, servername, text)) - self.irc.channels[target].topic = text - self.irc.channels[target].topicset = True + servername = self.servers[numeric].name + self._send_with_prefix(numeric, 'FTOPIC %s %s %s :%s' % (target, ts, servername, text)) + self.channels[target].topic = text + self.channels[target].topicset = True def invite(self, numeric, target, channel): """Sends an INVITE from a PyLink client..""" - if not self.irc.isInternalClient(numeric): + if not self.is_internal_client(numeric): raise LookupError('No such PyLink client exists.') - self._send(numeric, 'INVITE %s %s' % (target, channel)) + self._send_with_prefix(numeric, 'INVITE %s %s' % (target, channel)) def knock(self, numeric, target, text): """Sends a KNOCK from a PyLink client.""" - if not self.irc.isInternalClient(numeric): + if not self.is_internal_client(numeric): raise LookupError('No such PyLink client exists.') - self._send(numeric, 'ENCAP * KNOCK %s :%s' % (target, text)) + self._send_with_prefix(numeric, 'ENCAP * KNOCK %s :%s' % (target, text)) - def updateClient(self, target, field, text): + def update_client(self, target, field, text): """Updates the ident, host, or realname of any connected client.""" field = field.upper() @@ -263,58 +263,58 @@ class InspIRCdProtocol(TS6BaseProtocol): raise NotImplementedError("Changing field %r of a client is " "unsupported by this protocol." % field) - if self.irc.isInternalClient(target): + if self.is_internal_client(target): # It is one of our clients, use FIDENT/HOST/NAME. if field == 'IDENT': - self.irc.users[target].ident = text - self._send(target, 'FIDENT %s' % text) + self.users[target].ident = text + self._send_with_prefix(target, 'FIDENT %s' % text) elif field == 'HOST': - self.irc.users[target].host = text - self._send(target, 'FHOST %s' % text) + self.users[target].host = text + self._send_with_prefix(target, 'FHOST %s' % text) elif field in ('REALNAME', 'GECOS'): - self.irc.users[target].realname = text - self._send(target, 'FNAME :%s' % text) + self.users[target].realname = text + self._send_with_prefix(target, 'FNAME :%s' % text) else: # It is a client on another server, use CHGIDENT/HOST/NAME. if field == 'IDENT': if 'm_chgident.so' not in self.modsupport: - log.warning('(%s) Failed to change ident of %s to %r: load m_chgident.so!', self.irc.name, target, text) + log.warning('(%s) Failed to change ident of %s to %r: load m_chgident.so!', self.name, target, text) return - self.irc.users[target].ident = text - self._send(self.irc.sid, 'CHGIDENT %s %s' % (target, text)) + self.users[target].ident = text + self._send_with_prefix(self.sid, 'CHGIDENT %s %s' % (target, text)) # Send hook payloads for other plugins to listen to. - self.irc.callHooks([self.irc.sid, 'CHGIDENT', + self.call_hooks([self.sid, 'CHGIDENT', {'target': target, 'newident': text}]) elif field == 'HOST': if 'm_chghost.so' not in self.modsupport: - log.warning('(%s) Failed to change host of %s to %r: load m_chghost.so!', self.irc.name, target, text) + log.warning('(%s) Failed to change host of %s to %r: load m_chghost.so!', self.name, target, text) return - self.irc.users[target].host = text - self._send(self.irc.sid, 'CHGHOST %s %s' % (target, text)) + self.users[target].host = text + self._send_with_prefix(self.sid, 'CHGHOST %s %s' % (target, text)) - self.irc.callHooks([self.irc.sid, 'CHGHOST', + self.call_hooks([self.sid, 'CHGHOST', {'target': target, 'newhost': text}]) elif field in ('REALNAME', 'GECOS'): if 'm_chgname.so' not in self.modsupport: - log.warning('(%s) Failed to change real name of %s to %r: load m_chgname.so!', self.irc.name, target, text) + log.warning('(%s) Failed to change real name of %s to %r: load m_chgname.so!', self.name, target, text) return - self.irc.users[target].realname = text - self._send(self.irc.sid, 'CHGNAME %s :%s' % (target, text)) + self.users[target].realname = text + self._send_with_prefix(self.sid, 'CHGNAME %s :%s' % (target, text)) - self.irc.callHooks([self.irc.sid, 'CHGNAME', + self.call_hooks([self.sid, 'CHGNAME', {'target': target, 'newgecos': text}]) def ping(self, source=None, target=None): """Sends a PING to a target server. Periodic PINGs are sent to our uplink automatically by the Irc() internals; plugins shouldn't have to use this.""" - source = source or self.irc.sid - target = target or self.irc.uplink + source = source or self.sid + target = target or self.uplink if not (target is None or source is None): - self._send(source, 'PING %s %s' % (source, target)) + self._send_with_prefix(source, 'PING %s %s' % (source, target)) def numeric(self, source, numeric, target, text): """Sends raw numerics from a server to a remote client.""" @@ -327,18 +327,18 @@ class InspIRCdProtocol(TS6BaseProtocol): # : NUM <3 digit number> # Take this into consideration if we ever target InspIRCd 2.2, even though m_spanningtree # does provide backwards compatibility for commands like this. -GLolol - self._send(self.irc.sid, 'PUSH %s ::%s %s %s %s' % (target, source, numeric, target, text)) + self._send_with_prefix(self.sid, 'PUSH %s ::%s %s %s %s' % (target, source, numeric, target, text)) def away(self, source, text): """Sends an AWAY message from a PyLink client. can be an empty string to unset AWAY status.""" if text: - self._send(source, 'AWAY %s :%s' % (int(time.time()), text)) + self._send_with_prefix(source, 'AWAY %s :%s' % (int(time.time()), text)) else: - self._send(source, 'AWAY') - self.irc.users[source].away = text + self._send_with_prefix(source, 'AWAY') + self.users[source].away = text - def spawnServer(self, name, sid=None, uplink=None, desc=None, endburst_delay=0): + def spawn_server(self, name, sid=None, uplink=None, desc=None, endburst_delay=0): """ Spawns a server off a PyLink server. desc (server description) defaults to the one in the config. uplink defaults to the main PyLink @@ -351,68 +351,68 @@ class InspIRCdProtocol(TS6BaseProtocol): and prevent connections from filling up the snomasks too much. """ # -> :0AL SERVER test.server * 1 0AM :some silly pseudoserver - uplink = uplink or self.irc.sid + uplink = uplink or self.sid name = name.lower() # "desc" defaults to the configured server description. - desc = desc or self.irc.serverdata.get('serverdesc') or conf.conf['bot']['serverdesc'] + desc = desc or self.serverdata.get('serverdesc') or conf.conf['bot']['serverdesc'] if sid is None: # No sid given; generate one! sid = self.sidgen.next_sid() assert len(sid) == 3, "Incorrect SID length" - if sid in self.irc.servers: + if sid in self.servers: raise ValueError('A server with SID %r already exists!' % sid) - for server in self.irc.servers.values(): + for server in self.servers.values(): if name == server.name: raise ValueError('A server named %r already exists!' % name) - if not self.irc.isInternalServer(uplink): + if not self.is_internal_server(uplink): raise ValueError('Server %r is not a PyLink server!' % uplink) if not utils.isServerName(name): raise ValueError('Invalid server name %r' % name) - self._send(uplink, 'SERVER %s * 1 %s :%s' % (name, sid, desc)) - self.irc.servers[sid] = IrcServer(uplink, name, internal=True, desc=desc) + self._send_with_prefix(uplink, 'SERVER %s * 1 %s :%s' % (name, sid, desc)) + self.servers[sid] = Server(uplink, name, internal=True, desc=desc) def endburstf(): # Delay ENDBURST by X seconds if requested. - if self.irc.aborted.wait(endburst_delay): + if self.aborted.wait(endburst_delay): # We managed to catch the abort flag before sending ENDBURST, so break - log.debug('(%s) stopping endburstf() for %s as aborted was set', self.irc.name, sid) + log.debug('(%s) stopping endburstf() for %s as aborted was set', self.name, sid) return - self._send(sid, 'ENDBURST') + self._send_with_prefix(sid, 'ENDBURST') if endburst_delay: threading.Thread(target=endburstf).start() else: # Else, send burst immediately - self._send(sid, 'ENDBURST') + self._send_with_prefix(sid, 'ENDBURST') return sid def squit(self, source, target, text='No reason given'): """SQUITs a PyLink server.""" # -> :9PY SQUIT 9PZ :blah, blah - self._send(source, 'SQUIT %s :%s' % (target, text)) + self._send_with_prefix(source, 'SQUIT %s :%s' % (target, text)) self.handle_squit(source, 'SQUIT', [target, text]) ### Core / command handlers - def connect(self): + def post_connect(self): """Initializes a connection to a server.""" - ts = self.irc.start_ts + ts = self.start_ts # Track the modules supported by the uplink. self.modsupport = [] - f = self.irc.send + f = self.send f('CAPAB START %s' % self.proto_ver) f('CAPAB CAPABILITIES :PROTOCOL=%s' % self.proto_ver) f('CAPAB END') - host = self.irc.serverdata["hostname"] + host = self.serverdata["hostname"] f('SERVER {host} {Pass} 0 {sid} :{sdesc}'.format(host=host, - Pass=self.irc.serverdata["sendpass"], sid=self.irc.sid, - sdesc=self.irc.serverdata.get('serverdesc') or conf.conf['bot']['serverdesc'])) + Pass=self.serverdata["sendpass"], sid=self.sid, + sdesc=self.serverdata.get('serverdesc') or conf.conf['bot']['serverdesc'])) - self._send(self.irc.sid, 'BURST %s' % ts) + self._send_with_prefix(self.sid, 'BURST %s' % ts) # InspIRCd sends VERSION data on link, instead of whenever requested by a client. - self._send(self.irc.sid, 'VERSION :%s' % self.irc.version()) - self._send(self.irc.sid, 'ENDBURST') + self._send_with_prefix(self.sid, 'VERSION :%s' % self.version()) + self._send_with_prefix(self.sid, 'ENDBURST') def handle_capab(self, source, command, args): """ @@ -453,7 +453,7 @@ class InspIRCdProtocol(TS6BaseProtocol): name = 'owner' # We don't care about mode prefixes; just the mode char. - self.irc.cmodes[name] = char[-1] + self.cmodes[name] = char[-1] elif args[0] == 'USERMODES': @@ -467,7 +467,7 @@ class InspIRCdProtocol(TS6BaseProtocol): name, char = modepair.split('=') # Strip u_ prefixes to be consistent with other protocols. name = name.lstrip('u_') - self.irc.umodes[name] = char + self.umodes[name] = char elif args[0] == 'CAPABILITIES': # <- CAPAB CAPABILITIES :NICKMAX=21 CHANMAX=64 MAXMODES=20 @@ -477,8 +477,8 @@ class InspIRCdProtocol(TS6BaseProtocol): # USERMODES=,,s,BHIRSWcghikorwx GLOBOPS=1 SVSPART=1 # First, turn the arguments into a dict - caps = self.parseCapabilities(args[-1]) - log.debug("(%s) capabilities list: %s", self.irc.name, caps) + caps = self.parse_isupport(args[-1]) + log.debug("(%s) capabilities list: %s", self.name, caps) # Check the protocol version self.remote_proto_ver = protocol_version = int(caps['PROTOCOL']) @@ -491,30 +491,30 @@ class InspIRCdProtocol(TS6BaseProtocol): elif protocol_version > self.max_proto_ver: log.warning("(%s) PyLink support for InspIRCd 2.2+ is experimental, " "and should not be relied upon for anything important.", - self.irc.name) + self.name) # Store the max nick and channel lengths - self.irc.maxnicklen = int(caps['NICKMAX']) - self.irc.maxchanlen = int(caps['CHANMAX']) + self.maxnicklen = int(caps['NICKMAX']) + self.maxchanlen = int(caps['CHANMAX']) # Modes are divided into A, B, C, and D classes # See http://www.irc.org/tech_docs/005.html # FIXME: Find a neater way to assign/store this. - self.irc.cmodes['*A'], self.irc.cmodes['*B'], self.irc.cmodes['*C'], self.irc.cmodes['*D'] \ + self.cmodes['*A'], self.cmodes['*B'], self.cmodes['*C'], self.cmodes['*D'] \ = caps['CHANMODES'].split(',') - self.irc.umodes['*A'], self.irc.umodes['*B'], self.irc.umodes['*C'], self.irc.umodes['*D'] \ + self.umodes['*A'], self.umodes['*B'], self.umodes['*C'], self.umodes['*D'] \ = caps['USERMODES'].split(',') # Separate the prefixes field (e.g. "(Yqaohv)!~&@%+") into a # dict mapping mode characters to mode prefixes. - self.irc.prefixmodes = self.parsePrefixes(caps['PREFIX']) - log.debug('(%s) self.irc.prefixmodes set to %r', self.irc.name, - self.irc.prefixmodes) + self.prefixmodes = self.parse_isupport_prefixes(caps['PREFIX']) + log.debug('(%s) self.prefixmodes set to %r', self.name, + self.prefixmodes) # Finally, set the irc.connected (protocol negotiation complete) # state to True. - self.irc.connected.set() + self.connected.set() elif args[0] == 'MODSUPPORT': # <- CAPAB MODSUPPORT :m_alltime.so m_check.so m_chghost.so m_chgident.so m_chgname.so m_fullversion.so m_gecosban.so m_knock.so m_muteban.so m_nicklock.so m_nopartmsg.so m_opmoderated.so m_sajoin.so m_sanick.so m_sapart.so m_serverban.so m_services_account.so m_showwhois.so m_silence.so m_swhois.so m_uninvite.so m_watch.so self.modsupport = args[-1].split() @@ -523,19 +523,19 @@ class InspIRCdProtocol(TS6BaseProtocol): """Handles incoming PING commands, so we don't time out.""" # <- :70M PING 70M 0AL # -> :0AL PONG 0AL 70M - if self.irc.isInternalServer(args[1]): - self._send(args[1], 'PONG %s %s' % (args[1], source), queue=False) + if self.is_internal_server(args[1]): + self._send_with_prefix(args[1], 'PONG %s %s' % (args[1], source), queue=False) def handle_fjoin(self, servernumeric, command, args): """Handles incoming FJOIN commands (InspIRCd equivalent of JOIN/SJOIN).""" # :70M FJOIN #chat 1423790411 +AFPfjnt 6:5 7:5 9:5 :o,1SRAABIT4 v,1IOAAF53R <...> - channel = self.irc.toLower(args[0]) - chandata = self.irc.channels[channel].deepcopy() + channel = self.to_lower(args[0]) + chandata = self.channels[channel].deepcopy() # InspIRCd sends each channel's users in the form of 'modeprefix(es),UID' userlist = args[-1].split() modestring = args[2:-1] or args[2] - parsedmodes = self.irc.parseModes(channel, modestring) + parsedmodes = self.parse_modes(channel, modestring) namelist = [] # Keep track of other modes that are added due to prefix modes being joined too. @@ -545,18 +545,18 @@ class InspIRCdProtocol(TS6BaseProtocol): modeprefix, user = user.split(',', 1) # Don't crash when we get an invalid UID. - if user not in self.irc.users: + if user not in self.users: log.debug('(%s) handle_fjoin: tried to introduce user %s not in our user list, ignoring...', - self.irc.name, user) + self.name, user) continue namelist.append(user) - self.irc.users[user].channels.add(channel) + self.users[user].channels.add(channel) # Only save mode changes if the remote has lower TS than us. changedmodes |= {('+%s' % mode, user) for mode in modeprefix} - self.irc.channels[channel].users.add(user) + self.channels[channel].users.add(user) # Statekeeping with timestamps. Note: some service packages (Anope 1.8) send a trailing # 'd' after the timestamp, which we should strip out to prevent int() from erroring. @@ -565,7 +565,7 @@ class InspIRCdProtocol(TS6BaseProtocol): # <- :3AX FJOIN #monitor 1485462109d + :,3AXAAAAAK their_ts = int(''.join(char for char in args[1] if char.isdigit())) - our_ts = self.irc.channels[channel].ts + our_ts = self.channels[channel].ts self.updateTS(servernumeric, channel, their_ts, changedmodes) return {'channel': channel, 'users': namelist, 'modes': parsedmodes, 'ts': their_ts, @@ -577,35 +577,35 @@ class InspIRCdProtocol(TS6BaseProtocol): uid, ts, nick, realhost, host, ident, ip = args[0:7] self.check_nick_collision(nick) realname = args[-1] - self.irc.users[uid] = userobj = IrcUser(nick, ts, uid, numeric, ident, host, realname, realhost, ip) + self.users[uid] = userobj = User(nick, ts, uid, numeric, ident, host, realname, realhost, ip) - parsedmodes = self.irc.parseModes(uid, [args[8], args[9]]) - self.irc.applyModes(uid, parsedmodes) + parsedmodes = self.parse_modes(uid, [args[8], args[9]]) + self.apply_modes(uid, parsedmodes) - if (self.irc.umodes.get('servprotect'), None) in userobj.modes: + if (self.umodes.get('servprotect'), None) in userobj.modes: # Services are usually given a "Network Service" WHOIS, so # set that as the opertype. - self.irc.callHooks([uid, 'CLIENT_OPERED', {'text': 'Network Service'}]) + self.call_hooks([uid, 'CLIENT_OPERED', {'text': 'Network Service'}]) - self.irc.servers[numeric].users.add(uid) + self.servers[numeric].users.add(uid) return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip} def handle_server(self, numeric, command, args): """Handles incoming SERVER commands (introduction of servers).""" # Initial SERVER command on connect. - if self.irc.uplink is None: + if self.uplink is None: # <- SERVER whatever.net abcdefgh 0 10X :some server description servername = args[0].lower() numeric = args[3] - if args[1] != self.irc.serverdata['recvpass']: + if args[1] != self.serverdata['recvpass']: # Check if recvpass is correct raise ProtocolError('Error: recvpass from uplink server %s does not match configuration!' % servername) sdesc = args[-1] - self.irc.servers[numeric] = IrcServer(None, servername, desc=sdesc) - self.irc.uplink = numeric + self.servers[numeric] = Server(None, servername, desc=sdesc) + self.uplink = numeric return # Other server introductions. @@ -613,18 +613,18 @@ class InspIRCdProtocol(TS6BaseProtocol): servername = args[0].lower() sid = args[3] sdesc = args[-1] - self.irc.servers[sid] = IrcServer(numeric, servername, desc=sdesc) + self.servers[sid] = Server(numeric, servername, desc=sdesc) return {'name': servername, 'sid': args[3], 'text': sdesc} def handle_fmode(self, numeric, command, args): """Handles the FMODE command, used for channel mode changes.""" # <- :70MAAAAAA FMODE #chat 1433653462 +hhT 70MAAAAAA 70MAAAAAD - channel = self.irc.toLower(args[0]) - oldobj = self.irc.channels[channel].deepcopy() + channel = self.to_lower(args[0]) + oldobj = self.channels[channel].deepcopy() modes = args[2:] - changedmodes = self.irc.parseModes(channel, modes) - self.irc.applyModes(channel, changedmodes) + changedmodes = self.parse_modes(channel, modes) + self.apply_modes(channel, changedmodes) ts = int(args[1]) return {'target': channel, 'modes': changedmodes, 'ts': ts, 'channeldata': oldobj} @@ -636,8 +636,8 @@ class InspIRCdProtocol(TS6BaseProtocol): # <- :70MAAAAAA MODE 70MAAAAAA -i+xc target = args[0] modestrings = args[1:] - changedmodes = self.irc.parseModes(target, modestrings) - self.irc.applyModes(target, changedmodes) + changedmodes = self.parse_modes(target, modestrings) + self.apply_modes(target, changedmodes) return {'target': target, 'modes': changedmodes} def handle_idle(self, numeric, command, args): @@ -662,29 +662,21 @@ class InspIRCdProtocol(TS6BaseProtocol): def handle_ftopic(self, numeric, command, args): """Handles incoming FTOPIC (sets topic on burst).""" # <- :70M FTOPIC #channel 1434510754 GLo|o|!GLolol@escape.the.dreamland.ca :Some channel topic - channel = self.irc.toLower(args[0]) + channel = self.to_lower(args[0]) ts = args[1] setter = args[2] topic = args[-1] - self.irc.channels[channel].topic = topic - self.irc.channels[channel].topicset = True + self.channels[channel].topic = topic + self.channels[channel].topicset = True return {'channel': channel, 'setter': setter, 'ts': ts, 'text': topic} # SVSTOPIC is used by InspIRCd module m_topiclock - its arguments are the same as FTOPIC handle_svstopic = handle_ftopic - def handle_invite(self, numeric, command, args): - """Handles incoming INVITEs.""" - # <- :70MAAAAAC INVITE 0ALAAAAAA #blah 0 - target = args[0] - channel = self.irc.toLower(args[1]) - # We don't actually need to process this; just send the hook so plugins can use it - return {'target': target, 'channel': channel} - def handle_knock(self, numeric, command, args): """Handles channel KNOCKs.""" # <- :70MAAAAAA ENCAP * KNOCK #blah :abcdefg - channel = self.irc.toLower(args[0]) + channel = self.to_lower(args[0]) text = args[1] return {'channel': channel, 'text': text} @@ -701,29 +693,29 @@ class InspIRCdProtocol(TS6BaseProtocol): # Set umode +o on the target. omode = [('+o', None)] - self.irc.applyModes(target, omode) + self.apply_modes(target, omode) # Call the CLIENT_OPERED hook that protocols use. The MODE hook # payload is returned below. - self.irc.callHooks([target, 'CLIENT_OPERED', {'text': opertype}]) + self.call_hooks([target, 'CLIENT_OPERED', {'text': opertype}]) return {'target': target, 'modes': omode} def handle_fident(self, numeric, command, args): """Handles FIDENT, used for denoting ident changes.""" # <- :70MAAAAAB FIDENT test - self.irc.users[numeric].ident = newident = args[0] + self.users[numeric].ident = newident = args[0] return {'target': numeric, 'newident': newident} def handle_fhost(self, numeric, command, args): """Handles FHOST, used for denoting hostname changes.""" # <- :70MAAAAAB FHOST some.host - self.irc.users[numeric].host = newhost = args[0] + self.users[numeric].host = newhost = args[0] return {'target': numeric, 'newhost': newhost} def handle_fname(self, numeric, command, args): """Handles FNAME, used for denoting real name/gecos changes.""" # <- :70MAAAAAB FNAME :afdsafasf - self.irc.users[numeric].realname = newgecos = args[0] + self.users[numeric].realname = newgecos = args[0] return {'target': numeric, 'newgecos': newgecos} def handle_endburst(self, numeric, command, args): @@ -735,10 +727,10 @@ class InspIRCdProtocol(TS6BaseProtocol): # <- :1MLAAAAIG AWAY 1439371390 :Auto-away try: ts = args[0] - self.irc.users[numeric].away = text = args[1] + self.users[numeric].away = text = args[1] return {'text': text, 'ts': ts} except IndexError: # User is unsetting away status - self.irc.users[numeric].away = '' + self.users[numeric].away = '' return {'text': ''} def handle_rsquit(self, numeric, command, args): @@ -759,16 +751,16 @@ class InspIRCdProtocol(TS6BaseProtocol): # If we receive such a remote SQUIT, just forward it as a regular # SQUIT, in order to be consistent with other IRCds which make SQUITs # implicit. - target = self._getSid(args[0]) - if self.irc.isInternalServer(target): + target = self._get_SID(args[0]) + if self.is_internal_server(target): # The target has to be one of our servers in order to work... - uplink = self.irc.servers[target].uplink - reason = 'Requested by %s' % self.irc.getHostmask(numeric) - self._send(uplink, 'SQUIT %s :%s' % (target, reason)) + uplink = self.servers[target].uplink + reason = 'Requested by %s' % self.get_hostmask(numeric) + self._send_with_prefix(uplink, 'SQUIT %s :%s' % (target, reason)) return self.handle_squit(numeric, 'SQUIT', [target, reason]) else: log.debug("(%s) Got RSQUIT for '%s', which is either invalid or not " - "a server of ours!", self.irc.name, args[0]) + "a server of ours!", self.name, args[0]) def handle_metadata(self, numeric, command, args): """ @@ -777,12 +769,12 @@ class InspIRCdProtocol(TS6BaseProtocol): """ uid = args[0] - if args[1] == 'accountname' and uid in self.irc.users: + if args[1] == 'accountname' and uid in self.users: # <- :00A METADATA 1MLAAAJET accountname : # <- :00A METADATA 1MLAAAJET accountname :tester # Sets the services login name of the client. - self.irc.callHooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': args[-1]}]) + self.call_hooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': args[-1]}]) def handle_version(self, numeric, command, args): """ @@ -797,9 +789,9 @@ class InspIRCdProtocol(TS6BaseProtocol): # removed from our user list. # If not, we have to assume that KILL = QUIT and remove them # ourselves. - data = self.irc.users.get(killed) + data = self.users.get(killed) if data: - self.removeClient(killed) + self._remove_client(killed) return {'target': killed, 'text': args[1], 'userdata': data} def handle_sakick(self, source, command, args): @@ -808,20 +800,20 @@ class InspIRCdProtocol(TS6BaseProtocol): # ENCAP -> SAKICK args: ['#test', '0ALAAAAAB', 'test'] target = args[1] - channel = self.irc.toLower(args[0]) + channel = self.to_lower(args[0]) try: reason = args[2] except IndexError: # Kick reason is optional, strange... - reason = self.irc.getFriendlyName(source) + reason = self.get_friendly_name(source) - if not self.irc.isInternalClient(target): - log.warning("(%s) Got SAKICK for client that not one of ours: %s", self.irc.name, target) + if not self.is_internal_client(target): + log.warning("(%s) Got SAKICK for client that not one of ours: %s", self.name, target) return else: # Like RSQUIT, SAKICK requires that the receiving server acknowledge that a kick has # happened. This comes from the server hosting the target client. - server = self.irc.getServer(target) + server = self.get_server(target) self.kick(server, channel, target, reason) return {'channel': channel, 'target': target, 'text': reason} @@ -833,6 +825,6 @@ class InspIRCdProtocol(TS6BaseProtocol): # XXX: We override notice() here because that abstraction doesn't allow messages from servers. timestring = '%s (%s)' % (time.strftime('%Y-%m-%d %H:%M:%S'), int(time.time())) - self._send(self.irc.sid, 'NOTICE %s :System time is %s on %s' % (source, timestring, self.irc.hostname())) + self._send_with_prefix(self.sid, 'NOTICE %s :System time is %s on %s' % (source, timestring, self.hostname())) Class = InspIRCdProtocol diff --git a/protocols/ircs2s_common.py b/protocols/ircs2s_common.py index d636cc6..1c06368 100644 --- a/protocols/ircs2s_common.py +++ b/protocols/ircs2s_common.py @@ -3,16 +3,147 @@ ircs2s_common.py: Common base protocol class with functions shared by TS6 and P1 """ import time +import re +from collections import defaultdict -from pylinkirc.classes import Protocol +from pylinkirc.classes import IRCNetwork, ProtocolError from pylinkirc.log import log from pylinkirc import utils -class IRCS2SProtocol(Protocol): +class IRCCommonProtocol(IRCNetwork): + def validate_server_conf(self): + """Validates that the server block given contains the required keys.""" + for k in self.conf_keys: + assert k in self.serverdata, "Missing option %r in server block for network %s." % (k, self.name) + + port = self.serverdata['port'] + assert type(port) == int and 0 < port < 65535, "Invalid port %r for network %s" % (port, self.name) + + @staticmethod + def parse_args(args): + """ + Parses a string or list of of RFC1459-style arguments, where ":" may + be used for multi-word arguments that last until the end of a line. + """ + if isinstance(args, str): + args = args.split(' ') + + real_args = [] + for idx, arg in enumerate(args): + if arg.startswith(':') and idx != 0: + # ":" is used to begin multi-word arguments that last until the end of the message. + # Use list splicing here to join them into one argument, and then add it to our list of args. + joined_arg = ' '.join(args[idx:])[1:] # Cut off the leading : as well + real_args.append(joined_arg) + break + real_args.append(arg) + + return real_args + + @classmethod + def parse_prefixed_args(cls, args): + """Similar to parse_args(), but stripping leading colons from the first argument + of a line (usually the sender field).""" + args = cls.parse_args(args) + args[0] = args[0].split(':', 1)[1] + return args + + def _squit(self, numeric, command, args): + """Handles incoming SQUITs.""" + + split_server = self._get_SID(args[0]) + + # Normally we'd only need to check for our SID as the SQUIT target, but Nefarious + # actually uses the uplink server as the SQUIT target. + # <- ABAAE SQ nefarious.midnight.vpn 0 :test + if split_server in (self.sid, self.uplink): + raise ProtocolError('SQUIT received: (reason: %s)' % args[-1]) + + affected_users = [] + affected_nicks = defaultdict(list) + log.debug('(%s) Splitting server %s (reason: %s)', self.name, split_server, args[-1]) + + if split_server not in self.servers: + log.warning("(%s) Tried to split a server (%s) that didn't exist!", self.name, split_server) + return + + # Prevent RuntimeError: dictionary changed size during iteration + old_servers = self.servers.copy() + old_channels = self.channels.copy() + + # Cycle through our list of servers. If any server's uplink is the one that is being SQUIT, + # remove them and all their users too. + for sid, data in old_servers.items(): + if data.uplink == split_server: + log.debug('Server %s also hosts server %s, removing those users too...', split_server, sid) + # Recursively run SQUIT on any other hubs this server may have been connected to. + args = self._squit(sid, 'SQUIT', [sid, "0", + "PyLink: Automatically splitting leaf servers of %s" % sid]) + affected_users += args['users'] + + for user in self.servers[split_server].users.copy(): + affected_users.append(user) + nick = self.users[user].nick + + # Nicks affected is channel specific for SQUIT:. This makes Clientbot's SQUIT relaying + # much easier to implement. + for name, cdata in old_channels.items(): + if user in cdata.users: + affected_nicks[name].append(nick) + + log.debug('Removing client %s (%s)', user, nick) + self._remove_client(user) + + serverdata = self.servers[split_server] + sname = serverdata.name + uplink = serverdata.uplink + + del self.servers[split_server] + log.debug('(%s) Netsplit affected users: %s', self.name, affected_users) + + return {'target': split_server, 'users': affected_users, 'name': sname, + 'uplink': uplink, 'nicks': affected_nicks, 'serverdata': serverdata, + 'channeldata': old_channels} + + @staticmethod + def parse_isupport(args, fallback=''): + """ + Parses a string of capabilities in the 005 / RPL_ISUPPORT format. + """ + + if type(args) == str: + args = args.split(' ') + + caps = {} + for cap in args: + try: + # Try to split it as a KEY=VALUE pair. + key, value = cap.split('=', 1) + except ValueError: + key = cap + value = fallback + caps[key] = value + + return caps + + @staticmethod + def parse_isupport_prefixes(args): + """ + Separates prefixes field like "(qaohv)~&@%+" into a dict mapping mode characters to mode + prefixes. + """ + prefixsearch = re.search(r'\(([A-Za-z]+)\)(.*)', args) + return dict(zip(prefixsearch.group(1), prefixsearch.group(2))) + + def handle_error(self, numeric, command, args): + """Handles ERROR messages - these mean that our uplink has disconnected us!""" + raise ProtocolError('Received an ERROR, disconnecting!') + +class IRCS2SProtocol(IRCCommonProtocol): COMMAND_TOKENS = {} - def __init__(self, irc): - super().__init__(irc) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.protocol_caps = {'can-spawn-clients', 'has-ts', 'can-host-relay', 'can-track-servers'} @@ -28,46 +159,46 @@ class IRCS2SProtocol(Protocol): the SID of the uplink server. """ data = data.split(" ") - args = self.parseArgs(data) + args = self.parse_args(data) sender = args[0] sender = sender.lstrip(':') # If the sender isn't in numeric format, try to convert it automatically. - sender_sid = self._getSid(sender) - sender_uid = self._getUid(sender) + sender_sid = self._get_SID(sender) + sender_uid = self._get_UID(sender) - if sender_sid in self.irc.servers: + if sender_sid in self.servers: # Sender is a server (converting from name to SID gave a valid result). sender = sender_sid - elif sender_uid in self.irc.users: + elif sender_uid in self.users: # Sender is a user (converting from name to UID gave a valid result). sender = sender_uid else: # No sender prefix; treat as coming from uplink IRCd. - sender = self.irc.uplink + sender = self.uplink args.insert(0, sender) - if self.irc.isInternalClient(sender) or self.irc.isInternalServer(sender): - log.warning("(%s) Received command %s being routed the wrong way!", self.irc.name, command) + if self.is_internal_client(sender) or self.is_internal_server(sender): + log.warning("(%s) Received command %s being routed the wrong way!", self.name, command) return raw_command = args[1].upper() args = args[2:] - log.debug('(%s) Found message sender as %s', self.irc.name, sender) + log.debug('(%s) Found message sender as %s, raw_command=%r, args=%r', self.name, sender, raw_command, args) # For P10, convert the command token into a regular command, if present. command = self.COMMAND_TOKENS.get(raw_command, raw_command) if command != raw_command: - log.debug('(%s) Translating token %s to command %s', self.irc.name, raw_command, command) + log.debug('(%s) Translating token %s to command %s', self.name, raw_command, command) if command == 'ENCAP': # Special case for TS6 encapsulated commands (ENCAP), in forms like this: # <- :00A ENCAP * SU 42XAAAAAC :GLolol command = args[1] args = args[2:] - log.debug("(%s) Rewriting incoming ENCAP to command %s (args: %s)", self.irc.name, command, args) + log.debug("(%s) Rewriting incoming ENCAP to command %s (args: %s)", self.name, command, args) try: func = getattr(self, 'handle_'+command.lower()) @@ -78,46 +209,51 @@ class IRCS2SProtocol(Protocol): if parsed_args is not None: return [sender, command, parsed_args] - def handle_privmsg(self, source, command, args): - """Handles incoming PRIVMSG/NOTICE.""" - # TS6: - # <- :70MAAAAAA PRIVMSG #dev :afasfsa - # <- :70MAAAAAA NOTICE 0ALAAAAAA :afasfsa - # P10: - # <- ABAAA P AyAAA :privmsg text - # <- ABAAA O AyAAA :notice text - target = self._getUid(args[0]) - - # Coerse =#channel from Charybdis op moderated +z to @#channel. - if target.startswith('='): - target = '@' + target[1:] - - # We use lowercase channels internally, but uppercase UIDs. - # Strip the target of leading prefix modes (for targets like @#channel) - # before checking whether it's actually a channel. - split_channel = target.split('#', 1) - if len(split_channel) >= 2 and utils.isChannel('#' + split_channel[1]): - # Note: don't mess with the case of the channel prefix, or ~#channel - # messages will break on RFC1459 casemapping networks (it becomes ^#channel - # instead). - target = '#'.join((split_channel[0], self.irc.toLower(split_channel[1]))) - log.debug('(%s) Normalizing channel target %s to %s', self.irc.name, args[0], target) - - return {'target': target, 'text': args[1]} - - handle_notice = handle_privmsg - def check_nick_collision(self, nick): """ Nick collision checker. """ - uid = self.irc.nickToUid(nick) + uid = self.nick_to_uid(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, + log.info('(%s) Nick collision on %s/%s, forwarding this to plugins', self.name, uid, nick) - self.irc.callHooks([self.irc.sid, 'SAVE', {'target': uid}]) + self.call_hooks([self.sid, 'SAVE', {'target': uid}]) + + def handle_away(self, numeric, command, args): + """Handles incoming AWAY messages.""" + # TS6: + # <- :6ELAAAAAB AWAY :Auto-away + # P10: + # <- ABAAA A :blah + # <- ABAAA A + try: + self.users[numeric].away = text = args[0] + except IndexError: # User is unsetting away status + self.users[numeric].away = text = '' + return {'text': text} + + def handle_invite(self, numeric, command, args): + """Handles incoming INVITEs.""" + # TS6: + # <- :70MAAAAAC INVITE 0ALAAAAAA #blah 12345 + # P10: + # <- ABAAA I PyLink-devel #services 1460948992 + # Note that the target is a nickname, not a numeric. + + target = self._get_UID(args[0]) + channel = self.to_lower(args[1]) + + curtime = int(time.time()) + try: + ts = int(args[2]) + except IndexError: + ts = curtime + + ts = ts or curtime # Treat 0 timestamps (e.g. inspircd) as the current time. + + return {'target': target, 'channel': channel, 'ts': ts} def handle_kill(self, source, command, args): """Handles incoming KILLs.""" @@ -127,9 +263,9 @@ class IRCS2SProtocol(Protocol): # removed from our user list. # If not, we have to assume that KILL = QUIT and remove them # ourselves. - data = self.irc.users.get(killed) + data = self.users.get(killed) if data: - self.removeClient(killed) + self._remove_client(killed) # TS6-style kills look something like this: # <- :GL KILL 38QAAAAAA :hidden-1C620195!GL (test) @@ -138,7 +274,7 @@ class IRCS2SProtocol(Protocol): try: # Get the nick or server name of the caller. - killer = self.irc.getFriendlyName(source) + killer = self.get_friendly_name(source) except KeyError: # Killer was... neither? We must have aliens or something. Fallback # to the given "UID". @@ -151,26 +287,79 @@ class IRCS2SProtocol(Protocol): return {'target': killed, 'text': killmsg, 'userdata': data} + def handle_part(self, source, command, args): + """Handles incoming PART commands.""" + channels = self.to_lower(args[0]).split(',') + + for channel in channels: + self.channels[channel].remove_user(source) + try: + self.users[source].channels.discard(channel) + except KeyError: + log.debug("(%s) handle_part: KeyError trying to remove %r from %r's channel list?", self.name, channel, source) + + try: + reason = args[1] + except IndexError: + reason = '' + + # Clear empty non-permanent channels. + if not (self.channels[channel].users or ((self.cmodes.get('permanent'), None) in self.channels[channel].modes)): + del self.channels[channel] + + return {'channels': channels, 'text': reason} + + def handle_pong(self, source, command, args): + """Handles incoming PONG commands.""" + if source == self.uplink: + self.lastping = time.time() + + def handle_privmsg(self, source, command, args): + """Handles incoming PRIVMSG/NOTICE.""" + # TS6: + # <- :70MAAAAAA PRIVMSG #dev :afasfsa + # <- :70MAAAAAA NOTICE 0ALAAAAAA :afasfsa + # P10: + # <- ABAAA P AyAAA :privmsg text + # <- ABAAA O AyAAA :notice text + target = self._get_UID(args[0]) + + # Coerse =#channel from Charybdis op moderated +z to @#channel. + if target.startswith('='): + target = '@' + target[1:] + + # We use lowercase channels internally, but uppercase UIDs. + # Strip the target of leading prefix modes (for targets like @#channel) + # before checking whether it's actually a channel. + + split_channel = target.split('#', 1) + if len(split_channel) >= 2 and utils.isChannel('#' + split_channel[1]): + # Note: don't mess with the case of the channel prefix, or ~#channel + # messages will break on RFC1459 casemapping networks (it becomes ^#channel + # instead). + target = '#'.join((split_channel[0], self.to_lower(split_channel[1]))) + log.debug('(%s) Normalizing channel target %s to %s', self.name, args[0], target) + + return {'target': target, 'text': args[1]} + + handle_notice = handle_privmsg + + def handle_quit(self, numeric, command, args): + """Handles incoming QUIT commands.""" + # TS6: + # <- :1SRAAGB4T QUIT :Quit: quit message goes here + # P10: + # <- ABAAB Q :Killed (GL_ (bangbang)) + self._remove_client(numeric) + return {'text': args[0]} + def handle_squit(self, numeric, command, args): """Handles incoming SQUITs.""" return self._squit(numeric, command, args) - def handle_away(self, numeric, command, args): - """Handles incoming AWAY messages.""" - # TS6: - # <- :6ELAAAAAB AWAY :Auto-away - # P10: - # <- ABAAA A :blah - # <- ABAAA A - try: - self.irc.users[numeric].away = text = args[0] - except IndexError: # User is unsetting away status - self.irc.users[numeric].away = text = '' - return {'text': text} - - def handle_version(self, numeric, command, args): - """Handles requests for the PyLink server version.""" - return {} # See coremods/handlers.py for how this hook is used + def handle_time(self, numeric, command, args): + """Handles incoming /TIME requests.""" + return {'target': args[0]} def handle_whois(self, numeric, command, args): """Handles incoming WHOIS commands..""" @@ -184,22 +373,8 @@ class IRCS2SProtocol(Protocol): # WHOIS commands received are for us, since we don't host any real servers # to route it to. - return {'target': self._getUid(args[-1])} + return {'target': self._get_UID(args[-1])} - def handle_quit(self, numeric, command, args): - """Handles incoming QUIT commands.""" - # TS6: - # <- :1SRAAGB4T QUIT :Quit: quit message goes here - # P10: - # <- ABAAB Q :Killed (GL_ (bangbang)) - self.removeClient(numeric) - return {'text': args[0]} - - def handle_time(self, numeric, command, args): - """Handles incoming /TIME requests.""" - return {'target': args[0]} - - def handle_pong(self, source, command, args): - """Handles incoming PONG commands.""" - if source == self.irc.uplink: - self.irc.lastping = time.time() + def handle_version(self, numeric, command, args): + """Handles requests for the PyLink server version.""" + return {} # See coremods/handlers.py for how this hook is used diff --git a/protocols/nefarious.py b/protocols/nefarious.py index 9966745..55851d0 100644 --- a/protocols/nefarious.py +++ b/protocols/nefarious.py @@ -6,11 +6,11 @@ from pylinkirc.log import log from pylinkirc.protocols.p10 import * class NefariousProtocol(P10Protocol): - def __init__(self, irc): - super().__init__(irc) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) log.warning("(%s) protocols/nefarious.py has been renamed to protocols/p10.py, which " "now also supports other IRCu variants. Please update your configuration, " "as this migration stub will be removed in a future version.", - self.irc.name) + self.name) Class = NefariousProtocol diff --git a/protocols/p10.py b/protocols/p10.py index 76dd10d..876bb9b 100644 --- a/protocols/p10.py +++ b/protocols/p10.py @@ -151,21 +151,21 @@ class P10Protocol(IRCS2SProtocol): 'FA': 'FAKE' } - def __init__(self, irc): - super().__init__(irc) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) # Dictionary of UID generators (one for each server). self.uidgen = structures.KeyedDefaultdict(P10UIDGenerator) # SID generator for P10. - self.sidgen = P10SIDGenerator(irc) + self.sidgen = P10SIDGenerator(self) self.hook_map = {'END_OF_BURST': 'ENDBURST', 'OPMODE': 'MODE', 'CLEARMODE': 'MODE', 'BURST': 'JOIN'} self.protocol_caps |= {'slash-in-hosts', 'underscore-in-hosts'} - def _send(self, source, text, **kwargs): - self.irc.send("%s %s" % (source, text), **kwargs) + def _send_with_prefix(self, source, text, **kwargs): + self.send("%s %s" % (source, text), **kwargs) @staticmethod def access_sort(key): @@ -240,7 +240,7 @@ class P10Protocol(IRCS2SProtocol): ### COMMANDS - def spawnClient(self, nick, ident='null', host='null', realhost=None, modes=set(), + def spawn_client(self, nick, ident='null', host='null', realhost=None, modes=set(), server=None, ip='0.0.0.0', realname=None, ts=None, opertype='IRC Operator', manipulatable=False): """ @@ -261,8 +261,8 @@ class P10Protocol(IRCS2SProtocol): # -2 # -1 - server = server or self.irc.sid - if not self.irc.isInternalServer(server): + server = server or self.sid + if not self.is_internal_server(server): raise ValueError('Server %r is not a PyLink server!' % server) # Create an UIDGenerator instance for every SID, so that each gets @@ -273,16 +273,15 @@ class P10Protocol(IRCS2SProtocol): ts = ts or int(time.time()) realname = realname or conf.conf['bot']['realname'] realhost = realhost or host - raw_modes = self.irc.joinModes(modes) + raw_modes = self.join_modes(modes) - # Initialize an IrcUser instance - u = self.irc.users[uid] = IrcUser(nick, ts, uid, server, ident=ident, host=host, realname=realname, - realhost=realhost, ip=ip, manipulatable=manipulatable, - opertype=opertype) + # Initialize an User instance + u = self.users[uid] = User(nick, ts, uid, server, ident=ident, host=host, realname=realname, + realhost=realhost, ip=ip, manipulatable=manipulatable, opertype=opertype) # Fill in modes and add it to our users index - self.irc.applyModes(uid, modes) - self.irc.servers[server].users.add(uid) + self.apply_modes(uid, modes) + self.servers[server].users.add(uid) # Encode IPs when sending if ip_address(ip).version == 4: @@ -292,7 +291,7 @@ class P10Protocol(IRCS2SProtocol): else: # TODO: propagate IPv6 address, but only if uplink supports it b64ip = 'AAAAAA' - self._send(server, "N {nick} 1 {ts} {ident} {host} {modes} {ip} {uid} " + self._send_with_prefix(server, "N {nick} 1 {ts} {ident} {host} {modes} {ip} {uid} " ":{realname}".format(ts=ts, host=host, nick=nick, ident=ident, uid=uid, modes=raw_modes, ip=b64ip, realname=realname, realhost=realhost)) @@ -301,64 +300,64 @@ class P10Protocol(IRCS2SProtocol): def away(self, source, text): """Sends an AWAY message from a PyLink client. can be an empty string to unset AWAY status.""" - if not self.irc.isInternalClient(source): + if not self.is_internal_client(source): raise LookupError('No such PyLink client exists.') if text: - self._send(source, 'A :%s' % text) + self._send_with_prefix(source, 'A :%s' % text) else: - self._send(source, 'A') - self.irc.users[source].away = text + self._send_with_prefix(source, 'A') + self.users[source].away = text def invite(self, numeric, target, channel): """Sends INVITEs from a PyLink client.""" # Note: we have to send a nick as the target, not a UID. # <- ABAAA I PyLink-devel #services 1460948992 - if not self.irc.isInternalClient(numeric): + if not self.is_internal_client(numeric): raise LookupError('No such PyLink client exists.') - nick = self.irc.users[target].nick + nick = self.users[target].nick - self._send(numeric, 'I %s %s %s' % (nick, channel, self.irc.channels[channel].ts)) + self._send_with_prefix(numeric, 'I %s %s %s' % (nick, channel, self.channels[channel].ts)) def join(self, client, channel): """Joins a PyLink client to a channel.""" # <- ABAAB J #test3 1460744371 - channel = self.irc.toLower(channel) - ts = self.irc.channels[channel].ts + channel = self.to_lower(channel) + ts = self.channels[channel].ts - if not self.irc.isInternalClient(client): + if not self.is_internal_client(client): raise LookupError('No such PyLink client exists.') - if not self.irc.channels[channel].users: + if not self.channels[channel].users: # Empty channels should be created with the CREATE command. - self._send(client, "C {channel} {ts}".format(ts=ts, channel=channel)) + self._send_with_prefix(client, "C {channel} {ts}".format(ts=ts, channel=channel)) else: - self._send(client, "J {channel} {ts}".format(ts=ts, channel=channel)) + self._send_with_prefix(client, "J {channel} {ts}".format(ts=ts, channel=channel)) - self.irc.channels[channel].users.add(client) - self.irc.users[client].channels.add(channel) + self.channels[channel].users.add(client) + self.users[client].channels.add(channel) def kick(self, numeric, channel, target, reason=None): """Sends kicks from a PyLink client/server.""" - if (not self.irc.isInternalClient(numeric)) and \ - (not self.irc.isInternalServer(numeric)): + if (not self.is_internal_client(numeric)) and \ + (not self.is_internal_server(numeric)): raise LookupError('No such PyLink client/server exists.') - channel = self.irc.toLower(channel) + channel = self.to_lower(channel) if not reason: reason = 'No reason given' - cobj = self.irc.channels[channel] + cobj = self.channels[channel] # HACK: prevent kick bounces by sending our kick through the server if # the sender isn't op. - if numeric not in self.irc.servers and (not cobj.isOp(numeric)) and (not cobj.isHalfop(numeric)): - reason = '(%s) %s' % (self.irc.getFriendlyName(numeric), reason) - numeric = self.irc.getServer(numeric) + if numeric not in self.servers and (not cobj.isOp(numeric)) and (not cobj.isHalfop(numeric)): + reason = '(%s) %s' % (self.get_friendly_name(numeric), reason) + numeric = self.get_server(numeric) - self._send(numeric, 'K %s %s :%s' % (channel, target, reason)) + self._send_with_prefix(numeric, 'K %s %s :%s' % (channel, target, reason)) # We can pretend the target left by its own will; all we really care about # is that the target gets removed from the channel userlist, and calling @@ -369,30 +368,30 @@ class P10Protocol(IRCS2SProtocol): """Sends a kill from a PyLink client/server.""" # <- ABAAA D AyAAA :nefarious.midnight.vpn!GL (test) - if (not self.irc.isInternalClient(numeric)) and \ - (not self.irc.isInternalServer(numeric)): + if (not self.is_internal_client(numeric)) and \ + (not self.is_internal_server(numeric)): raise LookupError('No such PyLink client/server exists.') - self._send(numeric, 'D %s :Killed (%s)' % (target, reason)) - self.removeClient(target) + self._send_with_prefix(numeric, 'D %s :Killed (%s)' % (target, reason)) + self._remove_client(target) def knock(self, numeric, target, text): raise NotImplementedError('KNOCK is not supported on P10.') def message(self, numeric, target, text): """Sends a PRIVMSG from a PyLink client.""" - if not self.irc.isInternalClient(numeric): + if not self.is_internal_client(numeric): raise LookupError('No such PyLink client exists.') - self._send(numeric, 'P %s :%s' % (target, text)) + self._send_with_prefix(numeric, 'P %s :%s' % (target, text)) def mode(self, numeric, target, modes, ts=None): """Sends mode changes from a PyLink client/server.""" # <- ABAAA M GL -w # <- ABAAA M #test +v ABAAB 1460747615 - if (not self.irc.isInternalClient(numeric)) and \ - (not self.irc.isInternalServer(numeric)): + if (not self.is_internal_client(numeric)) and \ + (not self.is_internal_server(numeric)): raise LookupError('No such PyLink client/server exists.') modes = list(modes) @@ -404,13 +403,13 @@ class P10Protocol(IRCS2SProtocol): is_cmode = utils.isChannel(target) if is_cmode: # Channel mode changes have a trailing TS. User mode changes do not. - cobj = self.irc.channels[self.irc.toLower(target)] + cobj = self.channels[self.to_lower(target)] ts = ts or cobj.ts # HACK: prevent mode bounces by sending our mode through the server if # the sender isn't op. - if numeric not in self.irc.servers and (not cobj.isOp(numeric)) and (not cobj.isHalfop(numeric)): - numeric = self.irc.getServer(numeric) + if numeric not in self.servers and (not cobj.isOp(numeric)) and (not cobj.isHalfop(numeric)): + numeric = self.get_server(numeric) # Wrap modes: start with max bufsize and subtract the lengths of the source, target, # mode command, and whitespace. @@ -418,76 +417,76 @@ class P10Protocol(IRCS2SProtocol): real_target = target else: - assert target in self.irc.users, "Unknown mode target %s" % target + assert target in self.users, "Unknown mode target %s" % target # P10 uses nicks in user MODE targets, NOT UIDs. ~GL - real_target = self.irc.users[target].nick + real_target = self.users[target].nick - self.irc.applyModes(target, modes) + self.apply_modes(target, modes) while modes[:12]: - joinedmodes = self.irc.joinModes([m for m in modes[:12]]) + joinedmodes = self.join_modes([m for m in modes[:12]]) if is_cmode: - for wrapped_modes in self.irc.wrapModes(modes[:12], bufsize): - self._send(numeric, 'M %s %s %s' % (real_target, wrapped_modes, ts)) + for wrapped_modes in self.wrap_modes(modes[:12], bufsize): + self._send_with_prefix(numeric, 'M %s %s %s' % (real_target, wrapped_modes, ts)) else: - self._send(numeric, 'M %s %s' % (real_target, joinedmodes)) + self._send_with_prefix(numeric, 'M %s %s' % (real_target, joinedmodes)) modes = modes[12:] def nick(self, numeric, newnick): """Changes the nick of a PyLink client.""" # <- ABAAA N GL_ 1460753763 - if not self.irc.isInternalClient(numeric): + if not self.is_internal_client(numeric): raise LookupError('No such PyLink client exists.') - self._send(numeric, 'N %s %s' % (newnick, int(time.time()))) - self.irc.users[numeric].nick = newnick + self._send_with_prefix(numeric, 'N %s %s' % (newnick, int(time.time()))) + self.users[numeric].nick = newnick # Update the NICK TS. - self.irc.users[numeric].ts = int(time.time()) + self.users[numeric].ts = int(time.time()) def numeric(self, source, numeric, target, text): """Sends raw numerics from a server to a remote client. This is used for WHOIS replies.""" # <- AB 311 AyAAA GL ~gl nefarious.midnight.vpn * :realname - self._send(source, '%s %s %s' % (numeric, target, text)) + self._send_with_prefix(source, '%s %s %s' % (numeric, target, text)) def notice(self, numeric, target, text): """Sends a NOTICE from a PyLink client or server.""" - if (not self.irc.isInternalClient(numeric)) and \ - (not self.irc.isInternalServer(numeric)): + if (not self.is_internal_client(numeric)) and \ + (not self.is_internal_server(numeric)): raise LookupError('No such PyLink client/server exists.') - self._send(numeric, 'O %s :%s' % (target, text)) + self._send_with_prefix(numeric, 'O %s :%s' % (target, text)) def part(self, client, channel, reason=None): """Sends a part from a PyLink client.""" - channel = self.irc.toLower(channel) + channel = self.to_lower(channel) - if not self.irc.isInternalClient(client): + if not self.is_internal_client(client): raise LookupError('No such PyLink client exists.') msg = "L %s" % channel if reason: msg += " :%s" % reason - self._send(client, msg) + self._send_with_prefix(client, msg) self.handle_part(client, 'PART', [channel]) def ping(self, source=None, target=None): """Sends a PING to a target server. Periodic PINGs are sent to our uplink automatically by the Irc() internals; plugins shouldn't have to use this.""" - source = source or self.irc.sid + source = source or self.sid if source is None: return if target is not None: - self._send(source, 'G %s %s' % (source, target)) + self._send_with_prefix(source, 'G %s %s' % (source, target)) else: - self._send(source, 'G %s' % source) + self._send_with_prefix(source, 'G %s' % source) def quit(self, numeric, reason): """Quits a PyLink client.""" - if self.irc.isInternalClient(numeric): - self._send(numeric, "Q :%s" % reason) - self.removeClient(numeric) + if self.is_internal_client(numeric): + self._send_with_prefix(numeric, "Q :%s" % reason) + self._remove_client(numeric) else: raise LookupError("No such PyLink client exists.") @@ -500,14 +499,14 @@ class P10Protocol(IRCS2SProtocol): Example uses: sjoin('100', '#test', [('', '100AAABBC'), ('o', 100AAABBB'), ('v', '100AAADDD')]) - sjoin(self.irc.sid, '#test', [('o', self.irc.pseudoclient.uid)]) + sjoin(self.sid, '#test', [('o', self.pseudoclient.uid)]) """ # <- AB B #test 1460742014 +tnl 10 ABAAB,ABAAA:o :%*!*@other.bad.host ~ *!*@bad.host - channel = self.irc.toLower(channel) - server = server or self.irc.sid + channel = self.to_lower(channel) + server = server or self.sid assert users, "sjoin: No users sent?" - log.debug('(%s) sjoin: got %r for users', self.irc.name, users) + log.debug('(%s) sjoin: got %r for users', self.name, users) if not server: raise LookupError('No such PyLink client exists.') @@ -515,8 +514,8 @@ class P10Protocol(IRCS2SProtocol): # <- AB B #test 1460742014 +tnl 10 ABAAB,ABAAA:o :%*!*@other.bad.host *!*@bad.host # <- AB B #test2 1460743539 +l 10 ABAAA:vo :%*!*@bad.host # <- AB B #test 1460747615 ABAAA:o :% ~ *!*@test.host - modes = modes or self.irc.channels[channel].modes - orig_ts = self.irc.channels[channel].ts + modes = modes or self.channels[channel].modes + orig_ts = self.channels[channel].ts ts = ts or orig_ts bans = [] @@ -525,8 +524,8 @@ class P10Protocol(IRCS2SProtocol): for mode in modes: modechar = mode[0][-1] # Store bans and exempts in separate lists for processing, but don't reset bans that have already been set. - if modechar in self.irc.cmodes['*A']: - if (modechar, mode[1]) not in self.irc.channels[channel].modes: + if modechar in self.cmodes['*A']: + if (modechar, mode[1]) not in self.channels[channel].modes: if modechar == 'b': bans.append(mode[1]) elif modechar == 'e': @@ -534,7 +533,7 @@ class P10Protocol(IRCS2SProtocol): else: regularmodes.append(mode) - log.debug('(%s) sjoin: bans: %s, exempts: %s, other modes: %s', self.irc.name, bans, exempts, regularmodes) + log.debug('(%s) sjoin: bans: %s, exempts: %s, other modes: %s', self.name, bans, exempts, regularmodes) changedmodes = set(modes) changedusers = [] @@ -546,7 +545,7 @@ class P10Protocol(IRCS2SProtocol): msgprefix = '{sid} B {channel} {ts} '.format(sid=server, channel=channel, ts=ts) if regularmodes: - msgprefix += '%s ' % self.irc.joinModes(regularmodes) + msgprefix += '%s ' % self.join_modes(regularmodes) last_prefixes = '' for userpair in users: @@ -557,7 +556,7 @@ class P10Protocol(IRCS2SProtocol): # Keep track of all the users and modes that are added. namelist is used # to track what we actually send to the IRCd. changedusers.append(user) - log.debug('(%s) sjoin: adding %s:%s to namelist', self.irc.name, user, prefixes) + log.debug('(%s) sjoin: adding %s:%s to namelist', self.name, user, prefixes) if prefixes and prefixes != last_prefixes: namelist.append('%s:%s' % (user, prefixes)) @@ -569,10 +568,10 @@ class P10Protocol(IRCS2SProtocol): for prefix in prefixes: changedmodes.add(('+%s' % prefix, user)) - self.irc.users[user].channels.add(channel) + self.users[user].channels.add(channel) else: if namelist: - log.debug('(%s) sjoin: got %r for namelist', self.irc.name, namelist) + log.debug('(%s) sjoin: got %r for namelist', self.name, namelist) # Flip the (prefixmodes, user) pairs in users, and save it as a dict for easy lookup # later of what modes each target user should have. @@ -581,14 +580,14 @@ class P10Protocol(IRCS2SProtocol): # Wrap all users and send them to prevent cutoff. Subtract 4 off the maximum # buf size to account for user prefix data that may be re-added (e.g. ":ohv") for linenum, wrapped_msg in \ - enumerate(utils.wrapArguments(msgprefix, namelist, S2S_BUFSIZE-1-len(self.irc.prefixmodes), + enumerate(utils.wrapArguments(msgprefix, namelist, S2S_BUFSIZE-1-len(self.prefixmodes), separator=',')): if linenum: # Implies "if linenum > 0" # XXX: Ugh, this postprocessing sucks, but we have to make sure that mode prefixes are accounted # for in the burst. - wrapped_args = self.parseArgs(wrapped_msg.split(" ")) + wrapped_args = self.parse_args(wrapped_msg.split(" ")) wrapped_namelist = wrapped_args[-1].split(',') - log.debug('(%s) sjoin: wrapped args: %s (post-wrap fixing)', self.irc.name, + log.debug('(%s) sjoin: wrapped args: %s (post-wrap fixing)', self.name, wrapped_args) # If the first UID was supposed to have a prefix mode attached, re-add it here @@ -596,20 +595,20 @@ class P10Protocol(IRCS2SProtocol): # XXX: I'm not sure why the prefix list has to be reversed for it to match the # original string... first_prefix = names_dict.get(first_uid, '')[::-1] - log.debug('(%s) sjoin: prefixes for first user %s: %s (post-wrap fixing)', self.irc.name, + log.debug('(%s) sjoin: prefixes for first user %s: %s (post-wrap fixing)', self.name, first_uid, first_prefix) if (':' not in first_uid) and first_prefix: - log.debug('(%s) sjoin: re-adding prefix %s to user %s (post-wrap fixing)', self.irc.name, + log.debug('(%s) sjoin: re-adding prefix %s to user %s (post-wrap fixing)', self.name, first_uid, first_prefix) wrapped_namelist[0] += ':%s' % prefixes wrapped_msg = ' '.join(wrapped_args[:-1]) wrapped_msg += ' ' wrapped_msg += ','.join(wrapped_namelist) - self.irc.send(wrapped_msg) + self.send(wrapped_msg) - self.irc.channels[channel].users.update(changedusers) + self.channels[channel].users.update(changedusers) # Technically we can send bans together with the above user introductions, but # it's easier to line wrap them separately. @@ -617,16 +616,16 @@ class P10Protocol(IRCS2SProtocol): msgprefix += ':%' # Ban string starts with a % if there is anything if bans: for wrapped_msg in utils.wrapArguments(msgprefix, bans, S2S_BUFSIZE): - self.irc.send(wrapped_msg) + self.send(wrapped_msg) if exempts: # Now add exempts, which are separated from the ban list by a single argument "~". msgprefix += ' ~ ' for wrapped_msg in utils.wrapArguments(msgprefix, exempts, S2S_BUFSIZE): - self.irc.send(wrapped_msg) + self.send(wrapped_msg) self.updateTS(server, channel, ts, changedmodes) - def spawnServer(self, name, sid=None, uplink=None, desc=None, endburst_delay=0): + def spawn_server(self, name, sid=None, uplink=None, desc=None, endburst_delay=0): """ Spawns a server off a PyLink server. desc (server description) defaults to the one in the config. uplink defaults to the main PyLink @@ -637,39 +636,39 @@ class P10Protocol(IRCS2SProtocol): option will be ignored if given. """ # <- SERVER nefarious.midnight.vpn 1 1460673022 1460673239 J10 ABP]] +h6 :Nefarious2 test server - uplink = uplink or self.irc.sid + uplink = uplink or self.sid name = name.lower() - desc = desc or self.irc.serverdata.get('serverdesc') or conf.conf['bot']['serverdesc'] + desc = desc or self.serverdata.get('serverdesc') or conf.conf['bot']['serverdesc'] if sid is None: # No sid given; generate one! sid = self.sidgen.next_sid() assert len(sid) == 2, "Incorrect SID length" - if sid in self.irc.servers: + if sid in self.servers: raise ValueError('A server with SID %r already exists!' % sid) - for server in self.irc.servers.values(): + for server in self.servers.values(): if name == server.name: raise ValueError('A server named %r already exists!' % name) - if not self.irc.isInternalServer(uplink): + if not self.is_internal_server(uplink): raise ValueError('Server %r is not a PyLink server!' % uplink) if not utils.isServerName(name): raise ValueError('Invalid server name %r' % name) - self._send(uplink, 'SERVER %s 1 %s %s P10 %s]]] +h6 :%s' % \ - (name, self.irc.start_ts, int(time.time()), sid, desc)) + self._send_with_prefix(uplink, 'SERVER %s 1 %s %s P10 %s]]] +h6 :%s' % \ + (name, self.start_ts, int(time.time()), sid, desc)) - self.irc.servers[sid] = IrcServer(uplink, name, internal=True, desc=desc) + self.servers[sid] = Server(uplink, name, internal=True, desc=desc) return sid def squit(self, source, target, text='No reason given'): """SQUITs a PyLink server.""" # <- ABAAE SQ nefarious.midnight.vpn 0 :test - targetname = self.irc.servers[target].name + targetname = self.servers[target].name - self._send(source, 'SQ %s 0 :%s' % (targetname, text)) + self._send_with_prefix(source, 'SQ %s 0 :%s' % (targetname, text)) self.handle_squit(source, 'SQUIT', [target, text]) def topic(self, numeric, target, text): @@ -677,41 +676,41 @@ class P10Protocol(IRCS2SProtocol): # <- ABAAA T #test GL!~gl@nefarious.midnight.vpn 1460852591 1460855795 :blah # First timestamp is channel creation time, second is current time, - if not self.irc.isInternalClient(numeric): + if not self.is_internal_client(numeric): raise LookupError('No such PyLink client exists.') - sendername = self.irc.getHostmask(numeric) + sendername = self.get_hostmask(numeric) - creationts = self.irc.channels[target].ts + creationts = self.channels[target].ts - self._send(numeric, 'T %s %s %s %s :%s' % (target, sendername, creationts, + self._send_with_prefix(numeric, 'T %s %s %s %s :%s' % (target, sendername, creationts, int(time.time()), text)) - self.irc.channels[target].topic = text - self.irc.channels[target].topicset = True + self.channels[target].topic = text + self.channels[target].topicset = True - def topicBurst(self, numeric, target, text): + def topic_burst(self, numeric, target, text): """Sends a TOPIC change from a PyLink server.""" # <- AB T #test GL!~gl@nefarious.midnight.vpn 1460852591 1460855795 :blah - if not self.irc.isInternalServer(numeric): + if not self.is_internal_server(numeric): raise LookupError('No such PyLink server exists.') - sendername = self.irc.servers[numeric].name + sendername = self.servers[numeric].name - creationts = self.irc.channels[target].ts + creationts = self.channels[target].ts - self._send(numeric, 'T %s %s %s %s :%s' % (target, sendername, creationts, + self._send_with_prefix(numeric, 'T %s %s %s %s :%s' % (target, sendername, creationts, int(time.time()), text)) - self.irc.channels[target].topic = text - self.irc.channels[target].topicset = True + self.channels[target].topic = text + self.channels[target].topicset = True - def updateClient(self, target, field, text): + def update_client(self, target, field, text): """Updates the ident or host of any connected client.""" - uobj = self.irc.users[target] + uobj = self.users[target] - ircd = self.irc.serverdata.get('p10_ircd', 'nefarious').lower() + ircd = self.serverdata.get('p10_ircd', 'nefarious').lower() - if self.irc.isInternalClient(target): + if self.is_internal_client(target): # Host changing via SETHOST is only supported on nefarious and snircd. if ircd not in ('nefarious', 'snircd'): raise NotImplementedError("Host changing for internal clients (via SETHOST) is only " @@ -737,28 +736,28 @@ class P10Protocol(IRCS2SProtocol): "only available on nefarious, and we're using p10_ircd=%r" % ircd) # Use FAKE (FA) for external clients. - self._send(self.irc.sid, 'FA %s %s' % (target, text)) + self._send_with_prefix(self.sid, 'FA %s %s' % (target, text)) # Save the host change as a user mode (this is what P10 does on bursts), # so further host checks work. - self.irc.applyModes(target, [('+f', text)]) - self.mode(self.irc.sid, target, [('+x', None)]) + self.apply_modes(target, [('+f', text)]) + self.mode(self.sid, target, [('+x', None)]) else: raise NotImplementedError("Changing field %r of a client is " "unsupported by this protocol." % field) # P10 cloaks aren't as simple as just replacing the displayed host with the one we're # sending. Check for cloak changes properly. - # Note: we don't need to send any hooks here, check_cloak_change does that for us. - self.check_cloak_change(target) + # Note: we don't need to send any hooks here, _check_cloak_change does that for us. + self._check_cloak_change(target) ### HANDLERS - def connect(self): + def post_connect(self): """Initializes a connection to a server.""" - ts = self.irc.start_ts + ts = self.start_ts - self.irc.send("PASS :%s" % self.irc.serverdata["sendpass"]) + self.send("PASS :%s" % self.serverdata["sendpass"]) # {7S} *** SERVER @@ -771,15 +770,15 @@ class P10Protocol(IRCS2SProtocol): # 7 <-- Mark ourselves as a service with IPv6 support (+s & +6) -GLolol # -1 - name = self.irc.serverdata["hostname"] + name = self.serverdata["hostname"] # Encode our SID using P10 Base64. - self.irc.sid = sid = p10b64encode(self.irc.serverdata["sid"]) + self.sid = sid = p10b64encode(self.serverdata["sid"]) - desc = self.irc.serverdata.get('serverdesc') or conf.conf['bot']['serverdesc'] + desc = self.serverdata.get('serverdesc') or conf.conf['bot']['serverdesc'] # Enumerate modes, from https://github.com/evilnet/nefarious2/blob/master/doc/modes.txt - p10_ircd = self.irc.serverdata.get('p10_ircd', 'nefarious').lower() + p10_ircd = self.serverdata.get('p10_ircd', 'nefarious').lower() if p10_ircd == 'nefarious': cmodes = {'delayjoin': 'D', 'registered': 'R', 'key': 'k', 'banexception': 'e', 'redirect': 'L', 'oplevel_apass': 'A', 'oplevel_upass': 'U', @@ -787,7 +786,7 @@ class P10Protocol(IRCS2SProtocol): 'permanent': 'z', 'hidequits': 'Q', 'noctcp': 'C', 'noamsg': 'T', 'blockcolor': 'c', 'stripcolor': 'S', 'had_delayjoin': 'd', 'regonly': 'r', '*A': 'be', '*B': 'AUk', '*C': 'Ll', '*D': 'psmtinrDRaOMNzQCTcSd'} - self.irc.umodes.update({'servprotect': 'k', 'sno_debug': 'g', 'cloak': 'x', 'privdeaf': 'D', + self.umodes.update({'servprotect': 'k', 'sno_debug': 'g', 'cloak': 'x', 'privdeaf': 'D', 'hidechans': 'n', 'deaf_commonchan': 'q', 'bot': 'B', 'deaf': 'd', 'hideoper': 'H', 'hideidle': 'I', 'regdeaf': 'R', 'showwhois': 'W', 'admin': 'a', 'override': 'X', 'noforward': 'L', 'ssl': 'z', @@ -802,7 +801,7 @@ class P10Protocol(IRCS2SProtocol): '*A': 'b', '*B': 'AUk', '*C': 'l', '*D': 'imnpstrDducCMNT'} # From https://www.quakenet.org/help/general/what-user-modes-are-available-on-quakenet # plus my own testing. - self.irc.umodes.update({'servprotect': 'k', 'sno_debug': 'g', 'cloak': 'x', + self.umodes.update({'servprotect': 'k', 'sno_debug': 'g', 'cloak': 'x', 'hidechans': 'n', 'deaf': 'd', 'hideidle': 'I', 'regdeaf': 'R', 'override': 'X', 'registered': 'r', 'cloak_sethost': 'h', 'locop': 'O', '*A': '', '*B': '', '*C': 'h', '*D': 'imnpstrkgxndIRXO'}) @@ -811,18 +810,18 @@ class P10Protocol(IRCS2SProtocol): cmodes = {'oplevel_apass': 'A', 'oplevel_upass': 'U', 'delayjoin': 'D', 'regonly': 'r', 'had_delayjoin': 'd', 'blockcolor': 'c', 'noctcp': 'C', 'registered': 'R', '*A': 'b', '*B': 'AUk', '*C': 'l', '*D': 'imnpstrDdRcC'} - self.irc.umodes.update({'servprotect': 'k', 'sno_debug': 'g', 'cloak': 'x', + self.umodes.update({'servprotect': 'k', 'sno_debug': 'g', 'cloak': 'x', 'deaf': 'd', 'registered': 'r', 'locop': 'O', '*A': '', '*B': '', '*C': '', '*D': 'imnpstrkgxdO'}) - if self.irc.serverdata.get('use_halfop'): + if self.serverdata.get('use_halfop'): cmodes['halfop'] = 'h' - self.irc.prefixmodes['h'] = '%' - self.irc.cmodes.update(cmodes) + self.prefixmodes['h'] = '%' + self.cmodes.update(cmodes) - self.irc.send('SERVER %s 1 %s %s J10 %s]]] +s6 :%s' % (name, ts, ts, sid, desc)) - self._send(sid, "EB") - self.irc.connected.set() + self.send('SERVER %s 1 %s %s J10 %s]]] +s6 :%s' % (name, ts, ts, sid, desc)) + self._send_with_prefix(sid, "EB") + self.connected.set() def handle_server(self, source, command, args): """Handles incoming server introductions.""" @@ -830,11 +829,11 @@ class P10Protocol(IRCS2SProtocol): servername = args[0].lower() sid = args[5][:2] sdesc = args[-1] - self.irc.servers[sid] = IrcServer(source, servername, desc=sdesc) + self.servers[sid] = Server(source, servername, desc=sdesc) - if self.irc.uplink is None: + if self.uplink is None: # If we haven't already found our uplink, this is probably it. - self.irc.uplink = sid + self.uplink = sid return {'name': servername, 'sid': sid, 'text': sdesc} @@ -853,11 +852,11 @@ class P10Protocol(IRCS2SProtocol): realname = args[-1] log.debug('(%s) handle_nick got args: nick=%s ts=%s uid=%s ident=%s ' - 'host=%s realname=%s realhost=%s ip=%s', self.irc.name, nick, ts, uid, + 'host=%s realname=%s realhost=%s ip=%s', self.name, nick, ts, uid, ident, host, realname, realhost, ip) - uobj = self.irc.users[uid] = IrcUser(nick, ts, uid, source, ident, host, realname, realhost, ip) - self.irc.servers[source].users.add(uid) + uobj = self.users[uid] = User(nick, ts, uid, source, ident, host, realname, realhost, ip) + self.servers[source].users.add(uid) # https://github.com/evilnet/nefarious2/blob/master/doc/p10.txt#L708 # Mode list is optional, and can be detected if the 6th argument starts with a +. @@ -865,40 +864,40 @@ class P10Protocol(IRCS2SProtocol): # parameters attached. if args[5].startswith('+'): modes = args[5:-3] - parsedmodes = self.irc.parseModes(uid, modes) - self.irc.applyModes(uid, parsedmodes) + parsedmodes = self.parse_modes(uid, modes) + self.apply_modes(uid, parsedmodes) for modepair in parsedmodes: if modepair[0][-1] == 'r': # Parse account registrations, sent as usermode "+r accountname:TS" accountname = modepair[1].split(':', 1)[0] - self.irc.callHooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': accountname}]) + self.call_hooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': accountname}]) # Call the OPERED UP hook if +o is being added to the mode list. if ('+o', None) in parsedmodes: - self.irc.callHooks([uid, 'CLIENT_OPERED', {'text': 'IRC Operator'}]) + self.call_hooks([uid, 'CLIENT_OPERED', {'text': 'IRC Operator'}]) - self.check_cloak_change(uid) + self._check_cloak_change(uid) return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip} else: # <- ABAAA N GL_ 1460753763 - oldnick = self.irc.users[source].nick - newnick = self.irc.users[source].nick = args[0] + oldnick = self.users[source].nick + newnick = self.users[source].nick = args[0] - self.irc.users[source].ts = ts = int(args[1]) + self.users[source].ts = ts = int(args[1]) # Update the nick TS. return {'newnick': newnick, 'oldnick': oldnick, 'ts': ts} - def check_cloak_change(self, uid): + def _check_cloak_change(self, uid): """Checks for cloak changes (ident and host) on the given UID.""" - uobj = self.irc.users[uid] + uobj = self.users[uid] ident = uobj.ident modes = dict(uobj.modes) - log.debug('(%s) check_cloak_change: modes of %s are %s', self.irc.name, uid, modes) + log.debug('(%s) _check_cloak_change: modes of %s are %s', self.name, uid, modes) if 'x' not in modes: # +x isn't set, so cloaking is disabled. newhost = uobj.realhost @@ -912,7 +911,7 @@ class P10Protocol(IRCS2SProtocol): # +f represents another way of setting vHosts, via a command called FAKE. # Atheme uses this for vHosts, afaik. newhost = modes['f'] - elif uobj.services_account and self.irc.serverdata.get('use_account_cloaks'): + elif uobj.services_account and self.serverdata.get('use_account_cloaks'): # The user is registered. However, if account cloaks are enabled, we have to figure # out their new cloaked host. There can be oper cloaks and user cloaks, each with # a different suffix. Account cloaks take the format of .. @@ -920,24 +919,24 @@ class P10Protocol(IRCS2SProtocol): # someone opered and logged in as "person2" might get cloak "person.opers.somenet.org" # This is a lot of extra configuration on the services' side, but there's nothing else # we can do about it. - if self.irc.serverdata.get('use_oper_account_cloaks') and 'o' in modes: + if self.serverdata.get('use_oper_account_cloaks') and 'o' in modes: try: # These errors should be fatal. - suffix = self.irc.serverdata['oper_cloak_suffix'] + suffix = self.serverdata['oper_cloak_suffix'] except KeyError: raise ProtocolError("(%s) use_oper_account_cloaks was enabled, but " - "oper_cloak_suffix was not defined!" % self.irc.name) + "oper_cloak_suffix was not defined!" % self.name) else: try: - suffix = self.irc.serverdata['cloak_suffix'] + suffix = self.serverdata['cloak_suffix'] except KeyError: raise ProtocolError("(%s) use_account_cloaks was enabled, but " - "cloak_suffix was not defined!" % self.irc.name) + "cloak_suffix was not defined!" % self.name) accountname = uobj.services_account newhost = "%s.%s" % (accountname, suffix) - elif 'C' in modes and self.irc.serverdata.get('use_account_cloaks'): + elif 'C' in modes and self.serverdata.get('use_account_cloaks'): # +C propagates hashed IP cloaks, similar to UnrealIRCd. (thank god we don't # need to generate these ourselves) newhost = modes['C'] @@ -947,9 +946,9 @@ class P10Protocol(IRCS2SProtocol): # Propagate a hostname update to plugins, but only if the changed host is different. if newhost != uobj.host: - self.irc.callHooks([uid, 'CHGHOST', {'target': uid, 'newhost': newhost}]) + self.call_hooks([uid, 'CHGHOST', {'target': uid, 'newhost': newhost}]) if ident != uobj.ident: - self.irc.callHooks([uid, 'CHGIDENT', {'target': uid, 'newident': ident}]) + self.call_hooks([uid, 'CHGIDENT', {'target': uid, 'newident': ident}]) uobj.host = newhost uobj.ident = ident @@ -966,21 +965,21 @@ class P10Protocol(IRCS2SProtocol): # Why is this the way it is? I don't know... -GL target = args[1] - sid = self._getSid(target) + sid = self._get_SID(target) orig_pingtime = args[0][1:] # Strip the !, used to denote a TS instead of a server name. currtime = time.time() timediff = int(time.time() - float(orig_pingtime)) - if self.irc.isInternalServer(sid): + if self.is_internal_server(sid): # Only respond if the target server is ours. No forwarding is needed because # no IRCds can ever connect behind us... - self._send(self.irc.sid, 'Z %s %s %s %s' % (target, orig_pingtime, timediff, currtime), queue=False) + self._send_with_prefix(self.sid, 'Z %s %s %s %s' % (target, orig_pingtime, timediff, currtime), queue=False) def handle_pass(self, source, command, args): """Handles authentication with our uplink.""" # <- PASS :testpass - if args[0] != self.irc.serverdata['recvpass']: + if args[0] != self.serverdata['recvpass']: raise ProtocolError("Error: RECVPASS from uplink does not match configuration!") def handle_burst(self, source, command, args): @@ -1002,8 +1001,8 @@ class P10Protocol(IRCS2SProtocol): # No useful data was sent, ignore. return - channel = self.irc.toLower(args[0]) - chandata = self.irc.channels[channel].deepcopy() + channel = self.to_lower(args[0]) + chandata = self.channels[channel].deepcopy() bans = [] if args[-1].startswith('%'): @@ -1030,7 +1029,7 @@ class P10Protocol(IRCS2SProtocol): # If no modes are given, this will simply be empty. modestring = args[2:-1] if modestring: - parsedmodes = self.irc.parseModes(channel, modestring) + parsedmodes = self.parse_modes(channel, modestring) else: parsedmodes = [] @@ -1039,7 +1038,7 @@ class P10Protocol(IRCS2SProtocol): namelist = [] prefixes = '' userlist = args[-1].split(',') - log.debug('(%s) handle_burst: got userlist %r for %r', self.irc.name, userlist, channel) + log.debug('(%s) handle_burst: got userlist %r for %r', self.name, userlist, channel) if args[-1] != args[1]: # Make sure the user list is the right argument (not the TS). for userpair in userlist: @@ -1052,26 +1051,26 @@ class P10Protocol(IRCS2SProtocol): user, prefixes = userpair.split(':') except ValueError: user = userpair - log.debug('(%s) handle_burst: got mode prefixes %r for user %r', self.irc.name, prefixes, user) + log.debug('(%s) handle_burst: got mode prefixes %r for user %r', self.name, prefixes, user) # Don't crash when we get an invalid UID. - if user not in self.irc.users: + if user not in self.users: log.warning('(%s) handle_burst: tried to introduce user %s not in our user list, ignoring...', - self.irc.name, user) + self.name, user) continue namelist.append(user) - self.irc.users[user].channels.add(channel) + self.users[user].channels.add(channel) # Only save mode changes if the remote has lower TS than us. changedmodes |= {('+%s' % mode, user) for mode in prefixes} - self.irc.channels[channel].users.add(user) + self.channels[channel].users.add(user) # Statekeeping with timestamps their_ts = int(args[1]) - our_ts = self.irc.channels[channel].ts + our_ts = self.channels[channel].ts self.updateTS(source, channel, their_ts, changedmodes) return {'channel': channel, 'users': namelist, 'modes': parsedmodes, 'ts': their_ts, @@ -1090,33 +1089,33 @@ class P10Protocol(IRCS2SProtocol): if args[0] == '0' and command == 'JOIN': # /join 0; part the user from all channels - oldchans = self.irc.users[source].channels.copy() + oldchans = self.users[source].channels.copy() log.debug('(%s) Got /join 0 from %r, channel list is %r', - self.irc.name, source, oldchans) + self.name, source, oldchans) for channel in oldchans: - self.irc.channels[channel].users.discard(source) - self.irc.users[source].channels.discard(channel) + self.channels[channel].users.discard(source) + self.users[source].channels.discard(channel) return {'channels': oldchans, 'text': 'Left all channels.', 'parse_as': 'PART'} else: - channel = self.irc.toLower(args[0]) + channel = self.to_lower(args[0]) if ts: # Only update TS if one was sent. self.updateTS(source, channel, ts) - self.irc.users[source].channels.add(channel) - self.irc.channels[channel].users.add(source) + self.users[source].channels.add(channel) + self.channels[channel].users.add(source) return {'channel': channel, 'users': [source], 'modes': - self.irc.channels[channel].modes, 'ts': ts or int(time.time())} + self.channels[channel].modes, 'ts': ts or int(time.time())} handle_create = handle_join def handle_end_of_burst(self, source, command, args): """Handles end of burst from our uplink.""" # Send EOB acknowledgement; this is required by the P10 specification, # and needed if we want to be able to receive channel messages, etc. - if source == self.irc.uplink: - self._send(self.irc.sid, 'EA') + if source == self.uplink: + self._send_with_prefix(self.sid, 'EA') return {} def handle_mode(self, source, command, args): @@ -1124,106 +1123,68 @@ class P10Protocol(IRCS2SProtocol): # <- ABAAA M GL -w # <- ABAAA M #test +v ABAAB 1460747615 # <- ABAAA OM #test +h ABAAA - target = self._getUid(args[0]) + target = self._get_UID(args[0]) if utils.isChannel(target): - target = self.irc.toLower(target) + target = self.to_lower(target) modestrings = args[1:] - changedmodes = self.irc.parseModes(target, modestrings) - self.irc.applyModes(target, changedmodes) + changedmodes = self.parse_modes(target, modestrings) + self.apply_modes(target, changedmodes) # Call the CLIENT_OPERED hook if +o is being set. - if ('+o', None) in changedmodes and target in self.irc.users: - self.irc.callHooks([target, 'CLIENT_OPERED', {'text': 'IRC Operator'}]) + if ('+o', None) in changedmodes and target in self.users: + self.call_hooks([target, 'CLIENT_OPERED', {'text': 'IRC Operator'}]) - if target in self.irc.users: + if target in self.users: # Target was a user. Check for any cloak changes. - self.check_cloak_change(target) + self._check_cloak_change(target) return {'target': target, 'modes': changedmodes} # OPMODE is like SAMODE on other IRCds, and it follows the same modesetting syntax. handle_opmode = handle_mode - def handle_part(self, source, command, args): - """Handles user parts.""" - # <- ABAAA L #test,#test2 - # <- ABAAA L #test :test - - channels = self.irc.toLower(args[0]).split(',') - for channel in channels: - # We should only get PART commands for channels that exist, right?? - self.irc.channels[channel].removeuser(source) - - try: - self.irc.users[source].channels.discard(channel) - except KeyError: - log.debug("(%s) handle_part: KeyError trying to remove %r from %r's channel list?", - self.irc.name, channel, source) - try: - reason = args[1] - except IndexError: - reason = '' - - # Clear empty non-permanent channels. - if not self.irc.channels[channel].users: - del self.irc.channels[channel] - - return {'channels': channels, 'text': reason} - def handle_kick(self, source, command, args): """Handles incoming KICKs.""" # <- ABAAA K #TEST AyAAA :PyLink-devel - channel = self.irc.toLower(args[0]) + channel = self.to_lower(args[0]) kicked = args[1] self.handle_part(kicked, 'KICK', [channel, args[2]]) # Send PART in response to acknowledge the KICK, per # https://github.com/evilnet/nefarious2/blob/ed12d64/doc/p10.txt#L611-L616 - self._send(kicked, 'L %s :%s' % (channel, args[2])) + self._send_with_prefix(kicked, 'L %s :%s' % (channel, args[2])) return {'channel': channel, 'target': kicked, 'text': args[2]} def handle_topic(self, source, command, args): """Handles TOPIC changes.""" # <- ABAAA T #test GL!~gl@nefarious.midnight.vpn 1460852591 1460855795 :blah - channel = self.irc.toLower(args[0]) + channel = self.to_lower(args[0]) topic = args[-1] - oldtopic = self.irc.channels[channel].topic - self.irc.channels[channel].topic = topic - self.irc.channels[channel].topicset = True + oldtopic = self.channels[channel].topic + self.channels[channel].topic = topic + self.channels[channel].topicset = True return {'channel': channel, 'setter': args[1], 'text': topic, 'oldtopic': oldtopic} - def handle_invite(self, source, command, args): - """Handles incoming INVITEs.""" - # From P10 docs: - # 1 - # 2 - # - note that the target is a nickname, not a numeric. - # <- ABAAA I PyLink-devel #services 1460948992 - target = self._getUid(args[0]) - channel = self.irc.toLower(args[1]) - - return {'target': target, 'channel': channel} - def handle_clearmode(self, numeric, command, args): """Handles CLEARMODE, which is used to clear a channel's modes.""" # <- ABAAA CM #test ovpsmikbl - channel = self.irc.toLower(args[0]) + channel = self.to_lower(args[0]) modes = args[1] # Enumerate a list of our existing modes, including prefix modes. - existing = list(self.irc.channels[channel].modes) - for pmode, userlist in self.irc.channels[channel].prefixmodes.items(): + existing = list(self.channels[channel].modes) + for pmode, userlist in self.channels[channel].prefixmodes.items(): # Expand the prefix modes lists to individual ('o', 'UID') mode pairs. - modechar = self.irc.cmodes.get(pmode) + modechar = self.cmodes.get(pmode) existing += [(modechar, user) for user in userlist] # Back up the channel state. - oldobj = self.irc.channels[channel].deepcopy() + oldobj = self.channels[channel].deepcopy() changedmodes = [] @@ -1233,14 +1194,14 @@ class P10Protocol(IRCS2SProtocol): # Check if each mode matches any that we're unsetting. if modechar in modes: - if modechar in (self.irc.cmodes['*A']+self.irc.cmodes['*B']+''.join(self.irc.prefixmodes.keys())): + if modechar in (self.cmodes['*A']+self.cmodes['*B']+''.join(self.prefixmodes.keys())): # Mode is a list mode, prefix mode, or one that always takes a parameter when unsetting. changedmodes.append(('-%s' % modechar, data)) else: # Mode does not take an argument when unsetting. changedmodes.append(('-%s' % modechar, None)) - self.irc.applyModes(channel, changedmodes) + self.apply_modes(channel, changedmodes) return {'target': channel, 'modes': changedmodes, 'channeldata': oldobj} def handle_account(self, numeric, command, args): @@ -1250,7 +1211,7 @@ class P10Protocol(IRCS2SProtocol): target = args[0] - if self.irc.serverdata.get('use_extended_accounts'): + if self.serverdata.get('use_extended_accounts'): # Registration: <- AA AC ABAAA R GL 1459019072 # Logout: <- AA AC ABAAA U @@ -1274,10 +1235,10 @@ class P10Protocol(IRCS2SProtocol): accountname = args[1] # Call this manually because we need the UID to be the sender. - self.irc.callHooks([target, 'CLIENT_SERVICES_LOGIN', {'text': accountname}]) + self.call_hooks([target, 'CLIENT_SERVICES_LOGIN', {'text': accountname}]) # Check for any cloak changes now. - self.check_cloak_change(target) + self._check_cloak_change(target) def handle_fake(self, numeric, command, args): """Handles incoming FAKE hostmask changes.""" @@ -1285,10 +1246,10 @@ class P10Protocol(IRCS2SProtocol): text = args[1] # Assume a usermode +f change, and then update the cloak checking. - self.irc.applyModes(target, [('+f', text)]) + self.apply_modes(target, [('+f', text)]) - self.check_cloak_change(target) - # We don't need to send any hooks here, check_cloak_change does that for us. + self._check_cloak_change(target) + # We don't need to send any hooks here, _check_cloak_change does that for us. def handle_svsnick(self, source, command, args): """Handles SVSNICK (forced nickname change attempts).""" diff --git a/protocols/ratbox.py b/protocols/ratbox.py index fbf73cd..472affd 100644 --- a/protocols/ratbox.py +++ b/protocols/ratbox.py @@ -7,20 +7,21 @@ from pylinkirc.protocols.ts6 import * class RatboxProtocol(TS6Protocol): - def __init__(self, irc): - super().__init__(irc) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) # Don't require EUID for Ratbox self.required_caps.discard('EUID') self.hook_map['LOGIN'] = 'CLIENT_SERVICES_LOGIN' self.protocol_caps -= {'slash-in-hosts'} - def connect(self): + def post_connect(self): """Initializes a connection to a server.""" - super().connect() + + super().post_connect() # Note: +r, +e, and +I support will be negotiated on link - self.irc.cmodes = {'op': 'o', 'secret': 's', 'private': 'p', 'noextmsg': 'n', 'moderated': 'm', + self.cmodes = {'op': 'o', 'secret': 's', 'private': 'p', 'noextmsg': 'n', 'moderated': 'm', 'inviteonly': 'i', 'topiclock': 't', 'limit': 'l', 'ban': 'b', 'voice': 'v', 'key': 'k', 'sslonly': 'S', '*A': 'beI', @@ -28,7 +29,7 @@ class RatboxProtocol(TS6Protocol): '*C': 'l', '*D': 'imnpstrS'} - self.irc.umodes = { + self.umodes = { 'invisible': 'i', 'callerid': 'g', 'oper': 'o', 'admin': 'a', 'sno_botfloods': 'b', 'sno_clientconnections': 'c', 'sno_extclientconnections': 'C', 'sno_debug': 'd', 'sno_fullauthblock': 'f', 'sno_skill': 'k', 'locops': 'l', @@ -39,7 +40,7 @@ class RatboxProtocol(TS6Protocol): '*A': '', '*B': '', '*C': '', '*D': 'igoabcCdfklrsuwxyzZD' } - def spawnClient(self, nick, ident='null', host='null', realhost=None, modes=set(), + def spawn_client(self, nick, ident='null', host='null', realhost=None, modes=set(), server=None, ip='0.0.0.0', realname=None, ts=None, opertype=None, manipulatable=False): """ @@ -52,46 +53,47 @@ class RatboxProtocol(TS6Protocol): # parameters: nickname, hopcount, nickTS, umodes, username, visible hostname, IP address, # UID, gecos - server = server or self.irc.sid - if not self.irc.isInternalServer(server): + server = server or self.sid + if not self.is_internal_server(server): raise ValueError('Server %r is not a PyLink server!' % server) uid = self.uidgen[server].next_uid() ts = ts or int(time.time()) realname = realname or conf.conf['bot']['realname'] - raw_modes = self.irc.joinModes(modes) + raw_modes = self.join_modes(modes) orig_realhost = realhost realhost = realhost or host - u = self.irc.users[uid] = IrcUser(nick, ts, uid, server, ident=ident, host=host, realname=realname, + u = self.users[uid] = User(nick, ts, uid, server, ident=ident, host=host, realname=realname, realhost=realhost, ip=ip, manipulatable=manipulatable) - self.irc.applyModes(uid, modes) - self.irc.servers[server].users.add(uid) - self._send(server, "UID {nick} 1 {ts} {modes} {ident} {host} {ip} {uid} " - ":{realname}".format(ts=ts, host=host, - nick=nick, ident=ident, uid=uid, - modes=raw_modes, ip=ip, realname=realname)) + self.apply_modes(uid, modes) + self.servers[server].users.add(uid) + + self._send_with_prefix(server, "UID {nick} 1 {ts} {modes} {ident} {host} {ip} {uid} " + ":{realname}".format(ts=ts, host=host, + nick=nick, ident=ident, uid=uid, + modes=raw_modes, ip=ip, realname=realname)) if orig_realhost: # If real host is specified, send it using ENCAP REALHOST - self._send(uid, "ENCAP * REALHOST %s" % orig_realhost) + self._send_with_prefix(uid, "ENCAP * REALHOST %s" % orig_realhost) return u - def updateClient(self, target, field, text): - """updateClient() stub for ratbox.""" + def update_client(self, target, field, text): + """update_client() stub for ratbox.""" raise NotImplementedError("User data changing is not supported on ircd-ratbox.") def handle_realhost(self, uid, command, args): """Handles real host propagation.""" log.debug('(%s) Got REALHOST %s for %s', args[0], uid) - self.irc.users[uid].realhost = args[0] + self.users[uid].realhost = args[0] def handle_login(self, uid, command, args): """Handles login propagation on burst.""" - self.irc.users[uid].services_account = args[0] + self.users[uid].services_account = args[0] return {'text': args[0]} Class = RatboxProtocol diff --git a/protocols/ts6.py b/protocols/ts6.py index 8249e28..5a67e30 100644 --- a/protocols/ts6.py +++ b/protocols/ts6.py @@ -13,8 +13,8 @@ from pylinkirc.protocols.ts6_common import * S2S_BUFSIZE = 510 class TS6Protocol(TS6BaseProtocol): - def __init__(self, irc): - super().__init__(irc) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.protocol_caps |= {'slash-in-hosts'} self.casemapping = 'rfc1459' self.hook_map = {'SJOIN': 'JOIN', 'TB': 'TOPIC', 'TMODE': 'MODE', 'BMASK': 'MODE', @@ -27,7 +27,7 @@ class TS6Protocol(TS6BaseProtocol): ### OUTGOING COMMANDS - def spawnClient(self, nick, ident='null', host='null', realhost=None, modes=set(), + def spawn_client(self, nick, ident='null', host='null', realhost=None, modes=set(), server=None, ip='0.0.0.0', realname=None, ts=None, opertype='IRC Operator', manipulatable=False): """ @@ -36,9 +36,9 @@ class TS6Protocol(TS6BaseProtocol): Note: No nick collision / valid nickname checks are done here; it is up to plugins to make sure they don't introduce anything invalid. """ - server = server or self.irc.sid + server = server or self.sid - if not self.irc.isInternalServer(server): + if not self.is_internal_server(server): raise ValueError('Server %r is not a PyLink server!' % server) uid = self.uidgen[server].next_uid() @@ -49,14 +49,14 @@ class TS6Protocol(TS6BaseProtocol): ts = ts or int(time.time()) realname = realname or conf.conf['bot']['realname'] realhost = realhost or host - raw_modes = self.irc.joinModes(modes) - u = self.irc.users[uid] = IrcUser(nick, ts, uid, server, ident=ident, host=host, realname=realname, + raw_modes = self.join_modes(modes) + u = self.users[uid] = User(nick, ts, uid, server, ident=ident, host=host, realname=realname, realhost=realhost, ip=ip, manipulatable=manipulatable, opertype=opertype) - self.irc.applyModes(uid, modes) - self.irc.servers[server].users.add(uid) + self.apply_modes(uid, modes) + self.servers[server].users.add(uid) - self._send(server, "EUID {nick} 1 {ts} {modes} {ident} {host} {ip} {uid} " + self._send_with_prefix(server, "EUID {nick} 1 {ts} {modes} {ident} {host} {ip} {uid} " "{realhost} * :{realname}".format(ts=ts, host=host, nick=nick, ident=ident, uid=uid, modes=raw_modes, ip=ip, realname=realname, @@ -66,15 +66,15 @@ class TS6Protocol(TS6BaseProtocol): def join(self, client, channel): """Joins a PyLink client to a channel.""" - channel = self.irc.toLower(channel) + channel = self.to_lower(channel) # JOIN: # parameters: channelTS, channel, '+' (a plus sign) - if not self.irc.isInternalClient(client): - log.error('(%s) Error trying to join %r to %r (no such client exists)', self.irc.name, client, channel) + if not self.is_internal_client(client): + log.error('(%s) Error trying to join %r to %r (no such client exists)', self.name, client, channel) raise LookupError('No such PyLink client exists.') - self._send(client, "JOIN {ts} {channel} +".format(ts=self.irc.channels[channel].ts, channel=channel)) - self.irc.channels[channel].users.add(client) - self.irc.users[client].channels.add(channel) + self._send_with_prefix(client, "JOIN {ts} {channel} +".format(ts=self.channels[channel].ts, channel=channel)) + self.channels[channel].users.add(client) + self.users[client].channels.add(channel) def sjoin(self, server, channel, users, ts=None, modes=set()): """Sends an SJOIN for a group of users to a channel. @@ -85,7 +85,7 @@ class TS6Protocol(TS6BaseProtocol): Example uses: sjoin('100', '#test', [('', '100AAABBC'), ('o', 100AAABBB'), ('v', '100AAADDD')]) - sjoin(self.irc.sid, '#test', [('o', self.irc.pseudoclient.uid)]) + sjoin(self.sid, '#test', [('o', self.pseudoclient.uid)]) """ # https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L821 # parameters: channelTS, channel, simple modes, opt. mode parameters..., nicklist @@ -96,34 +96,34 @@ class TS6Protocol(TS6BaseProtocol): # their status ('@+', '@', '+' or ''), for example: # '@+1JJAAAAAB +2JJAAAA4C 1JJAAAADS'. All users must be behind the source server # so it is not possible to use this message to force users to join a channel. - channel = self.irc.toLower(channel) - server = server or self.irc.sid + channel = self.to_lower(channel) + server = server or self.sid assert users, "sjoin: No users sent?" - log.debug('(%s) sjoin: got %r for users', self.irc.name, users) + log.debug('(%s) sjoin: got %r for users', self.name, users) if not server: raise LookupError('No such PyLink client exists.') - modes = set(modes or self.irc.channels[channel].modes) - orig_ts = self.irc.channels[channel].ts + modes = set(modes or self.channels[channel].modes) + orig_ts = self.channels[channel].ts ts = ts or orig_ts # Get all the ban modes in a separate list. These are bursted using a separate BMASK # command. - banmodes = {k: [] for k in self.irc.cmodes['*A']} + banmodes = {k: [] for k in self.cmodes['*A']} regularmodes = [] - log.debug('(%s) Unfiltered SJOIN modes: %s', self.irc.name, modes) + log.debug('(%s) Unfiltered SJOIN modes: %s', self.name, modes) for mode in modes: modechar = mode[0][-1] - if modechar in self.irc.cmodes['*A']: + if modechar in self.cmodes['*A']: # Mode character is one of 'beIq' - if (modechar, mode[1]) in self.irc.channels[channel].modes: + if (modechar, mode[1]) in self.channels[channel].modes: # Don't reset modes that are already set. continue banmodes[modechar].append(mode[1]) else: regularmodes.append(mode) - log.debug('(%s) Filtered SJOIN modes to be regular modes: %s, banmodes: %s', self.irc.name, regularmodes, banmodes) + log.debug('(%s) Filtered SJOIN modes to be regular modes: %s, banmodes: %s', self.name, regularmodes, banmodes) changedmodes = modes while users[:12]: @@ -135,22 +135,22 @@ class TS6Protocol(TS6BaseProtocol): prefixes, user = userpair prefixchars = '' for prefix in prefixes: - pr = self.irc.prefixmodes.get(prefix) + pr = self.prefixmodes.get(prefix) if pr: prefixchars += pr changedmodes.add(('+%s' % prefix, user)) namelist.append(prefixchars+user) uids.append(user) try: - self.irc.users[user].channels.add(channel) + self.users[user].channels.add(channel) except KeyError: # Not initialized yet? - log.debug("(%s) sjoin: KeyError trying to add %r to %r's channel list?", self.irc.name, channel, user) + log.debug("(%s) sjoin: KeyError trying to add %r to %r's channel list?", self.name, channel, user) users = users[12:] namelist = ' '.join(namelist) - self._send(server, "SJOIN {ts} {channel} {modes} :{users}".format( + self._send_with_prefix(server, "SJOIN {ts} {channel} {modes} :{users}".format( ts=ts, users=namelist, channel=channel, - modes=self.irc.joinModes(regularmodes))) - self.irc.channels[channel].users.update(uids) + modes=self.join_modes(regularmodes))) + self.channels[channel].users.update(uids) # Now, burst bans. # <- :42X BMASK 1424222769 #dev b :*!test@*.isp.net *!badident@* @@ -158,12 +158,12 @@ class TS6Protocol(TS6BaseProtocol): # Max 15-3 = 12 bans per line to prevent cut off. (TS6 allows a max of 15 parameters per # line) if bans: - log.debug('(%s) sjoin: bursting mode %s with bans %s, ts:%s', self.irc.name, bmode, bans, ts) + log.debug('(%s) sjoin: bursting mode %s with bans %s, ts:%s', self.name, bmode, bans, ts) msgprefix = ':{sid} BMASK {ts} {channel} {bmode} :'.format(sid=server, ts=ts, channel=channel, bmode=bmode) # Actually, we cut off at 17 arguments/line, since the prefix and command name don't count. for msg in utils.wrapArguments(msgprefix, bans, S2S_BUFSIZE, max_args_per_line=17): - self.irc.send(msg) + self.send(msg) self.updateTS(server, channel, ts, changedmodes) @@ -172,15 +172,15 @@ class TS6Protocol(TS6BaseProtocol): # c <- :0UYAAAAAA TMODE 0 #a +o 0T4AAAAAC # u <- :0UYAAAAAA MODE 0UYAAAAAA :-Facdefklnou - if (not self.irc.isInternalClient(numeric)) and \ - (not self.irc.isInternalServer(numeric)): + if (not self.is_internal_client(numeric)) and \ + (not self.is_internal_server(numeric)): raise LookupError('No such PyLink client/server exists.') - self.irc.applyModes(target, modes) + self.apply_modes(target, modes) modes = list(modes) if utils.isChannel(target): - ts = ts or self.irc.channels[self.irc.toLower(target)].ts + ts = ts or self.channels[self.to_lower(target)].ts # TMODE: # parameters: channelTS, channel, cmode changes, opt. cmode parameters... @@ -189,54 +189,54 @@ class TS6Protocol(TS6BaseProtocol): msgprefix = ':%s TMODE %s %s ' % (numeric, ts, target) bufsize = S2S_BUFSIZE - len(msgprefix) - for modestr in self.irc.wrapModes(modes, bufsize, max_modes_per_msg=10): - self.irc.send(msgprefix + modestr) + for modestr in self.wrap_modes(modes, bufsize, max_modes_per_msg=10): + self.send(msgprefix + modestr) else: - joinedmodes = self.irc.joinModes(modes) - self._send(numeric, 'MODE %s %s' % (target, joinedmodes)) + joinedmodes = self.join_modes(modes) + self._send_with_prefix(numeric, 'MODE %s %s' % (target, joinedmodes)) - def topicBurst(self, numeric, target, text): + def topic_burst(self, numeric, target, text): """Sends a topic change from a PyLink server. This is usually used on burst.""" - if not self.irc.isInternalServer(numeric): + if not self.is_internal_server(numeric): raise LookupError('No such PyLink server exists.') # TB # capab: TB # source: server # propagation: broadcast # parameters: channel, topicTS, opt. topic setter, topic - ts = self.irc.channels[target].ts - servername = self.irc.servers[numeric].name - self._send(numeric, 'TB %s %s %s :%s' % (target, ts, servername, text)) - self.irc.channels[target].topic = text - self.irc.channels[target].topicset = True + ts = self.channels[target].ts + servername = self.servers[numeric].name + self._send_with_prefix(numeric, 'TB %s %s %s :%s' % (target, ts, servername, text)) + self.channels[target].topic = text + self.channels[target].topicset = True def invite(self, numeric, target, channel): """Sends an INVITE from a PyLink client..""" - if not self.irc.isInternalClient(numeric): + if not self.is_internal_client(numeric): raise LookupError('No such PyLink client exists.') - self._send(numeric, 'INVITE %s %s %s' % (target, channel, self.irc.channels[channel].ts)) + self._send_with_prefix(numeric, 'INVITE %s %s %s' % (target, channel, self.channels[channel].ts)) def knock(self, numeric, target, text): """Sends a KNOCK from a PyLink client.""" - if 'KNOCK' not in self.irc.caps: + if 'KNOCK' not in self.caps: log.debug('(%s) knock: Dropping KNOCK to %r since the IRCd ' - 'doesn\'t support it.', self.irc.name, target) + 'doesn\'t support it.', self.name, target) return - if not self.irc.isInternalClient(numeric): + if not self.is_internal_client(numeric): raise LookupError('No such PyLink client exists.') # No text value is supported here; drop it. - self._send(numeric, 'KNOCK %s' % target) + self._send_with_prefix(numeric, 'KNOCK %s' % target) - def updateClient(self, target, field, text): + def update_client(self, target, field, text): """Updates the hostname of any connected client.""" field = field.upper() if field == 'HOST': - self.irc.users[target].host = text - self._send(self.irc.sid, 'CHGHOST %s :%s' % (target, text)) - if not self.irc.isInternalClient(target): + self.users[target].host = text + self._send_with_prefix(self.sid, 'CHGHOST %s :%s' % (target, text)) + if not self.is_internal_client(target): # If the target isn't one of our clients, send hook payload # for other plugins to listen to. - self.irc.callHooks([self.irc.sid, 'CHGHOST', + self.call_hooks([self.sid, 'CHGHOST', {'target': target, 'newhost': text}]) else: raise NotImplementedError("Changing field %r of a client is " @@ -245,22 +245,22 @@ class TS6Protocol(TS6BaseProtocol): def ping(self, source=None, target=None): """Sends a PING to a target server. Periodic PINGs are sent to our uplink automatically by the Irc() internals; plugins shouldn't have to use this.""" - source = source or self.irc.sid + source = source or self.sid if source is None: return if target is not None: - self._send(source, 'PING %s %s' % (source, target)) + self._send_with_prefix(source, 'PING %s %s' % (source, target)) else: - self._send(source, 'PING %s' % source) + self._send_with_prefix(source, 'PING %s' % source) ### Core / handlers - def connect(self): + def post_connect(self): """Initializes a connection to a server.""" - ts = self.irc.start_ts + ts = self.start_ts self.has_eob = False - f = self.irc.send + f = self.send # https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L80 chary_cmodes = { # TS6 generic modes (note that +p is noknock instead of private): @@ -278,17 +278,17 @@ class TS6Protocol(TS6BaseProtocol): # Now, map all the ABCD type modes: '*A': 'beIq', '*B': 'k', '*C': 'lfj', '*D': 'mnprstFLPQcgzCOAST'} - if self.irc.serverdata.get('use_owner'): + if self.serverdata.get('use_owner'): chary_cmodes['owner'] = 'y' - self.irc.prefixmodes['y'] = '~' - if self.irc.serverdata.get('use_admin'): + self.prefixmodes['y'] = '~' + if self.serverdata.get('use_admin'): chary_cmodes['admin'] = 'a' - self.irc.prefixmodes['a'] = '!' - if self.irc.serverdata.get('use_halfop'): + self.prefixmodes['a'] = '!' + if self.serverdata.get('use_halfop'): chary_cmodes['halfop'] = 'h' - self.irc.prefixmodes['h'] = '%' + self.prefixmodes['h'] = '%' - self.irc.cmodes = chary_cmodes + self.cmodes = chary_cmodes # Define supported user modes chary_umodes = {'deaf': 'D', 'servprotect': 'S', 'admin': 'a', @@ -298,26 +298,26 @@ class TS6Protocol(TS6BaseProtocol): 'cloak': 'x', 'override': 'p', # Now, map all the ABCD type modes: '*A': '', '*B': '', '*C': '', '*D': 'DSaiowsQRgzlxp'} - self.irc.umodes = chary_umodes + self.umodes = chary_umodes # Toggles support of shadowircd/elemental-ircd specific channel modes: # +T (no notice), +u (hidden ban list), +E (no kicks), +J (blocks kickrejoin), # +K (no repeat messages), +d (no nick changes), and user modes: # +B (bot), +C (blocks CTCP), +D (deaf), +V (no invites), +I (hides channel list) - if self.irc.serverdata.get('use_elemental_modes'): + if self.serverdata.get('use_elemental_modes'): elemental_cmodes = {'hiddenbans': 'u', 'nokick': 'E', 'kicknorejoin': 'J', 'repeat': 'K', 'nonick': 'd', 'blockcaps': 'G'} - self.irc.cmodes.update(elemental_cmodes) - self.irc.cmodes['*D'] += ''.join(elemental_cmodes.values()) + self.cmodes.update(elemental_cmodes) + self.cmodes['*D'] += ''.join(elemental_cmodes.values()) elemental_umodes = {'noctcp': 'C', 'deaf': 'D', 'bot': 'B', 'noinvite': 'V', 'hidechans': 'I'} - self.irc.umodes.update(elemental_umodes) - self.irc.umodes['*D'] += ''.join(elemental_umodes.values()) + self.umodes.update(elemental_umodes) + self.umodes['*D'] += ''.join(elemental_umodes.values()) # https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L55 - f('PASS %s TS 6 %s' % (self.irc.serverdata["sendpass"], self.irc.sid)) + f('PASS %s TS 6 %s' % (self.serverdata["sendpass"], self.sid)) # We request the following capabilities (for charybdis): @@ -330,7 +330,7 @@ class TS6Protocol(TS6BaseProtocol): # KNOCK: support for /knock # SAVE: support for SAVE (forces user to UID in nick collision) # SERVICES: adds mode +r (only registered users can join a channel) - # TB: topic burst command; we send this in topicBurst + # TB: topic burst command; we send this in topic_burst # EUID: extended UID command, which includes real hostname + account data info, # and allows sending CHGHOST without ENCAP. # RSFNC: states that we support RSFNC (forced nick changed attempts). XXX: With atheme services, @@ -338,8 +338,8 @@ class TS6Protocol(TS6BaseProtocol): # EOPMOD: supports ETB (extended TOPIC burst) and =#channel messages for opmoderated +z f('CAPAB :QS ENCAP EX CHW IE KNOCK SAVE SERVICES TB EUID RSFNC EOPMOD SAVETS_100') - f('SERVER %s 0 :%s' % (self.irc.serverdata["hostname"], - self.irc.serverdata.get('serverdesc') or conf.conf['bot']['serverdesc'])) + f('SERVER %s 0 :%s' % (self.serverdata["hostname"], + self.serverdata.get('serverdesc') or conf.conf['bot']['serverdesc'])) # Finally, end all the initialization with a PING - that's Charybdis' # way of saying end-of-burst :) @@ -352,7 +352,7 @@ class TS6Protocol(TS6BaseProtocol): """ # <- PASS $somepassword TS 6 :42X - if args[0] != self.irc.serverdata['recvpass']: + if args[0] != self.serverdata['recvpass']: # Check if recvpass is correct raise ProtocolError('Recvpass from uplink server %s does not match configuration!' % servername) @@ -360,12 +360,12 @@ class TS6Protocol(TS6BaseProtocol): raise ProtocolError("Remote protocol version is too old! Is this even TS6?") numeric = args[-1] - log.debug('(%s) Found uplink SID as %r', self.irc.name, numeric) + log.debug('(%s) Found uplink SID as %r', self.name, numeric) # Server name and SID are sent in different messages, so we fill this # with dummy information until we get the actual sid. - self.irc.servers[numeric] = IrcServer(None, '') - self.irc.uplink = numeric + self.servers[numeric] = Server(None, '') + self.uplink = numeric def handle_capab(self, numeric, command, args): """ @@ -374,21 +374,21 @@ class TS6Protocol(TS6BaseProtocol): # We only get a list of keywords here. Charybdis obviously assumes that # we know what modes it supports (indeed, this is a standard list). # <- CAPAB :BAN CHW CLUSTER ENCAP EOPMOD EUID EX IE KLN KNOCK MLOCK QS RSFNC SAVE SERVICES TB UNKLN - self.irc.caps = caps = args[0].split() + self.caps = caps = args[0].split() for required_cap in self.required_caps: if required_cap not in caps: raise ProtocolError('%s not found in TS6 capabilities list; this is required! (got %r)' % (required_cap, caps)) if 'EX' in caps: - self.irc.cmodes['banexception'] = 'e' + self.cmodes['banexception'] = 'e' if 'IE' in caps: - self.irc.cmodes['invex'] = 'I' + self.cmodes['invex'] = 'I' if 'SERVICES' in caps: - self.irc.cmodes['regonly'] = 'r' + self.cmodes['regonly'] = 'r' - log.debug('(%s) self.irc.connected set!', self.irc.name) - self.irc.connected.set() + log.debug('(%s) self.connected set!', self.name) + self.connected.set() def handle_ping(self, source, command, args): """Handles incoming PING commands.""" @@ -405,11 +405,11 @@ class TS6Protocol(TS6BaseProtocol): try: destination = args[1] except IndexError: - destination = self.irc.sid - if self.irc.isInternalServer(destination): - self._send(destination, 'PONG %s %s' % (destination, source), queue=False) + destination = self.sid + if self.is_internal_server(destination): + self._send_with_prefix(destination, 'PONG %s %s' % (destination, source), queue=False) - if destination == self.irc.sid and not self.has_eob: + if destination == self.sid and not self.has_eob: # Charybdis' idea of endburst is just sending a PING. No, really! # https://github.com/charybdis-ircd/charybdis/blob/dc336d1/modules/core/m_server.c#L484-L485 self.has_eob = True @@ -421,18 +421,18 @@ class TS6Protocol(TS6BaseProtocol): """Handles incoming SJOIN commands.""" # parameters: channelTS, channel, simple modes, opt. mode parameters..., nicklist # <- :0UY SJOIN 1451041566 #channel +nt :@0UYAAAAAB - channel = self.irc.toLower(args[1]) - chandata = self.irc.channels[channel].deepcopy() + channel = self.to_lower(args[1]) + chandata = self.channels[channel].deepcopy() userlist = args[-1].split() modestring = args[2:-1] or args[2] - parsedmodes = self.irc.parseModes(channel, modestring) + parsedmodes = self.parse_modes(channel, modestring) namelist = [] # Keep track of other modes that are added due to prefix modes being joined too. changedmodes = set(parsedmodes) - log.debug('(%s) handle_sjoin: got userlist %r for %r', self.irc.name, userlist, channel) + log.debug('(%s) handle_sjoin: got userlist %r for %r', self.name, userlist, channel) for userpair in userlist: # charybdis sends this in the form "@+UID1, +UID2, UID3, @UID4" r = re.search(r'([^\d]*)(.*)', userpair) @@ -440,30 +440,30 @@ class TS6Protocol(TS6BaseProtocol): modeprefix = r.group(1) or '' finalprefix = '' assert user, 'Failed to get the UID from %r; our regex needs updating?' % userpair - log.debug('(%s) handle_sjoin: got modeprefix %r for user %r', self.irc.name, modeprefix, user) + log.debug('(%s) handle_sjoin: got modeprefix %r for user %r', self.name, modeprefix, user) # Don't crash when we get an invalid UID. - if user not in self.irc.users: + if user not in self.users: log.debug('(%s) handle_sjoin: tried to introduce user %s not in our user list, ignoring...', - self.irc.name, user) + self.name, user) continue for m in modeprefix: # Iterate over the mapping of prefix chars to prefixes, and # find the characters that match. - for char, prefix in self.irc.prefixmodes.items(): + for char, prefix in self.prefixmodes.items(): if m == prefix: finalprefix += char namelist.append(user) - self.irc.users[user].channels.add(channel) + self.users[user].channels.add(channel) # Only save mode changes if the remote has lower TS than us. changedmodes |= {('+%s' % mode, user) for mode in finalprefix} - self.irc.channels[channel].users.add(user) + self.channels[channel].users.add(user) # Statekeeping with timestamps their_ts = int(args[0]) - our_ts = self.irc.channels[channel].ts + our_ts = self.channels[channel].ts self.updateTS(servernumeric, channel, their_ts, changedmodes) return {'channel': channel, 'users': namelist, 'modes': parsedmodes, 'ts': their_ts, @@ -476,24 +476,24 @@ class TS6Protocol(TS6BaseProtocol): ts = int(args[0]) if args[0] == '0': # /join 0; part the user from all channels - oldchans = self.irc.users[numeric].channels.copy() + oldchans = self.users[numeric].channels.copy() log.debug('(%s) Got /join 0 from %r, channel list is %r', - self.irc.name, numeric, oldchans) + self.name, numeric, oldchans) for channel in oldchans: - self.irc.channels[channel].users.discard(numeric) - self.irc.users[numeric].channels.discard(channel) + self.channels[channel].users.discard(numeric) + self.users[numeric].channels.discard(channel) return {'channels': oldchans, 'text': 'Left all channels.', 'parse_as': 'PART'} else: - channel = self.irc.toLower(args[1]) + channel = self.to_lower(args[1]) self.updateTS(numeric, channel, ts) - self.irc.users[numeric].channels.add(channel) - self.irc.channels[channel].users.add(numeric) + self.users[numeric].channels.add(channel) + self.channels[channel].users.add(numeric) # We send users and modes here because SJOIN and JOIN both use one hook, # for simplicity's sake (with plugins). return {'channel': channel, 'users': [numeric], 'modes': - self.irc.channels[channel].modes, 'ts': ts} + self.channels[channel].modes, 'ts': ts} def handle_euid(self, numeric, command, args): """Handles incoming EUID commands (user introduction).""" @@ -505,28 +505,28 @@ class TS6Protocol(TS6BaseProtocol): realhost = None log.debug('(%s) handle_euid got args: nick=%s ts=%s uid=%s ident=%s ' - 'host=%s realname=%s realhost=%s ip=%s', self.irc.name, nick, ts, uid, + 'host=%s realname=%s realhost=%s ip=%s', self.name, nick, ts, uid, ident, host, realname, realhost, ip) assert ts != 0, "Bad TS 0 for user %s" % uid if ip == '0': # IP was invalid; something used for services. ip = '0.0.0.0' - self.irc.users[uid] = IrcUser(nick, ts, uid, numeric, ident, host, realname, realhost, ip) + self.users[uid] = User(nick, ts, uid, numeric, ident, host, realname, realhost, ip) - parsedmodes = self.irc.parseModes(uid, [modes]) + parsedmodes = self.parse_modes(uid, [modes]) log.debug('Applying modes %s for %s', parsedmodes, uid) - self.irc.applyModes(uid, parsedmodes) - self.irc.servers[numeric].users.add(uid) + self.apply_modes(uid, parsedmodes) + self.servers[numeric].users.add(uid) # Call the OPERED UP hook if +o is being added to the mode list. if ('+o', None) in parsedmodes: otype = 'Server Administrator' if ('+a', None) in parsedmodes else 'IRC Operator' - self.irc.callHooks([uid, 'CLIENT_OPERED', {'text': otype}]) + self.call_hooks([uid, 'CLIENT_OPERED', {'text': otype}]) # Set the accountname if present if accountname != "*": - self.irc.callHooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': accountname}]) + self.call_hooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': accountname}]) return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip} @@ -555,7 +555,7 @@ class TS6Protocol(TS6BaseProtocol): servername = args[0].lower() sid = args[2] sdesc = args[-1] - self.irc.servers[sid] = IrcServer(numeric, servername, desc=sdesc) + self.servers[sid] = Server(numeric, servername, desc=sdesc) return {'name': servername, 'sid': sid, 'text': sdesc} def handle_server(self, numeric, command, args): @@ -563,35 +563,35 @@ class TS6Protocol(TS6BaseProtocol): Handles 1) incoming legacy (no SID) server introductions, 2) Sending server data in initial connection. """ - if numeric == self.irc.uplink and not self.irc.servers[numeric].name: + if numeric == self.uplink and not self.servers[numeric].name: # <- SERVER charybdis.midnight.vpn 1 :charybdis test server sname = args[0].lower() - log.debug('(%s) Found uplink server name as %r', self.irc.name, sname) - self.irc.servers[numeric].name = sname - self.irc.servers[numeric].desc = args[-1] + log.debug('(%s) Found uplink server name as %r', self.name, sname) + self.servers[numeric].name = sname + self.servers[numeric].desc = args[-1] # According to the TS6 protocol documentation, we should send SVINFO # when we get our uplink's SERVER command. - self.irc.send('SVINFO 6 6 0 :%s' % int(time.time())) + self.send('SVINFO 6 6 0 :%s' % int(time.time())) return # <- :services.int SERVER a.bc 2 :(H) [GL] a servername = args[0].lower() sdesc = args[-1] - self.irc.servers[servername] = IrcServer(numeric, servername, desc=sdesc) + self.servers[servername] = Server(numeric, servername, desc=sdesc) return {'name': servername, 'sid': None, 'text': sdesc} def handle_tmode(self, numeric, command, args): """Handles incoming TMODE commands (channel mode change).""" # <- :42XAAAAAB TMODE 1437450768 #test -c+lkC 3 agte4 # <- :0UYAAAAAD TMODE 0 #a +h 0UYAAAAAD - channel = self.irc.toLower(args[1]) - oldobj = self.irc.channels[channel].deepcopy() + channel = self.to_lower(args[1]) + oldobj = self.channels[channel].deepcopy() modes = args[2:] - changedmodes = self.irc.parseModes(channel, modes) - self.irc.applyModes(channel, changedmodes) + changedmodes = self.parse_modes(channel, modes) + self.apply_modes(channel, changedmodes) ts = int(args[0]) return {'target': channel, 'modes': changedmodes, 'ts': ts, 'channeldata': oldobj} @@ -601,66 +601,54 @@ class TS6Protocol(TS6BaseProtocol): # <- :70MAAAAAA MODE 70MAAAAAA -i+xc target = args[0] modestrings = args[1:] - changedmodes = self.irc.parseModes(target, modestrings) - self.irc.applyModes(target, changedmodes) + changedmodes = self.parse_modes(target, modestrings) + self.apply_modes(target, changedmodes) # Call the OPERED UP hook if +o is being set. if ('+o', None) in changedmodes: - otype = 'Server Administrator' if ('a', None) in self.irc.users[target].modes else 'IRC Operator' - self.irc.callHooks([target, 'CLIENT_OPERED', {'text': otype}]) + otype = 'Server Administrator' if ('a', None) in self.users[target].modes else 'IRC Operator' + self.call_hooks([target, 'CLIENT_OPERED', {'text': otype}]) return {'target': target, 'modes': changedmodes} def handle_tb(self, numeric, command, args): """Handles incoming topic burst (TB) commands.""" # <- :42X TB #chat 1467427448 GL!~gl@127.0.0.1 :test - channel = self.irc.toLower(args[0]) + channel = self.to_lower(args[0]) ts = args[1] setter = args[2] topic = args[-1] - self.irc.channels[channel].topic = topic - self.irc.channels[channel].topicset = True + self.channels[channel].topic = topic + self.channels[channel].topicset = True return {'channel': channel, 'setter': setter, 'ts': ts, 'text': topic} def handle_etb(self, numeric, command, args): """Handles extended topic burst (ETB).""" # <- :00AAAAAAC ETB 0 #test 1470021157 GL :test | abcd # Same as TB, with extra TS and extensions arguments. - channel = self.irc.toLower(args[1]) + channel = self.to_lower(args[1]) ts = args[2] setter = args[3] topic = args[-1] - self.irc.channels[channel].topic = topic - self.irc.channels[channel].topicset = True + self.channels[channel].topic = topic + self.channels[channel].topicset = True return {'channel': channel, 'setter': setter, 'ts': ts, 'text': topic} - def handle_invite(self, numeric, command, args): - """Handles incoming INVITEs.""" - # <- :70MAAAAAC INVITE 0ALAAAAAA #blah 12345 - target = args[0] - channel = self.irc.toLower(args[1]) - try: - ts = args[3] - except IndexError: - ts = int(time.time()) - # We don't actually need to process this; it's just something plugins/hooks can use - return {'target': target, 'channel': channel, 'ts': ts} - def handle_chghost(self, numeric, command, args): """Handles incoming CHGHOST commands.""" - target = self._getUid(args[0]) - self.irc.users[target].host = newhost = args[1] + target = self._get_UID(args[0]) + self.users[target].host = newhost = args[1] return {'target': target, 'newhost': newhost} def handle_bmask(self, numeric, command, args): """Handles incoming BMASK commands (ban propagation on burst).""" # <- :42X BMASK 1424222769 #dev b :*!test@*.isp.net *!badident@* # This is used for propagating bans, not TMODE! - channel = self.irc.toLower(args[1]) + channel = self.to_lower(args[1]) mode = args[2] ts = int(args[0]) modes = [] for ban in args[-1].split(): modes.append(('+%s' % mode, ban)) - self.irc.applyModes(channel, modes) + self.apply_modes(channel, modes) return {'target': channel, 'modes': modes, 'ts': ts} def handle_472(self, numeric, command, args): @@ -681,7 +669,7 @@ class TS6Protocol(TS6BaseProtocol): log.warning('(%s) User %r attempted to set channel mode %r, but the ' 'extension providing it isn\'t loaded! To prevent possible' ' desyncs, try adding the line "loadmodule "extensions/%s.so";" to ' - 'your IRCd configuration.', self.irc.name, setter, badmode, + 'your IRCd configuration.', self.name, setter, badmode, charlist[badmode]) def handle_su(self, numeric, command, args): @@ -696,7 +684,7 @@ class TS6Protocol(TS6BaseProtocol): account = '' # No account name means a logout uid = args[0] - self.irc.callHooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': account}]) + self.call_hooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': account}]) def handle_rsfnc(self, numeric, command, args): """ diff --git a/protocols/ts6_common.py b/protocols/ts6_common.py index 3a88630..a0d5080 100644 --- a/protocols/ts6_common.py +++ b/protocols/ts6_common.py @@ -99,19 +99,18 @@ class TS6UIDGenerator(utils.IncrementalUIDGenerator): super().__init__(sid) class TS6BaseProtocol(IRCS2SProtocol): - - def __init__(self, irc): - super().__init__(irc) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) # Dictionary of UID generators (one for each server). self.uidgen = structures.KeyedDefaultdict(TS6UIDGenerator) # SID generator for TS6. - self.sidgen = TS6SIDGenerator(irc) + self.sidgen = TS6SIDGenerator(self) - def _send(self, source, msg, **kwargs): + def _send_with_prefix(self, source, msg, **kwargs): """Sends a TS6-style raw command from a source numeric to the self.irc connection given.""" - self.irc.send(':%s %s' % (source, msg), **kwargs) + self.send(':%s %s' % (source, msg), **kwargs) def _expandPUID(self, uid): """ @@ -130,23 +129,23 @@ class TS6BaseProtocol(IRCS2SProtocol): # Mangle the target for IRCds that require it. target = self._expandPUID(target) - self._send(source, '%s %s %s' % (numeric, target, text)) + self._send_with_prefix(source, '%s %s %s' % (numeric, target, text)) def kick(self, numeric, channel, target, reason=None): """Sends kicks from a PyLink client/server.""" - if (not self.irc.isInternalClient(numeric)) and \ - (not self.irc.isInternalServer(numeric)): + if (not self.is_internal_client(numeric)) and \ + (not self.is_internal_server(numeric)): raise LookupError('No such PyLink client/server exists.') - channel = self.irc.toLower(channel) + channel = self.to_lower(channel) if not reason: reason = 'No reason given' # Mangle kick targets for IRCds that require it. real_target = self._expandPUID(target) - self._send(numeric, 'KICK %s %s :%s' % (channel, real_target, reason)) + self._send_with_prefix(numeric, 'KICK %s %s :%s' % (channel, real_target, reason)) # We can pretend the target left by its own will; all we really care about # is that the target gets removed from the channel userlist, and calling @@ -156,8 +155,8 @@ class TS6BaseProtocol(IRCS2SProtocol): def kill(self, numeric, target, reason): """Sends a kill from a PyLink client/server.""" - if (not self.irc.isInternalClient(numeric)) and \ - (not self.irc.isInternalServer(numeric)): + if (not self.is_internal_client(numeric)) and \ + (not self.is_internal_server(numeric)): raise LookupError('No such PyLink client/server exists.') # From TS6 docs: @@ -168,86 +167,86 @@ class TS6BaseProtocol(IRCS2SProtocol): # the kill followed by a space and a parenthesized reason. To avoid overflow, # it is recommended not to add anything to the path. - assert target in self.irc.users, "Unknown target %r for kill()!" % target + assert target in self.users, "Unknown target %r for kill()!" % target - if numeric in self.irc.users: + if numeric in self.users: # Killer was an user. Follow examples of setting the path to be "killer.host!killer.nick". - userobj = self.irc.users[numeric] + userobj = self.users[numeric] killpath = '%s!%s' % (userobj.host, userobj.nick) - elif numeric in self.irc.servers: + elif numeric in self.servers: # Sender was a server; killpath is just its name. - killpath = self.irc.servers[numeric].name + killpath = self.servers[numeric].name else: # Invalid sender?! This shouldn't happen, but make the killpath our server name anyways. log.warning('(%s) Invalid sender %s for kill(); using our server name instead.', - self.irc.name, numeric) - killpath = self.irc.servers[self.irc.sid].name + self.name, numeric) + killpath = self.servers[self.sid].name - self._send(numeric, 'KILL %s :%s (%s)' % (target, killpath, reason)) - self.removeClient(target) + self._send_with_prefix(numeric, 'KILL %s :%s (%s)' % (target, killpath, reason)) + self._remove_client(target) def nick(self, numeric, newnick): """Changes the nick of a PyLink client.""" - if not self.irc.isInternalClient(numeric): + if not self.is_internal_client(numeric): raise LookupError('No such PyLink client exists.') - self._send(numeric, 'NICK %s %s' % (newnick, int(time.time()))) + self._send_with_prefix(numeric, 'NICK %s %s' % (newnick, int(time.time()))) - self.irc.users[numeric].nick = newnick + self.users[numeric].nick = newnick # Update the NICK TS. - self.irc.users[numeric].ts = int(time.time()) + self.users[numeric].ts = int(time.time()) def part(self, client, channel, reason=None): """Sends a part from a PyLink client.""" - channel = self.irc.toLower(channel) - if not self.irc.isInternalClient(client): - log.error('(%s) Error trying to part %r from %r (no such client exists)', self.irc.name, client, channel) + channel = self.to_lower(channel) + if not self.is_internal_client(client): + log.error('(%s) Error trying to part %r from %r (no such client exists)', self.name, client, channel) raise LookupError('No such PyLink client exists.') msg = "PART %s" % channel if reason: msg += " :%s" % reason - self._send(client, msg) + self._send_with_prefix(client, msg) self.handle_part(client, 'PART', [channel]) def quit(self, numeric, reason): """Quits a PyLink client.""" - if self.irc.isInternalClient(numeric): - self._send(numeric, "QUIT :%s" % reason) - self.removeClient(numeric) + if self.is_internal_client(numeric): + self._send_with_prefix(numeric, "QUIT :%s" % reason) + self._remove_client(numeric) else: raise LookupError("No such PyLink client exists.") def message(self, numeric, target, text): """Sends a PRIVMSG from a PyLink client.""" - if not self.irc.isInternalClient(numeric): + if not self.is_internal_client(numeric): raise LookupError('No such PyLink client exists.') # Mangle message targets for IRCds that require it. target = self._expandPUID(target) - self._send(numeric, 'PRIVMSG %s :%s' % (target, text)) + self._send_with_prefix(numeric, 'PRIVMSG %s :%s' % (target, text)) def notice(self, numeric, target, text): """Sends a NOTICE from a PyLink client or server.""" - if (not self.irc.isInternalClient(numeric)) and \ - (not self.irc.isInternalServer(numeric)): + if (not self.is_internal_client(numeric)) and \ + (not self.is_internal_server(numeric)): raise LookupError('No such PyLink client/server exists.') # Mangle message targets for IRCds that require it. target = self._expandPUID(target) - self._send(numeric, 'NOTICE %s :%s' % (target, text)) + self._send_with_prefix(numeric, 'NOTICE %s :%s' % (target, text)) def topic(self, numeric, target, text): """Sends a TOPIC change from a PyLink client.""" - if not self.irc.isInternalClient(numeric): + if not self.is_internal_client(numeric): raise LookupError('No such PyLink client exists.') - self._send(numeric, 'TOPIC %s :%s' % (target, text)) - self.irc.channels[target].topic = text - self.irc.channels[target].topicset = True + self._send_with_prefix(numeric, 'TOPIC %s :%s' % (target, text)) + self.channels[target].topic = text + self.channels[target].topicset = True - def spawnServer(self, name, sid=None, uplink=None, desc=None, endburst_delay=0): + def spawn_server(self, name, sid=None, uplink=None, desc=None, endburst_delay=0): """ Spawns a server off a PyLink server. desc (server description) defaults to the one in the config. uplink defaults to the main PyLink @@ -258,65 +257,65 @@ class TS6BaseProtocol(IRCS2SProtocol): option will be ignored if given. """ # -> :0AL SID test.server 1 0XY :some silly pseudoserver - uplink = uplink or self.irc.sid + uplink = uplink or self.sid name = name.lower() - desc = desc or self.irc.serverdata.get('serverdesc') or conf.conf['bot']['serverdesc'] + desc = desc or self.serverdata.get('serverdesc') or conf.conf['bot']['serverdesc'] if sid is None: # No sid given; generate one! sid = self.sidgen.next_sid() assert len(sid) == 3, "Incorrect SID length" - if sid in self.irc.servers: + if sid in self.servers: raise ValueError('A server with SID %r already exists!' % sid) - for server in self.irc.servers.values(): + for server in self.servers.values(): if name == server.name: raise ValueError('A server named %r already exists!' % name) - if not self.irc.isInternalServer(uplink): + if not self.is_internal_server(uplink): raise ValueError('Server %r is not a PyLink server!' % uplink) if not utils.isServerName(name): raise ValueError('Invalid server name %r' % name) - self._send(uplink, 'SID %s 1 %s :%s' % (name, sid, desc)) - self.irc.servers[sid] = IrcServer(uplink, name, internal=True, desc=desc) + self._send_with_prefix(uplink, 'SID %s 1 %s :%s' % (name, sid, desc)) + self.servers[sid] = Server(uplink, name, internal=True, desc=desc) return sid def squit(self, source, target, text='No reason given'): """SQUITs a PyLink server.""" # -> SQUIT 9PZ :blah, blah log.debug('source=%s, target=%s', source, target) - self._send(source, 'SQUIT %s :%s' % (target, text)) + self._send_with_prefix(source, 'SQUIT %s :%s' % (target, text)) self.handle_squit(source, 'SQUIT', [target, text]) def away(self, source, text): """Sends an AWAY message from a PyLink client. can be an empty string to unset AWAY status.""" if text: - self._send(source, 'AWAY :%s' % text) + self._send_with_prefix(source, 'AWAY :%s' % text) else: - self._send(source, 'AWAY') - self.irc.users[source].away = text + self._send_with_prefix(source, 'AWAY') + self.users[source].away = text ### HANDLERS def handle_kick(self, source, command, args): """Handles incoming KICKs.""" # :70MAAAAAA KICK #test 70MAAAAAA :some reason - channel = self.irc.toLower(args[0]) - kicked = self._getUid(args[1]) + channel = self.to_lower(args[0]) + kicked = self._get_UID(args[1]) try: reason = args[2] except IndexError: reason = '' - log.debug('(%s) Removing kick target %s from %s', self.irc.name, kicked, channel) + log.debug('(%s) Removing kick target %s from %s', self.name, kicked, channel) self.handle_part(kicked, 'KICK', [channel, reason]) return {'channel': channel, 'target': kicked, 'text': reason} def handle_nick(self, numeric, command, args): """Handles incoming NICK changes.""" # <- :70MAAAAAA NICK GL-devel 1434744242 - oldnick = self.irc.users[numeric].nick - newnick = self.irc.users[numeric].nick = args[0] + oldnick = self.users[numeric].nick + newnick = self.users[numeric].nick = args[0] # Update the nick TS. - self.irc.users[numeric].ts = ts = int(args[1]) + self.users[numeric].ts = ts = int(args[1]) return {'newnick': newnick, 'oldnick': oldnick, 'ts': ts} @@ -331,12 +330,12 @@ class TS6BaseProtocol(IRCS2SProtocol): # -> :0AL000001 NICK Derp_ 1433728673 # <- :70M SAVE 0AL000001 1433728673 user = args[0] - oldnick = self.irc.users[user].nick - self.irc.users[user].nick = user + oldnick = self.users[user].nick + self.users[user].nick = user # TS6 SAVE sets nick TS to 100. This is hardcoded in InspIRCd and # charybdis. - self.irc.users[user].ts = 100 + self.users[user].ts = 100 return {'target': user, 'ts': 100, 'oldnick': oldnick} @@ -344,35 +343,16 @@ class TS6BaseProtocol(IRCS2SProtocol): """Handles incoming TOPIC changes from clients. For topic bursts, TB (TS6/charybdis) and FTOPIC (InspIRCd) are used instead.""" # <- :70MAAAAAA TOPIC #test :test - channel = self.irc.toLower(args[0]) + channel = self.to_lower(args[0]) topic = args[1] - oldtopic = self.irc.channels[channel].topic - self.irc.channels[channel].topic = topic - self.irc.channels[channel].topicset = True + oldtopic = self.channels[channel].topic + self.channels[channel].topic = topic + self.channels[channel].topicset = True return {'channel': channel, 'setter': numeric, 'text': topic, 'oldtopic': oldtopic} - def handle_part(self, source, command, args): - """Handles incoming PART commands.""" - channels = self.irc.toLower(args[0]).split(',') - for channel in channels: - # We should only get PART commands for channels that exist, right?? - self.irc.channels[channel].removeuser(source) - try: - self.irc.users[source].channels.discard(channel) - except KeyError: - log.debug("(%s) handle_part: KeyError trying to remove %r from %r's channel list?", self.irc.name, channel, source) - try: - reason = args[1] - except IndexError: - reason = '' - # Clear empty non-permanent channels. - if not (self.irc.channels[channel].users or ((self.irc.cmodes.get('permanent'), None) in self.irc.channels[channel].modes)): - del self.irc.channels[channel] - return {'channels': channels, 'text': reason} - def handle_svsnick(self, source, command, args): """Handles SVSNICK (forced nickname change attempts).""" # InspIRCd: @@ -381,4 +361,4 @@ class TS6BaseProtocol(IRCS2SProtocol): # UnrealIRCd: # <- :services.midnight.vpn SVSNICK GL Guest87795 1468303726 - return {'target': self._getUid(args[0]), 'newnick': args[1]} + return {'target': self._get_UID(args[0]), 'newnick': args[1]} diff --git a/protocols/unreal.py b/protocols/unreal.py index b072464..219fb87 100644 --- a/protocols/unreal.py +++ b/protocols/unreal.py @@ -21,8 +21,8 @@ SJOIN_PREFIXES = {'q': '*', 'a': '~', 'o': '@', 'h': '%', 'v': '+', 'b': '&', 'e S2S_BUFSIZE = 427 class UnrealProtocol(TS6BaseProtocol): - def __init__(self, irc): - super().__init__(irc) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.protocol_caps |= {'slash-in-nicks', 'underscore-in-hosts'} # Set our case mapping (rfc1459 maps "\" and "|" together, for example) self.casemapping = 'ascii' @@ -34,7 +34,7 @@ class UnrealProtocol(TS6BaseProtocol): 'EOS': 'ENDBURST'} self.caps = [] - self.irc.prefixmodes = {'q': '~', 'a': '&', 'o': '@', 'h': '%', 'v': '+'} + self.prefixmodes = {'q': '~', 'a': '&', 'o': '@', 'h': '%', 'v': '+'} self.needed_caps = ["VL", "SID", "CHANMODES", "NOQUIT", "SJ3", "NICKIP", "UMODE2", "SJOIN"] @@ -47,16 +47,16 @@ class UnrealProtocol(TS6BaseProtocol): 3.2 users), this will change the PUID given to the actual user's nick, so that that the older IRCds can understand it. """ - if uid in self.irc.users and '@' in uid: + if uid in self.users and '@' in uid: # UID exists and has a @ in it, meaning it's a PUID (orignick@counter style). # Return this user's nick accordingly. - nick = self.irc.users[uid].nick - log.debug('(%s) Mangling target PUID %s to nick %s', self.irc.name, uid, nick) + nick = self.users[uid].nick + log.debug('(%s) Mangling target PUID %s to nick %s', self.name, uid, nick) return nick return uid ### OUTGOING COMMAND FUNCTIONS - def spawnClient(self, nick, ident='null', host='null', realhost=None, modes=set(), + def spawn_client(self, nick, ident='null', host='null', realhost=None, modes=set(), server=None, ip='0.0.0.0', realname=None, ts=None, opertype='IRC Operator', manipulatable=False): """ @@ -65,8 +65,8 @@ class UnrealProtocol(TS6BaseProtocol): Note: No nick collision / valid nickname checks are done here; it is up to plugins to make sure they don't introduce anything invalid. """ - server = server or self.irc.sid - if not self.irc.isInternalServer(server): + server = server or self.sid + if not self.is_internal_server(server): raise ValueError('Server %r is not a PyLink server!' % server) # Unreal 4.0 uses TS6-style UIDs. They don't start from AAAAAA like other IRCd's @@ -81,11 +81,11 @@ class UnrealProtocol(TS6BaseProtocol): modes = set(modes) # Ensure type safety modes |= {('+x', None), ('+t', None)} - raw_modes = self.irc.joinModes(modes) - u = self.irc.users[uid] = IrcUser(nick, ts, uid, server, ident=ident, host=host, realname=realname, + raw_modes = self.join_modes(modes) + u = self.users[uid] = User(nick, ts, uid, server, ident=ident, host=host, realname=realname, realhost=realhost, ip=ip, manipulatable=manipulatable, opertype=opertype) - self.irc.applyModes(uid, modes) - self.irc.servers[server].users.add(uid) + self.apply_modes(uid, modes) + self.servers[server].users.add(uid) # UnrealIRCd requires encoding the IP by first packing it into a binary format, # and then encoding the binary with Base64. @@ -106,7 +106,7 @@ class UnrealProtocol(TS6BaseProtocol): encoded_ip = encoded_ip.strip().decode() # <- :001 UID GL 0 1441306929 gl localhost 0018S7901 0 +iowx * midnight-1C620195 fwAAAQ== :realname - self._send(server, "UID {nick} 0 {ts} {ident} {realhost} {uid} 0 {modes} " + self._send_with_prefix(server, "UID {nick} 0 {ts} {ident} {realhost} {uid} 0 {modes} " "{host} * {ip} :{realname}".format(ts=ts, host=host, nick=nick, ident=ident, uid=uid, modes=raw_modes, realname=realname, @@ -116,12 +116,12 @@ class UnrealProtocol(TS6BaseProtocol): def join(self, client, channel): """Joins a PyLink client to a channel.""" - channel = self.irc.toLower(channel) - if not self.irc.isInternalClient(client): + channel = self.to_lower(channel) + if not self.is_internal_client(client): raise LookupError('No such PyLink client exists.') - self._send(client, "JOIN %s" % channel) - self.irc.channels[channel].users.add(client) - self.irc.users[client].channels.add(channel) + self._send_with_prefix(client, "JOIN %s" % channel) + self.channels[channel].users.add(client) + self.users[client].channels.add(channel) def sjoin(self, server, channel, users, ts=None, modes=set()): """Sends an SJOIN for a group of users to a channel. @@ -132,17 +132,17 @@ class UnrealProtocol(TS6BaseProtocol): Example uses: sjoin('100', '#test', [('', '100AAABBC'), ('o', 100AAABBB'), ('v', '100AAADDD')]) - sjoin(self.irc.sid, '#test', [('o', self.irc.pseudoclient.uid)]) + sjoin(self.sid, '#test', [('o', self.pseudoclient.uid)]) """ # <- :001 SJOIN 1444361345 #test :*@+1JJAAAAAB %2JJAAAA4C 1JJAAAADS - channel = self.irc.toLower(channel) - server = server or self.irc.sid + channel = self.to_lower(channel) + server = server or self.sid assert users, "sjoin: No users sent?" if not server: raise LookupError('No such PyLink server exists.') - changedmodes = set(modes or self.irc.channels[channel].modes) - orig_ts = self.irc.channels[channel].ts + changedmodes = set(modes or self.channels[channel].modes) + orig_ts = self.channels[channel].ts ts = ts or orig_ts uids = [] itemlist = [] @@ -163,17 +163,17 @@ class UnrealProtocol(TS6BaseProtocol): uids.append(user) try: - self.irc.users[user].channels.add(channel) + self.users[user].channels.add(channel) except KeyError: # Not initialized yet? - log.debug("(%s) sjoin: KeyError trying to add %r to %r's channel list?", self.irc.name, channel, user) + log.debug("(%s) sjoin: KeyError trying to add %r to %r's channel list?", self.name, channel, user) # Track simple modes separately. simplemodes = set() for modepair in modes: - if modepair[0][-1] in self.irc.cmodes['*A']: + if modepair[0][-1] in self.cmodes['*A']: # Bans, exempts, invex get expanded to forms like "&*!*@some.host" in SJOIN. - if (modepair[0][-1], modepair[1]) in self.irc.channels[channel].modes: + if (modepair[0][-1], modepair[1]) in self.channels[channel].modes: # Mode is already set; skip it. continue @@ -189,49 +189,49 @@ class UnrealProtocol(TS6BaseProtocol): # Modes are optional; add them if they exist if modes: - sjoin_prefix += " %s" % self.irc.joinModes(simplemodes) + sjoin_prefix += " %s" % self.join_modes(simplemodes) sjoin_prefix += " :" # Wrap arguments to the max supported S2S line length to prevent cutoff # (https://github.com/GLolol/PyLink/issues/378) for line in utils.wrapArguments(sjoin_prefix, itemlist, S2S_BUFSIZE): - self.irc.send(line) + self.send(line) - self.irc.channels[channel].users.update(uids) + self.channels[channel].users.update(uids) self.updateTS(server, channel, ts, changedmodes) def ping(self, source=None, target=None): """Sends a PING to a target server. Periodic PINGs are sent to our uplink automatically by the Irc() internals; plugins shouldn't have to use this.""" - source = source or self.irc.sid - target = target or self.irc.uplink + source = source or self.sid + target = target or self.uplink if not (target is None or source is None): - self._send(source, 'PING %s %s' % (self.irc.servers[source].name, self.irc.servers[target].name)) + self._send_with_prefix(source, 'PING %s %s' % (self.servers[source].name, self.servers[target].name)) def mode(self, numeric, target, modes, ts=None): """ Sends mode changes from a PyLink client/server. The mode list should be - a list of (mode, arg) tuples, i.e. the format of utils.parseModes() output. + a list of (mode, arg) tuples, i.e. the format of utils.parse_modes() output. """ # <- :unreal.midnight.vpn MODE #test +ntCo GL 1444361345 - if (not self.irc.isInternalClient(numeric)) and \ - (not self.irc.isInternalServer(numeric)): + if (not self.is_internal_client(numeric)) and \ + (not self.is_internal_server(numeric)): raise LookupError('No such PyLink client/server exists.') - self.irc.applyModes(target, modes) + self.apply_modes(target, modes) if utils.isChannel(target): # Make sure we expand any PUIDs when sending outgoing modes... for idx, mode in enumerate(modes): - if mode[0][-1] in self.irc.prefixmodes: - log.debug('(%s) mode: expanding PUID of mode %s', self.irc.name, str(mode)) + if mode[0][-1] in self.prefixmodes: + log.debug('(%s) mode: expanding PUID of mode %s', self.name, str(mode)) modes[idx] = (mode[0], self._expandPUID(mode[1])) # The MODE command is used for channel mode changes only - ts = ts or self.irc.channels[self.irc.toLower(target)].ts + ts = ts or self.channels[self.to_lower(target)].ts # 7 characters for "MODE", the space between MODE and the target, the space between the # target and mode list, and the space between the mode list and TS. @@ -242,7 +242,7 @@ class UnrealProtocol(TS6BaseProtocol): bufsize -= len(target) # Subtract the prefix (":SID " for servers or ":SIDAAAAAA " for servers) - bufsize -= (5 if self.irc.isInternalServer(numeric) else 11) + bufsize -= (5 if self.is_internal_server(numeric) else 11) # There is also an (undocumented) 15 args per line limit for MODE. The target, mode # characters, and TS take up three args, so we're left with 12 spaces for parameters. @@ -251,30 +251,30 @@ class UnrealProtocol(TS6BaseProtocol): # * *** Warning! Possible desynch: MODE for channel #test ('+bbbbbbbbbbbb *!*@0.1 *!*@1.1 *!*@2.1 *!*@3.1 *!*@4.1 *!*@5.1 *!*@6.1 *!*@7.1 *!*@8.1 *!*@9.1 *!*@10.1 *!*@11.1') has fishy timestamp (12) (from pylink.local/pylink.local) # Thanks to kevin and Jobe for helping me debug this! - for modestring in self.irc.wrapModes(modes, bufsize, max_modes_per_msg=12): - self._send(numeric, 'MODE %s %s %s' % (target, modestring, ts)) + for modestring in self.wrap_modes(modes, bufsize, max_modes_per_msg=12): + self._send_with_prefix(numeric, 'MODE %s %s %s' % (target, modestring, ts)) else: # For user modes, the only way to set modes (for non-U:Lined servers) # is through UMODE2, which sets the modes on the caller. # U:Lines can use SVSMODE/SVS2MODE, but I won't expect people to # U:Line a PyLink daemon... - if not self.irc.isInternalClient(target): + if not self.is_internal_client(target): raise ProtocolError('Cannot force mode change on external clients!') # XXX: I don't expect usermode changes to ever get cut off, but length # checks could be added just to be safe... - joinedmodes = self.irc.joinModes(modes) - self._send(target, 'UMODE2 %s' % joinedmodes) + joinedmodes = self.join_modes(modes) + self._send_with_prefix(target, 'UMODE2 %s' % joinedmodes) - def topicBurst(self, numeric, target, text): + def topic_burst(self, numeric, target, text): """Sends a TOPIC change from a PyLink server.""" - if not self.irc.isInternalServer(numeric): + if not self.is_internal_server(numeric): raise LookupError('No such PyLink server exists.') - self._send(numeric, 'TOPIC %s :%s' % (target, text)) - self.irc.channels[target].topic = text - self.irc.channels[target].topicset = True + self._send_with_prefix(numeric, 'TOPIC %s :%s' % (target, text)) + self.channels[target].topic = text + self.channels[target].topicset = True - def updateClient(self, target, field, text): + def update_client(self, target, field, text): """Updates the ident, host, or realname of any connected client.""" field = field.upper() @@ -282,46 +282,46 @@ class UnrealProtocol(TS6BaseProtocol): raise NotImplementedError("Changing field %r of a client is " "unsupported by this protocol." % field) - if self.irc.isInternalClient(target): + if self.is_internal_client(target): # It is one of our clients, use SETIDENT/HOST/NAME. if field == 'IDENT': - self.irc.users[target].ident = text - self._send(target, 'SETIDENT %s' % text) + self.users[target].ident = text + self._send_with_prefix(target, 'SETIDENT %s' % text) elif field == 'HOST': - self.irc.users[target].host = text - self._send(target, 'SETHOST %s' % text) + self.users[target].host = text + self._send_with_prefix(target, 'SETHOST %s' % text) elif field in ('REALNAME', 'GECOS'): - self.irc.users[target].realname = text - self._send(target, 'SETNAME :%s' % text) + self.users[target].realname = text + self._send_with_prefix(target, 'SETNAME :%s' % text) else: # It is a client on another server, use CHGIDENT/HOST/NAME. if field == 'IDENT': - self.irc.users[target].ident = text - self._send(self.irc.sid, 'CHGIDENT %s %s' % (target, text)) + self.users[target].ident = text + self._send_with_prefix(self.sid, 'CHGIDENT %s %s' % (target, text)) # Send hook payloads for other plugins to listen to. - self.irc.callHooks([self.irc.sid, 'CHGIDENT', + self.call_hooks([self.sid, 'CHGIDENT', {'target': target, 'newident': text}]) elif field == 'HOST': - self.irc.users[target].host = text - self._send(self.irc.sid, 'CHGHOST %s %s' % (target, text)) + self.users[target].host = text + self._send_with_prefix(self.sid, 'CHGHOST %s %s' % (target, text)) - self.irc.callHooks([self.irc.sid, 'CHGHOST', + self.call_hooks([self.sid, 'CHGHOST', {'target': target, 'newhost': text}]) elif field in ('REALNAME', 'GECOS'): - self.irc.users[target].realname = text - self._send(self.irc.sid, 'CHGNAME %s :%s' % (target, text)) + self.users[target].realname = text + self._send_with_prefix(self.sid, 'CHGNAME %s :%s' % (target, text)) - self.irc.callHooks([self.irc.sid, 'CHGNAME', + self.call_hooks([self.sid, 'CHGNAME', {'target': target, 'newgecos': text}]) def invite(self, numeric, target, channel): """Sends an INVITE from a PyLink client..""" - if not self.irc.isInternalClient(numeric): + if not self.is_internal_client(numeric): raise LookupError('No such PyLink client exists.') - self._send(numeric, 'INVITE %s %s' % (target, channel)) + self._send_with_prefix(numeric, 'INVITE %s %s' % (target, channel)) def knock(self, numeric, target, text): """Sends a KNOCK from a PyLink client.""" @@ -329,21 +329,21 @@ class UnrealProtocol(TS6BaseProtocol): # sent to all ops in a channel. # <- :unreal.midnight.vpn NOTICE @#test :[Knock] by GL|!gl@hidden-1C620195 (test) assert utils.isChannel(target), "Can only knock on channels!" - sender = self.irc.getServer(numeric) - s = '[Knock] by %s (%s)' % (self.irc.getHostmask(numeric), text) - self._send(sender, 'NOTICE @%s :%s' % (target, s)) + sender = self.get_server(numeric) + s = '[Knock] by %s (%s)' % (self.get_hostmask(numeric), text) + self._send_with_prefix(sender, 'NOTICE @%s :%s' % (target, s)) ### HANDLERS - def connect(self): + def post_connect(self): """Initializes a connection to a server.""" - ts = self.irc.start_ts - self.irc.prefixmodes = {'q': '~', 'a': '&', 'o': '@', 'h': '%', 'v': '+'} + ts = self.start_ts + self.prefixmodes = {'q': '~', 'a': '&', 'o': '@', 'h': '%', 'v': '+'} # Track usages of legacy (Unreal 3.2) nicks. self.legacy_uidgen = utils.PUIDGenerator('U32user') - self.irc.umodes.update({'deaf': 'd', 'invisible': 'i', 'hidechans': 'p', + self.umodes.update({'deaf': 'd', 'invisible': 'i', 'hidechans': 'p', 'protected': 'q', 'registered': 'r', 'snomask': 's', 'vhost': 't', 'wallops': 'w', 'bot': 'B', 'cloak': 'x', 'ssl': 'z', @@ -352,10 +352,10 @@ class UnrealProtocol(TS6BaseProtocol): 'noctcp': 'T', 'showwhois': 'W', '*A': '', '*B': '', '*C': '', '*D': 'dipqrstwBxzGHIRSTW'}) - f = self.irc.send - host = self.irc.serverdata["hostname"] + f = self.send + host = self.serverdata["hostname"] - f('PASS :%s' % self.irc.serverdata["sendpass"]) + f('PASS :%s' % self.serverdata["sendpass"]) # https://github.com/unrealircd/unrealircd/blob/2f8cb55e/doc/technical/protoctl.txt # We support the following protocol features: # SJOIN - supports SJOIN for user introduction @@ -374,11 +374,11 @@ class UnrealProtocol(TS6BaseProtocol): # not work for any UnrealIRCd 3.2 users. # ESVID - Supports account names in services stamps instead of just the signon time. # AFAIK this doesn't actually affect services' behaviour? - f('PROTOCTL SJOIN SJ3 NOQUIT NICKv2 VL UMODE2 PROTOCTL NICKIP EAUTH=%s SID=%s VHP ESVID' % (self.irc.serverdata["hostname"], self.irc.sid)) - sdesc = self.irc.serverdata.get('serverdesc') or conf.conf['bot']['serverdesc'] - f('SERVER %s 1 U%s-h6e-%s :%s' % (host, self.proto_ver, self.irc.sid, sdesc)) - f('NETINFO 1 %s %s * 0 0 0 :%s' % (self.irc.start_ts, self.proto_ver, self.irc.serverdata.get("netname", self.irc.name))) - self._send(self.irc.sid, 'EOS') + f('PROTOCTL SJOIN SJ3 NOQUIT NICKv2 VL UMODE2 PROTOCTL NICKIP EAUTH=%s SID=%s VHP ESVID' % (self.serverdata["hostname"], self.sid)) + sdesc = self.serverdata.get('serverdesc') or conf.conf['bot']['serverdesc'] + f('SERVER %s 1 U%s-h6e-%s :%s' % (host, self.proto_ver, self.sid, sdesc)) + f('NETINFO 1 %s %s * 0 0 0 :%s' % (self.start_ts, self.proto_ver, self.serverdata.get("netname", self.name))) + self._send_with_prefix(self.sid, 'EOS') def handle_eos(self, numeric, command, args): """EOS is used to denote end of burst.""" @@ -418,50 +418,50 @@ class UnrealProtocol(TS6BaseProtocol): realname = args[-1] - self.irc.users[uid] = IrcUser(nick, ts, uid, numeric, ident, host, realname, realhost, ip) - self.irc.servers[numeric].users.add(uid) + self.users[uid] = User(nick, ts, uid, numeric, ident, host, realname, realhost, ip) + self.servers[numeric].users.add(uid) # Handle user modes - parsedmodes = self.irc.parseModes(uid, [modestring]) - self.irc.applyModes(uid, parsedmodes) + parsedmodes = self.parse_modes(uid, [modestring]) + self.apply_modes(uid, parsedmodes) # The cloaked (+x) host is completely separate from the displayed host # and real host in that it is ONLY shown if the user is +x (cloak mode # enabled) but NOT +t (vHost set). - self.irc.users[uid].cloaked_host = args[9] + self.users[uid].cloaked_host = args[9] if ('+o', None) in parsedmodes: # If +o being set, call the CLIENT_OPERED internal hook. - self.irc.callHooks([uid, 'CLIENT_OPERED', {'text': 'IRC Operator'}]) + self.call_hooks([uid, 'CLIENT_OPERED', {'text': 'IRC Operator'}]) if ('+x', None) not in parsedmodes: # If +x is not set, update to use the person's real host. - self.irc.users[uid].host = realhost + self.users[uid].host = realhost # Set the account name if present: if this is a number, set it to the user nick. if ('+r', None) in parsedmodes and accountname.isdigit(): accountname = nick if not accountname.isdigit(): - self.irc.callHooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': accountname}]) + self.call_hooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': accountname}]) return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip} def handle_pass(self, numeric, command, args): # <- PASS :abcdefg - if args[0] != self.irc.serverdata['recvpass']: + if args[0] != self.serverdata['recvpass']: raise ProtocolError("Error: RECVPASS from uplink does not match configuration!") def handle_ping(self, numeric, command, args): - if numeric == self.irc.uplink: - self.irc.send('PONG %s :%s' % (self.irc.serverdata['hostname'], args[-1]), queue=False) + if numeric == self.uplink: + self.send('PONG %s :%s' % (self.serverdata['hostname'], args[-1]), queue=False) def handle_server(self, numeric, command, args): """Handles the SERVER command, which is used for both authentication and introducing legacy (non-SID) servers.""" # <- SERVER unreal.midnight.vpn 1 :U3999-Fhin6OoEM UnrealIRCd test server sname = args[0] - if numeric == self.irc.uplink and not self.irc.connected.is_set(): # We're doing authentication + if numeric == self.uplink and not self.connected.is_set(): # We're doing authentication for cap in self.needed_caps: if cap not in self.caps: raise ProtocolError("Not all required capabilities were met " @@ -485,17 +485,17 @@ class UnrealProtocol(TS6BaseProtocol): if protover < self.min_proto_ver: raise ProtocolError("Protocol version too old! (needs at least %s " "(Unreal 4.x), got %s)" % (self.min_proto_ver, protover)) - self.irc.servers[numeric] = IrcServer(None, sname, desc=sdesc) + self.servers[numeric] = Server(None, sname, desc=sdesc) # Set irc.connected to True, meaning that protocol negotiation passed. - log.debug('(%s) self.irc.connected set!', self.irc.name) - self.irc.connected.set() + log.debug('(%s) self.connected set!', self.name) + self.connected.set() else: # Legacy (non-SID) servers can still be introduced using the SERVER command. # <- :services.int SERVER a.bc 2 :(H) [GL] a servername = args[0].lower() sdesc = args[-1] - self.irc.servers[servername] = IrcServer(numeric, servername, desc=sdesc) + self.servers[servername] = Server(numeric, servername, desc=sdesc) return {'name': servername, 'sid': None, 'text': sdesc} def handle_sid(self, numeric, command, args): @@ -504,14 +504,14 @@ class UnrealProtocol(TS6BaseProtocol): sname = args[0].lower() sid = args[2] sdesc = args[-1] - self.irc.servers[sid] = IrcServer(numeric, sname, desc=sdesc) + self.servers[sid] = Server(numeric, sname, desc=sdesc) return {'name': sname, 'sid': sid, 'text': sdesc} def handle_squit(self, numeric, command, args): """Handles the SQUIT command.""" # <- SQUIT services.int :Read error # Convert the server name to a SID... - args[0] = self._getSid(args[0]) + args[0] = self._get_SID(args[0]) # Then, use the SQUIT handler in TS6BaseProtocol as usual. return super().handle_squit(numeric, 'SQUIT', args) @@ -534,18 +534,18 @@ class UnrealProtocol(TS6BaseProtocol): # <- PROTOCTL CHANMODES=beI,k,l,psmntirzMQNRTOVKDdGPZSCc NICKCHARS= SID=001 MLOCK TS=1441314501 EXTSWHOIS for cap in args: if cap.startswith('SID'): - self.irc.uplink = cap.split('=', 1)[1] + self.uplink = cap.split('=', 1)[1] elif cap.startswith('CHANMODES'): # Parse all the supported channel modes. supported_cmodes = cap.split('=', 1)[1] - self.irc.cmodes['*A'], self.irc.cmodes['*B'], self.irc.cmodes['*C'], self.irc.cmodes['*D'] = supported_cmodes.split(',') + self.cmodes['*A'], self.cmodes['*B'], self.cmodes['*C'], self.cmodes['*D'] = supported_cmodes.split(',') for namedmode, modechar in cmodes.items(): if modechar in supported_cmodes: - self.irc.cmodes[namedmode] = modechar - self.irc.cmodes['*B'] += 'f' # Add +f to the list too, dunno why it isn't there. + self.cmodes[namedmode] = modechar + self.cmodes['*B'] += 'f' # Add +f to the list too, dunno why it isn't there. # Add in the supported prefix modes. - self.irc.cmodes.update({'halfop': 'h', 'admin': 'a', 'owner': 'q', + self.cmodes.update({'halfop': 'h', 'admin': 'a', 'owner': 'q', 'op': 'o', 'voice': 'v'}) def handle_join(self, numeric, command, args): @@ -553,38 +553,38 @@ class UnrealProtocol(TS6BaseProtocol): # <- :GL JOIN #pylink,#test if args[0] == '0': # /join 0; part the user from all channels - oldchans = self.irc.users[numeric].channels.copy() + oldchans = self.users[numeric].channels.copy() log.debug('(%s) Got /join 0 from %r, channel list is %r', - self.irc.name, numeric, oldchans) + self.name, numeric, oldchans) for ch in oldchans: - self.irc.channels[ch].users.discard(numeric) - self.irc.users[numeric].channels.discard(ch) + self.channels[ch].users.discard(numeric) + self.users[numeric].channels.discard(ch) return {'channels': oldchans, 'text': 'Left all channels.', 'parse_as': 'PART'} else: for channel in args[0].split(','): # Normalize channel case. - channel = self.irc.toLower(channel) + channel = self.to_lower(channel) - c = self.irc.channels[channel] + c = self.channels[channel] - self.irc.users[numeric].channels.add(channel) - self.irc.channels[channel].users.add(numeric) + self.users[numeric].channels.add(channel) + self.channels[channel].users.add(numeric) # Call hooks manually, because one JOIN command in UnrealIRCd can # have multiple channels... - self.irc.callHooks([numeric, command, {'channel': channel, 'users': [numeric], 'modes': + self.call_hooks([numeric, command, {'channel': channel, 'users': [numeric], 'modes': c.modes, 'ts': c.ts}]) def handle_sjoin(self, numeric, command, args): """Handles the UnrealIRCd SJOIN command.""" # <- :001 SJOIN 1444361345 #test :001AAAAAA @001AAAAAB +001AAAAAC # <- :001 SJOIN 1483250129 #services +nt :+001OR9V02 @*~001DH6901 &*!*@test "*!*@blah.blah '*!*@yes.no - channel = self.irc.toLower(args[1]) - chandata = self.irc.channels[channel].deepcopy() + channel = self.to_lower(args[1]) + chandata = self.channels[channel].deepcopy() userlist = args[-1].split() namelist = [] - log.debug('(%s) handle_sjoin: got userlist %r for %r', self.irc.name, userlist, channel) + log.debug('(%s) handle_sjoin: got userlist %r for %r', self.name, userlist, channel) modestring = '' @@ -599,7 +599,7 @@ class UnrealProtocol(TS6BaseProtocol): # Strip extra spaces between the mode argument and the user list, if # there are any. XXX: report this as a bug in unreal's s2s protocol? modestring = [m for m in modestring if m] - parsedmodes = self.irc.parseModes(channel, modestring) + parsedmodes = self.parse_modes(channel, modestring) changedmodes = set(parsedmodes) except IndexError: pass @@ -624,28 +624,28 @@ class UnrealProtocol(TS6BaseProtocol): # <- :002 SJOIN 1486361658 #idlerpg :@ continue - user = self._getUid(user) # Normalize nicks to UIDs for Unreal 3.2 links + user = self._get_UID(user) # Normalize nicks to UIDs for Unreal 3.2 links # Unreal uses slightly different prefixes in SJOIN. +q is * instead of ~, # and +a is ~ instead of &. modeprefix = (r.group(1) or '').replace("~", "&").replace("*", "~") finalprefix = '' - log.debug('(%s) handle_sjoin: got modeprefix %r for user %r', self.irc.name, modeprefix, user) + log.debug('(%s) handle_sjoin: got modeprefix %r for user %r', self.name, modeprefix, user) for m in modeprefix: # Iterate over the mapping of prefix chars to prefixes, and # find the characters that match. - for char, prefix in self.irc.prefixmodes.items(): + for char, prefix in self.prefixmodes.items(): if m == prefix: finalprefix += char namelist.append(user) - self.irc.users[user].channels.add(channel) + self.users[user].channels.add(channel) # Only merge the remote's prefix modes if their TS is smaller or equal to ours. changedmodes |= {('+%s' % mode, user) for mode in finalprefix} - self.irc.channels[channel].users.add(user) + self.channels[channel].users.add(user) - our_ts = self.irc.channels[channel].ts + our_ts = self.channels[channel].ts their_ts = int(args[0]) self.updateTS(numeric, channel, their_ts, changedmodes) @@ -666,7 +666,7 @@ class UnrealProtocol(TS6BaseProtocol): # <- NICK GL32 2 1470699865 gl localhost unreal32.midnight.vpn GL +iowx hidden-1C620195 AAAAAAAAAAAAAAAAAAAAAQ== :realname # to this: # <- :001 UID GL 0 1441306929 gl localhost 0018S7901 0 +iowx * hidden-1C620195 fwAAAQ== :realname - log.debug('(%s) got legacy NICK args: %s', self.irc.name, ' '.join(args)) + log.debug('(%s) got legacy NICK args: %s', self.name, ' '.join(args)) new_args = args[:] # Clone the old args list servername = new_args[5].lower() # Get the name of the users' server. @@ -682,7 +682,7 @@ class UnrealProtocol(TS6BaseProtocol): # hosts from UnrealIRCd 3.2 users. Otherwise, +x host cloaking won't work! new_args.insert(-2, args[4]) - log.debug('(%s) translating legacy NICK args to: %s', self.irc.name, ' '.join(new_args)) + log.debug('(%s) translating legacy NICK args to: %s', self.name, ' '.join(new_args)) return self.handle_uid(servername, 'UID_LEGACY', new_args) else: @@ -705,11 +705,11 @@ class UnrealProtocol(TS6BaseProtocol): # Also, we need to get rid of that extra space following the +f argument. :| if utils.isChannel(args[0]): - channel = self.irc.toLower(args[0]) - oldobj = self.irc.channels[channel].deepcopy() + channel = self.to_lower(args[0]) + oldobj = self.channels[channel].deepcopy() modes = [arg for arg in args[1:] if arg] # normalize whitespace - parsedmodes = self.irc.parseModes(channel, modes) + parsedmodes = self.parse_modes(channel, modes) if parsedmodes: if parsedmodes[0][0] == '+&': @@ -717,12 +717,12 @@ class UnrealProtocol(TS6BaseProtocol): # attempt to set modes by us was rejected for some reason (usually due to # timestamps). Drop the mode change to prevent mode floods. log.debug("(%s) Received mode bounce %s in channel %s! Our TS: %s", - self.irc.name, modes, channel, self.irc.channels[channel].ts) + self.name, modes, channel, self.channels[channel].ts) return - self.irc.applyModes(channel, parsedmodes) + self.apply_modes(channel, parsedmodes) - if numeric in self.irc.servers and args[-1].isdigit(): + if numeric in self.servers and args[-1].isdigit(): # Sender is a server AND last arg is number. Perform TS updates. their_ts = int(args[-1]) if their_ts > 0: @@ -732,13 +732,13 @@ class UnrealProtocol(TS6BaseProtocol): # User mode change: pass those on to handle_umode2() self.handle_umode2(numeric, 'MODE', args[1:]) - def checkCloakChange(self, uid, parsedmodes): + def _check_cloak_change(self, uid, parsedmodes): """ Checks whether +x/-x was set in the mode query, and changes the hostname of the user given to or from their cloaked host if True. """ - userobj = self.irc.users[uid] + userobj = self.users[uid] final_modes = userobj.modes oldhost = userobj.host @@ -763,20 +763,20 @@ class UnrealProtocol(TS6BaseProtocol): if newhost != oldhost: # Only send a payload if the old and new hosts are different. - self.irc.callHooks([uid, 'SETHOST', + self.call_hooks([uid, 'SETHOST', {'target': uid, 'newhost': newhost}]) def handle_svsmode(self, numeric, command, args): """Handles SVSMODE, used by services for setting user modes on others.""" # <- :source SVSMODE target +usermodes - target = self._getUid(args[0]) + target = self._get_UID(args[0]) modes = args[1:] - parsedmodes = self.irc.parseModes(target, modes) - self.irc.applyModes(target, parsedmodes) + parsedmodes = self.parse_modes(target, modes) + self.apply_modes(target, parsedmodes) # If +x/-x is being set, update cloaked host info. - self.checkCloakChange(target, parsedmodes) + self._check_cloak_change(target, parsedmodes) return {'target': target, 'modes': parsedmodes} @@ -817,8 +817,8 @@ class UnrealProtocol(TS6BaseProtocol): # <- :NickServ SVS2MODE 001SALZ01 +d GL # <- :NickServ SVS2MODE 001SALZ01 +r - target = self._getUid(args[0]) - parsedmodes = self.irc.parseModes(target, args[1:]) + target = self._get_UID(args[0]) + parsedmodes = self.parse_modes(target, args[1:]) if ('+r', None) in parsedmodes: # Umode +r is being set (log in) @@ -828,19 +828,19 @@ class UnrealProtocol(TS6BaseProtocol): except IndexError: # If one doesn't exist, make it the same as the nick, but only if the account name # wasn't set already. - if not self.irc.users[target].services_account: - account = self.irc.getFriendlyName(target) + if not self.users[target].services_account: + account = self.get_friendly_name(target) else: return else: if account.isdigit(): # If the +d argument is a number, ignore it and set the account name to the nick. - account = self.irc.getFriendlyName(target) + account = self.get_friendly_name(target) elif ('-r', None) in parsedmodes: # Umode -r being set. - if not self.irc.users[target].services_account: + if not self.users[target].services_account: # User already has no account; ignore. return @@ -853,19 +853,19 @@ class UnrealProtocol(TS6BaseProtocol): else: return - self.irc.callHooks([target, 'CLIENT_SERVICES_LOGIN', {'text': account}]) + self.call_hooks([target, 'CLIENT_SERVICES_LOGIN', {'text': account}]) def handle_umode2(self, numeric, command, args): """Handles UMODE2, used to set user modes on oneself.""" # <- :GL UMODE2 +W - parsedmodes = self.irc.parseModes(numeric, args) - self.irc.applyModes(numeric, parsedmodes) + parsedmodes = self.parse_modes(numeric, args) + self.apply_modes(numeric, parsedmodes) if ('+o', None) in parsedmodes: # If +o being set, call the CLIENT_OPERED internal hook. - self.irc.callHooks([numeric, 'CLIENT_OPERED', {'text': 'IRC Operator'}]) + self.call_hooks([numeric, 'CLIENT_OPERED', {'text': 'IRC Operator'}]) - self.checkCloakChange(numeric, parsedmodes) + self._check_cloak_change(numeric, parsedmodes) return {'target': numeric, 'modes': parsedmodes} @@ -873,14 +873,14 @@ class UnrealProtocol(TS6BaseProtocol): """Handles the TOPIC command.""" # <- GL TOPIC #services GL 1444699395 :weeee # <- TOPIC #services devel.relay 1452399682 :test - channel = self.irc.toLower(args[0]) + channel = self.to_lower(args[0]) topic = args[-1] setter = args[1] ts = args[2] - oldtopic = self.irc.channels[channel].topic - self.irc.channels[channel].topic = topic - self.irc.channels[channel].topicset = True + oldtopic = self.channels[channel].topic + self.channels[channel].topic = topic + self.channels[channel].topicset = True return {'channel': channel, 'setter': setter, 'ts': ts, 'text': topic, 'oldtopic': oldtopic} @@ -888,66 +888,58 @@ class UnrealProtocol(TS6BaseProtocol): def handle_setident(self, numeric, command, args): """Handles SETIDENT, used for self ident changes.""" # <- :70MAAAAAB SETIDENT test - self.irc.users[numeric].ident = newident = args[0] + self.users[numeric].ident = newident = args[0] return {'target': numeric, 'newident': newident} def handle_sethost(self, numeric, command, args): """Handles CHGHOST, used for self hostname changes.""" # <- :70MAAAAAB SETIDENT some.host - self.irc.users[numeric].host = newhost = args[0] + self.users[numeric].host = newhost = args[0] # When SETHOST or CHGHOST is used, modes +xt are implicitly set on the # target. - self.irc.applyModes(numeric, [('+x', None), ('+t', None)]) + self.apply_modes(numeric, [('+x', None), ('+t', None)]) return {'target': numeric, 'newhost': newhost} def handle_setname(self, numeric, command, args): """Handles SETNAME, used for self real name/gecos changes.""" # <- :70MAAAAAB SETNAME :afdsafasf - self.irc.users[numeric].realname = newgecos = args[0] + self.users[numeric].realname = newgecos = args[0] return {'target': numeric, 'newgecos': newgecos} def handle_chgident(self, numeric, command, args): """Handles CHGIDENT, used for denoting ident changes.""" # <- :GL CHGIDENT GL test - target = self._getUid(args[0]) - self.irc.users[target].ident = newident = args[1] + target = self._get_UID(args[0]) + self.users[target].ident = newident = args[1] return {'target': target, 'newident': newident} def handle_chghost(self, numeric, command, args): """Handles CHGHOST, used for denoting hostname changes.""" # <- :GL CHGHOST GL some.host - target = self._getUid(args[0]) - self.irc.users[target].host = newhost = args[1] + target = self._get_UID(args[0]) + self.users[target].host = newhost = args[1] # When SETHOST or CHGHOST is used, modes +xt are implicitly set on the # target. - self.irc.applyModes(target, [('+x', None), ('+t', None)]) + self.apply_modes(target, [('+x', None), ('+t', None)]) return {'target': target, 'newhost': newhost} def handle_chgname(self, numeric, command, args): """Handles CHGNAME, used for denoting real name/gecos changes.""" # <- :GL CHGNAME GL :afdsafasf - target = self._getUid(args[0]) - self.irc.users[target].realname = newgecos = args[1] + target = self._get_UID(args[0]) + self.users[target].realname = newgecos = args[1] return {'target': target, 'newgecos': newgecos} - def handle_invite(self, numeric, command, args): - """Handles incoming INVITEs.""" - # <- :GL INVITE PyLink-devel :#a - target = self._getUid(args[0]) - channel = self.irc.toLower(args[1]) - # We don't actually need to process this; it's just something plugins/hooks can use - return {'target': target, 'channel': channel} - def handle_kill(self, numeric, command, args): """Handles incoming KILLs.""" # <- :GL| KILL GLolol :hidden-1C620195!GL| (test) # Use ts6_common's handle_kill, but coerse UIDs to nicks first. - new_args = [self._getUid(args[0])] + new_args = [self._get_UID(args[0])] new_args.extend(args[1:]) return super().handle_kill(numeric, command, new_args) @@ -958,6 +950,6 @@ class UnrealProtocol(TS6BaseProtocol): if args[0] == 'alltime': # XXX: We override notice() here because that abstraction doesn't allow messages from servers. - self._send(self.irc.sid, 'NOTICE %s :*** Server=%s time()=%d' % (source, self.irc.hostname(), time.time())) + self._send_with_prefix(self.sid, 'NOTICE %s :*** Server=%s time()=%d' % (source, self.hostname(), time.time())) Class = UnrealProtocol diff --git a/pylink b/pylink index aea3b45..30a48f1 100755 --- a/pylink +++ b/pylink @@ -83,9 +83,13 @@ if __name__ == '__main__': except (KeyError, TypeError): log.error("(%s) Configuration error: No protocol module specified, aborting.", network) else: - # Fetch the correct protocol module + # Fetch the correct protocol module. proto = utils.getProtocolModule(protoname) - world.networkobjects[network] = irc = classes.Irc(network, proto, conf.conf) + + # Create and connect the network. + world.networkobjects[network] = irc = proto.Class(network) + log.debug('Connecting to network %r', network) + irc.connect() world.started.set() log.info("Loaded plugins: %s", ', '.join(sorted(world.plugins.keys()))) diff --git a/utils.py b/utils.py index 48b9033..0526889 100644 --- a/utils.py +++ b/utils.py @@ -129,25 +129,6 @@ def isHostmask(text): # Band-aid patch here to prevent bad bans set by Janus forwarding people into invalid channels. return hostmaskRe.match(text) and '#' not in text -def parseModes(irc, target, args): - """Parses a modestring list into a list of (mode, argument) tuples. - ['+mitl-o', '3', 'person'] => [('+m', None), ('+i', None), ('+t', None), ('+l', '3'), ('-o', 'person')] - - This method is deprecated. Use irc.parseModes() instead. - """ - log.warning("(%s) utils.parseModes is deprecated. Use irc.parseModes() instead!", irc.name) - return irc.parseModes(target, args) - -def applyModes(irc, target, changedmodes): - """Takes a list of parsed IRC modes, and applies them on the given target. - - The target can be either a channel or a user; this is handled automatically. - - This method is deprecated. Use irc.applyModes() instead. - """ - log.warning("(%s) utils.applyModes is deprecated. Use irc.applyModes() instead!", irc.name) - return irc.applyModes(target, changedmodes) - def expandpath(path): """ Returns a path expanded with environment variables and home folders (~) expanded, in that order.""" @@ -250,7 +231,7 @@ class ServiceBot(): # which is handled by coreplugin. if irc is None: for irc in world.networkobjects.values(): - irc.callHooks([None, 'PYLINK_NEW_SERVICE', {'name': self.name}]) + irc.call_hooks([None, 'PYLINK_NEW_SERVICE', {'name': self.name}]) else: raise NotImplementedError("Network specific plugins not supported yet.") @@ -301,7 +282,7 @@ class ServiceBot(): else: irc.proto.join(u, chan) - irc.callHooks([irc.sid, 'PYLINK_SERVICE_JOIN', {'channel': chan, 'users': [u]}]) + irc.call_hooks([irc.sid, 'PYLINK_SERVICE_JOIN', {'channel': chan, 'users': [u]}]) else: log.warning('(%s) Ignoring invalid autojoin channel %r.', irc.name, chan) @@ -343,10 +324,10 @@ class ServiceBot(): if cmd and show_unknown_cmds and not cmd.startswith('\x01'): # Ignore empty commands and invalid command errors from CTCPs. self.reply(irc, 'Error: Unknown command %r.' % cmd) - log.info('(%s/%s) Received unknown command %r from %s', irc.name, self.name, cmd, irc.getHostmask(source)) + log.info('(%s/%s) Received unknown command %r from %s', irc.name, self.name, cmd, irc.get_hostmask(source)) return - log.info('(%s/%s) Calling command %r for %s', irc.name, self.name, cmd, irc.getHostmask(source)) + log.info('(%s/%s) Calling command %r for %s', irc.name, self.name, cmd, irc.get_hostmask(source)) for func in self.commands[cmd]: try: func(irc, source, cmd_args) @@ -601,8 +582,36 @@ class DeprecatedAttributesObject(): def __getattribute__(self, attr): # Note: "self.deprecated_attributes" calls this too, so the != check is # needed to prevent a recursive loop! - if attr != 'deprecated_attributes' and attr in self.deprecated_attributes: + # Also ignore reserved names beginning with "__". + if attr != 'deprecated_attributes' and not attr.startswith('__') and attr in self.deprecated_attributes: log.warning('Attribute %s.%s is deprecated: %s' % (self.__class__.__name__, attr, self.deprecated_attributes.get(attr))) return object.__getattribute__(self, attr) + +class CamelCaseToSnakeCase(): + """ + Class which automatically converts missing attributes from camel case to snake case. + """ + + def __getattr__(self, attr): + """ + Attribute fetching fallback function which normalizes camel case attributes to snake case. + """ + assert isinstance(attr, str), "Requested attribute %r is not a string!" % attr + + normalized_attr = '' # Start off with the first letter, which is ignored when processing + for char in attr: + if char in string.ascii_uppercase: + char = '_' + char.lower() + normalized_attr += char + + classname = self.__class__.__name__ + if normalized_attr == attr: + # __getattr__ only fires if normal attribute fetching fails, so we can assume that + # the attribute was tried already and failed. + raise AttributeError('%s object has no attribute with normalized name %r' % (classname, attr)) + + target = getattr(self, normalized_attr) + log.warning('%s.%s is deprecated, considering migrating to %s.%s!', classname, attr, classname, normalized_attr) + return target