diff --git a/classes.py b/classes.py index 0cf1c64..3f65ea8 100644 --- a/classes.py +++ b/classes.py @@ -35,7 +35,7 @@ class ProtocolError(RuntimeError): 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, @@ -48,12 +48,26 @@ class PyLinkNetworkCore(utils.DeprecatedAttributesObject, utils.CamelCaseToSnake 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 = None # This is updated in init_vars() + self.proto = self.irc = self # Backwards compat + + # Protocol stuff + 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() # These options depend on self.serverdata from above to be set. self.encoding = None @@ -105,6 +119,8 @@ class PyLinkNetworkCore(utils.DeprecatedAttributesObject, utils.CamelCaseToSnake (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.protoname = __name__.split('.')[-1] # Remove leading pylinkirc.protocols. + self.encoding = self.serverdata.get('encoding') or 'utf-8' self.pingfreq = self.serverdata.get('pingfreq') or 90 self.pingtimeout = self.pingfreq * 3 @@ -394,6 +410,31 @@ class PyLinkNetworkCore(utils.DeprecatedAttributesObject, utils.CamelCaseToSnake 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.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.get_server(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) + + class PyLinkNetworkCoreWithUtils(PyLinkNetworkCore): def to_lower(self, text): """Returns a lowercase representation of text based on the IRC object's @@ -1009,6 +1050,85 @@ class PyLinkNetworkCoreWithUtils(PyLinkNetworkCore): 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.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.is_internal_client(user): + p.discard(user) + + def _apply(): + if modes: + log.debug("(%s) Applying modes on channel %s (TS ok)", self.irc.name, + channel) + self.irc.apply_modes(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.is_internal_client(sender) or self.irc.is_internal_server(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() + + # 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.irc.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.irc.nick_to_uid(target) or target + return target + _getUid = _get_UID + class IRCNetwork(PyLinkNetworkCoreWithUtils): def __init__(self, *args): super().__init__(*args) @@ -1433,122 +1553,3 @@ class Channel(): return sorted(result, key=self.sortPrefixes) IrcChannel = Channel - -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): - return - - 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.get_server(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.is_internal_client(user): - p.discard(user) - - def _apply(): - if modes: - log.debug("(%s) Applying modes on channel %s (TS ok)", self.irc.name, - channel) - self.irc.apply_modes(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.is_internal_client(sender) or self.irc.is_internal_server(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 _get_SID(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 - _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.irc.nick_to_uid(target) or target - return target - _getUid = _get_UID diff --git a/protocols/ircs2s_common.py b/protocols/ircs2s_common.py index 5dbfe70..fa0d7bf 100644 --- a/protocols/ircs2s_common.py +++ b/protocols/ircs2s_common.py @@ -6,11 +6,11 @@ import time import re from collections import defaultdict -from pylinkirc.classes import Protocol +from pylinkirc.classes import IRCNetwork from pylinkirc.log import log from pylinkirc import utils -class IRCCommonProtocol(Protocol): +class IRCCommonProtocol(IRCNetwork): def validateServerConf(self): """Validates that the server block given contains the required keys.""" for k in self.conf_keys: diff --git a/pylink b/pylink index aea3b45..2cc8956 100755 --- a/pylink +++ b/pylink @@ -85,7 +85,7 @@ if __name__ == '__main__': else: # Fetch the correct protocol module proto = utils.getProtocolModule(protoname) - world.networkobjects[network] = irc = classes.Irc(network, proto, conf.conf) + world.networkobjects[network] = proto.Class(network) world.started.set() log.info("Loaded plugins: %s", ', '.join(sorted(world.plugins.keys())))