import time import string from pylinkirc import utils, conf from pylinkirc.log import log from pylinkirc.classes import Protocol, IrcUser, IrcServer class ClientbotWrapperProtocol(Protocol): def __init__(self, irc): super().__init__(irc) # This is just a fallback. Actual casemapping is fetched by handle_005() self.casemapping = 'ascii' self.caps = {} # Initialize counter-based pseudo UID generators self.uidgen = utils.PUIDGenerator('PUID') self.sidgen = utils.PUIDGenerator('PSID') def _expandPUID(self, uid): """ Returns the real nick for the given PUID. """ if uid in self.irc.users: nick = self.irc.users[uid].nick log.debug('(%s) Mangling target PUID %s to nick %s', self.irc.name, uid, nick) return nick return uid def _formatText(self, source, text): """ Formats text with the given sender as a prefix. """ if self.irc.pseudoclient and source == self.irc.pseudoclient.uid: return text else: # TODO: configurable formatting return '<%s> %s' % (self.irc.getFriendlyName(source), text) def connect(self): """Initializes a connection to a server.""" ts = self.irc.start_ts f = self.irc.send # TODO: fetch channel/user/prefix modes from RPL_ISUPPORT. #self.irc.prefixmodes = {'q': '~', 'a': '&', 'o': '@', 'h': '%', 'v': '+'} # HACK: Replace the SID from the config options with our own. old_sid = self.irc.sid self.irc.sid = sid = self.sidgen.next_uid() self.irc.servers[sid] = self.irc.servers[old_sid] del self.irc.servers[old_sid] sendpass = self.irc.serverdata.get("sendpass") if sendpass: f('PASS %s' % sendpass) # This is a really gross hack to get the defined NICK/IDENT/HOST/GECOS. # But this connection stuff is done before any of the spawnClient stuff in # services_support fires. f('NICK %s' % (self.irc.serverdata.get('pylink_nick') or conf.conf["bot"].get("nick", "PyLink"))) ident = self.irc.serverdata.get('pylink_ident') or conf.conf["bot"].get("ident", "pylink") f('USER %s 8 * %s' % (ident, # TODO: per net realnames or hostnames aren't implemented yet. conf.conf["bot"].get("realname", "PyLink Clientbot"))) 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, manipulatable=False): """ STUB: Pretends to spawn a new client with a subset of the given options. """ server = server or self.irc.sid uid = self.uidgen.next_uid() ts = ts or int(time.time()) realname = realname or '' log.debug('(%s) spawnClient stub called, saving nick %s as PUID %s', self.irc.name, nick, uid) u = self.irc.users[uid] = IrcUser(nick, ts, uid, ident=ident, host=host, realname=realname, manipulatable=manipulatable) log.debug('(%s) self.irc.users: %s', self.irc.name, self.irc.users) self.irc.servers[server].users.add(uid) return u def spawnServer(self, name, sid=None, uplink=None, desc=None, endburst_delay=0): """ STUB: Pretends to spawn a new server with a subset of the given options. """ name = name.lower() sid = self.sidgen.next_sid() self.irc.servers[sid] = IrcServer(uplink, name) return sid def invite(self, client, target, channel): """Invites a user to a channel.""" self.irc.send('INVITE %s %s' % (self.irc.getFriendlyName(target), channel)) def join(self, client, channel): """STUB: Joins a user to a channel.""" channel = self.irc.toLower(channel) self.irc.channels[channel].users.add(client) self.irc.users[client].channels.add(channel) # Only joins for the main PyLink client are actually forwarded. Others are ignored. if self.irc.pseudoclient and client == self.irc.pseudoclient.uid: self.irc.send('JOIN %s' % channel) else: log.debug('(%s) join: faking JOIN of client %s/%s to %s', self.irc.name, client, self.irc.getFriendlyName(client), channel) def kick(self, source, channel, target, reason=''): """Sends channel kicks.""" # TODO: handle kick failures and send rejoin hooks for the target reason = self._formatText(source, reason) self.irc.send('KICK %s %s :%s' % (channel, self._expandPUID(target), reason)) # Don't update our state here: wait for the IRCd to send an acknowledgement instead. def message(self, source, target, text, notice=False): """Sends messages to the target.""" command = 'NOTICE' if notice else 'PRIVMSG' target = self._expandPUID(target) self.irc.send('%s %s :%s' % (command, target, self._formatText(source, text))) def nick(self, source, newnick): """STUB: Sends NICK changes.""" if source == self.irc.pseudoclient.uid: self.irc.send('NICK :%s' % (channel, self._expandPUID(target), reason)) self.irc.users[source].nick = newnick def notice(self, source, target, text): """Sends notices to the target.""" # Wrap around message(), which does all the text formatting for us. self.message(source, target, text, notice=True) def ping(self, source=None, target=None): """ Sends PING to the uplink. """ if self.irc.uplink: self.irc.send('PING %s' % self.irc.getFriendlyName(self.irc.uplink)) def part(self, source, channel, reason=''): """STUB: Parts a user from a channel.""" self.irc.channels[channel].removeuser(source) self.irc.users[source].channels.discard(channel) # Only parts for the main PyLink client are actually forwarded. Others are ignored. if self.irc.pseudoclient and source == self.irc.pseudoclient.uid: self.irc.send('PART %s :%s' % (channel, reason)) def quit(self, source, reason): """STUB: Quits a client.""" self.removeClient(source) def sjoin(self, server, channel, users, ts=None, modes=set()): """STUB: bursts joins from a server.""" # This stub only updates the state internally with the users # given. modes and TS are currently ignored. puids = {u[-1] for u in users} for user in puids: self.irc.users[user].channels.add(channel) self.irc.channels[channel].users |= puids def squit(self, source, target, text): """STUB: SQUITs a server.""" # What this actually does is just handle the SQUIT internally: i.e. # Removing pseudoclients and pseudoservers. self._squit(source, target, text) def _stub(self, *args): """Stub outgoing command function (does nothing).""" return kill = away = mode = topic = topicBurst = knock = updateClient = numeric = _stub def updateClient(self, target, field, text): """Updates the known ident, host, or realname of a client.""" if field == 'IDENT': self.irc.users[target].ident = text elif field == 'HOST': self.irc.users[target].host = text elif field in ('REALNAME', 'GECOS'): self.irc.users[target].realname = text else: raise NotImplementedError def handle_events(self, data): """Event handler for the RFC1459/2812 (clientbot) protocol.""" data = data.split(" ") try: args = self.parsePrefixedArgs(data) sender = args[0] command = args[1] args = args[2:] except IndexError: # Raw command without an explicit sender; assume it's being sent by our uplink. args = self.parseArgs(data) idsource = sender = self.irc.uplink command = args[0] args = args[1:] else: # PyLink as a services framework expects UIDs and SIDs for everythiung. Since we connect # as a bot here, there's no explicit user introduction, so we're going to generate # pseudo-uids and pseudo-sids as we see prefixes. if '!' not in sender: # Sender is a server name. idsource = self._getSid(sender) if idsource not in self.irc.servers: idsource = self.spawnServer(sender) else: # Sender is a nick!user@host prefix. Split it into its relevant parts. nick, ident, host = utils.splitHostmask(sender) idsource = self.irc.nickToUid(nick) if not idsource: idsource = self.spawnClient(nick, ident, host, server=self.irc.uplink).uid try: func = getattr(self, 'handle_'+command.lower()) except AttributeError: # unhandled command pass else: parsed_args = func(idsource, command, args) if parsed_args is not None: return [idsource, command, parsed_args] def handle_001(self, source, command, args): """ Handles 001 / RPL_WELCOME. """ # enumerate our uplink self.irc.uplink = source def handle_005(self, source, command, args): """ Handles 005 / RPL_ISUPPORT. """ self.caps.update(self.parseCapabilities(args[1:-1])) log.debug('(%s) handle_005: self.caps is %s', self.irc.name, self.caps) if 'CHANMODES' in self.caps: self.irc.cmodes['*A'], self.irc.cmodes['*B'], self.irc.cmodes['*C'], self.irc.cmodes['*D'] = \ self.caps['CHANMODES'].split(',') log.debug('(%s) handle_005: cmodes: %s', self.irc.name, self.irc.cmodes) if 'USERMODES' in self.caps: self.irc.umodes['*A'], self.irc.umodes['*B'], self.irc.umodes['*C'], self.irc.umodes['*D'] = \ self.caps['USERMODES'].split(',') log.debug('(%s) handle_005: umodes: %s', self.irc.name, self.irc.umodes) self.casemapping = self.caps.get('CASEMAPPING', self.casemapping) log.debug('(%s) handle_005: casemapping set to %s', self.irc.name, self.casemapping) if 'PREFIX' in self.caps: self.irc.prefixmodes = self.parsePrefixes(self.caps['PREFIX']) log.debug('(%s) handle_005: prefix modes set to %s', self.irc.name, self.irc.prefixmodes) if not self.irc.connected.is_set(): self.irc.connected.set() # Run autoperform commands. for line in self.irc.serverdata.get("autoperform", []): self.irc.send(line) return {'parse_as': 'ENDBURST'} def handle_353(self, source, command, args): """ Handles 353 / RPL_NAMREPLY. """ # <- :charybdis.midnight.vpn 353 ice = #test :ice @GL # Mark "@"-type channels as secret automatically, per RFC2812. channel = self.irc.toLower(args[2]) if args[1] == '@': self.irc.applyModes(channel, [('+s', None)]) names = set() modes = set() prefix_to_mode = {v:k for k, v in self.irc.prefixmodes.items()} prefixes = ''.join(self.irc.prefixmodes.values()) for name in args[-1].split(): nick = name.lstrip(prefixes) # Get the PUID for the given nick. If one doesn't exist, spawn # a new virtual user. TODO: wait for WHO responses for each nick before # spawning in order to get a real ident/host. idsource = self.irc.nickToUid(nick) or self.spawnClient(nick, server=self.irc.uplink).uid # Queue these virtual users to be joined if they're not already in the channel. if idsource not in self.irc.channels[channel].users: names.add(idsource) self.irc.users[idsource].channels.add(channel) # Process prefix modes for char in name: if char in self.irc.prefixmodes.values(): modes.add(('+' + prefix_to_mode[char], idsource)) else: break # Statekeeping: make sure the channel's user list is updated! self.irc.channels[channel].users |= names self.irc.applyModes(channel, modes) log.debug('(%s) handle_353: adding users %s to %s', self.irc.name, names, channel) log.debug('(%s) handle_353: adding modes %s to %s', self.irc.name, modes, channel) return {'channel': channel, 'users': names, 'modes': self.irc.channels[channel].modes, 'parse_as': "JOIN"} def handle_join(self, source, command, args): """ Handles incoming JOINs. """ # <- :GL|!~GL@127.0.0.1 JOIN #whatever channel = self.irc.toLower(args[0]) self.join(source, channel) return {'channel': channel, 'users': [source], 'modes': self.irc.channels[channel].modes} def handle_kick(self, source, command, args): """ Handles incoming KICKs. """ # <- :GL!~gl@127.0.0.1 KICK #whatever GL| :xd channel = self.irc.toLower(args[0]) target = self.irc.nickToUid(args[1]) try: reason = args[2] except IndexError: reason = '' self.part(target, channel, reason) return {'channel': channel, 'target': target, 'text': reason} def handle_nick(self, source, command, args): # <- :GL|!~GL@127.0.0.1 NICK :GL_ oldnick = self.irc.users[source].nick self.nick(source, args[0]) return {'newnick': args[0], 'oldnick': oldnick} def handle_part(self, source, command, args): """ Handles incoming PARTs. """ # <- :GL|!~GL@127.0.0.1 PART #whatever channels = list(map(self.irc.toLower, args[0].split(','))) try: reason = args[1] except IndexError: reason = '' for channel in channels: self.part(source, channel, reason) return {'channels': channels, 'text': reason} def handle_ping(self, source, command, args): """ Handles incoming PING requests. """ self.irc.send('PONG :%s' % args[0]) def handle_pong(self, source, command, args): """ Handles incoming PONG. """ if source == self.irc.uplink: self.irc.lastping = time.time() def handle_privmsg(self, source, command, args): """Handles incoming PRIVMSG/NOTICE.""" # <- :sender PRIVMSG #dev :afasfsa # <- :sender NOTICE somenick :afasfsa target = args[0] # We use lowercase channels internally. if utils.isChannel(target): target = self.irc.toLower(target) else: target = self._getUid(target) return {'target': target, 'text': args[1]} def handle_quit(self, source, command, args): """Handles incoming QUITs.""" self.quit(source, args[0]) return {'text': args[0]} handle_notice = handle_privmsg Class = ClientbotWrapperProtocol