diff --git a/classes.py b/classes.py index 09b50f2..20722b9 100644 --- a/classes.py +++ b/classes.py @@ -1067,24 +1067,37 @@ class Protocol(): log.debug('Removing client %s from self.irc.servers[%s].users', numeric, sid) self.irc.servers[sid].users.discard(numeric) - def updateTS(self, channel, their_ts): + def updateTS(self, channel, their_ts, modes=[]): """ - Compares the current TS of the channel given with the new TS, resetting - all modes we have if the one given is older. + Merges modes of a channel given the remote TS and a list of modes. + + This returns True when our modes apply (our TS <= theirs) """ our_ts = self.irc.channels[channel].ts if their_ts < our_ts: - # Channel timestamp was reset on burst + # Their TS is older than ours. Clear all modes. log.debug('(%s) Setting channel TS of %s to %s from %s', self.irc.name, channel, their_ts, our_ts) self.irc.channels[channel].ts = their_ts - # When TS is reset, clear all modes we currently have + self.irc.channels[channel].modes.clear() for p in self.irc.channels[channel].prefixmodes.values(): p.clear() + return False + + elif their_ts == our_ts: + # Their TS is equal to ours. Merge modes. + self.irc.applyModes(channel, modes) + return True + elif their_ts > our_ts: + # Their TS is younger than ours. Replace their modes with ours. + self.irc.channels[channel].modes.clear() + self.irc.applyModes(channel, modes) + return True + def _getSid(self, sname): """Returns the SID of a server with the given name, if present.""" name = sname.lower() diff --git a/protocols/inspircd.py b/protocols/inspircd.py index d04274f..5a27a15 100644 --- a/protocols/inspircd.py +++ b/protocols/inspircd.py @@ -85,7 +85,7 @@ class InspIRCdProtocol(TS6BaseProtocol): self.irc.channels[channel].users.add(client) self.irc.users[client].channels.add(channel) - def sjoin(self, server, channel, users, ts=None): + def 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 @@ -100,20 +100,27 @@ class InspIRCdProtocol(TS6BaseProtocol): server = server or self.irc.sid assert users, "sjoin: No users sent?" log.debug('(%s) sjoin: got %r for users', self.irc.name, users) + if not server: raise LookupError('No such PyLink client exists.') - orig_ts = self.irc.channels[channel].ts - ts = ts or orig_ts - self.updateTS(channel, ts) - - log.debug("sending SJOIN to %s%s with ts %s (that's %r)", channel, self.irc.name, ts, - time.strftime("%c", time.localtime(ts))) # Strip out list-modes, they shouldn't ever be sent in FJOIN (protocol rules). - modes = [m for m in self.irc.channels[channel].modes if m[0] not in self.irc.cmodes['*A']] + modes = modes or self.irc.channels[channel].modes + + banmodes = [] + regularmodes = [] + for mode in modes: + modechar = mode[0][-1] + # Don't reset bans that have already been set + if modechar in self.irc.cmodes['*A'] and (modechar, mode[1]) not in self.irc.channels[channel].modes: + banmodes.append(mode) + else: + regularmodes.append(mode) + uids = [] - changedmodes = [] + changedmodes = set(modes) namelist = [] + # We take as a list of (prefixmodes, uid) pairs. for userpair in users: assert len(userpair) == 2, "Incorrect format of userpair: %r" % userpair @@ -121,20 +128,28 @@ class InspIRCdProtocol(TS6BaseProtocol): namelist.append(','.join(userpair)) uids.append(user) for m in prefixes: - changedmodes.append(('+%s' % m, user)) + changedmodes.add(('+%s' % m, user)) try: self.irc.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) - if ts <= orig_ts: - # Only save our prefix modes in the channel state if our TS is lower than or equal to theirs. - self.irc.applyModes(channel, changedmodes) + namelist = ' '.join(namelist) self._send(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) + 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))) + + orig_ts = self.irc.channels[channel].ts + ts = ts or orig_ts + self.updateTS(channel, ts, changedmodes) + def _operUp(self, target, opertype=None): """Opers a client up (internal function specific to InspIRCd). diff --git a/protocols/nefarious.py b/protocols/nefarious.py index 15388e4..c70aec1 100644 --- a/protocols/nefarious.py +++ b/protocols/nefarious.py @@ -450,7 +450,7 @@ class P10Protocol(Protocol): else: raise LookupError("No such PyLink client exists.") - def sjoin(self, server, channel, users, ts=None): + def 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 @@ -470,14 +470,11 @@ class P10Protocol(Protocol): if not server: raise LookupError('No such PyLink client exists.') - orig_ts = self.irc.channels[channel].ts - ts = ts or orig_ts - self.updateTS(channel, ts) - # Only send non-list modes in BURST. TODO: burst bans and banexempts too - modes = [m for m in self.irc.channels[channel].modes if m[0] not in self.irc.cmodes['*A']] + modes = modes or self.irc.channels[channel].modes + modes = [m for m in modes if m[0] not in self.irc.cmodes['*A']] - changedmodes = [] + changedmodes = modes changedusers = [] namelist = [] @@ -532,9 +529,9 @@ class P10Protocol(Protocol): self.irc.channels[channel].users.update(changedusers) - if ts <= orig_ts: - # Only save our prefix modes in the channel state if our TS is lower than or equal to theirs. - self.irc.applyModes(channel, changedmodes) + orig_ts = self.irc.channels[channel].ts + ts = ts or orig_ts + self.updateTS(channel, ts, changedmodes) def spawnServer(self, name, sid=None, uplink=None, desc=None, endburst_delay=0): """ @@ -1032,9 +1029,11 @@ class P10Protocol(Protocol): oldchans = self.irc.users[numeric].channels.copy() log.debug('(%s) Got /join 0 from %r, channel list is %r', self.irc.name, numeric, oldchans) + for channel in oldchans: self.irc.channels[channel].users.discard(source) self.irc.users[source].channels.discard(channel) + return {'channels': oldchans, 'text': 'Left all channels.', 'parse_as': 'PART'} else: channel = self.irc.toLower(args[0]) diff --git a/protocols/ts6.py b/protocols/ts6.py index 4fbacce..abd3afc 100644 --- a/protocols/ts6.py +++ b/protocols/ts6.py @@ -73,7 +73,7 @@ class TS6Protocol(TS6BaseProtocol): self.irc.channels[channel].users.add(client) self.irc.users[client].channels.add(channel) - def sjoin(self, server, channel, users, ts=None): + def 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 @@ -100,14 +100,23 @@ class TS6Protocol(TS6BaseProtocol): if not server: raise LookupError('No such PyLink client exists.') - orig_ts = self.irc.channels[channel].ts - ts = ts or orig_ts - self.updateTS(channel, ts) + modes = set(modes or self.irc.channels[channel].modes) - log.debug("(%s) sending SJOIN to %s with ts %s (that's %r)", self.irc.name, channel, ts, - time.strftime("%c", time.localtime(ts))) - modes = [m for m in self.irc.channels[channel].modes if m[0] not in self.irc.cmodes['*A']] - changedmodes = [] + # Get all the ban modes in a separate list. These are bursted using a separate BMASK + # command. + banmodes = {k: set() for k in self.irc.cmodes['*A']} + regularmodes = [] + log.debug('(%s) Unfiltered SJOIN modes: %s', self.irc.name, modes) + for mode in modes: + modechar = mode[0][-1] + if modechar in self.irc.cmodes['*A']: + # Mode character is one of 'beIq' + banmodes[modechar].add(mode[1]) + else: + regularmodes.append(mode) + log.debug('(%s) Filtered SJOIN modes to be regular modes: %s, banmodes: %s', self.irc.name, regularmodes, banmodes) + + changedmodes = modes while users[:10]: uids = [] namelist = [] @@ -120,7 +129,7 @@ class TS6Protocol(TS6BaseProtocol): pr = self.irc.prefixmodes.get(prefix) if pr: prefixchars += pr - changedmodes.append(('+%s' % prefix, user)) + changedmodes.add(('+%s' % prefix, user)) namelist.append(prefixchars+user) uids.append(user) try: @@ -131,11 +140,20 @@ class TS6Protocol(TS6BaseProtocol): namelist = ' '.join(namelist) self._send(server, "SJOIN {ts} {channel} {modes} :{users}".format( ts=ts, users=namelist, channel=channel, - modes=self.irc.joinModes(modes))) + modes=self.irc.joinModes(regularmodes))) self.irc.channels[channel].users.update(uids) - if ts <= orig_ts: - # Only save our prefix modes in the channel state if our TS is lower than or equal to theirs. - self.irc.applyModes(channel, changedmodes) + + # Now, burst bans. + # <- :42X BMASK 1424222769 #dev b :*!test@*.isp.net *!badident@* + for bmode, bans in banmodes.items(): + if bans: + log.debug('(%s) sjoin: bursting mode %s with bans %s', self.irc.name, bmode, bans) + self._send(server, "BMASK {ts} {channel} {bmode} :{bans}".format(ts=ts, + channel=channel, bmode=bmode, bans=' '.join(bans))) + + orig_ts = self.irc.channels[channel].ts + ts = ts or orig_ts + self.updateTS(channel, ts, changedmodes) def mode(self, numeric, target, modes, ts=None): """Sends mode changes from a PyLink client/server.""" diff --git a/protocols/unreal.py b/protocols/unreal.py index eec9fbd..46cd2c1 100644 --- a/protocols/unreal.py +++ b/protocols/unreal.py @@ -123,7 +123,7 @@ class UnrealProtocol(TS6BaseProtocol): self.irc.channels[channel].users.add(client) self.irc.users[client].channels.add(channel) - def sjoin(self, server, channel, users, ts=None): + def 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 (SID). TS is optional, and defaults @@ -147,36 +147,46 @@ class UnrealProtocol(TS6BaseProtocol): if not server: raise LookupError('No such PyLink server exists.') - orig_ts = self.irc.channels[channel].ts - ts = ts or orig_ts - self.updateTS(channel, ts) - - changedmodes = [] + changedmodes = set(modes or self.irc.channels[channel].modes) uids = [] namelist = [] + for userpair in users: assert len(userpair) == 2, "Incorrect format of userpair: %r" % userpair prefixes, user = userpair + # Unreal uses slightly different prefixes in SJOIN. +q is * instead of ~, # and +a is ~ instead of &. # &, ", and ' are used for bursting bans. sjoin_prefixes = {'q': '*', 'a': '~', 'o': '@', 'h': '%', 'v': '+'} prefixchars = ''.join([sjoin_prefixes.get(prefix, '') for prefix in prefixes]) + if prefixchars: - changedmodes + [('+%s' % prefix, user) for prefix in prefixes] + changedmodes |= {('+%s' % prefix, user) for prefix in prefixes} + namelist.append(prefixchars+user) uids.append(user) + try: self.irc.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) + namelist = ' '.join(namelist) + self._send(server, "SJOIN {ts} {channel} :{users}".format( ts=ts, users=namelist, channel=channel)) + + # Burst modes separately. No really, this is what I see UnrealIRCd do! It sends + # JOINs on burst and then MODE! + if modes: + self.mode(server, channel, modes, ts=ts) + self.irc.channels[channel].users.update(uids) - if ts <= orig_ts: - # Only save our prefix modes in the channel state if our TS is lower than or equal to theirs. - self.irc.applyModes(channel, changedmodes) + + orig_ts = self.irc.channels[channel].ts + ts = ts or orig_ts + self.updateTS(channel, ts, changedmodes) def ping(self, source=None, target=None): """Sends a PING to a target server. Periodic PINGs are sent to our uplink