def showuser(irc, source, args): """<user> Shows information about <user>.""" try: target = args[0] except IndexError: irc.reply("Error: Not enough arguments. Needs 1: nick.") return u = utils.nickToUid(irc, target) or target # Only show private info if the person is calling 'showuser' on themselves, # or is an oper. verbose = utils.isOper(irc, source) or u == source if u not in irc.users: irc.reply('Error: Unknown user %r.' % target) return f = lambda s: irc.msg(source, s) userobj = irc.users[u] f('Information on user \x02%s\x02 (%s@%s): %s' % (userobj.nick, userobj.ident, userobj.host, userobj.realname)) sid = utils.clientToServer(irc, u) serverobj = irc.servers[sid] ts = userobj.ts f('\x02Home server\x02: %s (%s); \x02Signon time:\x02 %s (%s)' % \ (serverobj.name, sid, ctime(float(ts)), ts)) if verbose: f('\x02Protocol UID\x02: %s; \x02PyLink identification\x02: %s' % \ (u, userobj.identified)) f('\x02User modes\x02: %s' % utils.joinModes(userobj.modes)) f('\x02Real host\x02: %s; \x02IP\x02: %s; \x02Away status\x02: %s' % \ (userobj.realhost, userobj.ip, userobj.away or '\x1D(not set)\x1D')) f('\x02Channels\x02: %s' % (' '.join(userobj.channels).strip() or '\x1D(none)\x1D'))
def joinClient(self, client, channel): """Joins a PyLink 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(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. <users> is a list of (prefix mode, UID) pairs: Example uses: sjoinServer('100', '#test', [('', '100AAABBC'), ('qo', 100AAABBB'), ('h', '100AAADDD')]) sjoinServer(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 self.updateTS(channel, ts) log.debug("sending SJOIN to %s%s with ts %s (that's %r)", channel, self.irc.name, ts, time.strftime("%c", time.localtime(ts))) # Strip out list-modes, they shouldn't ever be sent in FJOIN (protocol rules). modes = [ m for m in self.irc.channels[channel].modes if m[0] not in self.irc.cmodes['*A'] ] uids = [] changedmodes = [] namelist = [] # We take <users> 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 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): """Spawns a client with nick <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 self.irc.sid if not utils.isInternalServer(self.irc, server): raise ValueError( 'Server %r is not a PyLink internal PseudoServer!' % server) # Create an UIDGenerator instance for every SID, so that each gets # distinct values. uid = self.uidgen.setdefault(server, utils.TS6UIDGenerator(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, manipulatable=manipulatable) 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 _sendModes(self, numeric, target, modes, ts=None): """Internal function to send mode changes from a PyLink client/server.""" utils.applyModes(self.irc, target, modes) modes = list(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 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 showchan(irc, source, args): """<channel> Shows information about <channel>.""" try: channel = utils.toLower(irc, args[0]) except IndexError: irc.reply("Error: Not enough arguments. Needs 1: channel.") return if channel not in irc.channels: irc.reply('Error: Unknown channel %r.' % channel) return f = lambda s: irc.msg(source, s) c = irc.channels[channel] # Only show verbose info if caller is oper or is in the target channel. verbose = source in c.users or utils.isOper(irc, source) secret = ('s', None) in c.modes if secret and not verbose: # Hide secret channels from normal users. irc.msg(source, 'Error: Unknown channel %r.' % channel) return nicks = [irc.users[u].nick for u in c.users] pmodes = ('owner', 'admin', 'op', 'halfop', 'voice') f('Information on channel \x02%s\x02:' % channel) f('\x02Channel topic\x02: %s' % c.topic) f('\x02Channel creation time\x02: %s (%s)' % (ctime(c.ts), c.ts)) # Show only modes that aren't list-style modes. modes = utils.joinModes( [m for m in c.modes if m[0] not in irc.cmodes['*A']]) f('\x02Channel modes\x02: %s' % modes) if verbose: nicklist = [] # Iterate over the user list, sorted by nick. for user, nick in sorted(zip(c.users, nicks), key=lambda userpair: userpair[1].lower()): prefixmodes = [ irc.prefixmodes.get(irc.cmodes.get(pmode, ''), '') for pmode in pmodes if user in c.prefixmodes[pmode + 's'] ] nicklist.append(''.join(prefixmodes) + nick) while nicklist[:20]: # 20 nicks per line to prevent message cutoff. f('\x02User list\x02: %s' % ' '.join(nicklist[:20])) nicklist = nicklist[20:]
def _sendModes(self, numeric, target, modes, ts=None): """Internal function to send mode changes 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', 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. 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))
def _sendModes(self, numeric, target, modes, ts=None): """Internal function to send mode changes from a PyLink client/server.""" # <- :unreal.midnight.vpn MODE #endlessvoid +ntCo GL 1444361345 utils.applyModes(self.irc, target, modes) joinedmodes = utils.joinModes(modes) if utils.isChannel(target): # The MODE command is used for channel mode changes only ts = ts or self.irc.channels[utils.toLower(self.irc, target)].ts self._send(numeric, 'MODE %s %s %s' % (target, joinedmodes, ts)) else: # For user modes, the only way to set modes (for non-U:Lined servers) # is through UMODE2, which sets the modes on the caller. # U:Lines can use SVSMODE/SVS2MODE, but I won't expect people to # U:Line a PyLink daemon... if not utils.isInternalClient(self.irc, target): raise ProtocolError( 'Cannot force mode change on external clients!') self._send(target, 'UMODE2 %s' % joinedmodes)
def testJoinModes(self): res = utils.joinModes({('+l', '50'), ('+n', None), ('+t', None)}) # Sets are orderless, so the end mode could be scrambled in a number of ways. # Basically, we're looking for a string that looks like '+ntl 50' or '+lnt 50'. possible = ['+%s 50' % ''.join(x) for x in itertools.permutations('lnt', 3)] self.assertIn(res, possible) # Without any arguments, make sure there is no trailing space. self.assertEqual(utils.joinModes({('+t', None)}), '+t') # The +/- in the mode is not required; if it doesn't exist, assume we're # adding modes always. self.assertEqual(utils.joinModes([('t', None), ('n', None)]), '+tn') # An empty query should return just '+' self.assertEqual(utils.joinModes(set()), '+') # More complex query now with both + and - modes being set res = utils.joinModes([('+l', '50'), ('-n', None)]) self.assertEqual(res, '+l-n 50') # If one modepair in the list lacks a +/- prefix, just follow the # previous one's. res = utils.joinModes([('+l', '50'), ('-n', None), ('m', None)]) self.assertEqual(res, '+l-nm 50') res = utils.joinModes([('+l', '50'), ('m', None)]) self.assertEqual(res, '+lm 50') res = utils.joinModes([('l', '50'), ('-m', None)]) self.assertEqual(res, '+l-m 50') # Rarely in real life will we get a mode string this complex. # Let's make sure it works, just in case. res = utils.joinModes([('-o', '9PYAAAAAA'), ('+l', '50'), ('-n', None), ('-m', None), ('+k', 'hello'), ('+b', '*!*@*.badisp.net')]) self.assertEqual(res, '-o+l-nm+kb 9PYAAAAAA 50 hello *!*@*.badisp.net')
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): """Spawns a client with nick <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 self.irc.sid if not utils.isInternalServer(self.irc, server): raise ValueError( 'Server %r is not a PyLink internal PseudoServer!' % server) # Unreal 3.4 uses TS6-style UIDs. They don't start from AAAAAA like other IRCd's # do, but we can do that fine... uid = self.uidgen.setdefault(server, utils.TS6UIDGenerator(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, manipulatable=manipulatable) utils.applyModes(self.irc, uid, modes) self.irc.servers[server].users.add(uid) # UnrealIRCd requires encoding the IP by first packing it into a binary format, # and then encoding the binary with Base64. if ip == '0.0.0.0': # Dummy IP (for services, etc.) use a single *. encoded_ip = '*' else: try: # Try encoding as IPv4 first. binary_ip = socket.inet_pton(socket.AF_INET, ip) except OSError: try: # That failed, try IPv6 next. binary_ip = socket.inet_pton(socket.AF_INET6, ip) except OSError: raise ValueError("Invalid IPv4 or IPv6 address %r." % ip) # Encode in Base64. encoded_ip = codecs.encode(binary_ip, "base64") # Now, strip the trailing \n and decode into a string again. encoded_ip = encoded_ip.strip().decode() # <- :001 UID GL 0 1441306929 gl localhost 0018S7901 0 +iowx * midnight-1C620195 fwAAAQ== :realname self._send( server, "UID {nick} 0 {ts} {ident} {realhost} {uid} 0 {modes} " "{host} * {ip} :{realname}".format(ts=ts, host=host, nick=nick, ident=ident, uid=uid, modes=raw_modes, realname=realname, realhost=realhost, ip=encoded_ip)) # Force the virtual hostname to show correctly by running SETHOST on # the user. Otherwise, Unreal will show the real host of the person # instead, which is probably not what we want. self.updateClient(uid, 'HOST', host) return u
def handle_whois(irc, source, command, args): """Handle WHOIS queries, for IRCds that send them across servers (charybdis, UnrealIRCd; NOT InspIRCd).""" target = args['target'] user = irc.users.get(target) if user is None: log.warning('(%s) Got a WHOIS request for %r from %r, but the target ' 'doesn\'t exist in irc.users!', irc.name, target, source) return f = irc.proto.numericServer server = utils.clientToServer(irc, target) or irc.sid nick = user.nick sourceisOper = ('o', None) in irc.users[source].modes # https://www.alien.net.au/irc/irc2numerics.html # 311: sends nick!user@host information 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: # Here, we'll want to hide secret/private channels from non-opers # who are not in them. c = irc.channels[chan] if ((irc.cmodes.get('secret'), None) in c.modes or \ (irc.cmodes.get('private'), None) in c.modes) \ and not (sourceisOper or source in c.users): continue # Show prefix modes like a regular IRCd does. for prefixmode, prefixchar in irc.prefixmodes.items(): modename = [mname for mname, char in irc.cmodes.items() if char == prefixmode] if modename and target in c.prefixmodes[modename[0]+'s']: chan = prefixchar + chan public_chans.append(chan) if 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(server, 312, source, "%s %s :%s" % (nick, irc.servers[server].name, irc.servers[server].desc)) # 313: sends a string denoting the target's operator privilege, # only if they have umode +o. if ('o', None) in user.modes: if hasattr(user, 'opertype'): opertype = user.opertype else: opertype = "IRC Operator" # Let's be gramatically correct. n = 'n' if opertype[0].lower() in 'aeiou' else '' 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(server, 378, source, "%s :is connecting from %s@%s %s" % (nick, user.ident, user.realhost, user.ip)) 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(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. try: res = func(irc, target) if res: num, text = res 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(server, 318, source, "%s :End of /WHOIS list" % nick)
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. <users> is a list of (prefix mode, UID) pairs: Example uses: sjoinServer('100', '#test', [('', '100AAABBC'), ('o', 100AAABBB'), ('v', '100AAADDD')]) sjoinServer(self.irc.sid, '#test', [('o', self.irc.pseudoclient.uid)]) """ # 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. # 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 self.updateTS(channel, ts) log.debug("(%s) sending SJOIN to %s with ts %s (that's %r)", self.irc.name, channel, ts, time.strftime("%c", time.localtime(ts))) modes = [ m for m in self.irc.channels[channel].modes if m[0] not in self.irc.cmodes['*A'] ] changedmodes = [] while users[:10]: uids = [] namelist = [] # We take <users> 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)