diff --git a/classes.py b/classes.py index 9536746..df03bdf 100644 --- a/classes.py +++ b/classes.py @@ -468,16 +468,13 @@ class PyLinkNetworkCore(structures.DeprecatedAttributesObject, structures.CamelC 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 + # Look for the "service" attribute in the User object,sname = userobj.service + # Warn if the service name we fetched isn't a registered service. + sname = userobj.service + if sname is not None and 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) structures._BLACKLISTED_COPY_TYPES.append(PyLinkNetworkCore) @@ -1596,6 +1593,9 @@ class User(): # Cloaked host for IRCds that use it self.cloaked_host = None + # Stores service bot name if applicable + self.service = None + def __repr__(self): return 'User(%s/%s)' % (self.uid, self.nick) IrcUser = User @@ -1619,6 +1619,9 @@ class Server(): self.desc = desc self._irc = irc + # Has the server finished bursting yet? + self.has_eob = False + def __repr__(self): return 'Server(%s)' % self.name diff --git a/coremods/service_support.py b/coremods/service_support.py index 9b8b2bf..c29e073 100644 --- a/coremods/service_support.py +++ b/coremods/service_support.py @@ -22,8 +22,17 @@ def spawn_service(irc, source, command, args): # Get the ServiceBot object. sbot = world.services[name] + old_userobj = irc.users.get(sbot.uids.get(irc.name)) + if old_userobj and old_userobj.service: + # A client already exists, so don't respawn it. + log.debug('(%s) spawn_service: Not respawning service %r as service client %r already exists.', irc.name, name, + irc.pseudoclient.nick) + return + if name == 'pylink' and irc.pseudoclient: - # irc.pseudoclient already exists, for protocols like clientbot + # irc.pseudoclient already exists, reuse values from it but + # spawn a new client. This is used for protocols like Clientbot, + # so that they can override the main service nick, among other things. log.debug('(%s) spawn_service: Using existing nick %r for service %r', irc.name, irc.pseudoclient.nick, name) userobj = irc.pseudoclient userobj.opertype = "PyLink Service" diff --git a/plugins/relay.py b/plugins/relay.py index 6d4f7d3..66a5e2c 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -217,7 +217,7 @@ def get_prefix_modes(irc, remoteirc, channel, user, mlist=None): return modes def spawn_relay_server(irc, remoteirc): - if irc.connected.wait(TCONDITION_TIMEOUT): + if irc.connected.is_set(): try: # ENDBURST is delayed by 3 secs on supported IRCds to prevent # triggering join-flood protection and the like. @@ -358,7 +358,7 @@ def get_remote_user(irc, remoteirc, user, spawn_if_missing=True, times_tagged=0) spawning one if it doesn't exist and spawn_if_missing is True.""" # Wait until the network is working before trying to spawn anything. - if irc.connected.wait(TCONDITION_TIMEOUT): + if irc.connected.is_set(): # Don't spawn clones for registered service bots. sbot = irc.get_service_bot(user) if sbot: diff --git a/protocols/clientbot.py b/protocols/clientbot.py index 00b1daf..b763e14 100644 --- a/protocols/clientbot.py +++ b/protocols/clientbot.py @@ -633,10 +633,9 @@ class ClientbotWrapperProtocol(IRCCommonProtocol): self.send(line) # Virtual endburst hook. - self.connected.set() # Note, this should always be set before sending ENDBURST - if not self.has_eob: - self.has_eob = True - return {'parse_as': 'ENDBURST'} + self.connected.set() # Note, this should always be set before the actual ENDBURST hook + self.servers[self.uplink].has_eob = True + return {'parse_as': 'ENDBURST'} handle_422 = handle_376 diff --git a/protocols/hybrid.py b/protocols/hybrid.py index f5672ae..60df8de 100644 --- a/protocols/hybrid.py +++ b/protocols/hybrid.py @@ -17,13 +17,11 @@ class HybridProtocol(TS6Protocol): self.casemapping = 'ascii' self.caps = {} self.hook_map = {'EOB': 'ENDBURST', 'TBURST': 'TOPIC', 'SJOIN': 'JOIN'} - self.has_eob = False self.protocol_caps -= {'slash-in-hosts'} def post_connect(self): """Initializes a connection to a server.""" ts = self.start_ts - self.has_eob = False f = self.send # https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L80 @@ -227,11 +225,15 @@ class HybridProtocol(TS6Protocol): return {'channel': channel, 'setter': setter, 'ts': ts, 'text': topic} def handle_eob(self, numeric, command, args): - log.debug('(%s) end of burst received', self.name) - if not self.has_eob: # Only call ENDBURST hooks if we haven't already. - return {} + """EOB (end-of-burst) handler.""" + log.debug('(%s) end of burst received from %s', self.name, numeric) + if not self.servers[numeric].has_eob: + # Don't fight with TS6's generic PING-as-EOB + self.servers[numeric].has_eob = True - self.has_eob = True + if numeric == self.uplink: + self.connected.set() + return {} def handle_svsmode(self, numeric, command, args): """ diff --git a/protocols/inspircd.py b/protocols/inspircd.py index 38fc127..1e3bfa0 100644 --- a/protocols/inspircd.py +++ b/protocols/inspircd.py @@ -520,9 +520,6 @@ class InspIRCdProtocol(TS6BaseProtocol): log.debug('(%s) self.prefixmodes set to %r', self.name, self.prefixmodes) - # Finally, set the irc.connected (protocol negotiation complete) - # state to True. - 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() @@ -716,6 +713,9 @@ class InspIRCdProtocol(TS6BaseProtocol): def handle_endburst(self, numeric, command, args): """ENDBURST handler; sends a hook with empty contents.""" + self.servers[numeric].has_eob = True + if numeric == self.uplink: + self.connected.set() return {} def handle_away(self, numeric, command, args): diff --git a/protocols/ngircd.py b/protocols/ngircd.py index 8714683..15a92e0 100644 --- a/protocols/ngircd.py +++ b/protocols/ngircd.py @@ -229,26 +229,32 @@ class NgIRCdProtocol(IRCS2SProtocol): raise LookupError('No such PyLink client exists.') log.debug('(%s) sjoin: got %r for users', self.name, users) - njoin_prefix = ':%s NJOIN %s :' % (self._expandPUID(self.sid), channel) + njoin_prefix = ':%s NJOIN %s :' % (self._expandPUID(server), channel) # Format the user list into strings such as @user1, +user2, user3, etc. - nicks_to_send = ['%s%s' % (''.join(self.prefixmodes[modechar] for modechar in userpair[0] if modechar in self.prefixmodes), - self._expandPUID(userpair[1])) for userpair in users] - - # Use 13 args max per line: this is equal to the max of 15 minus the command name and target channel. - for message in utils.wrapArguments(njoin_prefix, nicks_to_send, self.S2S_BUFSIZE, separator=',', max_args_per_line=13): - self.send(message) - - # Add the affected users to our state. + nicks_to_send = [] for userpair in users: - uid = userpair[1] + prefixes, uid = userpair + + if uid not in self.users: + log.warning('(%s) Trying to NJOIN missing user %s?', self.name, uid) + continue + elif uid in self._channels[channel].users: + # Don't rejoin users already in the channel, this causes errors with ngIRCd. + continue + self._channels[channel].users.add(uid) - try: - self.users[uid].channels.add(channel) - except KeyError: # Not initialized yet? - log.warning("(%s) sjoin: KeyError trying to add %r to %r's channel list?", self.name, channel, uid) + self.users[uid].channels.add(channel) self.apply_modes(channel, (('+%s' % prefix, uid) for prefix in userpair[0])) + nicks_to_send.append(''.join(self.prefixmodes[modechar] for modechar in userpair[0]) + \ + self._expandPUID(userpair[1])) + + if nicks_to_send: + # Use 13 args max per line: this is equal to the max of 15 minus the command name and target channel. + for message in utils.wrapArguments(njoin_prefix, nicks_to_send, self.S2S_BUFSIZE, separator=',', max_args_per_line=13): + self.send(message) + if modes: # Burst modes separately if there are any. log.debug("(%s) sjoin: bursting modes %r for channel %r now", self.name, modes, channel) @@ -504,16 +510,20 @@ class NgIRCdProtocol(IRCS2SProtocol): assert 'IRC+' in args[1], "Linking to non-ngIRCd server using this protocol module is not supported" def handle_ping(self, source, command, args): - if source == self.uplink: - self._send_with_prefix(self.sid, 'PONG %s :%s' % (self._expandPUID(self.sid), args[-1]), queue=False) + """ + Handles incoming PINGs (and implicit end of burst). + """ + self._send_with_prefix(self.sid, 'PONG %s :%s' % (self._expandPUID(self.sid), args[-1]), queue=False) - if not self.has_eob: - # Treat the first PING we receive as end of burst. - self.has_eob = True + if not self.servers[source].has_eob: + # Treat the first PING we receive as end of burst. + self.servers[source].has_eob = True + + if source == self.uplink: self.connected.set() - # Return the endburst hook. - return {'parse_as': 'ENDBURST'} + # Return the endburst hook. + return {'parse_as': 'ENDBURST'} def handle_server(self, source, command, args): """ diff --git a/protocols/p10.py b/protocols/p10.py index 264a52c..4af3421 100644 --- a/protocols/p10.py +++ b/protocols/p10.py @@ -824,7 +824,6 @@ class P10Protocol(IRCS2SProtocol): 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.""" @@ -1110,15 +1109,19 @@ class P10Protocol(IRCS2SProtocol): return {'channel': channel, 'users': [source], 'modes': 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.""" + """Handles end of burst from servers.""" + # 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.uplink: self._send_with_prefix(self.sid, 'EA') - return {} + self.connected.set() + + self.servers[source].has_eob = True + return {} def handle_kick(self, source, command, args): """Handles incoming KICKs.""" diff --git a/protocols/ts6.py b/protocols/ts6.py index 2c42bd4..aab7dd4 100644 --- a/protocols/ts6.py +++ b/protocols/ts6.py @@ -20,9 +20,6 @@ class TS6Protocol(TS6BaseProtocol): self.hook_map = {'SJOIN': 'JOIN', 'TB': 'TOPIC', 'TMODE': 'MODE', 'BMASK': 'MODE', 'EUID': 'UID', 'RSFNC': 'SVSNICK', 'ETB': 'TOPIC', 'USERMODE': 'MODE'} - # Track whether we've received end-of-burst from the uplink. - self.has_eob = False - self.required_caps = {'EUID', 'SAVE', 'TB', 'ENCAP', 'QS', 'CHW'} # From ChatIRCd: https://github.com/ChatLounge/ChatIRCd/blob/master/doc/technical/ChatIRCd-extra.txt @@ -264,7 +261,6 @@ class TS6Protocol(TS6BaseProtocol): def post_connect(self): """Initializes a connection to a server.""" ts = self.start_ts - self.has_eob = False f = self.send @@ -423,9 +419,6 @@ class TS6Protocol(TS6BaseProtocol): if 'SERVICES' in caps: self.cmodes['regonly'] = 'r' - log.debug('(%s) self.connected set!', self.name) - self.connected.set() - def handle_ping(self, source, command, args): """Handles incoming PING commands.""" # PING: @@ -445,10 +438,14 @@ class TS6Protocol(TS6BaseProtocol): if self.is_internal_server(destination): self._send_with_prefix(destination, 'PONG %s %s' % (destination, source), queue=False) - if destination == self.sid and not self.has_eob: - # Charybdis' endburst is just sending a PING to the other server. + if not self.servers[source].has_eob: + # TS6 endburst is just sending a PING to the other server. # https://github.com/charybdis-ircd/charybdis/blob/dc336d1/modules/core/m_server.c#L484-L485 - self.has_eob = True + self.servers[source].has_eob = True + + if source == self.uplink: + log.debug('(%s) self.connected set!', self.name) + self.connected.set() # Return the endburst hook. return {'parse_as': 'ENDBURST'} diff --git a/protocols/unreal.py b/protocols/unreal.py index 18444b9..ad9b2d2 100644 --- a/protocols/unreal.py +++ b/protocols/unreal.py @@ -379,6 +379,9 @@ class UnrealProtocol(TS6BaseProtocol): def handle_eos(self, numeric, command, args): """EOS is used to denote end of burst.""" + self.servers[numeric].has_eob = True + if numeric == self.uplink: + self.connected.set() return {} def handle_uid(self, numeric, command, args): @@ -482,9 +485,6 @@ class UnrealProtocol(TS6BaseProtocol): "(Unreal 4.x), got %s)" % (self.min_proto_ver, protover)) self.servers[numeric] = Server(self, None, sname, desc=sdesc) - # Set irc.connected to True, meaning that protocol negotiation passed. - 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