From 57da0aa3b375d0e5362a4369ce5d73894298553e Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 5 Sep 2015 18:00:57 -0700 Subject: [PATCH] Classify all our protocol modules - why didn't I do this earlier --- classes.py | 22 +- coreplugin.py | 18 +- plugins/bots.py | 20 +- plugins/relay.py | 72 +-- protocols/inspircd.py | 1024 +++++++++++++++++----------------- protocols/ts6.py | 1171 ++++++++++++++++++++------------------- protocols/ts6_common.py | 439 +++++++-------- utils.py | 4 +- 8 files changed, 1393 insertions(+), 1377 deletions(-) diff --git a/classes.py b/classes.py index 51a5259..794be27 100644 --- a/classes.py +++ b/classes.py @@ -65,7 +65,8 @@ class Irc(): self.serverdata = conf['servers'][netname] self.sid = self.serverdata["sid"] self.botdata = conf['bot'] - self.proto = proto + self.protoname = proto.__name__ + self.proto = proto.Class(self) self.pingfreq = self.serverdata.get('pingfreq') or 30 self.pingtimeout = self.pingfreq * 2 @@ -142,7 +143,7 @@ class Irc(): sha1fp) if checks_ok: - self.proto.connect(self) + self.proto.connect() self.spawnMain() log.info('(%s) Starting ping schedulers....', self.name) self.schedulePing() @@ -200,7 +201,7 @@ class Irc(): log.debug("(%s) <- %s", self.name, line) hook_args = None try: - hook_args = self.proto.handle_events(self, line) + hook_args = self.proto.handle_events(line) except Exception: log.exception('(%s) Caught error in handle_events, disconnecting!', self.name) return @@ -249,7 +250,7 @@ class Irc(): log.debug("(%s) Dropping message %r; network isn't connected!", self.name, stripped_data) def schedulePing(self): - self.proto.pingServer(self) + self.proto.pingServer() self.pingTimer = threading.Timer(self.pingfreq, self.schedulePing) self.pingTimer.daemon = True self.pingTimer.start() @@ -261,9 +262,9 @@ class Irc(): host = self.serverdata["hostname"] log.info('(%s) Connected! Spawning main client %s.', self.name, nick) olduserobj = self.pseudoclient - self.pseudoclient = self.proto.spawnClient(self, nick, ident, host, modes={("+o", None)}) + self.pseudoclient = self.proto.spawnClient(nick, ident, host, modes={("+o", None)}) for chan in self.serverdata['channels']: - self.proto.joinClient(self, self.pseudoclient.uid, chan) + self.proto.joinClient(self.pseudoclient.uid, chan) # PyLink internal hook called when spawnMain is called and the # contents of Irc().pseudoclient change. self.callHooks([self.sid, 'PYLINK_SPAWNMAIN', {'olduser': olduserobj}]) @@ -339,7 +340,7 @@ class FakeIRC(Irc): def run(self, data): """Queues a message to the fake IRC server.""" log.debug('<- ' + data) - hook_args = self.proto.handle_events(self, data) + hook_args = self.proto.handle_events(data) if hook_args is not None: self.hookmsgs.append(hook_args) self.callHooks(hook_args) @@ -374,6 +375,13 @@ class FakeIRC(Irc): self.hookmsgs = [] return hookmsgs +class Protocol(): + # TODO: Future state-keeping things will go here + def __init__(self, irc): + self.irc = irc + self.casemapping = 'rfc1459' + self.hook_map = {} + class FakeProto(): """Dummy protocol module for testing purposes.""" def __init__(self): diff --git a/coreplugin.py b/coreplugin.py index 5e4b4d5..6db3508 100644 --- a/coreplugin.py +++ b/coreplugin.py @@ -15,7 +15,7 @@ def handle_kick(irc, source, command, args): kicked = args['target'] channel = args['channel'] if kicked == irc.pseudoclient.uid: - irc.proto.joinClient(irc, irc.pseudoclient.uid, channel) + irc.proto.joinClient(irc.pseudoclient.uid, channel) utils.add_hook(handle_kick, 'KICK') # Handle commands sent to the PyLink client (PRIVMSG) @@ -51,7 +51,7 @@ def handle_whois(irc, source, command, args): sourceisOper = ('o', None) in irc.users[source].modes # https://www.alien.net.au/irc/irc2numerics.html # 311: sends nick!user@host information - f(irc, server, 311, source, "%s %s %s * :%s" % (nick, user.ident, user.host, user.realname)) + f(server, 311, source, "%s %s %s * :%s" % (nick, user.ident, user.host, user.realname)) # 319: RPL_WHOISCHANNELS, shows channel list public_chans = [] for chan in user.channels: @@ -69,9 +69,9 @@ def handle_whois(irc, source, command, args): chan = prefixchar + chan public_chans.append(chan) if public_chans: - f(irc, server, 319, source, '%s :%s' % (nick, ' '.join(public_chans))) + f(server, 319, source, '%s :%s' % (nick, ' '.join(public_chans))) # 312: sends the server the target is on, and its server description. - f(irc, server, 312, source, "%s %s :%s" % (nick, irc.serverdata['hostname'], + f(server, 312, source, "%s %s :%s" % (nick, irc.serverdata['hostname'], irc.serverdata.get('serverdesc') or irc.botdata['serverdesc'])) # 313: sends a string denoting the target's operator privilege, # only if they have umode +o. @@ -82,15 +82,15 @@ def handle_whois(irc, source, command, args): opertype = "IRC Operator" # Let's be gramatically correct. n = 'n' if opertype[0].lower() in 'aeiou' else '' - f(irc, server, 313, source, "%s :is a%s %s" % (nick, n, opertype)) + f(server, 313, source, "%s :is a%s %s" % (nick, n, opertype)) # 379: RPL_WHOISMODES, used by UnrealIRCd and InspIRCd. # Only show this to opers! if sourceisOper: - f(irc, server, 379, source, '%s :is using modes %s' % (nick, utils.joinModes(user.modes))) + f(server, 379, source, '%s :is using modes %s' % (nick, utils.joinModes(user.modes))) # 317: shows idle and signon time. However, we don't track the user's real # idle time, so we simply return 0. # <- 317 GL GL 15 1437632859 :seconds idle, signon time - f(irc, server, 317, source, "%s 0 %s :seconds idle, signon time" % (nick, user.ts)) + f(server, 317, source, "%s 0 %s :seconds idle, signon time" % (nick, user.ts)) for func in world.whois_handlers: # Iterate over custom plugin WHOIS handlers. They return a tuple # or list with two arguments: the numeric, and the text to send. @@ -98,11 +98,11 @@ def handle_whois(irc, source, command, args): res = func(irc, target) if res: num, text = res - f(irc, server, num, source, text) + f(server, num, source, text) except Exception as e: # Again, we wouldn't want this to crash our service, in case # something goes wrong! log.exception('(%s) Error caught in WHOIS handler: %s', irc.name, e) # 318: End of WHOIS. - f(irc, server, 318, source, "%s :End of /WHOIS list" % nick) + f(server, 318, source, "%s :End of /WHOIS list" % nick) utils.add_hook(handle_whois, 'WHOIS') diff --git a/plugins/bots.py b/plugins/bots.py index a3fb899..120da17 100644 --- a/plugins/bots.py +++ b/plugins/bots.py @@ -22,7 +22,7 @@ def spawnclient(irc, source, args): except ValueError: utils.msg(irc, source, "Error: Not enough arguments. Needs 3: nick, user, host.") return - irc.proto.spawnClient(irc, nick, ident, host) + irc.proto.spawnClient(nick, ident, host) @utils.add_cmd def quit(irc, source, args): @@ -40,7 +40,7 @@ def quit(irc, source, args): return u = utils.nickToUid(irc, nick) quitmsg = ' '.join(args[1:]) or 'Client Quit' - irc.proto.quitClient(irc, u, quitmsg) + irc.proto.quitClient(u, quitmsg) irc.callHooks([u, 'PYLINK_BOTSPLUGIN_QUIT', {'text': quitmsg, 'parse_as': 'QUIT'}]) def joinclient(irc, source, args): @@ -61,7 +61,7 @@ def joinclient(irc, source, args): if not utils.isChannel(channel): utils.msg(irc, source, "Error: Invalid channel name %r." % channel) return - irc.proto.joinClient(irc, u, channel) + irc.proto.joinClient(u, channel) irc.callHooks([u, 'PYLINK_BOTSPLUGIN_JOIN', {'channel': channel, 'users': [u], 'modes': irc.channels[channel].modes, 'parse_as': 'JOIN'}]) @@ -85,7 +85,7 @@ def nick(irc, source, args): elif not utils.isNick(newnick): utils.msg(irc, source, 'Error: Invalid nickname %r.' % newnick) return - irc.proto.nickClient(irc, u, newnick) + irc.proto.nickClient(u, newnick) irc.callHooks([u, 'PYLINK_BOTSPLUGIN_NICK', {'newnick': newnick, 'oldnick': nick, 'parse_as': 'NICK'}]) @utils.add_cmd @@ -106,7 +106,7 @@ def part(irc, source, args): if not utils.isChannel(channel): utils.msg(irc, source, "Error: Invalid channel name %r." % channel) return - irc.proto.partClient(irc, u, channel, reason) + irc.proto.partClient(u, channel, reason) irc.callHooks([u, 'PYLINK_BOTSPLUGIN_PART', {'channels': clist, 'text': reason, 'parse_as': 'PART'}]) @utils.add_cmd @@ -129,9 +129,9 @@ def kick(irc, source, args): utils.msg(irc, source, "Error: Invalid channel name %r." % channel) return if utils.isInternalServer(irc, u): - irc.proto.kickServer(irc, u, channel, targetu, reason) + irc.proto.kickServer(u, channel, targetu, reason) else: - irc.proto.kickClient(irc, u, channel, targetu, reason) + irc.proto.kickClient(u, channel, targetu, reason) irc.callHooks([u, 'PYLINK_BOTSPLUGIN_KICK', {'channel': channel, 'target': targetu, 'text': reason, 'parse_as': 'KICK'}]) @utils.add_cmd @@ -156,11 +156,11 @@ def mode(irc, source, args): utils.msg(irc, source, "Error: Invalid channel or nick %r." % target) return if utils.isInternalServer(irc, modesource): - irc.proto.modeServer(irc, modesource, target, parsedmodes) + irc.proto.modeServer(modesource, target, parsedmodes) irc.callHooks([modesource, 'PYLINK_BOTSPLUGIN_MODE', {'target': target, 'modes': parsedmodes, 'parse_as': 'MODE'}]) else: sourceuid = utils.nickToUid(irc, modesource) - irc.proto.modeClient(irc, sourceuid, target, parsedmodes) + irc.proto.modeClient(sourceuid, target, parsedmodes) irc.callHooks([sourceuid, 'PYLINK_BOTSPLUGIN_MODE', {'target': target, 'modes': parsedmodes, 'parse_as': 'MODE'}]) @utils.add_cmd @@ -188,5 +188,5 @@ def msg(irc, source, args): if not text: utils.msg(irc, source, 'Error: No text given.') return - irc.proto.messageClient(irc, sourceuid, real_target, text) + irc.proto.messageClient(sourceuid, real_target, text) irc.callHooks([sourceuid, 'PYLINK_BOTSPLUGIN_MSG', {'target': real_target, 'text': text, 'parse_as': 'PRIVMSG'}]) diff --git a/plugins/relay.py b/plugins/relay.py index 286956e..84e418a 100644 --- a/plugins/relay.py +++ b/plugins/relay.py @@ -39,7 +39,7 @@ def normalizeNick(irc, netname, nick, separator=None, uid=''): separator = separator or irc.serverdata.get('separator') or "/" log.debug('(%s) normalizeNick: using %r as separator.', irc.name, separator) orig_nick = nick - protoname = irc.proto.__name__ + protoname = irc.protoname maxnicklen = irc.maxnicklen if '/' not in separator or not protoname.startswith(('insp', 'unreal')): # Charybdis doesn't allow / in usernames, and will SQUIT with @@ -172,7 +172,7 @@ def getRemoteUser(irc, remoteirc, user, spawnIfMissing=True): hideoper_mode = remoteirc.umodes.get('hideoper') if hideoper_mode: modes.append((hideoper_mode, None)) - u = remoteirc.proto.spawnClient(remoteirc, nick, ident=ident, + u = remoteirc.proto.spawnClient(nick, ident=ident, host=host, realname=realname, modes=modes, ts=userobj.ts, opertype=opertype).uid @@ -180,7 +180,7 @@ def getRemoteUser(irc, remoteirc, user, spawnIfMissing=True): remoteirc.users[u].opertype = opertype away = userobj.away if away: - remoteirc.proto.awayClient(remoteirc, u, away) + remoteirc.proto.awayClient(u, away) relayusers[(irc.name, user)][remoteirc.name] = u return u @@ -275,11 +275,11 @@ def initializeChannel(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.topicServer(irc, irc.sid, channel, topic) + irc.proto.topicServer(irc.sid, channel, topic) # Send our users and channel modes to the other nets log.debug('(%s) initializeChannel: joining our users: %s', irc.name, c.users) relayJoins(irc, channel, c.users, c.ts) - irc.proto.joinClient(irc, irc.pseudoclient.uid, channel) + irc.proto.joinClient(irc.pseudoclient.uid, channel) def handle_join(irc, numeric, command, args): channel = args['channel'] @@ -294,7 +294,7 @@ utils.add_hook(handle_join, 'JOIN') def handle_quit(irc, numeric, command, args): for netname, user in relayusers[(irc.name, numeric)].copy().items(): remoteirc = world.networkobjects[netname] - remoteirc.proto.quitClient(remoteirc, user, args['text']) + remoteirc.proto.quitClient(user, args['text']) del relayusers[(irc.name, numeric)] utils.add_hook(handle_quit, 'QUIT') @@ -310,7 +310,7 @@ def handle_nick(irc, numeric, command, args): remoteirc = world.networkobjects[netname] newnick = normalizeNick(remoteirc, irc.name, args['newnick'], uid=user) if remoteirc.users[user].nick != newnick: - remoteirc.proto.nickClient(remoteirc, user, newnick) + remoteirc.proto.nickClient(user, newnick) utils.add_hook(handle_nick, 'NICK') def handle_part(irc, numeric, command, args): @@ -325,9 +325,9 @@ def handle_part(irc, numeric, command, args): remotechan = findRemoteChan(irc, remoteirc, channel) if remotechan is None: continue - remoteirc.proto.partClient(remoteirc, user, remotechan, text) + remoteirc.proto.partClient(user, remotechan, text) if not remoteirc.users[user].channels: - remoteirc.proto.quitClient(remoteirc, user, 'Left all shared channels.') + remoteirc.proto.quitClient(user, 'Left all shared channels.') del relayusers[(irc.name, numeric)][remoteirc.name] utils.add_hook(handle_part, 'PART') @@ -363,9 +363,9 @@ def handle_privmsg(irc, numeric, command, args): continue real_target = prefix + real_target if notice: - remoteirc.proto.noticeClient(remoteirc, user, real_target, text) + remoteirc.proto.noticeClient(user, real_target, text) else: - remoteirc.proto.messageClient(remoteirc, user, real_target, text) + remoteirc.proto.messageClient(user, real_target, text) else: remoteuser = getLocalUser(irc, target) if remoteuser is None: @@ -383,9 +383,9 @@ def handle_privmsg(irc, numeric, command, args): remoteirc = world.networkobjects[homenet] user = getRemoteUser(irc, remoteirc, numeric, spawnIfMissing=False) if notice: - remoteirc.proto.noticeClient(remoteirc, user, real_target, text) + remoteirc.proto.noticeClient(user, real_target, text) else: - remoteirc.proto.messageClient(remoteirc, user, real_target, text) + remoteirc.proto.messageClient(user, real_target, text) utils.add_hook(handle_privmsg, 'PRIVMSG') utils.add_hook(handle_privmsg, 'NOTICE') @@ -431,7 +431,7 @@ def handle_kick(irc, source, command, args): # kick ops, admins can't kick owners, etc. modes = getPrefixModes(remoteirc, irc, remotechan, real_target) # Join the kicked client back with its respective modes. - irc.proto.sjoinServer(irc, irc.sid, channel, [(modes, target)]) + irc.proto.sjoinServer(irc.sid, channel, [(modes, target)]) if kicker in irc.users: log.info('(%s) Relay claim: Blocked KICK (reason %r) from %s to relay client %s/%s on %s.', irc.name, args['text'], irc.users[source].nick, @@ -450,7 +450,7 @@ def handle_kick(irc, source, command, args): # Propogate the kick! if real_kicker: log.debug('(%s) Relay 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.kickClient(remoteirc, real_kicker, + remoteirc.proto.kickClient(real_kicker, remotechan, real_target, text) else: # Kick originated from a server, or the kicker isn't in any @@ -464,17 +464,17 @@ def handle_kick(irc, source, command, args): text = "(%s/%s) %s" % (kname, irc.name, text) except AttributeError: text = "(@%s) %s" % (irc.name, text) - remoteirc.proto.kickServer(remoteirc, remoteirc.sid, + remoteirc.proto.kickServer(remoteirc.sid, remotechan, real_target, text) # If the target isn't on any channels, quit them. if origuser and origuser[0] != remoteirc.name and not remoteirc.users[real_target].channels: del relayusers[origuser][remoteirc.name] - remoteirc.proto.quitClient(remoteirc, real_target, 'Left all shared channels.') + remoteirc.proto.quitClient(real_target, 'Left all shared channels.') if origuser and not irc.users[target].channels: del relayusers[origuser][irc.name] - irc.proto.quitClient(irc, target, 'Left all shared channels.') + irc.proto.quitClient(target, 'Left all shared channels.') utils.add_hook(handle_kick, 'KICK') @@ -493,7 +493,7 @@ def handle_chgclient(irc, source, command, args): for netname, user in relayusers[(irc.name, target)].items(): remoteirc = world.networkobjects[netname] try: - remoteirc.proto.updateClient(remoteirc, user, field, text) + remoteirc.proto.updateClient(user, field, text) except NotImplementedError: # IRCd doesn't support changing the field we want log.debug('(%s) Ignoring changing field %r of %s on %s (for %s/%s);' ' remote IRCd doesn\'t support it', irc.name, field, @@ -584,9 +584,9 @@ def relayModes(irc, remoteirc, sender, channel, modes=None): # Check if the sender is a user; remember servers are allowed to set modes too. u = getRemoteUser(irc, remoteirc, sender, spawnIfMissing=False) if u: - remoteirc.proto.modeClient(remoteirc, u, remotechan, supported_modes) + remoteirc.proto.modeClient(u, remotechan, supported_modes) else: - remoteirc.proto.modeServer(remoteirc, remoteirc.sid, remotechan, supported_modes) + remoteirc.proto.modeServer(remoteirc.sid, remotechan, supported_modes) def getSupportedUmodes(irc, remoteirc, modes): supported_modes = [] @@ -613,7 +613,7 @@ def getSupportedUmodes(irc, remoteirc, modes): log.debug("(%s) getSupportedUmodes: skipping mode (%r, %r) because " "the remote network (%s)'s IRCd (%s) doesn't support it.", irc.name, modechar, arg, remoteirc.name, - remoteirc.proto.__name__) + remoteirc.protoname) return supported_modes def handle_mode(irc, numeric, command, args): @@ -636,7 +636,7 @@ def handle_mode(irc, numeric, command, args): modes.append(('-%s' % hideoper_mode, None)) remoteuser = getRemoteUser(irc, remoteirc, target, spawnIfMissing=False) if remoteuser and modes: - remoteirc.proto.modeClient(remoteirc, remoteuser, remoteuser, modes) + remoteirc.proto.modeClient(remoteuser, remoteuser, modes) utils.add_hook(handle_mode, 'MODE') @@ -654,9 +654,9 @@ def handle_topic(irc, numeric, command, args): # This might originate from a server too. remoteuser = getRemoteUser(irc, remoteirc, numeric, spawnIfMissing=False) if remoteuser: - remoteirc.proto.topicClient(remoteirc, remoteuser, remotechan, topic) + remoteirc.proto.topicClient(remoteuser, remotechan, topic) else: - remoteirc.proto.topicServer(remoteirc, remoteirc.sid, remotechan, topic) + remoteirc.proto.topicServer(remoteirc.sid, remotechan, topic) utils.add_hook(handle_topic, 'TOPIC') def handle_kill(irc, numeric, command, args): @@ -676,7 +676,7 @@ def handle_kill(irc, numeric, command, args): modes = getPrefixModes(remoteirc, irc, localchan, realuser[1]) log.debug('(%s) relay handle_kill: userpair: %s, %s', irc.name, modes, realuser) client = getRemoteUser(remoteirc, irc, realuser[1]) - irc.proto.sjoinServer(irc, irc.sid, localchan, [(modes, client)]) + irc.proto.sjoinServer(irc.sid, localchan, [(modes, client)]) if userdata and numeric in irc.users: log.info('(%s) Relay claim: Blocked KILL (reason %r) from %s to relay client %s/%s.', irc.name, args['text'], irc.users[numeric].nick, @@ -745,10 +745,10 @@ def relayJoins(irc, channel, users, ts, burst=True): # Burst was explicitly given, or we're trying to join multiple # users/someone with a prefix. if burst or len(queued_users) > 1 or queued_users[0][0]: - remoteirc.proto.sjoinServer(remoteirc, remoteirc.sid, remotechan, queued_users, ts=ts) + remoteirc.proto.sjoinServer(remoteirc.sid, remotechan, queued_users, ts=ts) relayModes(irc, remoteirc, irc.sid, channel, irc.channels[channel].modes) else: - remoteirc.proto.joinClient(remoteirc, queued_users[0][1], remotechan) + remoteirc.proto.joinClient(queued_users[0][1], remotechan) def relayPart(irc, channel, user): for name, remoteirc in world.networkobjects.items(): @@ -762,16 +762,16 @@ def relayPart(irc, channel, user): log.debug('(%s) relayPart: remoteuser for %s/%s found as %s', irc.name, user, irc.name, remoteuser) if remotechan is None or remoteuser is None: continue - remoteirc.proto.partClient(remoteirc, remoteuser, remotechan, 'Channel delinked.') + remoteirc.proto.partClient(remoteuser, remotechan, 'Channel delinked.') if isRelayClient(remoteirc, remoteuser) and not remoteirc.users[remoteuser].channels: - remoteirc.proto.quitClient(remoteirc, remoteuser, 'Left all shared channels.') + remoteirc.proto.quitClient(remoteuser, 'Left all shared channels.') del relayusers[(irc.name, user)][remoteirc.name] def removeChannel(irc, channel): if irc is None: return if channel not in map(str.lower, irc.serverdata['channels']): - irc.proto.partClient(irc, irc.pseudoclient.uid, channel, 'Channel delinked.') + irc.proto.partClient(irc.pseudoclient.uid, channel, 'Channel delinked.') relay = findRelay((irc.name, channel)) if relay: for user in irc.channels[channel].users.copy(): @@ -782,12 +782,12 @@ def removeChannel(irc, channel): if user == irc.pseudoclient.uid and channel in \ irc.serverdata['channels']: continue - irc.proto.partClient(irc, user, channel, 'Channel delinked.') + irc.proto.partClient(user, channel, 'Channel delinked.') # Don't ever quit it either... if user != irc.pseudoclient.uid and not irc.users[user].channels: remoteuser = getLocalUser(irc, user) del relayusers[remoteuser][irc.name] - irc.proto.quitClient(irc, user, 'Left all shared channels.') + irc.proto.quitClient(user, 'Left all shared channels.') @utils.add_cmd def create(irc, source, args): @@ -986,7 +986,7 @@ def handle_save(irc, numeric, command, args): newnick = normalizeNick(irc, remotenet, nick) log.info('(%s) SAVE received for relay client %r (%s), fixing nick to %s', irc.name, target, nick, newnick) - irc.proto.nickClient(irc, target, newnick) + irc.proto.nickClient(target, newnick) else: log.warning('(%s) SAVE received for relay client %r (%s), not ' 'fixing nick again due to 5 failed attempts in ' @@ -1020,7 +1020,7 @@ def linked(irc, source, args): def handle_away(irc, numeric, command, args): for netname, user in relayusers[(irc.name, numeric)].items(): remoteirc = world.networkobjects[netname] - remoteirc.proto.awayClient(remoteirc, user, args['text']) + remoteirc.proto.awayClient(user, args['text']) utils.add_hook(handle_away, 'AWAY') def handle_spawnmain(irc, numeric, command, args): @@ -1048,7 +1048,7 @@ def handle_invite(irc, source, command, args): 'channel not on their network!', notice=True) else: - remoteirc.proto.inviteClient(remoteirc, remotesource, remoteuser, + remoteirc.proto.inviteClient(remotesource, remoteuser, remotechan) utils.add_hook(handle_invite, 'INVITE') diff --git a/protocols/inspircd.py b/protocols/inspircd.py index bfa98af..f6e4df8 100644 --- a/protocols/inspircd.py +++ b/protocols/inspircd.py @@ -11,568 +11,568 @@ import utils from log import log from classes import * -# Some functions are shared with the charybdis module (ts6_common) -from ts6_common import nickClient, kickServer, kickClient, _sendKick, quitClient, \ - removeClient, partClient, messageClient, noticeClient, topicClient, parseTS6Args -from ts6_common import handle_privmsg, handle_kill, handle_kick, handle_error, \ - handle_quit, handle_nick, handle_save, handle_squit, handle_mode, handle_topic, \ - handle_notice, _send, handle_part +from ts6_common import TS6BaseProtocol -# Set our case mapping (rfc1459 maps "\" and "|" together, for example". -casemapping = 'rfc1459' +class InspIRCdProtocol(TS6BaseProtocol): + def __init__(self, irc): + super(InspIRCdProtocol, self).__init__(irc) + # Set our case mapping (rfc1459 maps "\" and "|" together, for example". + self.casemapping = 'rfc1459' -# Raw commands sent from servers vary from protocol to protocol. Here, we map -# non-standard names to our hook handlers, so command handlers' outputs -# are called with the right hooks. -hook_map = {'FJOIN': 'JOIN', 'RSQUIT': 'SQUIT', 'FMODE': 'MODE', - 'FTOPIC': 'TOPIC', 'OPERTYPE': 'MODE', 'FHOST': 'CHGHOST', - 'FIDENT': 'CHGIDENT', 'FNAME': 'CHGNAME'} + # Raw commands sent from servers vary from protocol to protocol. Here, we map + # non-standard names to our hook handlers, so command handlers' outputs + # are called with the right hooks. + self.hook_map = {'FJOIN': 'JOIN', 'RSQUIT': 'SQUIT', 'FMODE': 'MODE', + 'FTOPIC': 'TOPIC', 'OPERTYPE': 'MODE', 'FHOST': 'CHGHOST', + 'FIDENT': 'CHGIDENT', 'FNAME': 'CHGNAME'} -def spawnClient(irc, nick, ident='null', host='null', realhost=None, modes=set(), - server=None, ip='0.0.0.0', realname=None, ts=None, opertype=None): - """Spawns a client with nick on the given IRC connection. + def spawnClient(self, nick, ident='null', host='null', realhost=None, modes=set(), + server=None, ip='0.0.0.0', realname=None, ts=None, opertype=None): + """Spawns a client with nick on the given IRC connection. - 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 irc.sid - if not utils.isInternalServer(irc, server): - raise ValueError('Server %r is not a PyLink internal PseudoServer!' % server) - # We need a separate UID generator instance for every PseudoServer - # we spawn. Otherwise, things won't wrap around properly. - if server not in irc.uidgen: - irc.uidgen[server] = utils.TS6UIDGenerator(server) - uid = irc.uidgen[server].next_uid() - ts = ts or int(time.time()) - realname = realname or irc.botdata['realname'] - realhost = realhost or host - raw_modes = utils.joinModes(modes) - u = irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname, - realhost=realhost, ip=ip) - utils.applyModes(irc, uid, modes) - irc.servers[server].users.add(uid) - _send(irc, 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, - realhost=realhost)) - if ('o', None) in modes or ('+o', None) in modes: - _operUp(irc, uid, opertype=opertype or 'IRC_Operator') - return u + 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 utils.isInternalServer(self.irc, server): + raise ValueError('Server %r is not a PyLink internal PseudoServer!' % server) + # We need a separate UID generator instance for every PseudoServer + # we spawn. Otherwise, things won't wrap around properly. + if server not in self.irc.uidgen: + self.irc.uidgen[server] = utils.TS6UIDGenerator(server) + uid = self.irc.uidgen[server].next_uid() + ts = ts or int(time.time()) + realname = realname or self.irc.botdata['realname'] + realhost = realhost or host + raw_modes = utils.joinModes(modes) + u = self.irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname, + realhost=realhost, ip=ip) + utils.applyModes(self.irc, uid, modes) + self.irc.servers[server].users.add(uid) + self._send(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, + realhost=realhost)) + if ('o', None) in modes or ('+o', None) in modes: + self._operUp(uid, opertype=opertype or 'IRC_Operator') + return u -def joinClient(irc, client, channel): - """Joins an internal spawned client to a channel.""" - # 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 = utils.toLower(irc, channel) - server = utils.isInternalClient(irc, client) - if not server: - log.error('(%s) Error trying to join client %r to %r (no such pseudoclient exists)', irc.name, client, channel) - raise LookupError('No such PyLink PseudoClient exists.') - # Strip out list-modes, they shouldn't be ever sent in FJOIN. - modes = [m for m in irc.channels[channel].modes if m[0] not in irc.cmodes['*A']] - _send(irc, server, "FJOIN {channel} {ts} {modes} :,{uid}".format( - ts=irc.channels[channel].ts, uid=client, channel=channel, - modes=utils.joinModes(modes))) - irc.channels[channel].users.add(client) - irc.users[client].channels.add(channel) + def joinClient(self, client, channel): + """Joins an internal spawned client to a channel.""" + # 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 = utils.toLower(self.irc, channel) + server = utils.isInternalClient(self.irc, client) + if not server: + log.error('(%s) Error trying to join client %r to %r (no such pseudoclient exists)', self.irc.name, client, channel) + raise LookupError('No such PyLink PseudoClient 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=utils.joinModes(modes))) + self.irc.channels[channel].users.add(client) + self.irc.users[client].channels.add(channel) -def sjoinServer(irc, server, channel, users, ts=None): - """Sends an SJOIN for a group of users to a channel. + def sjoinServer(self, server, channel, users, ts=None): + """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. - is a list of (prefix mode, UID) pairs: + 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. + is a list of (prefix mode, UID) pairs: - Example uses: - sjoinServer(irc, '100', '#test', [('', '100AAABBC'), ('qo', 100AAABBB'), ('h', '100AAADDD')]) - sjoinServer(irc, irc.sid, '#test', [('o', irc.pseudoclient.uid)]) - """ - channel = utils.toLower(irc, channel) - server = server or irc.sid - assert users, "sjoinServer: No users sent?" - log.debug('(%s) sjoinServer: got %r for users', irc.name, users) - if not server: - raise LookupError('No such PyLink PseudoClient exists.') - orig_ts = irc.channels[channel].ts - ts = ts or orig_ts - if ts < orig_ts: - # If the TS we're sending is lower than the one that existing, clear the - # mode lists from our channel state and reset the timestamp. - log.debug('(%s) sjoinServer: resetting TS of %r from %s to %s (clearing modes)', - irc.name, channel, orig_ts, ts) - irc.channels[channel].ts = ts - irc.channels[channel].modes.clear() - for p in irc.channels[channel].prefixmodes.values(): - p.clear() - log.debug("sending SJOIN to %s%s with ts %s (that's %r)", channel, 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 irc.channels[channel].modes if m[0] not in irc.cmodes['*A']] - uids = [] - changedmodes = [] - namelist = [] - # We take as a list of (prefixmodes, uid) pairs. - for userpair in users: - assert len(userpair) == 2, "Incorrect format of userpair: %r" % userpair - prefixes, user = userpair - namelist.append(','.join(userpair)) - uids.append(user) - for m in prefixes: - changedmodes.append(('+%s' % m, user)) + Example uses: + sjoinServer(self.irc, '100', '#test', [('', '100AAABBC'), ('qo', 100AAABBB'), ('h', '100AAADDD')]) + sjoinServer(self.irc, self.irc.sid, '#test', [('o', self.irc.pseudoclient.uid)]) + """ + channel = utils.toLower(self.irc, channel) + server = server or self.irc.sid + assert users, "sjoinServer: No users sent?" + log.debug('(%s) sjoinServer: got %r for users', self.irc.name, users) + if not server: + raise LookupError('No such PyLink PseudoClient exists.') + orig_ts = self.irc.channels[channel].ts + ts = ts or orig_ts + if ts < orig_ts: + # If the TS we're sending is lower than the one that existing, clear the + # mode lists from our channel state and reset the timestamp. + log.debug('(%s) sjoinServer: resetting TS of %r from %s to %s (clearing modes)', + self.irc.name, channel, orig_ts, ts) + self.irc.channels[channel].ts = ts + self.irc.channels[channel].modes.clear() + for p in self.irc.channels[channel].prefixmodes.values(): + p.clear() + 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']] + uids = [] + changedmodes = [] + namelist = [] + # We take as a list of (prefixmodes, uid) pairs. + for userpair in users: + assert len(userpair) == 2, "Incorrect format of userpair: %r" % userpair + prefixes, user = userpair + namelist.append(','.join(userpair)) + uids.append(user) + for m in prefixes: + changedmodes.append(('+%s' % m, user)) + try: + self.irc.users[user].channels.add(channel) + except KeyError: # Not initialized yet? + log.debug("(%s) sjoinServer: 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. + utils.applyModes(self.irc, channel, changedmodes) + namelist = ' '.join(namelist) + self._send(server, "FJOIN {channel} {ts} {modes} :{users}".format( + ts=ts, users=namelist, channel=channel, + modes=utils.joinModes(modes))) + self.irc.channels[channel].users.update(uids) + + def _operUp(self, target, opertype=None): + """Opers a client up (internal function specific to InspIRCd). + + This should be called whenever user mode +o is set on anyone, because + InspIRCd requires a special command (OPERTYPE) to be sent in order to + 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), + and the change will be reflected here.""" + userobj = self.irc.users[target] try: - irc.users[user].channels.add(channel) - except KeyError: # Not initialized yet? - log.debug("(%s) sjoinServer: KeyError trying to add %r to %r's channel list?", 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. - utils.applyModes(irc, channel, changedmodes) - namelist = ' '.join(namelist) - _send(irc, server, "FJOIN {channel} {ts} {modes} :{users}".format( - ts=ts, users=namelist, channel=channel, - modes=utils.joinModes(modes))) - irc.channels[channel].users.update(uids) + otype = opertype or userobj.opertype + except AttributeError: + log.debug('(%s) opertype field for %s (%s) isn\'t filled yet!', + self.irc.name, target, userobj.nick) + # whatever, this is non-standard anyways. + otype = 'IRC_Operator' + log.debug('(%s) Sending OPERTYPE from %s to oper them up.', + self.irc.name, target) + userobj.opertype = otype + self._send(target, 'OPERTYPE %s' % otype) -def _operUp(irc, target, opertype=None): - """Opers a client up (internal function specific to InspIRCd). + def _sendModes(self, numeric, target, modes, ts=None): + """Internal function to send modes from a PyLink client/server.""" + # -> :9PYAAAAAA FMODE #pylink 1433653951 +os 9PYAAAAAA + # -> :9PYAAAAAA MODE 9PYAAAAAA -i+w + log.debug('(%s) inspself.ircd._sendModes: received %r for mode list', self.irc.name, modes) + if ('+o', None) in modes and not utils.isChannel(target): + # https://github.com/inspself.ircd/inspself.ircd/blob/master/src/modules/m_spanningtree/opertype.cpp#L26-L28 + # Servers need a special command to set umode +o on people. + # Why isn't this documented anywhere, InspIRCd? + self._operUp(target) + utils.applyModes(self.irc, target, modes) + joinedmodes = utils.joinModes(modes) + if utils.isChannel(target): + ts = ts or self.irc.channels[utils.toLower(self.irc, target)].ts + self._send(numeric, 'FMODE %s %s %s' % (target, ts, joinedmodes)) + else: + self._send(numeric, 'MODE %s %s' % (target, joinedmodes)) - This should be called whenever user mode +o is set on anyone, because - InspIRCd requires a special command (OPERTYPE) to be sent in order to - recognize ANY non-burst oper ups. + def modeClient(self, numeric, target, modes, ts=None): + """ + Sends modes from a PyLink client. should be + a list of (mode, arg) tuples, i.e. the format of utils.parseModes() output. + """ + if not utils.isInternalClient(self.irc, numeric): + raise LookupError('No such PyLink PseudoClient exists.') + self._sendModes(numeric, target, modes, ts=ts) - Plugins don't have to call this function themselves, but they can - set the opertype attribute of an IrcUser object (in irc.users), - and the change will be reflected here.""" - userobj = irc.users[target] - try: - otype = opertype or userobj.opertype - except AttributeError: - log.debug('(%s) opertype field for %s (%s) isn\'t filled yet!', - irc.name, target, userobj.nick) - # whatever, this is non-standard anyways. - otype = 'IRC_Operator' - log.debug('(%s) Sending OPERTYPE from %s to oper them up.', - irc.name, target) - userobj.opertype = otype - _send(irc, target, 'OPERTYPE %s' % otype) + def modeServer(self, numeric, target, modes, ts=None): + """ + Sends modes from a PyLink server. should be + a list of (mode, arg) tuples, i.e. the format of utils.parseModes() output. + """ + if not utils.isInternalServer(self.irc, numeric): + raise LookupError('No such PyLink PseudoServer exists.') + self._sendModes(numeric, target, modes, ts=ts) -def _sendModes(irc, numeric, target, modes, ts=None): - """Internal function to send modes from a PyLink client/server.""" - # -> :9PYAAAAAA FMODE #pylink 1433653951 +os 9PYAAAAAA - # -> :9PYAAAAAA MODE 9PYAAAAAA -i+w - log.debug('(%s) inspircd._sendModes: received %r for mode list', irc.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. - # Why isn't this documented anywhere, InspIRCd? - _operUp(irc, target) - utils.applyModes(irc, target, modes) - joinedmodes = utils.joinModes(modes) - if utils.isChannel(target): - ts = ts or irc.channels[utils.toLower(irc, target)].ts - _send(irc, numeric, 'FMODE %s %s %s' % (target, ts, joinedmodes)) - else: - _send(irc, numeric, 'MODE %s %s' % (target, joinedmodes)) + def killServer(self, numeric, target, reason): + """ -def modeClient(irc, numeric, target, modes, ts=None): - """ - Sends modes from a PyLink client. should be - a list of (mode, arg) tuples, i.e. the format of utils.parseModes() output. - """ - if not utils.isInternalClient(irc, numeric): - raise LookupError('No such PyLink PseudoClient exists.') - _sendModes(irc, numeric, target, modes, ts=ts) + Sends a kill to from a PyLink PseudoServer. + """ + if not utils.isInternalServer(self.irc, numeric): + raise LookupError('No such PyLink PseudoServer exists.') + self._send(numeric, 'KILL %s :%s' % (target, reason)) + # We don't need to call removeClient here, since the remote server + # will send a QUIT from the target if the command succeeds. -def modeServer(irc, numeric, target, modes, ts=None): - """ - Sends modes from a PyLink server. should be - a list of (mode, arg) tuples, i.e. the format of utils.parseModes() output. - """ - if not utils.isInternalServer(irc, numeric): - raise LookupError('No such PyLink PseudoServer exists.') - _sendModes(irc, numeric, target, modes, ts=ts) + def killClient(self, numeric, target, reason): + """ -def killServer(irc, numeric, target, reason): - """ + Sends a kill to from a PyLink PseudoClient. + """ + if not utils.isInternalClient(self.irc, numeric): + raise LookupError('No such PyLink PseudoClient exists.') + self._send(numeric, 'KILL %s :%s' % (target, reason)) + # We don't need to call removeClient here, since the remote server + # will send a QUIT from the target if the command succeeds. - Sends a kill to from a PyLink PseudoServer. - """ - if not utils.isInternalServer(irc, numeric): - raise LookupError('No such PyLink PseudoServer exists.') - _send(irc, numeric, 'KILL %s :%s' % (target, reason)) - # We don't need to call removeClient here, since the remote server - # will send a QUIT from the target if the command succeeds. + def topicServer(self, numeric, target, text): + """Sends a burst topic from a PyLink server. This is usally used on burst.""" + if not utils.isInternalServer(self.irc, numeric): + raise LookupError('No such PyLink PseudoServer 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 -def killClient(irc, numeric, target, reason): - """ + def inviteClient(self, numeric, target, channel): + """Sends an INVITE from a PyLink client..""" + if not utils.isInternalClient(self.irc, numeric): + raise LookupError('No such PyLink PseudoClient exists.') + self._send(numeric, 'INVITE %s %s' % (target, channel)) - Sends a kill to from a PyLink PseudoClient. - """ - if not utils.isInternalClient(irc, numeric): - raise LookupError('No such PyLink PseudoClient exists.') - _send(irc, numeric, 'KILL %s :%s' % (target, reason)) - # We don't need to call removeClient here, since the remote server - # will send a QUIT from the target if the command succeeds. + def knockClient(self, numeric, target, text): + """Sends a KNOCK from a PyLink client.""" + if not utils.isInternalClient(self.irc, numeric): + raise LookupError('No such PyLink PseudoClient exists.') + self._send(numeric, 'ENCAP * KNOCK %s :%s' % (target, text)) -def topicServer(irc, numeric, target, text): - """Sends a burst topic from a PyLink server. This is usally used on burst.""" - if not utils.isInternalServer(irc, numeric): - raise LookupError('No such PyLink PseudoServer exists.') - ts = int(time.time()) - servername = irc.servers[numeric].name - _send(irc, numeric, 'FTOPIC %s %s %s :%s' % (target, ts, servername, text)) - irc.channels[target].topic = text - irc.channels[target].topicset = True + def updateClient(self, numeric, field, text): + """Updates the ident, host, or realname of a PyLink client.""" + field = field.upper() + if field == 'IDENT': + self.irc.users[numeric].ident = text + self._send(numeric, 'FIDENT %s' % text) + elif field == 'HOST': + self.irc.users[numeric].host = text + self._send(numeric, 'FHOST %s' % text) + elif field in ('REALNAME', 'GECOS'): + self.irc.users[numeric].realname = text + self._send(numeric, 'FNAME :%s' % text) + else: + raise NotImplementedError("Changing field %r of a client is unsupported by this protocol." % field) -def inviteClient(irc, numeric, target, channel): - """Sends an INVITE from a PyLink client..""" - if not utils.isInternalClient(irc, numeric): - raise LookupError('No such PyLink PseudoClient exists.') - _send(irc, numeric, 'INVITE %s %s' % (target, channel)) + def pingServer(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 + if not (target is None or source is None): + self._send(source, 'PING %s %s' % (source, target)) -def knockClient(irc, numeric, target, text): - """Sends a KNOCK from a PyLink client.""" - if not utils.isInternalClient(irc, numeric): - raise LookupError('No such PyLink PseudoClient exists.') - _send(irc, numeric, 'ENCAP * KNOCK %s :%s' % (target, text)) + def numericServer(self, source, numeric, text): + raise NotImplementedError("Numeric sending is not yet implemented by this " + "protocol module. WHOIS requests are handled " + "locally by InspIRCd servers, so there is no " + "need for PyLink to send numerics directly yet.") -def updateClient(irc, numeric, field, text): - """Updates the ident, host, or realname of a PyLink client.""" - field = field.upper() - if field == 'IDENT': - irc.users[numeric].ident = text - _send(irc, numeric, 'FIDENT %s' % text) - elif field == 'HOST': - irc.users[numeric].host = text - _send(irc, numeric, 'FHOST %s' % text) - elif field in ('REALNAME', 'GECOS'): - irc.users[numeric].realname = text - _send(irc, numeric, 'FNAME :%s' % text) - else: - raise NotImplementedError("Changing field %r of a client is unsupported by this protocol." % field) + def awayClient(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)) + else: + self._send(source, 'AWAY') -def pingServer(irc, 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 irc.sid - target = target or irc.uplink - if not (target is None or source is None): - _send(irc, source, 'PING %s %s' % (source, target)) + def spawnServer(self, name, sid=None, uplink=None, desc=None): + """Spawns a server off a PyLink server.""" + # -> :0AL SERVER test.server * 1 0AM :some silly pseudoserver + uplink = uplink or self.irc.sid + name = name.lower() + # "desc" defaults to the configured server description. + desc = desc or self.irc.serverdata.get('serverdesc') or self.irc.botdata['serverdesc'] + if sid is None: # No sid given; generate one! + self.irc.sidgen = utils.TS6SIDGenerator(self.irc.serverdata["sidrange"]) + sid = self.irc.sidgen.next_sid() + assert len(sid) == 3, "Incorrect SID length" + if sid in self.irc.servers: + raise ValueError('A server with SID %r already exists!' % sid) + for server in self.irc.servers.values(): + if name == server.name: + raise ValueError('A server named %r already exists!' % name) + if not utils.isInternalServer(self.irc, uplink): + raise ValueError('Server %r is not a PyLink internal PseudoServer!' % 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) + self._send(sid, 'ENDBURST') + return sid -def numericServer(irc, source, numeric, text): - raise NotImplementedError("Numeric sending is not yet implemented by this " - "protocol module. WHOIS requests are handled " - "locally by InspIRCd servers, so there is no " - "need for PyLink to send numerics directly yet.") + def squitServer(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.handle_squit(source, 'SQUIT', [target, text]) -def awayClient(irc, source, text): - """Sends an AWAY message from a PyLink client. can be an empty string - to unset AWAY status.""" - if text: - _send(irc, source, 'AWAY %s :%s' % (int(time.time()), text)) - else: - _send(irc, source, 'AWAY') + def connect(self): + """Initializes a connection to a server.""" + ts = self.irc.start_ts -def spawnServer(irc, name, sid=None, uplink=None, desc=None): - """Spawns a server off a PyLink server.""" - # -> :0AL SERVER test.server * 1 0AM :some silly pseudoserver - uplink = uplink or irc.sid - name = name.lower() - # "desc" defaults to the configured server description. - desc = desc or irc.serverdata.get('serverdesc') or irc.botdata['serverdesc'] - if sid is None: # No sid given; generate one! - irc.sidgen = utils.TS6SIDGenerator(irc.serverdata["sidrange"]) - sid = irc.sidgen.next_sid() - assert len(sid) == 3, "Incorrect SID length" - if sid in irc.servers: - raise ValueError('A server with SID %r already exists!' % sid) - for server in irc.servers.values(): - if name == server.name: - raise ValueError('A server named %r already exists!' % name) - if not utils.isInternalServer(irc, uplink): - raise ValueError('Server %r is not a PyLink internal PseudoServer!' % uplink) - if not utils.isServerName(name): - raise ValueError('Invalid server name %r' % name) - _send(irc, uplink, 'SERVER %s * 1 %s :%s' % (name, sid, desc)) - irc.servers[sid] = IrcServer(uplink, name, internal=True) - _send(irc, sid, 'ENDBURST') - return sid + f = self.irc.send + f('CAPAB START 1202') + f('CAPAB CAPABILITIES :PROTOCOL=1202') + f('CAPAB END') + f('SERVER {host} {Pass} 0 {sid} :{sdesc}'.format(host=self.irc.serverdata["hostname"], + Pass=self.irc.serverdata["sendpass"], sid=self.irc.sid, + sdesc=self.irc.serverdata.get('serverdesc') or self.irc.botdata['serverdesc'])) + f(':%s BURST %s' % (self.irc.sid, ts)) + f(':%s ENDBURST' % (self.irc.sid)) -def squitServer(irc, source, target, text='No reason given'): - """SQUITs a PyLink server.""" - # -> :9PY SQUIT 9PZ :blah, blah - _send(irc, source, 'SQUIT %s :%s' % (target, text)) - handle_squit(irc, source, 'SQUIT', [target, text]) + def handle_events(self, data): + """Event handler for the InspIRCd protocol. This passes most commands to + the various handle_ABCD() functions elsewhere in this module, but also + handles commands sent in the initial server linking phase.""" + # Each server message looks something like this: + # :70M FJOIN #chat 1423790411 +AFPfjnt 6:5 7:5 9:5 :v,1SRAAESWE + # : ... :final multi word argument + args = data.split(" ") + if not args: + # No data?? + return + if args[0] == 'SERVER': + # <- SERVER whatever.net abcdefgh 0 10X :something + servername = args[1].lower() + numeric = args[4] + if args[2] != self.irc.serverdata['recvpass']: + # Check if recvpass is correct + raise ProtocolError('Error: recvpass from uplink server %s does not match configuration!' % servername) + self.irc.servers[numeric] = IrcServer(None, servername) + self.irc.uplink = numeric + return + elif args[0] == 'CAPAB': + # Capability negotiation with our uplink + if args[1] == 'CHANMODES': + # <- CAPAB CHANMODES :admin=&a allowinvite=A autoop=w ban=b banexception=e blockcolor=c c_registered=r exemptchanops=X filter=g flood=f halfop=%h history=H invex=I inviteonly=i joinflood=j key=k kicknorejoin=J limit=l moderated=m nickflood=F noctcp=C noextmsg=n nokick=Q noknock=K nonick=N nonotice=T official-join=!Y op=@o operonly=O opmoderated=U owner=~q permanent=P private=p redirect=L reginvite=R regmoderated=M secret=s sslonly=z stripcolor=S topiclock=t voice=+v -def connect(irc): - """Initializes a connection to a server.""" - ts = irc.start_ts + # Named modes are essential for a cross-protocol IRC service. We + # can use InspIRCd as a model here and assign a similar mode map to our cmodes list. + for modepair in args[2:]: + name, char = modepair.split('=') + if name == 'reginvite': # Reginvite? That's a dumb name. + name = 'regonly' + if name == 'founder': # Channel mode +q + # Founder, owner; same thing. m_customprefix allows you to name it anything you like + # (the former is config default, but I personally prefer the latter.) + name = 'owner' + # We don't really care about mode prefixes; just the mode char + self.irc.cmodes[name.lstrip(':')] = char[-1] + elif args[1] == 'USERMODES': + # <- CAPAB USERMODES :bot=B callerid=g cloak=x deaf_commonchan=c helpop=h hidechans=I hideoper=H invisible=i oper=o regdeaf=R servprotect=k showwhois=W snomask=s u_registered=r u_stripcolor=S wallops=w + # Ditto above. + for modepair in args[2:]: + name, char = modepair.split('=') + self.irc.umodes[name.lstrip(':')] = char + elif args[1] == 'CAPABILITIES': + # <- CAPAB CAPABILITIES :NICKMAX=21 CHANMAX=64 MAXMODES=20 IDENTMAX=11 MAXQUIT=255 MAXTOPIC=307 MAXKICK=255 MAXGECOS=128 MAXAWAY=200 IP6SUPPORT=1 PROTOCOL=1202 PREFIX=(Yqaohv)!~&@%+ CHANMODES=IXbegw,k,FHJLfjl,ACKMNOPQRSTUcimnprstz USERMODES=,,s,BHIRSWcghikorwx GLOBOPS=1 SVSPART=1 + caps = dict([x.lstrip(':').split('=') for x in args[2:]]) + protocol_version = int(caps['PROTOCOL']) + if protocol_version < 1202: + raise ProtocolError("Remote protocol version is too old! At least 1202 (InspIRCd 2.0.x) is needed. (got %s)" % protocol_version) + self.irc.maxnicklen = int(caps['NICKMAX']) + self.irc.maxchanlen = int(caps['CHANMAX']) + # Modes are divided into A, B, C, and D classes + # See http://www.self.irc.org/tech_docs/005.html - f = irc.send - f('CAPAB START 1202') - f('CAPAB CAPABILITIES :PROTOCOL=1202') - f('CAPAB END') - f('SERVER {host} {Pass} 0 {sid} :{sdesc}'.format(host=irc.serverdata["hostname"], - Pass=irc.serverdata["sendpass"], sid=irc.sid, - sdesc=irc.serverdata.get('serverdesc') or irc.botdata['serverdesc'])) - f(':%s BURST %s' % (irc.sid, ts)) - f(':%s ENDBURST' % (irc.sid)) + # FIXME: Find a better way to assign/store this. + self.irc.cmodes['*A'], self.irc.cmodes['*B'], self.irc.cmodes['*C'], self.irc.cmodes['*D'] \ + = caps['CHANMODES'].split(',') + self.irc.umodes['*A'], self.irc.umodes['*B'], self.irc.umodes['*C'], self.irc.umodes['*D'] \ + = caps['USERMODES'].split(',') + prefixsearch = re.search(r'\(([A-Za-z]+)\)(.*)', caps['PREFIX']) + self.irc.prefixmodes = dict(zip(prefixsearch.group(1), prefixsearch.group(2))) + log.debug('(%s) self.irc.prefixmodes set to %r', self.irc.name, self.irc.prefixmodes) + # Sanity check: set this AFTER we fetch the capabilities for the network! + self.irc.connected.set() + try: + args = self.parseTS6Args(args) + numeric = args[0] + command = args[1] + args = args[2:] + except IndexError: + return -def handle_events(irc, data): - """Event handler for the InspIRCd protocol. This passes most commands to - the various handle_ABCD() functions elsewhere in this module, but also - handles commands sent in the initial server linking phase.""" - # Each server message looks something like this: - # :70M FJOIN #chat 1423790411 +AFPfjnt 6:5 7:5 9:5 :v,1SRAAESWE - # : ... :final multi word argument - args = data.split(" ") - if not args: - # No data?? - return - if args[0] == 'SERVER': - # <- SERVER whatever.net abcdefgh 0 10X :something - servername = args[1].lower() - numeric = args[4] - if args[2] != irc.serverdata['recvpass']: - # Check if recvpass is correct - raise ProtocolError('Error: recvpass from uplink server %s does not match configuration!' % servername) - irc.servers[numeric] = IrcServer(None, servername) - irc.uplink = numeric - return - elif args[0] == 'CAPAB': - # Capability negotiation with our uplink - if args[1] == 'CHANMODES': - # <- CAPAB CHANMODES :admin=&a allowinvite=A autoop=w ban=b banexception=e blockcolor=c c_registered=r exemptchanops=X filter=g flood=f halfop=%h history=H invex=I inviteonly=i joinflood=j key=k kicknorejoin=J limit=l moderated=m nickflood=F noctcp=C noextmsg=n nokick=Q noknock=K nonick=N nonotice=T official-join=!Y op=@o operonly=O opmoderated=U owner=~q permanent=P private=p redirect=L reginvite=R regmoderated=M secret=s sslonly=z stripcolor=S topiclock=t voice=+v + # We will do wildcard event handling here. Unhandled events are just ignored. + try: + func = getattr(self, 'handle_'+command.lower()) + except AttributeError: # unhandled event + pass + else: + parsed_args = func(numeric, command, args) + if parsed_args is not None: + return [numeric, command, parsed_args] - # Named modes are essential for a cross-protocol IRC service. We - # can use InspIRCd as a model here and assign a similar mode map to our cmodes list. - for modepair in args[2:]: - name, char = modepair.split('=') - if name == 'reginvite': # Reginvite? That's a dumb name. - name = 'regonly' - if name == 'founder': # Channel mode +q - # Founder, owner; same thing. m_customprefix allows you to name it anything you like - # (the former is config default, but I personally prefer the latter.) - name = 'owner' - # We don't really care about mode prefixes; just the mode char - irc.cmodes[name.lstrip(':')] = char[-1] - elif args[1] == 'USERMODES': - # <- CAPAB USERMODES :bot=B callerid=g cloak=x deaf_commonchan=c helpop=h hidechans=I hideoper=H invisible=i oper=o regdeaf=R servprotect=k showwhois=W snomask=s u_registered=r u_stripcolor=S wallops=w - # Ditto above. - for modepair in args[2:]: - name, char = modepair.split('=') - irc.umodes[name.lstrip(':')] = char - elif args[1] == 'CAPABILITIES': - # <- CAPAB CAPABILITIES :NICKMAX=21 CHANMAX=64 MAXMODES=20 IDENTMAX=11 MAXQUIT=255 MAXTOPIC=307 MAXKICK=255 MAXGECOS=128 MAXAWAY=200 IP6SUPPORT=1 PROTOCOL=1202 PREFIX=(Yqaohv)!~&@%+ CHANMODES=IXbegw,k,FHJLfjl,ACKMNOPQRSTUcimnprstz USERMODES=,,s,BHIRSWcghikorwx GLOBOPS=1 SVSPART=1 - caps = dict([x.lstrip(':').split('=') for x in args[2:]]) - protocol_version = int(caps['PROTOCOL']) - if protocol_version < 1202: - raise ProtocolError("Remote protocol version is too old! At least 1202 (InspIRCd 2.0.x) is needed. (got %s)" % protocol_version) - irc.maxnicklen = int(caps['NICKMAX']) - irc.maxchanlen = int(caps['CHANMAX']) - # Modes are divided into A, B, C, and D classes - # See http://www.irc.org/tech_docs/005.html + def handle_ping(self, source, command, args): + """Handles incoming PING commands, so we don't time out.""" + # <- :70M PING 70M 0AL + # -> :0AL PONG 0AL 70M + if utils.isInternalServer(self.irc, args[1]): + self._send(args[1], 'PONG %s %s' % (args[1], source)) - # FIXME: Find a better way to assign/store this. - irc.cmodes['*A'], irc.cmodes['*B'], irc.cmodes['*C'], irc.cmodes['*D'] \ - = caps['CHANMODES'].split(',') - irc.umodes['*A'], irc.umodes['*B'], irc.umodes['*C'], irc.umodes['*D'] \ - = caps['USERMODES'].split(',') - prefixsearch = re.search(r'\(([A-Za-z]+)\)(.*)', caps['PREFIX']) - irc.prefixmodes = dict(zip(prefixsearch.group(1), prefixsearch.group(2))) - log.debug('(%s) irc.prefixmodes set to %r', irc.name, irc.prefixmodes) - # Sanity check: set this AFTER we fetch the capabilities for the network! - irc.connected.set() - try: - args = parseTS6Args(args) - numeric = args[0] - command = args[1] - args = args[2:] - except IndexError: - return + def handle_pong(self, source, command, args): + """Handles incoming PONG commands. This is used to keep track of whether + the uplink is alive by the Irc() internals - a server that fails to reply + to our PINGs eventually times out and is disconnected.""" + if source == self.irc.uplink and args[1] == self.irc.sid: + self.irc.lastping = time.time() - # We will do wildcard event handling here. Unhandled events are just ignored. - try: - func = globals()['handle_'+command.lower()] - except KeyError: # unhandled event - pass - else: - parsed_args = func(irc, numeric, command, args) - if parsed_args is not None: - return [numeric, command, parsed_args] + 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 = utils.toLower(self.irc, args[0]) + # InspIRCd sends each channel's users in the form of 'modeprefix(es),UID' + userlist = args[-1].split() + our_ts = self.irc.channels[channel].ts + their_ts = int(args[1]) + if their_ts < our_ts: + # Channel timestamp was reset on burst + 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 + self.irc.channels[channel].modes.clear() + for p in self.irc.channels[channel].prefixmodes.values(): + p.clear() + modestring = args[2:-1] or args[2] + parsedmodes = utils.parseModes(self.irc, channel, modestring) + utils.applyModes(self.irc, channel, parsedmodes) + namelist = [] + for user in userlist: + modeprefix, user = user.split(',', 1) + namelist.append(user) + self.irc.users[user].channels.add(channel) + if their_ts <= our_ts: + utils.applyModes(self.irc, channel, [('+%s' % mode, user) for mode in modeprefix]) + self.irc.channels[channel].users.add(user) + return {'channel': channel, 'users': namelist, 'modes': parsedmodes, 'ts': their_ts} -def handle_ping(irc, source, command, args): - """Handles incoming PING commands, so we don't time out.""" - # <- :70M PING 70M 0AL - # -> :0AL PONG 0AL 70M - if utils.isInternalServer(irc, args[1]): - _send(irc, args[1], 'PONG %s %s' % (args[1], source)) + def handle_uid(self, numeric, command, args): + """Handles incoming UID commands (user introduction).""" + # :70M UID 70MAAAAAB 1429934638 GL 0::1 hidden-7j810p.9mdf.lrek.0000.0000.IP gl 0::1 1429934638 +Wioswx +ACGKNOQXacfgklnoqvx :realname + uid, ts, nick, realhost, host, ident, ip = args[0:7] + realname = args[-1] + self.irc.users[uid] = IrcUser(nick, ts, uid, ident, host, realname, realhost, ip) + parsedmodes = utils.parseModes(self.irc, uid, [args[8], args[9]]) + log.debug('Applying modes %s for %s', parsedmodes, uid) + utils.applyModes(self.irc, uid, parsedmodes) + self.irc.servers[numeric].users.add(uid) + return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip} -def handle_pong(irc, source, command, args): - """Handles incoming PONG commands. This is used to keep track of whether - the uplink is alive by the Irc() internals - a server that fails to reply - to our PINGs eventually times out and is disconnected.""" - if source == irc.uplink and args[1] == irc.sid: - irc.lastping = time.time() + def handle_server(self, numeric, command, args): + """Handles incoming SERVER commands (introduction of servers).""" + # SERVER is sent by our uplink or any other server to introduce others. + # <- :00A SERVER test.server * 1 00C :testing raw message syntax + # <- :70M SERVER millennium.overdrive.pw * 1 1ML :a relatively long period of time... (Fremont, California) + servername = args[0].lower() + sid = args[3] + sdesc = args[-1] + self.irc.servers[sid] = IrcServer(numeric, servername) + return {'name': servername, 'sid': args[3], 'text': sdesc} -def handle_fjoin(irc, 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 = utils.toLower(irc, args[0]) - # InspIRCd sends each channel's users in the form of 'modeprefix(es),UID' - userlist = args[-1].split() - our_ts = irc.channels[channel].ts - their_ts = int(args[1]) - if their_ts < our_ts: - # Channel timestamp was reset on burst - log.debug('(%s) Setting channel TS of %s to %s from %s', - irc.name, channel, their_ts, our_ts) - irc.channels[channel].ts = their_ts - irc.channels[channel].modes.clear() - for p in irc.channels[channel].prefixmodes.values(): - p.clear() - modestring = args[2:-1] or args[2] - parsedmodes = utils.parseModes(irc, channel, modestring) - utils.applyModes(irc, channel, parsedmodes) - namelist = [] - for user in userlist: - modeprefix, user = user.split(',', 1) - namelist.append(user) - irc.users[user].channels.add(channel) - if their_ts <= our_ts: - utils.applyModes(irc, channel, [('+%s' % mode, user) for mode in modeprefix]) - irc.channels[channel].users.add(user) - return {'channel': channel, 'users': namelist, 'modes': parsedmodes, 'ts': their_ts} + def handle_fmode(self, numeric, command, args): + """Handles the FMODE command, used for channel mode changes.""" + # <- :70MAAAAAA FMODE #chat 1433653462 +hhT 70MAAAAAA 70MAAAAAD + channel = utils.toLower(self.irc, args[0]) + modes = args[2:] + changedmodes = utils.parseModes(self.irc, channel, modes) + utils.applyModes(self.irc, channel, changedmodes) + ts = int(args[1]) + return {'target': channel, 'modes': changedmodes, 'ts': ts} -def handle_uid(irc, numeric, command, args): - """Handles incoming UID commands (user introduction).""" - # :70M UID 70MAAAAAB 1429934638 GL 0::1 hidden-7j810p.9mdf.lrek.0000.0000.IP gl 0::1 1429934638 +Wioswx +ACGKNOQXacfgklnoqvx :realname - uid, ts, nick, realhost, host, ident, ip = args[0:7] - realname = args[-1] - irc.users[uid] = IrcUser(nick, ts, uid, ident, host, realname, realhost, ip) - parsedmodes = utils.parseModes(irc, uid, [args[8], args[9]]) - log.debug('Applying modes %s for %s', parsedmodes, uid) - utils.applyModes(irc, uid, parsedmodes) - irc.servers[numeric].users.add(uid) - return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip} + def handle_idle(self, numeric, command, args): + """Handles the IDLE command, sent between servers in remote WHOIS queries.""" + # <- :70MAAAAAA IDLE 1MLAAAAIG + # -> :1MLAAAAIG IDLE 70MAAAAAA 1433036797 319 + sourceuser = numeric + targetuser = args[0] + self._send(targetuser, 'IDLE %s %s 0' % (sourceuser, self.irc.users[targetuser].ts)) -def handle_server(irc, numeric, command, args): - """Handles incoming SERVER commands (introduction of servers).""" - # SERVER is sent by our uplink or any other server to introduce others. - # <- :00A SERVER test.server * 1 00C :testing raw message syntax - # <- :70M SERVER millennium.overdrive.pw * 1 1ML :a relatively long period of time... (Fremont, California) - servername = args[0].lower() - sid = args[3] - sdesc = args[-1] - irc.servers[sid] = IrcServer(numeric, servername) - return {'name': servername, 'sid': args[3], 'text': sdesc} + 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 = utils.toLower(self.irc, args[0]) + ts = args[1] + setter = args[2] + topic = args[-1] + self.irc.channels[channel].topic = topic + self.irc.channels[channel].topicset = True + return {'channel': channel, 'setter': setter, 'ts': ts, 'topic': topic} -def handle_fmode(irc, numeric, command, args): - """Handles the FMODE command, used for channel mode changes.""" - # <- :70MAAAAAA FMODE #chat 1433653462 +hhT 70MAAAAAA 70MAAAAAD - channel = utils.toLower(irc, args[0]) - modes = args[2:] - changedmodes = utils.parseModes(irc, channel, modes) - utils.applyModes(irc, channel, changedmodes) - ts = int(args[1]) - return {'target': channel, 'modes': changedmodes, 'ts': ts} + def handle_invite(self, numeric, command, args): + """Handles incoming INVITEs.""" + # <- :70MAAAAAC INVITE 0ALAAAAAA #blah 0 + target = args[0] + channel = utils.toLower(self.irc, 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_idle(irc, numeric, command, args): - """Handles the IDLE command, sent between servers in remote WHOIS queries.""" - # <- :70MAAAAAA IDLE 1MLAAAAIG - # -> :1MLAAAAIG IDLE 70MAAAAAA 1433036797 319 - sourceuser = numeric - targetuser = args[0] - _send(irc, targetuser, 'IDLE %s %s 0' % (sourceuser, irc.users[targetuser].ts)) + def handle_encap(self, numeric, command, args): + """Handles incoming encapsulated commands (ENCAP). Hook arguments + returned by this should have a parse_as field, that sets the correct + hook name for the message. -def handle_ftopic(irc, 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 = utils.toLower(irc, args[0]) - ts = args[1] - setter = args[2] - topic = args[-1] - irc.channels[channel].topic = topic - irc.channels[channel].topicset = True - return {'channel': channel, 'setter': setter, 'ts': ts, 'topic': topic} + For InspIRCd, the only ENCAP command we handle right now is KNOCK.""" + # <- :70MAAAAAA ENCAP * KNOCK #blah :agsdfas + # From charybdis TS6 docs: https://github.com/grawity/self.irc-docs/blob/03ba884a54f1cef2193cd62b6a86803d89c1ac41/server/ts6.txt -def handle_invite(irc, numeric, command, args): - """Handles incoming INVITEs.""" - # <- :70MAAAAAC INVITE 0ALAAAAAA #blah 0 - target = args[0] - channel = utils.toLower(irc, args[1]) - # We don't actually need to process this; just send the hook so plugins can use it - return {'target': target, 'channel': channel} + # ENCAP + # source: any + # parameters: target server mask, subcommand, opt. parameters... -def handle_encap(irc, numeric, command, args): - """Handles incoming encapsulated commands (ENCAP). Hook arguments - returned by this should have a parse_as field, that sets the correct - hook name for the message. + # Sends a command to matching servers. Propagation is independent of + # understanding the subcommand. - For InspIRCd, the only ENCAP command we handle right now is KNOCK.""" - # <- :70MAAAAAA ENCAP * KNOCK #blah :agsdfas - # From charybdis TS6 docs: https://github.com/grawity/irc-docs/blob/03ba884a54f1cef2193cd62b6a86803d89c1ac41/server/ts6.txt + targetmask = args[0] + real_command = args[1] + if targetmask == '*' and real_command == 'KNOCK': + channel = utils.toLower(self.irc, args[2]) + text = args[3] + return {'parse_as': real_command, 'channel': channel, + 'text': text} - # ENCAP - # source: any - # parameters: target server mask, subcommand, opt. parameters... + def handle_opertype(self, numeric, command, args): + """Handles incoming OPERTYPE, which is used to denote an oper up. - # Sends a command to matching servers. Propagation is independent of - # understanding the subcommand. + This calls the internal hook PYLINK_CLIENT_OPERED, sets the internal + opertype of the client, and assumes setting user mode +o on the caller.""" + # This is used by InspIRCd to denote an oper up; there is no MODE + # command sent for it. + # <- :70MAAAAAB OPERTYPE Network_Owner + omode = [('+o', None)] + self.irc.users[numeric].opertype = opertype = args[0] + utils.applyModes(self.irc, numeric, omode) + # OPERTYPE is essentially umode +o and metadata in one command; + # we'll call that too. + self.irc.callHooks([numeric, 'PYLINK_CLIENT_OPERED', {'text': opertype}]) + return {'target': numeric, 'modes': omode} - targetmask = args[0] - real_command = args[1] - if targetmask == '*' and real_command == 'KNOCK': - channel = utils.toLower(irc, args[2]) - text = args[3] - return {'parse_as': real_command, 'channel': channel, - 'text': text} + 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] + return {'target': numeric, 'newident': newident} -def handle_opertype(irc, numeric, command, args): - """Handles incoming OPERTYPE, which is used to denote an oper up. + def handle_fhost(self, numeric, command, args): + """Handles FHOST, used for denoting hostname changes.""" + # <- :70MAAAAAB FIDENT some.host + self.irc.users[numeric].host = newhost = args[0] + return {'target': numeric, 'newhost': newhost} - This calls the internal hook PYLINK_CLIENT_OPERED, sets the internal - opertype of the client, and assumes setting user mode +o on the caller.""" - # This is used by InspIRCd to denote an oper up; there is no MODE - # command sent for it. - # <- :70MAAAAAB OPERTYPE Network_Owner - omode = [('+o', None)] - irc.users[numeric].opertype = opertype = args[0] - utils.applyModes(irc, numeric, omode) - # OPERTYPE is essentially umode +o and metadata in one command; - # we'll call that too. - irc.callHooks([numeric, 'PYLINK_CLIENT_OPERED', {'text': opertype}]) - return {'target': numeric, 'modes': omode} + 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] + return {'target': numeric, 'newgecos': newgecos} -def handle_fident(irc, numeric, command, args): - """Handles FIDENT, used for denoting ident changes.""" - # <- :70MAAAAAB FIDENT test - irc.users[numeric].ident = newident = args[0] - return {'target': numeric, 'newident': newident} + def handle_endburst(self, numeric, command, args): + """ENDBURST handler; sends a hook with empty contents.""" + return {} -def handle_fhost(irc, numeric, command, args): - """Handles FHOST, used for denoting hostname changes.""" - # <- :70MAAAAAB FIDENT some.host - irc.users[numeric].host = newhost = args[0] - return {'target': numeric, 'newhost': newhost} + def handle_away(self, numeric, command, args): + """Handles incoming AWAY messages.""" + # <- :1MLAAAAIG AWAY 1439371390 :Auto-away + try: + ts = args[0] + self.irc.users[numeric].away = text = args[1] + return {'text': text, 'ts': ts} + except IndexError: # User is unsetting away status + self.irc.users[numeric].away = '' + return {'text': ''} -def handle_fname(irc, numeric, command, args): - """Handles FNAME, used for denoting real name/gecos changes.""" - # <- :70MAAAAAB FNAME :afdsafasf - irc.users[numeric].realname = newgecos = args[0] - return {'target': numeric, 'newgecos': newgecos} - -def handle_endburst(irc, numeric, command, args): - """ENDBURST handler; sends a hook with empty contents.""" - return {} - -def handle_away(irc, numeric, command, args): - """Handles incoming AWAY messages.""" - # <- :1MLAAAAIG AWAY 1439371390 :Auto-away - try: - ts = args[0] - irc.users[numeric].away = text = args[1] - return {'text': text, 'ts': ts} - except IndexError: # User is unsetting away status - irc.users[numeric].away = '' - return {'text': ''} +Class = InspIRCdProtocol diff --git a/protocols/ts6.py b/protocols/ts6.py index 3e0f852..15e2883 100644 --- a/protocols/ts6.py +++ b/protocols/ts6.py @@ -9,663 +9,670 @@ import utils from log import log from classes import * +'''' # Some functions are shared with the InspIRCd module (ts6_common) from ts6_common import nickClient, kickServer, kickClient, _sendKick, quitClient, \ removeClient, partClient, messageClient, noticeClient, topicClient, parseTS6Args from ts6_common import handle_privmsg, handle_kill, handle_kick, handle_error, \ handle_quit, handle_nick, handle_save, handle_squit, handle_mode, handle_topic, \ handle_notice, _send, handle_part +''' +from ts6_common import TS6BaseProtocol -casemapping = 'rfc1459' -hook_map = {'SJOIN': 'JOIN', 'TB': 'TOPIC', 'TMODE': 'MODE', 'BMASK': 'MODE'} +class TS6Protocol(TS6BaseProtocol): + def __init__(self, irc): + super(TS6Protocol, self).__init__(irc) + self.casemapping = 'rfc1459' + self.hook_map = {'SJOIN': 'JOIN', 'TB': 'TOPIC', 'TMODE': 'MODE', 'BMASK': 'MODE'} -def spawnClient(irc, nick, ident='null', host='null', realhost=None, modes=set(), - server=None, ip='0.0.0.0', realname=None, ts=None, opertype=None): - server = server or irc.sid - if not utils.isInternalServer(irc, server): - raise ValueError('Server %r is not a PyLink internal PseudoServer!' % server) - # We need a separate UID generator instance for every PseudoServer - # we spawn. Otherwise, things won't wrap around properly. - if server not in irc.uidgen: - irc.uidgen[server] = utils.TS6UIDGenerator(server) - uid = irc.uidgen[server].next_uid() - # EUID: - # parameters: nickname, hopcount, nickTS, umodes, username, - # visible hostname, IP address, UID, real hostname, account name, gecos - ts = ts or int(time.time()) - realname = realname or irc.botdata['realname'] - realhost = realhost or host - raw_modes = utils.joinModes(modes) - u = irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname, - realhost=realhost, ip=ip) - utils.applyModes(irc, uid, modes) - irc.servers[server].users.add(uid) - _send(irc, 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, - realhost=realhost)) - return u + def spawnClient(self, nick, ident='null', host='null', realhost=None, modes=set(), + server=None, ip='0.0.0.0', realname=None, ts=None, opertype=None): + server = server or self.irc.sid + if not utils.isInternalServer(self.irc, server): + raise ValueError('Server %r is not a PyLink internal PseudoServer!' % server) + # We need a separate UID generator instance for every PseudoServer + # we spawn. Otherwise, things won't wrap around properly. + if server not in self.irc.uidgen: + self.irc.uidgen[server] = utils.TS6UIDGenerator(server) + uid = self.irc.uidgen[server].next_uid() + # EUID: + # parameters: nickname, hopcount, nickTS, umodes, username, + # visible hostname, IP address, UID, real hostname, account name, gecos + ts = ts or int(time.time()) + realname = realname or self.irc.botdata['realname'] + realhost = realhost or host + raw_modes = utils.joinModes(modes) + u = self.irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname, + realhost=realhost, ip=ip) + utils.applyModes(self.irc, uid, modes) + self.irc.servers[server].users.add(uid) + self._send(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, + realhost=realhost)) + return u -def joinClient(irc, client, channel): - channel = utils.toLower(irc, channel) - # JOIN: - # parameters: channelTS, channel, '+' (a plus sign) - if not utils.isInternalClient(irc, client): - log.error('(%s) Error trying to join client %r to %r (no such pseudoclient exists)', irc.name, client, channel) - raise LookupError('No such PyLink PseudoClient exists.') - _send(irc, client, "JOIN {ts} {channel} +".format(ts=irc.channels[channel].ts, channel=channel)) - irc.channels[channel].users.add(client) - irc.users[client].channels.add(channel) + def joinClient(self, client, channel): + channel = utils.toLower(self.irc, channel) + # JOIN: + # parameters: channelTS, channel, '+' (a plus sign) + if not utils.isInternalClient(self.irc, client): + log.error('(%s) Error trying to join client %r to %r (no such pseudoclient exists)', self.irc.name, client, channel) + raise LookupError('No such PyLink PseudoClient 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) -def sjoinServer(irc, server, channel, users, ts=None): - # https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L821 - # parameters: channelTS, channel, simple modes, opt. mode parameters..., nicklist + def sjoinServer(self, server, channel, users, ts=None): + # https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L821 + # parameters: channelTS, channel, simple modes, opt. mode parameters..., nicklist - # Broadcasts a channel creation or bursts a channel. + # Broadcasts a channel creation or bursts a channel. - # The nicklist consists of users joining the channel, with status prefixes for - # 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 = utils.toLower(irc, channel) - server = server or irc.sid - assert users, "sjoinServer: No users sent?" - log.debug('(%s) sjoinServer: got %r for users', irc.name, users) - if not server: - raise LookupError('No such PyLink PseudoClient exists.') - orig_ts = irc.channels[channel].ts - ts = ts or orig_ts - if ts < orig_ts: - # If the TS we're sending is lower than the one that existing, clear the - # mode lists from our channel state and reset the timestamp. - log.debug('(%s) sjoinServer: resetting TS of %r from %s to %s (clearing modes)', - irc.name, channel, orig_ts, ts) - irc.channels[channel].ts = ts - irc.channels[channel].modes.clear() - for p in irc.channels[channel].prefixmodes.values(): - p.clear() - log.debug("sending SJOIN to %s%s with ts %s (that's %r)", channel, irc.name, ts, - time.strftime("%c", time.localtime(ts))) - modes = [m for m in irc.channels[channel].modes if m[0] not in irc.cmodes['*A']] - changedmodes = [] - while users[:10]: - uids = [] - namelist = [] - # We take as a list of (prefixmodes, uid) pairs. - for userpair in users[:10]: - assert len(userpair) == 2, "Incorrect format of userpair: %r" % userpair - prefixes, user = userpair - prefixchars = '' - for prefix in prefixes: - pr = irc.prefixmodes.get(prefix) - if pr: - prefixchars += pr - changedmodes.append(('+%s' % prefix, user)) - namelist.append(prefixchars+user) - uids.append(user) - try: - irc.users[user].channels.add(channel) - except KeyError: # Not initialized yet? - log.debug("(%s) sjoinServer: KeyError trying to add %r to %r's channel list?", irc.name, channel, user) - users = users[10:] - namelist = ' '.join(namelist) - _send(irc, server, "SJOIN {ts} {channel} {modes} :{users}".format( - ts=ts, users=namelist, channel=channel, - modes=utils.joinModes(modes))) - 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. - utils.applyModes(irc, channel, changedmodes) + # The nicklist consists of users joining the channel, with status prefixes for + # 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 = utils.toLower(self.irc, channel) + server = server or self.irc.sid + assert users, "sjoinServer: No users sent?" + log.debug('(%s) sjoinServer: got %r for users', self.irc.name, users) + if not server: + raise LookupError('No such PyLink PseudoClient exists.') + orig_ts = self.irc.channels[channel].ts + ts = ts or orig_ts + if ts < orig_ts: + # If the TS we're sending is lower than the one that existing, clear the + # mode lists from our channel state and reset the timestamp. + log.debug('(%s) sjoinServer: resetting TS of %r from %s to %s (clearing modes)', + self.irc.name, channel, orig_ts, ts) + self.irc.channels[channel].ts = ts + self.irc.channels[channel].modes.clear() + for p in self.irc.channels[channel].prefixmodes.values(): + p.clear() + log.debug("sending SJOIN to %s%s with ts %s (that's %r)", channel, self.irc.name, 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 = [] + while users[:10]: + uids = [] + namelist = [] + # We take as a list of (prefixmodes, uid) pairs. + for userpair in users[:10]: + assert len(userpair) == 2, "Incorrect format of userpair: %r" % userpair + prefixes, user = userpair + prefixchars = '' + for prefix in prefixes: + pr = self.irc.prefixmodes.get(prefix) + if pr: + prefixchars += pr + changedmodes.append(('+%s' % prefix, user)) + namelist.append(prefixchars+user) + uids.append(user) + try: + self.irc.users[user].channels.add(channel) + except KeyError: # Not initialized yet? + log.debug("(%s) sjoinServer: KeyError trying to add %r to %r's channel list?", self.irc.name, channel, user) + users = users[10:] + namelist = ' '.join(namelist) + self._send(server, "SJOIN {ts} {channel} {modes} :{users}".format( + ts=ts, users=namelist, channel=channel, + modes=utils.joinModes(modes))) + 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. + utils.applyModes(self.irc, channel, changedmodes) -def _sendModes(irc, numeric, target, modes, ts=None): - utils.applyModes(irc, target, modes) - if utils.isChannel(target): - ts = ts or irc.channels[utils.toLower(irc, target)].ts - # TMODE: - # parameters: channelTS, channel, cmode changes, opt. cmode parameters... + def _sendModes(self, numeric, target, modes, ts=None): + utils.applyModes(self.irc, target, modes) + if utils.isChannel(target): + ts = ts or self.irc.channels[utils.toLower(self.irc, target)].ts + # TMODE: + # parameters: channelTS, channel, cmode changes, opt. cmode parameters... - # On output, at most ten cmode parameters should be sent; if there are more, - # multiple TMODE messages should be sent. - while modes[:9]: - joinedmodes = utils.joinModes(modes = [m for m in modes[:9] if m[0] not in irc.cmodes['*A']]) - modes = modes[9:] - _send(irc, numeric, 'TMODE %s %s %s' % (ts, target, joinedmodes)) - else: - joinedmodes = utils.joinModes(modes) - _send(irc, numeric, 'MODE %s %s' % (target, joinedmodes)) + # On output, at most ten cmode parameters should be sent; if there are more, + # multiple TMODE messages should be sent. + while modes[:9]: + joinedmodes = utils.joinModes(modes = [m for m in modes[:9] if m[0] not in self.irc.cmodes['*A']]) + modes = modes[9:] + self._send(numeric, 'TMODE %s %s %s' % (ts, target, joinedmodes)) + else: + joinedmodes = utils.joinModes(modes) + self._send(numeric, 'MODE %s %s' % (target, joinedmodes)) -def modeClient(irc, numeric, target, modes, ts=None): - """ + def modeClient(self, numeric, target, modes, ts=None): + """ - Sends modes from a PyLink PseudoClient. should be - a list of (mode, arg) tuples, in the format of utils.parseModes() output. - """ - if not utils.isInternalClient(irc, numeric): - raise LookupError('No such PyLink PseudoClient exists.') - _sendModes(irc, numeric, target, modes, ts=ts) + Sends modes from a PyLink PseudoClient. should be + a list of (mode, arg) tuples, in the format of utils.parseModes() output. + """ + if not utils.isInternalClient(self.irc, numeric): + raise LookupError('No such PyLink PseudoClient exists.') + self._sendModes(numeric, target, modes, ts=ts) -def modeServer(irc, numeric, target, modes, ts=None): - """ + def modeServer(self, numeric, target, modes, ts=None): + """ - Sends modes from a PyLink PseudoServer. should be - a list of (mode, arg) tuples, in the format of utils.parseModes() output. - """ - if not utils.isInternalServer(irc, numeric): - raise LookupError('No such PyLink PseudoServer exists.') - _sendModes(irc, numeric, target, modes, ts=ts) + Sends modes from a PyLink PseudoServer. should be + a list of (mode, arg) tuples, in the format of utils.parseModes() output. + """ + if not utils.isInternalServer(self.irc, numeric): + raise LookupError('No such PyLink PseudoServer exists.') + self._sendModes(numeric, target, modes, ts=ts) -def killServer(irc, numeric, target, reason): - """ + def killServer(self, numeric, target, reason): + """ - Sends a kill to from a PyLink PseudoServer. - """ - if not utils.isInternalServer(irc, numeric): - raise LookupError('No such PyLink PseudoServer exists.') - # KILL: - # parameters: target user, path + Sends a kill to from a PyLink PseudoServer. + """ + if not utils.isInternalServer(self.irc, numeric): + raise LookupError('No such PyLink PseudoServer exists.') + # KILL: + # parameters: target user, path - # The format of the path parameter is some sort of description of the source of - # the kill followed by a space and a parenthesized reason. To avoid overflow, - # it is recommended not to add anything to the path. + # The format of the path parameter is some sort of description of the source of + # 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 irc.users, "Unknown target %r for killServer!" % target - _send(irc, numeric, 'KILL %s :Killed (%s)' % (target, reason)) - removeClient(irc, target) + assert target in self.irc.users, "Unknown target %r for killServer!" % target + self._send(numeric, 'KILL %s :Killed (%s)' % (target, reason)) + removeClient(self.irc, target) -def killClient(irc, numeric, target, reason): - """ + def killClient(self, numeric, target, reason): + """ - Sends a kill to from a PyLink PseudoClient. - """ - if not utils.isInternalClient(irc, numeric): - raise LookupError('No such PyLink PseudoClient exists.') - assert target in irc.users, "Unknown target %r for killClient!" % target - _send(irc, numeric, 'KILL %s :Killed (%s)' % (target, reason)) - removeClient(irc, target) + Sends a kill to from a PyLink PseudoClient. + """ + if not utils.isInternalClient(self.irc, numeric): + raise LookupError('No such PyLink PseudoClient exists.') + assert target in self.irc.users, "Unknown target %r for killClient!" % target + self._send(numeric, 'KILL %s :Killed (%s)' % (target, reason)) + removeClient(self.irc, target) -def topicServer(irc, numeric, target, text): - if not utils.isInternalServer(irc, numeric): - raise LookupError('No such PyLink PseudoServer exists.') - # TB - # capab: TB - # source: server - # propagation: broadcast - # parameters: channel, topicTS, opt. topic setter, topic - ts = irc.channels[target].ts - servername = irc.servers[numeric].name - _send(irc, numeric, 'TB %s %s %s :%s' % (target, ts, servername, text)) - irc.channels[target].topic = text - irc.channels[target].topicset = True + def topicServer(self, numeric, target, text): + if not utils.isInternalServer(self.irc, numeric): + raise LookupError('No such PyLink PseudoServer 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 -def inviteClient(irc, numeric, target, channel): - """ + def inviteClient(self, numeric, target, channel): + """ - Invites to to from PyLink client .""" - if not utils.isInternalClient(irc, numeric): - raise LookupError('No such PyLink PseudoClient exists.') - _send(irc, numeric, 'INVITE %s %s %s' % (target, channel, irc.channels[channel].ts)) + Invites to to from PyLink client .""" + if not utils.isInternalClient(self.irc, numeric): + raise LookupError('No such PyLink PseudoClient exists.') + self._send(numeric, 'INVITE %s %s %s' % (target, channel, self.irc.channels[channel].ts)) -def knockClient(irc, numeric, target, text): - """ + def knockClient(self, numeric, target, text): + """ - Knocks on with from PyLink client .""" - if 'KNOCK' not in irc.caps: - log.debug('(%s) knockClient: Dropping KNOCK to %r since the IRCd ' - 'doesn\'t support it.', irc.name, target) - return - if not utils.isInternalClient(irc, numeric): - raise LookupError('No such PyLink PseudoClient exists.') - # No text value is supported here; drop it. - _send(irc, numeric, 'KNOCK %s' % target) + Knocks on with from PyLink client .""" + if 'KNOCK' not in self.irc.caps: + log.debug('(%s) knockClient: Dropping KNOCK to %r since the IRCd ' + 'doesn\'t support it.', self.irc.name, target) + return + if not utils.isInternalClient(self.irc, numeric): + raise LookupError('No such PyLink PseudoClient exists.') + # No text value is supported here; drop it. + self._send(numeric, 'KNOCK %s' % target) -def updateClient(irc, numeric, field, text): - """ + def updateClient(self, numeric, field, text): + """ - Changes the field of PyLink PseudoClient .""" - field = field.upper() - if field == 'HOST': - irc.users[numeric].host = text - _send(irc, irc.sid, 'CHGHOST %s :%s' % (numeric, text)) - else: - raise NotImplementedError("Changing field %r of a client is unsupported by this protocol." % field) + Changes the field of PyLink PseudoClient .""" + field = field.upper() + if field == 'HOST': + self.irc.users[numeric].host = text + self._send(self.irc.sid, 'CHGHOST %s :%s' % (numeric, text)) + else: + raise NotImplementedError("Changing field %r of a client is unsupported by this protocol." % field) -def pingServer(irc, source=None, target=None): - source = source or irc.sid - if source is None: - return - if target is not None: - _send(irc, source, 'PING %s %s' % (source, target)) - else: - _send(irc, source, 'PING %s' % source) + def pingServer(self, source=None, target=None): + source = source or self.irc.sid + if source is None: + return + if target is not None: + self._send(source, 'PING %s %s' % (source, target)) + else: + self._send(source, 'PING %s' % source) -def numericServer(irc, source, numeric, target, text): - _send(irc, source, '%s %s %s' % (numeric, target, text)) + def numericServer(self, source, numeric, target, text): + self._send(source, '%s %s %s' % (numeric, target, text)) -def awayClient(irc, source, text): - """ + def awayClient(self, source, text): + """ - Sends an AWAY message with text from PyLink client . - can be an empty string to unset AWAY status.""" - if text: - _send(irc, source, 'AWAY :%s' % text) - else: - _send(irc, source, 'AWAY') + Sends an AWAY message with text from PyLink client . + can be an empty string to unset AWAY status.""" + if text: + self._send(source, 'AWAY :%s' % text) + else: + self._send(source, 'AWAY') -def connect(irc): - ts = irc.start_ts + def connect(self): + ts = self.irc.start_ts - f = irc.send - # Valid keywords (from mostly InspIRCd's named modes): - # admin allowinvite autoop ban banexception blockcolor - # c_registered exemptchanops filter forward flood halfop history invex - # inviteonly joinflood key kicknorejoin limit moderated nickflood - # noctcp noextmsg nokick noknock nonick nonotice official-join op - # operonly opmoderated owner permanent private redirect regonly - # regmoderated secret sslonly stripcolor topiclock voice + f = self.irc.send + # Valid keywords (from mostly InspIRCd's named modes): + # admin allowinvite autoop ban banexception blockcolor + # c_registered exemptchanops filter forward flood halfop history invex + # inviteonly joinflood key kicknorejoin limit moderated nickflood + # noctcp noextmsg nokick noknock nonick nonotice official-join op + # operonly opmoderated owner permanent private redirect regonly + # regmoderated secret sslonly stripcolor topiclock voice - # https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L80 - chary_cmodes = { # TS6 generic modes: - # Note: charybdis +p has the effect of being both - # noknock AND private. Surprisingly, mapping it twice - # works pretty well: setting +p on a charybdis relay - # server sets +pK on an InspIRCd network. - 'op': 'o', 'voice': 'v', 'ban': 'b', 'key': 'k', 'limit': - 'l', 'moderated': 'm', 'noextmsg': 'n', 'noknock': 'p', - 'secret': 's', 'topiclock': 't', - # charybdis-specific modes: - 'quiet': 'q', 'redirect': 'f', 'freetarget': 'F', - 'joinflood': 'j', 'largebanlist': 'L', 'permanent': 'P', - 'c_noforwards': 'Q', 'stripcolor': 'c', 'allowinvite': - 'g', 'opmoderated': 'z', 'noctcp': 'C', - # charybdis-specific modes provided by EXTENSIONS - 'operonly': 'O', 'adminonly': 'A', 'sslonly': 'S', - # Now, map all the ABCD type modes: - '*A': 'beIq', '*B': 'k', '*C': 'l', '*D': 'mnprst'} + # https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L80 + chary_cmodes = { # TS6 generic modes: + # Note: charybdis +p has the effect of being both + # noknock AND private. Surprisingly, mapping it twice + # works pretty well: setting +p on a charybdis relay + # server sets +pK on an InspIRCd network. + 'op': 'o', 'voice': 'v', 'ban': 'b', 'key': 'k', 'limit': + 'l', 'moderated': 'm', 'noextmsg': 'n', 'noknock': 'p', + 'secret': 's', 'topiclock': 't', + # charybdis-specific modes: + 'quiet': 'q', 'redirect': 'f', 'freetarget': 'F', + 'joinflood': 'j', 'largebanlist': 'L', 'permanent': 'P', + 'c_noforwards': 'Q', 'stripcolor': 'c', 'allowinvite': + 'g', 'opmoderated': 'z', 'noctcp': 'C', + # charybdis-specific modes provided by EXTENSIONS + 'operonly': 'O', 'adminonly': 'A', 'sslonly': 'S', + # Now, map all the ABCD type modes: + '*A': 'beIq', '*B': 'k', '*C': 'l', '*D': 'mnprst'} - if irc.serverdata.get('use_owner'): - chary_cmodes['owner'] = 'y' - irc.prefixmodes['y'] = '~' - if irc.serverdata.get('use_admin'): - chary_cmodes['admin'] = 'a' - irc.prefixmodes['a'] = '!' - if irc.serverdata.get('use_halfop'): - chary_cmodes['halfop'] = 'h' - irc.prefixmodes['h'] = '%' + if self.irc.serverdata.get('use_owner'): + chary_cmodes['owner'] = 'y' + self.irc.prefixmodes['y'] = '~' + if self.irc.serverdata.get('use_admin'): + chary_cmodes['admin'] = 'a' + self.irc.prefixmodes['a'] = '!' + if self.irc.serverdata.get('use_halfop'): + chary_cmodes['halfop'] = 'h' + self.irc.prefixmodes['h'] = '%' - irc.cmodes.update(chary_cmodes) + self.irc.cmodes.update(chary_cmodes) - # Same thing with umodes: - # bot callerid cloak deaf_commonchan helpop hidechans hideoper invisible oper regdeaf servprotect showwhois snomask u_registered u_stripcolor wallops - chary_umodes = {'deaf': 'D', 'servprotect': 'S', 'u_admin': 'a', - 'invisible': 'i', 'oper': 'o', 'wallops': 'w', - 'snomask': 's', 'u_noforward': 'Q', 'regdeaf': 'R', - 'callerid': 'g', 'chary_operwall': 'z', 'chary_locops': - 'l', - # Now, map all the ABCD type modes: - '*A': '', '*B': '', '*C': '', '*D': 'DSaiowsQRgzl'} - irc.umodes.update(chary_umodes) + # Same thing with umodes: + # bot callerid cloak deaf_commonchan helpop hidechans hideoper invisible oper regdeaf servprotect showwhois snomask u_registered u_stripcolor wallops + chary_umodes = {'deaf': 'D', 'servprotect': 'S', 'u_admin': 'a', + 'invisible': 'i', 'oper': 'o', 'wallops': 'w', + 'snomask': 's', 'u_noforward': 'Q', 'regdeaf': 'R', + 'callerid': 'g', 'chary_operwall': 'z', 'chary_locops': + 'l', + # Now, map all the ABCD type modes: + '*A': '', '*B': '', '*C': '', '*D': 'DSaiowsQRgzl'} + self.irc.umodes.update(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 irc.serverdata.get('use_elemental_modes'): - elemental_cmodes = {'nonotice': 'T', 'hiddenbans': 'u', 'nokick': 'E', - 'kicknorejoin': 'J', 'repeat': 'K', 'nonick': 'd'} - irc.cmodes.update(elemental_cmodes) - irc.cmodes['*D'] += ''.join(elemental_cmodes.values()) - elemental_umodes = {'u_noctcp': 'C', 'deaf': 'D', 'bot': 'B', 'u_noinvite': 'V', - 'hidechans': 'I'} - irc.umodes.update(elemental_umodes) - irc.umodes['*D'] += ''.join(elemental_umodes.values()) + # 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'): + elemental_cmodes = {'nonotice': 'T', 'hiddenbans': 'u', 'nokick': 'E', + 'kicknorejoin': 'J', 'repeat': 'K', 'nonick': 'd'} + self.irc.cmodes.update(elemental_cmodes) + self.irc.cmodes['*D'] += ''.join(elemental_cmodes.values()) + elemental_umodes = {'u_noctcp': 'C', 'deaf': 'D', 'bot': 'B', 'u_noinvite': 'V', + 'hidechans': 'I'} + self.irc.umodes.update(elemental_umodes) + self.irc.umodes['*D'] += ''.join(elemental_umodes.values()) - # https://github.com/grawity/irc-docs/blob/master/server/ts6.txt#L55 - f('PASS %s TS 6 %s' % (irc.serverdata["sendpass"], irc.sid)) + # 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)) - # We request the following capabilities (for charybdis): + # We request the following capabilities (for charybdis): - # QS: SQUIT doesn't send recursive quits for each users; required - # by charybdis (Source: https://github.com/grawity/irc-docs/blob/master/server/ts-capab.txt) + # QS: SQUIT doesn't send recursive quits for each users; required + # by charybdis (Source: https://github.com/grawity/irc-docs/blob/master/server/ts-capab.txt) - # ENCAP: message encapsulation for certain commands, only because - # charybdis requires it to link + # ENCAP: message encapsulation for certain commands, only because + # charybdis requires it to link - # EX: Support for ban exemptions (+e) - # IE: Support for invite exemptions (+e) - # CHW: Allow sending messages to @#channel and the like. - # 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 topicServer - # EUID: extended UID command, which includes real hostname + account data info, - # and allows sending CHGHOST without ENCAP. - f('CAPAB :QS ENCAP EX CHW IE KNOCK SAVE SERVICES TB EUID') + # EX: Support for ban exemptions (+e) + # IE: Support for invite exemptions (+e) + # CHW: Allow sending messages to @#channel and the like. + # 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 topicServer + # EUID: extended UID command, which includes real hostname + account data info, + # and allows sending CHGHOST without ENCAP. + f('CAPAB :QS ENCAP EX CHW IE KNOCK SAVE SERVICES TB EUID') - f('SERVER %s 0 :%s' % (irc.serverdata["hostname"], - irc.serverdata.get('serverdesc') or irc.botdata['serverdesc'])) + f('SERVER %s 0 :%s' % (self.irc.serverdata["hostname"], + self.irc.serverdata.get('serverdesc') or self.irc.botdata['serverdesc'])) -def handle_ping(irc, source, command, args): - # PING: - # source: any - # parameters: origin, opt. destination server - # PONG: - # source: server - # parameters: origin, destination + def handle_ping(self, source, command, args): + # PING: + # source: any + # parameters: origin, opt. destination server + # PONG: + # source: server + # parameters: origin, destination - # Sends a PING to the destination server, which will reply with a PONG. If the - # destination server parameter is not present, the server receiving the message - # must reply. - try: - destination = args[1] - except IndexError: - destination = irc.sid - if utils.isInternalServer(irc, destination): - _send(irc, destination, 'PONG %s %s' % (destination, source)) + # Sends a PING to the destination server, which will reply with a PONG. If the + # destination server parameter is not present, the server receiving the message + # must reply. + try: + destination = args[1] + except IndexError: + destination = self.irc.sid + if utils.isInternalServer(self.irc, destination): + self._send(destination, 'PONG %s %s' % (destination, source)) -def handle_pong(irc, source, command, args): - if source == irc.uplink: - irc.lastping = time.time() + def handle_pong(self, source, command, args): + if source == self.irc.uplink: + self.irc.lastping = time.time() -def handle_sjoin(irc, servernumeric, command, args): - # parameters: channelTS, channel, simple modes, opt. mode parameters..., nicklist - channel = utils.toLower(irc, args[1]) - userlist = args[-1].split() - our_ts = irc.channels[channel].ts - their_ts = int(args[0]) - if their_ts < our_ts: - # Channel timestamp was reset on burst - log.debug('(%s) Setting channel TS of %s to %s from %s', - irc.name, channel, their_ts, our_ts) - irc.channels[channel].ts = their_ts - irc.channels[channel].modes.clear() - for p in irc.channels[channel].prefixmodes.values(): - p.clear() - modestring = args[2:-1] or args[2] - parsedmodes = utils.parseModes(irc, channel, modestring) - utils.applyModes(irc, channel, parsedmodes) - namelist = [] - log.debug('(%s) handle_sjoin: got userlist %r for %r', irc.name, userlist, channel) - for userpair in userlist: - # charybdis sends this in the form "@+UID1, +UID2, UID3, @UID4" - r = re.search(r'([^\d]*)(.*)', userpair) - user = r.group(2) - 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', irc.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 irc.prefixmodes.items(): - if m == prefix: - finalprefix += char - namelist.append(user) - irc.users[user].channels.add(channel) - if their_ts <= our_ts: - utils.applyModes(irc, channel, [('+%s' % mode, user) for mode in finalprefix]) - irc.channels[channel].users.add(user) - return {'channel': channel, 'users': namelist, 'modes': parsedmodes, 'ts': their_ts} - -def handle_join(irc, numeric, command, args): - # parameters: channelTS, channel, '+' (a plus sign) - ts = int(args[0]) - if args[0] == '0': - # /join 0; part the user from all channels - oldchans = irc.users[numeric].channels.copy() - log.debug('(%s) Got /join 0 from %r, channel list is %r', - irc.name, numeric, oldchans) - for channel in oldchans: - irc.channels[channel].users.discard(numeric) - irc.users[numeric].channels.discard(channel) - return {'channels': oldchans, 'text': 'Left all channels.', 'parse_as': 'PART'} - else: - channel = utils.toLower(irc, args[1]) - our_ts = irc.channels[channel].ts - if ts < our_ts: + def handle_sjoin(self, servernumeric, command, args): + # parameters: channelTS, channel, simple modes, opt. mode parameters..., nicklist + channel = utils.toLower(self.irc, args[1]) + userlist = args[-1].split() + our_ts = self.irc.channels[channel].ts + their_ts = int(args[0]) + if their_ts < our_ts: # Channel timestamp was reset on burst log.debug('(%s) Setting channel TS of %s to %s from %s', - irc.name, channel, ts, our_ts) - irc.channels[channel].ts = ts - irc.channels[channel].users.add(numeric) - irc.users[numeric].channels.add(channel) - # 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': - irc.channels[channel].modes, 'ts': ts} + self.irc.name, channel, their_ts, our_ts) + self.irc.channels[channel].ts = their_ts + self.irc.channels[channel].modes.clear() + for p in self.irc.channels[channel].prefixmodes.values(): + p.clear() + modestring = args[2:-1] or args[2] + parsedmodes = utils.parseModes(self.irc, channel, modestring) + utils.applyModes(self.irc, channel, parsedmodes) + namelist = [] + log.debug('(%s) handle_sjoin: got userlist %r for %r', self.irc.name, userlist, channel) + for userpair in userlist: + # charybdis sends this in the form "@+UID1, +UID2, UID3, @UID4" + r = re.search(r'([^\d]*)(.*)', userpair) + user = r.group(2) + 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) + 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(): + if m == prefix: + finalprefix += char + namelist.append(user) + self.irc.users[user].channels.add(channel) + if their_ts <= our_ts: + utils.applyModes(self.irc, channel, [('+%s' % mode, user) for mode in finalprefix]) + self.irc.channels[channel].users.add(user) + return {'channel': channel, 'users': namelist, 'modes': parsedmodes, 'ts': their_ts} -def handle_euid(irc, numeric, command, args): - # <- :42X EUID GL 1 1437505322 +ailoswz ~gl 127.0.0.1 127.0.0.1 42XAAAAAB * * :realname - nick = args[0] - ts, modes, ident, host, ip, uid, realhost = args[2:9] - if realhost == '*': - realhost = None - realname = args[-1] - log.debug('(%s) handle_euid got args: nick=%s ts=%s uid=%s ident=%s ' - 'host=%s realname=%s realhost=%s ip=%s', irc.name, nick, ts, uid, - ident, host, realname, realhost, ip) + def handle_join(self, numeric, command, args): + # parameters: channelTS, channel, '+' (a plus sign) + ts = int(args[0]) + if args[0] == '0': + # /join 0; part the user from all channels + 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(numeric) + self.irc.users[numeric].channels.discard(channel) + return {'channels': oldchans, 'text': 'Left all channels.', 'parse_as': 'PART'} + else: + channel = utils.toLower(self.irc, args[1]) + our_ts = self.irc.channels[channel].ts + if ts < our_ts: + # Channel timestamp was reset on burst + log.debug('(%s) Setting channel TS of %s to %s from %s', + self.irc.name, channel, ts, our_ts) + self.irc.channels[channel].ts = ts + self.irc.channels[channel].users.add(numeric) + self.irc.users[numeric].channels.add(channel) + # 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} - irc.users[uid] = IrcUser(nick, ts, uid, ident, host, realname, realhost, ip) - parsedmodes = utils.parseModes(irc, uid, [modes]) - log.debug('Applying modes %s for %s', parsedmodes, uid) - utils.applyModes(irc, uid, parsedmodes) - irc.servers[numeric].users.add(uid) - if ('o', None) in parsedmodes: - otype = 'Server_Administrator' if ('a', None) in parsedmodes else 'IRC_Operator' - irc.callHooks([uid, 'PYLINK_CLIENT_OPERED', {'text': otype}]) - return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip} + def handle_euid(self, numeric, command, args): + # <- :42X EUID GL 1 1437505322 +ailoswz ~gl 127.0.0.1 127.0.0.1 42XAAAAAB * * :realname + nick = args[0] + ts, modes, ident, host, ip, uid, realhost = args[2:9] + if realhost == '*': + realhost = None + realname = args[-1] + 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, + ident, host, realname, realhost, ip) -def handle_uid(irc, numeric, command, args): - raise ProtocolError("Servers should use EUID instead of UID to send users! " - "This IS a required capability after all...") + self.irc.users[uid] = IrcUser(nick, ts, uid, ident, host, realname, realhost, ip) + parsedmodes = utils.parseModes(self.irc, uid, [modes]) + log.debug('Applying modes %s for %s', parsedmodes, uid) + utils.applyModes(self.irc, uid, parsedmodes) + self.irc.servers[numeric].users.add(uid) + if ('o', None) in parsedmodes: + otype = 'Server_Administrator' if ('a', None) in parsedmodes else 'IRC_Operator' + self.irc.callHooks([uid, 'PYLINK_CLIENT_OPERED', {'text': otype}]) + return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip} -def handle_server(irc, numeric, command, args): - # parameters: server name, hopcount, sid, server description - servername = args[0].lower() - try: - sid = args[2] - except IndexError: - # It is allowed to send JUPEd servers that exist without a SID. - # That's not very fun to handle, though. - # XXX: don't just save these by their server names; that's ugly! - sid = servername - sdesc = args[-1] - irc.servers[sid] = IrcServer(numeric, servername) - return {'name': servername, 'sid': sid, 'text': sdesc} + def handle_uid(self, numeric, command, args): + raise ProtocolError("Servers should use EUID instead of UID to send users! " + "This IS a required capability after all...") -handle_sid = handle_server + def handle_server(self, numeric, command, args): + # parameters: server name, hopcount, sid, server description + servername = args[0].lower() + try: + sid = args[2] + except IndexError: + # It is allowed to send JUPEd servers that exist without a SID. + # That's not very fun to handle, though. + # XXX: don't just save these by their server names; that's ugly! + sid = servername + sdesc = args[-1] + self.irc.servers[sid] = IrcServer(numeric, servername) + return {'name': servername, 'sid': sid, 'text': sdesc} -def handle_tmode(irc, numeric, command, args): - # <- :42XAAAAAB TMODE 1437450768 #endlessvoid -c+lkC 3 agte4 - channel = utils.toLower(irc, args[1]) - modes = args[2:] - changedmodes = utils.parseModes(irc, channel, modes) - utils.applyModes(irc, channel, changedmodes) - ts = int(args[0]) - return {'target': channel, 'modes': changedmodes, 'ts': ts} + handle_sid = handle_server -def handle_mode(irc, numeric, command, args): - # <- :70MAAAAAA MODE 70MAAAAAA -i+xc - target = args[0] - modestrings = args[1:] - changedmodes = utils.parseModes(irc, numeric, modestrings) - utils.applyModes(irc, target, changedmodes) - if ('+o', None) in changedmodes: - otype = 'Server_Administrator' if ('a', None) in irc.users[target].modes else 'IRC_Operator' - irc.callHooks([target, 'PYLINK_CLIENT_OPERED', {'text': otype}]) - return {'target': target, 'modes': changedmodes} + def handle_tmode(self, numeric, command, args): + # <- :42XAAAAAB TMODE 1437450768 #endlessvoid -c+lkC 3 agte4 + channel = utils.toLower(self.irc, args[1]) + modes = args[2:] + changedmodes = utils.parseModes(self.irc, channel, modes) + utils.applyModes(self.irc, channel, changedmodes) + ts = int(args[0]) + return {'target': channel, 'modes': changedmodes, 'ts': ts} -def handle_events(irc, data): - # TS6 messages: - # :42X COMMAND arg1 arg2 :final long arg - # :42XAAAAAA PRIVMSG #somewhere :hello! - args = data.split(" ") - if not args: - # No data?? - return - if args[0] == 'PASS': - # <- PASS $somepassword TS 6 :42X - if args[1] != irc.serverdata['recvpass']: - # Check if recvpass is correct - raise ProtocolError('Error: recvpass from uplink server %s does not match configuration!' % servername) - if 'TS 6' not in data: - raise ProtocolError("Remote protocol version is too old! Is this even TS6?") - # Server name and SID are sent in different messages, grr - numeric = data.rsplit(':', 1)[1] - log.debug('(%s) Found uplink SID as %r', irc.name, numeric) - irc.servers[numeric] = IrcServer(None, 'unknown') - irc.uplink = numeric - return - elif args[0] == 'SERVER': - # <- SERVER charybdis.midnight.vpn 1 :charybdis test server - sname = args[1].lower() - log.debug('(%s) Found uplink server name as %r', irc.name, sname) - irc.servers[irc.uplink].name = sname - # According to the TS6 protocol documentation, we should send SVINFO - # when we get our uplink's SERVER command. - irc.send('SVINFO 6 6 0 :%s' % int(time.time())) - elif args[0] == 'SQUIT': - # What? Charybdis send this in a different format! - # <- SQUIT 00A :Remote host closed the connection - split_server = args[1] - res = handle_squit(irc, split_server, 'SQUIT', [split_server]) - irc.callHooks([split_server, 'SQUIT', res]) - elif args[0] == 'CAPAB': - # 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 - irc.caps = caps = data.split(':', 1)[1].split() - for required_cap in ('EUID', 'SAVE', 'TB', 'ENCAP', 'QS'): - if required_cap not in caps: - raise ProtocolError('%s not found in TS6 capabilities list; this is required! (got %r)' % (required_cap, caps)) + def handle_mode(self, numeric, command, args): + # <- :70MAAAAAA MODE 70MAAAAAA -i+xc + target = args[0] + modestrings = args[1:] + changedmodes = utils.parseModes(self.irc, numeric, modestrings) + utils.applyModes(self.irc, target, changedmodes) + if ('+o', None) in changedmodes: + otype = 'Server_Administrator' if ('a', None) in self.irc.users[target].modes else 'IRC_Operator' + self.irc.callHooks([target, 'PYLINK_CLIENT_OPERED', {'text': otype}]) + return {'target': target, 'modes': changedmodes} - if 'EX' in caps: - irc.cmodes['banexception'] = 'e' - if 'IE' in caps: - irc.cmodes['invex'] = 'I' - if 'SERVICES' in caps: - irc.cmodes['regonly'] = 'r' + def handle_events(self, data): + # TS6 messages: + # :42X COMMAND arg1 arg2 :final long arg + # :42XAAAAAA PRIVMSG #somewhere :hello! + args = data.split(" ") + if not args: + # No data?? + return + if args[0] == 'PASS': + # <- PASS $somepassword TS 6 :42X + if args[1] != self.irc.serverdata['recvpass']: + # Check if recvpass is correct + raise ProtocolError('Error: recvpass from uplink server %s does not match configuration!' % servername) + if 'TS 6' not in data: + raise ProtocolError("Remote protocol version is too old! Is this even TS6?") + # Server name and SID are sent in different messages, grr + numeric = data.rsplit(':', 1)[1] + log.debug('(%s) Found uplink SID as %r', self.irc.name, numeric) + self.irc.servers[numeric] = IrcServer(None, 'unknown') + self.irc.uplink = numeric + return + elif args[0] == 'SERVER': + # <- SERVER charybdis.midnight.vpn 1 :charybdis test server + sname = args[1].lower() + log.debug('(%s) Found uplink server name as %r', self.irc.name, sname) + self.irc.servers[self.irc.uplink].name = sname + # 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())) + elif args[0] == 'SQUIT': + # What? Charybdis send this in a different format! + # <- SQUIT 00A :Remote host closed the connection + split_server = args[1] + res = self.handle_squit(split_server, 'SQUIT', [split_server]) + self.irc.callHooks([split_server, 'SQUIT', res]) + elif args[0] == 'CAPAB': + # 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 = data.split(':', 1)[1].split() + for required_cap in ('EUID', 'SAVE', 'TB', 'ENCAP', 'QS'): + 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) irc.connected set!', irc.name) - irc.connected.set() + if 'EX' in caps: + self.irc.cmodes['banexception'] = 'e' + if 'IE' in caps: + self.irc.cmodes['invex'] = 'I' + if 'SERVICES' in caps: + self.irc.cmodes['regonly'] = 'r' - # Charybdis doesn't have the idea of an explicit endburst; but some plugins - # like relay require it to know that the network's connected. - # We'll set a timer to manually call endburst. It's not beautiful, - # but it's the best we can do. - endburst_timer = threading.Timer(1, irc.callHooks, args=([irc.uplink, 'ENDBURST', {}],)) - log.debug('(%s) Starting delay to send ENDBURST', irc.name) - endburst_timer.start() - try: - args = parseTS6Args(args) + log.debug('(%s) self.irc.connected set!', self.irc.name) + self.irc.connected.set() - numeric = args[0] - command = args[1] - args = args[2:] - except IndexError: - return + # Charybdis doesn't have the idea of an explicit endburst; but some plugins + # like relay require it to know that the network's connected. + # We'll set a timer to manually call endburst. It's not beautiful, + # but it's the best we can do. + endburst_timer = threading.Timer(1, self.irc.callHooks, args=([self.irc.uplink, 'ENDBURST', {}],)) + log.debug('(%s) Starting delay to send ENDBURST', self.irc.name) + endburst_timer.start() + try: + args = self.parseTS6Args(args) - # We will do wildcard event handling here. Unhandled events are just ignored. - try: - func = globals()['handle_'+command.lower()] - except KeyError: # unhandled event - pass - else: - parsed_args = func(irc, numeric, command, args) - if parsed_args is not None: - return [numeric, command, parsed_args] + numeric = args[0] + command = args[1] + args = args[2:] + except IndexError: + return -def spawnServer(irc, name, sid=None, uplink=None, desc=None): - # -> :0AL SID test.server 1 0XY :some silly pseudoserver - uplink = uplink or irc.sid - name = name.lower() - desc = desc or irc.serverdata.get('serverdesc') or irc.botdata['serverdesc'] - if sid is None: # No sid given; generate one! - irc.sidgen = utils.TS6SIDGenerator(irc.serverdata["sidrange"]) - sid = irc.sidgen.next_sid() - assert len(sid) == 3, "Incorrect SID length" - if sid in irc.servers: - raise ValueError('A server with SID %r already exists!' % sid) - for server in irc.servers.values(): - if name == server.name: - raise ValueError('A server named %r already exists!' % name) - if not utils.isInternalServer(irc, uplink): - raise ValueError('Server %r is not a PyLink internal PseudoServer!' % uplink) - if not utils.isServerName(name): - raise ValueError('Invalid server name %r' % name) - _send(irc, uplink, 'SID %s 1 %s :%s' % (name, sid, desc)) - irc.servers[sid] = IrcServer(uplink, name, internal=True) - return sid + # We will do wildcard command handling here. Unhandled commands are just ignored. + try: + func = getattr(self, 'handle_'+command.lower()) + except AttributeError: # Unhandled command + pass + else: + parsed_args = func(numeric, command, args) + if parsed_args is not None: + return [numeric, command, parsed_args] -def squitServer(irc, source, target, text='No reason given'): - # -> SQUIT 9PZ :blah, blah - irc.send('SQUIT %s :%s' % (target, text)) - handle_squit(irc, source, 'SQUIT', [target, text]) + def spawnServer(self, name, sid=None, uplink=None, desc=None): + # -> :0AL SID test.server 1 0XY :some silly pseudoserver + uplink = uplink or self.irc.sid + name = name.lower() + desc = desc or self.irc.serverdata.get('serverdesc') or self.irc.botdata['serverdesc'] + if sid is None: # No sid given; generate one! + self.irc.sidgen = utils.TS6SIDGenerator(self.irc.serverdata["sidrange"]) + sid = self.irc.sidgen.next_sid() + assert len(sid) == 3, "Incorrect SID length" + if sid in self.irc.servers: + raise ValueError('A server with SID %r already exists!' % sid) + for server in self.irc.servers.values(): + if name == server.name: + raise ValueError('A server named %r already exists!' % name) + if not utils.isInternalServer(self.irc, uplink): + raise ValueError('Server %r is not a PyLink internal PseudoServer!' % 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) + return sid -def handle_tb(irc, numeric, command, args): - # <- :42X TB 1434510754 #channel GLo|o|!GLolol@escape.the.dreamland.ca :Some channel topic - channel = args[1].lower() - ts = args[0] - setter = args[2] - topic = args[-1] - irc.channels[channel].topic = topic - irc.channels[channel].topicset = True - return {'channel': channel, 'setter': setter, 'ts': ts, 'topic': topic} + def squitServer(self, source, target, text='No reason given'): + # -> SQUIT 9PZ :blah, blah + self.irc.send('SQUIT %s :%s' % (target, text)) + self.handle_squit(source, 'SQUIT', [target, text]) -def handle_invite(irc, numeric, command, args): - # <- :70MAAAAAC INVITE 0ALAAAAAA #blah 12345 - target = args[0] - channel = args[1].lower() - 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} + def handle_tb(self, numeric, command, args): + # <- :42X TB 1434510754 #channel GLo|o|!GLolol@escape.the.dreamland.ca :Some channel topic + channel = args[1].lower() + ts = args[0] + setter = args[2] + topic = args[-1] + self.irc.channels[channel].topic = topic + self.irc.channels[channel].topicset = True + return {'channel': channel, 'setter': setter, 'ts': ts, 'topic': topic} -def handle_chghost(irc, numeric, command, args): - target = args[0] - irc.users[target].host = newhost = args[1] - return {'target': numeric, 'newhost': newhost} + def handle_invite(self, numeric, command, args): + # <- :70MAAAAAC INVITE 0ALAAAAAA #blah 12345 + target = args[0] + channel = args[1].lower() + 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} -def handle_bmask(irc, numeric, command, args): - # <- :42X BMASK 1424222769 #dev b :*!test@*.isp.net *!badident@* - # This is used for propagating bans, not TMODE! - channel = args[1].lower() - mode = args[2] - ts = int(args[0]) - modes = [] - for ban in args[-1].split(): - modes.append(('+%s' % mode, ban)) - utils.applyModes(irc, channel, modes) - return {'target': channel, 'modes': modes, 'ts': ts} + def handle_chghost(self, numeric, command, args): + target = args[0] + self.irc.users[target].host = newhost = args[1] + return {'target': numeric, 'newhost': newhost} -def handle_whois(irc, numeric, command, args): - # <- :42XAAAAAB WHOIS 5PYAAAAAA :pylink-devel - return {'target': args[0]} + def handle_bmask(self, numeric, command, args): + # <- :42X BMASK 1424222769 #dev b :*!test@*.isp.net *!badident@* + # This is used for propagating bans, not TMODE! + channel = args[1].lower() + mode = args[2] + ts = int(args[0]) + modes = [] + for ban in args[-1].split(): + modes.append(('+%s' % mode, ban)) + utils.applyModes(self.irc, channel, modes) + return {'target': channel, 'modes': modes, 'ts': ts} -def handle_472(irc, numeric, command, args): - # <- :charybdis.midnight.vpn 472 GL|devel O :is an unknown mode char to me - # 472 is sent to us when one of our clients tries to set a mode the server - # doesn't support. In this case, we'll raise a warning to alert the user - # about it. - badmode = args[1] - reason = args[-1] - setter = args[0] - charlist = {'A': 'chm_adminonly', 'O': 'chm_operonly', 'S': 'chm_sslonly'} - if badmode in charlist: - 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.', irc.name, setter, badmode, - charlist[badmode]) + def handle_whois(self, numeric, command, args): + # <- :42XAAAAAB WHOIS 5PYAAAAAA :pylink-devel + return {'target': args[0]} -def handle_away(irc, numeric, command, args): - # <- :6ELAAAAAB AWAY :Auto-away + def handle_472(self, numeric, command, args): + # <- :charybdis.midnight.vpn 472 GL|devel O :is an unknown mode char to me + # 472 is sent to us when one of our clients tries to set a mode the server + # doesn't support. In this case, we'll raise a warning to alert the user + # about it. + badmode = args[1] + reason = args[-1] + setter = args[0] + charlist = {'A': 'chm_adminonly', 'O': 'chm_operonly', 'S': 'chm_sslonly'} + if badmode in charlist: + 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, + charlist[badmode]) - try: - irc.users[numeric].away = text = args[0] - except IndexError: # User is unsetting away status - irc.users[numeric].away = text = '' - return {'text': text} + def handle_away(self, numeric, command, args): + # <- :6ELAAAAAB AWAY :Auto-away + 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} + +Class = TS6Protocol diff --git a/protocols/ts6_common.py b/protocols/ts6_common.py index 47666a6..211840f 100644 --- a/protocols/ts6_common.py +++ b/protocols/ts6_common.py @@ -7,251 +7,252 @@ import utils from log import log from classes import * -def _send(irc, source, msg): - """Sends a TS6-style raw command from a source numeric to the IRC connection given.""" - irc.send(':%s %s' % (source, msg)) +class TS6BaseProtocol(Protocol): + def _send(self, source, msg): + """Sends a TS6-style raw command from a source numeric to the self.irc connection given.""" + self.irc.send(':%s %s' % (source, msg)) -def parseArgs(args): - """Parses a string of RFC1459-style arguments split into a list, where ":" may - be used for multi-word arguments that last until the end of a line. - """ - real_args = [] - for idx, arg in enumerate(args): - real_args.append(arg) - # If the argument starts with ':' and ISN'T the first argument. - # The first argument is used for denoting the source UID/SID. - if arg.startswith(':') and idx != 0: - # : is used for multi-word arguments that last until the end - # of the message. We can use list splicing here to turn them all - # into one argument. - # Set the last arg to a joined version of the remaining args - arg = args[idx:] - arg = ' '.join(arg)[1:] - # Cut the original argument list right before the multi-word arg, - # and then append the multi-word arg. - real_args = args[:idx] + def parseArgs(self, args): + """Parses a string of RFC1459-style arguments split into a list, where ":" may + be used for multi-word arguments that last until the end of a line. + """ + real_args = [] + for idx, arg in enumerate(args): real_args.append(arg) - break - return real_args + # If the argument starts with ':' and ISN'T the first argument. + # The first argument is used for denoting the source UID/SID. + if arg.startswith(':') and idx != 0: + # : is used for multi-word arguments that last until the end + # of the message. We can use list splicing here to turn them all + # into one argument. + # Set the last arg to a joined version of the remaining args + arg = args[idx:] + arg = ' '.join(arg)[1:] + # Cut the original argument list right before the multi-word arg, + # and then append the multi-word arg. + real_args = args[:idx] + real_args.append(arg) + break + return real_args -def parseTS6Args(args): - """Similar to parseArgs(), but stripping leading colons from the first argument - of a line (usually the sender field).""" - args = parseArgs(args) - args[0] = args[0].split(':', 1)[1] - return args + def parseTS6Args(self, args): + """Similar to parseArgs(), but stripping leading colons from the first argument + of a line (usually the sender field).""" + args = self.parseArgs(args) + args[0] = args[0].split(':', 1)[1] + return args -### OUTGOING COMMANDS + ### OUTGOING COMMANDS -def _sendKick(irc, numeric, channel, target, reason=None): - """Internal function to send kicks from a PyLink client/server.""" - channel = utils.toLower(irc, channel) - if not reason: - reason = 'No reason given' - _send(irc, numeric, 'KICK %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 - # handle_part() does that just fine. - handle_part(irc, target, 'KICK', [channel]) + def _sendKick(self, numeric, channel, target, reason=None): + """Internal function to send kicks from a PyLink client/server.""" + channel = utils.toLower(self.irc, channel) + if not reason: + reason = 'No reason given' + self._send(numeric, 'KICK %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 + # handle_part() does that just fine. + self.handle_part(target, 'KICK', [channel]) -def kickClient(irc, numeric, channel, target, reason=None): - """Sends a kick from a PyLink client.""" - if not utils.isInternalClient(irc, numeric): - raise LookupError('No such PyLink PseudoClient exists.') - _sendKick(irc, numeric, channel, target, reason=reason) + def kickClient(self, numeric, channel, target, reason=None): + """Sends a kick from a PyLink client.""" + if not utils.isInternalClient(self.irc, numeric): + raise LookupError('No such PyLink PseudoClient exists.') + self._sendKick(numeric, channel, target, reason=reason) -def kickServer(irc, numeric, channel, target, reason=None): - """Sends a kick from a PyLink server.""" - if not utils.isInternalServer(irc, numeric): - raise LookupError('No such PyLink PseudoServer exists.') - _sendKick(irc, numeric, channel, target, reason=reason) + def kickServer(self, numeric, channel, target, reason=None): + """Sends a kick from a PyLink server.""" + if not utils.isInternalServer(self.irc, numeric): + raise LookupError('No such PyLink PseudoServer exists.') + self._sendKick(numeric, channel, target, reason=reason) -def nickClient(irc, numeric, newnick): - """Changes the nick of a PyLink client.""" - if not utils.isInternalClient(irc, numeric): - raise LookupError('No such PyLink PseudoClient exists.') - _send(irc, numeric, 'NICK %s %s' % (newnick, int(time.time()))) - irc.users[numeric].nick = newnick + def nickClient(self, numeric, newnick): + """Changes the nick of a PyLink client.""" + if not utils.isInternalClient(self.irc, numeric): + raise LookupError('No such PyLink PseudoClient exists.') + self._send(numeric, 'NICK %s %s' % (newnick, int(time.time()))) + self.irc.users[numeric].nick = newnick -def removeClient(irc, numeric): - """Internal function to remove a client from our internal state.""" - for c, v in irc.channels.copy().items(): - v.removeuser(numeric) - # Clear empty non-permanent channels. - if not (irc.channels[c].users or ((irc.cmodes.get('permanent'), None) in irc.channels[c].modes)): - del irc.channels[c] + 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] - sid = numeric[:3] - log.debug('Removing client %s from irc.users', numeric) - del irc.users[numeric] - log.debug('Removing client %s from irc.servers[%s]', numeric, sid) - irc.servers[sid].users.discard(numeric) + sid = numeric[:3] + 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]', numeric, sid) + self.irc.servers[sid].users.discard(numeric) -def partClient(irc, client, channel, reason=None): - """Sends a part from a PyLink client.""" - channel = utils.toLower(irc, channel) - if not utils.isInternalClient(irc, client): - log.error('(%s) Error trying to part client %r to %r (no such pseudoclient exists)', irc.name, client, channel) - raise LookupError('No such PyLink PseudoClient exists.') - msg = "PART %s" % channel - if reason: - msg += " :%s" % reason - _send(irc, client, msg) - handle_part(irc, client, 'PART', [channel]) + def partClient(self, client, channel, reason=None): + """Sends a part from a PyLink client.""" + channel = utils.toLower(self.irc, channel) + if not utils.isInternalClient(self.irc, client): + log.error('(%s) Error trying to part client %r to %r (no such pseudoclient exists)', self.irc.name, client, channel) + raise LookupError('No such PyLink PseudoClient exists.') + msg = "PART %s" % channel + if reason: + msg += " :%s" % reason + self._send(client, msg) + self.handle_part(client, 'PART', [channel]) -def quitClient(irc, numeric, reason): - """Quits a PyLink client.""" - if utils.isInternalClient(irc, numeric): - _send(irc, numeric, "QUIT :%s" % reason) - removeClient(irc, numeric) - else: - raise LookupError("No such PyLink PseudoClient exists.") + def quitClient(self, numeric, reason): + """Quits a PyLink client.""" + if utils.isInternalClient(self.irc, numeric): + self._send(numeric, "QUIT :%s" % reason) + self.removeClient(numeric) + else: + raise LookupError("No such PyLink PseudoClient exists.") -def messageClient(irc, numeric, target, text): - """Sends a PRIVMSG from a PyLink client.""" - if not utils.isInternalClient(irc, numeric): - raise LookupError('No such PyLink PseudoClient exists.') - _send(irc, numeric, 'PRIVMSG %s :%s' % (target, text)) + def messageClient(self, numeric, target, text): + """Sends a PRIVMSG from a PyLink client.""" + if not utils.isInternalClient(self.irc, numeric): + raise LookupError('No such PyLink PseudoClient exists.') + self._send(numeric, 'PRIVMSG %s :%s' % (target, text)) -def noticeClient(irc, numeric, target, text): - """Sends a NOTICE from a PyLink client.""" - if not utils.isInternalClient(irc, numeric): - raise LookupError('No such PyLink PseudoClient exists.') - _send(irc, numeric, 'NOTICE %s :%s' % (target, text)) + def noticeClient(self, numeric, target, text): + """Sends a NOTICE from a PyLink client.""" + if not utils.isInternalClient(self.irc, numeric): + raise LookupError('No such PyLink PseudoClient exists.') + self._send(numeric, 'NOTICE %s :%s' % (target, text)) -def topicClient(irc, numeric, target, text): - """Sends a ROPIC from a PyLink client.""" - if not utils.isInternalClient(irc, numeric): - raise LookupError('No such PyLink PseudoClient exists.') - _send(irc, numeric, 'TOPIC %s :%s' % (target, text)) - irc.channels[target].topic = text - irc.channels[target].topicset = True + def topicClient(self, numeric, target, text): + """Sends a ROPIC from a PyLink client.""" + if not utils.isInternalClient(self.irc, numeric): + raise LookupError('No such PyLink PseudoClient exists.') + self._send(numeric, 'TOPIC %s :%s' % (target, text)) + self.irc.channels[target].topic = text + self.irc.channels[target].topicset = True -### HANDLERS + ### HANDLERS -def handle_privmsg(irc, source, command, args): - """Handles incoming PRIVMSG/NOTICE.""" - # <- :70MAAAAAA PRIVMSG #dev :afasfsa - # <- :70MAAAAAA NOTICE 0ALAAAAAA :afasfsa - target = args[0] - # We use lowercase channels internally, but uppercase UIDs. - if utils.isChannel(target): - target = utils.toLower(irc, target) - return {'target': target, 'text': args[1]} + def handle_privmsg(self, source, command, args): + """Handles incoming PRIVMSG/NOTICE.""" + # <- :70MAAAAAA PRIVMSG #dev :afasfsa + # <- :70MAAAAAA NOTICE 0ALAAAAAA :afasfsa + target = args[0] + # We use lowercase channels internally, but uppercase UIDs. + if utils.isChannel(target): + target = utils.toLower(self.irc, target) + return {'target': target, 'text': args[1]} -handle_notice = handle_privmsg + handle_notice = handle_privmsg -def handle_kill(irc, source, command, args): - """Handles incoming KILLs.""" - killed = args[0] - # Depending on whether the IRCd sends explicit QUIT messages for - # KILLed clients, the user may or may not have automatically been removed. - # If not, we have to assume that KILL = QUIT and remove them ourselves. - data = irc.users.get(killed) - if data: - removeClient(irc, killed) - return {'target': killed, 'text': args[1], 'userdata': data} + def handle_kill(self, source, command, args): + """Handles incoming KILLs.""" + killed = args[0] + # Depending on whether the self.ircd sends explicit QUIT messages for + # KILLed clients, the user may or may not have automatically been removed. + # If not, we have to assume that KILL = QUIT and remove them ourselves. + data = self.irc.users.get(killed) + if data: + self.removeClient(killed) + return {'target': killed, 'text': args[1], 'userdata': data} -def handle_kick(irc, source, command, args): - """Handles incoming KICKs.""" - # :70MAAAAAA KICK #endlessvoid 70MAAAAAA :some reason - channel = utils.toLower(irc, args[0]) - kicked = args[1] - handle_part(irc, kicked, 'KICK', [channel, args[2]]) - return {'channel': channel, 'target': kicked, 'text': args[2]} + def handle_kick(self, source, command, args): + """Handles incoming KICKs.""" + # :70MAAAAAA KICK #endlessvoid 70MAAAAAA :some reason + channel = utils.toLower(self.irc, args[0]) + kicked = args[1] + self.handle_part(kicked, 'KICK', [channel, args[2]]) + return {'channel': channel, 'target': kicked, 'text': args[2]} -def handle_error(irc, numeric, command, args): - """Handles ERROR messages - these mean that our uplink has disconnected us!""" - irc.connected.clear() - raise ProtocolError('Received an ERROR, disconnecting!') + def handle_error(self, numeric, command, args): + """Handles ERROR messages - these mean that our uplink has disconnected us!""" + self.irc.connected.clear() + raise ProtocolError('Received an ERROR, disconnecting!') -def handle_nick(irc, numeric, command, args): - """Handles incoming NICK changes.""" - # <- :70MAAAAAA NICK GL-devel 1434744242 - oldnick = irc.users[numeric].nick - newnick = irc.users[numeric].nick = args[0] - return {'newnick': newnick, 'oldnick': oldnick, 'ts': int(args[1])} + 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] + return {'newnick': newnick, 'oldnick': oldnick, 'ts': int(args[1])} -def handle_quit(irc, numeric, command, args): - """Handles incoming QUITs.""" - # <- :1SRAAGB4T QUIT :Quit: quit message goes here - removeClient(irc, numeric) - return {'text': args[0]} + def handle_quit(self, numeric, command, args): + """Handles incoming QUITs.""" + # <- :1SRAAGB4T QUIT :Quit: quit message goes here + self.removeClient(numeric) + return {'text': args[0]} -def handle_save(irc, numeric, command, args): - """Handles incoming SAVE messages, used to handle nick collisions.""" - # In this below example, the client Derp_ already exists, - # and trying to change someone's nick to it will cause a nick - # collision. On TS6 IRCds, this will simply set the collided user's - # nick to its UID. + def handle_save(self, numeric, command, args): + """Handles incoming SAVE messages, used to handle nick collisions.""" + # In this below example, the client Derp_ already exists, + # and trying to change someone's nick to it will cause a nick + # collision. On TS6 self.ircds, this will simply set the collided user's + # nick to its UID. - # <- :70MAAAAAA PRIVMSG 0AL000001 :nickclient PyLink Derp_ - # -> :0AL000001 NICK Derp_ 1433728673 - # <- :70M SAVE 0AL000001 1433728673 - user = args[0] - oldnick = irc.users[user].nick - irc.users[user].nick = user - return {'target': user, 'ts': int(args[1]), 'oldnick': oldnick} + # <- :70MAAAAAA PRIVMSG 0AL000001 :nickclient PyLink Derp_ + # -> :0AL000001 NICK Derp_ 1433728673 + # <- :70M SAVE 0AL000001 1433728673 + user = args[0] + oldnick = self.irc.users[user].nick + self.irc.users[user].nick = user + return {'target': user, 'ts': int(args[1]), 'oldnick': oldnick} -def handle_squit(irc, numeric, command, args): - """Handles incoming SQUITs (netsplits).""" - # :70M SQUIT 1ML :Server quit by GL!gl@0::1 - split_server = args[0] - affected_users = [] - log.info('(%s) Netsplit on server %s', irc.name, split_server) - # Prevent RuntimeError: dictionary changed size during iteration - old_servers = irc.servers.copy() - 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) - args = handle_squit(irc, sid, 'SQUIT', [sid, "PyLink: Automatically splitting leaf servers of %s" % sid]) - affected_users += args['users'] - for user in irc.servers[split_server].users.copy(): - affected_users.append(user) - log.debug('Removing client %s (%s)', user, irc.users[user].nick) - removeClient(irc, user) - del irc.servers[split_server] - log.debug('(%s) Netsplit affected users: %s', irc.name, affected_users) - return {'target': split_server, 'users': affected_users} + def handle_squit(self, numeric, command, args): + """Handles incoming SQUITs (netsplits).""" + # :70M SQUIT 1ML :Server quit by GL!gl@0::1 + split_server = args[0] + affected_users = [] + log.info('(%s) Netsplit on server %s', self.irc.name, split_server) + # Prevent RuntimeError: dictionary changed size during iteration + old_servers = self.irc.servers.copy() + 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) + args = self.handle_squit(sid, 'SQUIT', [sid, "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) + log.debug('Removing client %s (%s)', user, self.irc.users[user].nick) + self.removeClient(user) + 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} -def handle_mode(irc, numeric, command, args): - """Handles incoming user mode changes. For channel mode changes, - TMODE (TS6/charybdis) and FMODE (InspIRCd) are used instead.""" - # In InspIRCd, MODE is used for setting user modes and - # FMODE is used for channel modes: - # <- :70MAAAAAA MODE 70MAAAAAA -i+xc - target = args[0] - modestrings = args[1:] - changedmodes = utils.parseModes(irc, numeric, modestrings) - utils.applyModes(irc, target, changedmodes) - return {'target': target, 'modes': changedmodes} + def handle_mode(self, numeric, command, args): + """Handles incoming user mode changes. For channel mode changes, + TMODE (TS6/charybdis) and FMODE (Inspself.ircd) are used instead.""" + # In Inspself.ircd, MODE is used for setting user modes and + # FMODE is used for channel modes: + # <- :70MAAAAAA MODE 70MAAAAAA -i+xc + target = args[0] + modestrings = args[1:] + changedmodes = utils.parseModes(self.irc, numeric, modestrings) + utils.applyModes(self.irc, target, changedmodes) + return {'target': target, 'modes': changedmodes} -def handle_topic(irc, numeric, command, args): - """Handles incoming TOPIC changes from clients. For topic bursts, - TB (TS6/charybdis) and FTOPIC (InspIRCd) are used instead.""" - # <- :70MAAAAAA TOPIC #test :test - channel = utils.toLower(irc, args[0]) - topic = args[1] - ts = int(time.time()) - irc.channels[channel].topic = topic - irc.channels[channel].topicset = True - return {'channel': channel, 'setter': numeric, 'ts': ts, 'topic': topic} + def handle_topic(self, numeric, command, args): + """Handles incoming TOPIC changes from clients. For topic bursts, + TB (TS6/charybdis) and FTOPIC (Inspself.ircd) are used instead.""" + # <- :70MAAAAAA TOPIC #test :test + channel = utils.toLower(self.irc, args[0]) + topic = args[1] + ts = int(time.time()) + self.irc.channels[channel].topic = topic + self.irc.channels[channel].topicset = True + return {'channel': channel, 'setter': numeric, 'ts': ts, 'topic': topic} -def handle_part(irc, source, command, args): - """Handles incoming PART commands.""" - channels = utils.toLower(irc, args[0]).split(',') - for channel in channels: - # We should only get PART commands for channels that exist, right?? - irc.channels[channel].removeuser(source) - try: - irc.users[source].channels.discard(channel) - except KeyError: - log.debug("(%s) handle_part: KeyError trying to remove %r from %r's channel list?", irc.name, channel, source) - try: - reason = args[1] - except IndexError: - reason = '' - # Clear empty non-permanent channels. - if not (irc.channels[channel].users or ((irc.cmodes.get('permanent'), None) in irc.channels[channel].modes)): - del irc.channels[channel] - return {'channels': channels, 'text': reason} + def handle_part(self, source, command, args): + """Handles incoming PART commands.""" + channels = utils.toLower(self.irc, 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} diff --git a/utils.py b/utils.py index 69ae101..6a90301 100644 --- a/utils.py +++ b/utils.py @@ -100,9 +100,9 @@ class TS6SIDGenerator(): def msg(irc, target, text, notice=False): if notice: - irc.proto.noticeClient(irc, irc.pseudoclient.uid, target, text) + irc.proto.noticeClient(irc.pseudoclient.uid, target, text) else: - irc.proto.messageClient(irc, irc.pseudoclient.uid, target, text) + irc.proto.messageClient(irc.pseudoclient.uid, target, text) def add_cmd(func, name=None): if name is None: