def handle_privmsg(self, source, command, args): # Convert nicks to UIDs, where they exist. target = self._getNick(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]}
def handle_mode(self, numeric, command, args): # <- :unreal.midnight.vpn MODE #endlessvoid +bb test!*@* *!*@bad.net # <- :unreal.midnight.vpn MODE #endlessvoid +q GL 1444361345 # <- :unreal.midnight.vpn MODE #endlessvoid +ntCo GL 1444361345 # <- :unreal.midnight.vpn MODE #endlessvoid +mntClfo 5 [10t]:5 GL 1444361345 # <- :GL MODE #services +v GL # This seems pretty relatively inconsistent - why do some commands have a TS at the end while others don't? # Answer: the first syntax (MODE sent by SERVER) is used for channel bursts - according to Unreal 3.2 docs, # the last argument should be interpreted as a timestamp ONLY if it is a number and the sender is a server. # Ban bursting does not give any TS, nor do normal users setting modes. SAMODE is special though, it will # send 0 as a TS argument (which should be ignored unless breaking the internal channel TS is desired). # Also, we need to get rid of that extra space following the +f argument. :| if utils.isChannel(args[0]): channel = utils.toLower(self.irc, args[0]) oldobj = self.irc.channels[channel].deepcopy() modes = list(filter(None, args[1:])) # normalize whitespace parsedmodes = utils.parseModes(self.irc, channel, modes) if parsedmodes: utils.applyModes(self.irc, channel, parsedmodes) if numeric in self.irc.servers and args[-1].isdigit(): # Sender is a server AND last arg is number. Perform TS updates. their_ts = int(args[-1]) if their_ts > 0: self.updateTS(channel, their_ts) return {'target': channel, 'modes': parsedmodes, 'oldchan': oldobj} else: log.warning("(%s) received MODE for non-channel target: %r", self.irc.name, args) raise NotImplementedError
def joinclient(irc, source, args): """<target> <channel1>,[<channel2>], etc. Admin-only. Joins <target>, the nick of a PyLink client, to a comma-separated list of channels.""" utils.checkAuthenticated(irc, source, allowOper=False) try: nick = args[0] clist = args[1].split(',') if not clist: raise IndexError except IndexError: irc.reply( "Error: Not enough arguments. Needs 2: nick, comma separated list of channels." ) return u = utils.nickToUid(irc, nick) if not utils.isManipulatableClient(irc, u): irc.reply( "Error: Cannot force join a protected PyLink services client.") return for channel in clist: if not utils.isChannel(channel): irc.reply("Error: Invalid channel name %r." % channel) return irc.proto.joinClient(u, channel) irc.callHooks([ u, 'PYLINK_BOTSPLUGIN_JOIN', { 'channel': channel, 'users': [u], 'modes': irc.channels[channel].modes, 'parse_as': 'JOIN' } ])
def kick(irc, source, args): """<source> <channel> <user> [<reason>] Admin-only. Kicks <user> from <channel> via <source>, where <source> is the nick of a PyLink client.""" utils.checkAuthenticated(irc, source, allowOper=False) try: nick = args[0] channel = args[1] target = args[2] reason = ' '.join(args[3:]) except IndexError: irc.reply( "Error: Not enough arguments. Needs 3-4: source nick, channel, target, reason (optional)." ) return u = utils.nickToUid(irc, nick) or nick targetu = utils.nickToUid(irc, target) if not utils.isChannel(channel): irc.reply("Error: Invalid channel name %r." % channel) return if utils.isInternalServer(irc, u): irc.proto.kickServer(u, channel, targetu, reason) else: irc.proto.kickClient(u, channel, targetu, reason) irc.callHooks([ u, 'PYLINK_BOTSPLUGIN_KICK', { 'channel': channel, 'target': targetu, 'text': reason, 'parse_as': 'KICK' } ])
def msg(irc, source, args): """<source> <target> <text> Admin-only. Sends message <text> from <source>, where <source> is the nick of a PyLink client.""" utils.checkAuthenticated(irc, source, allowOper=False) try: msgsource, target, text = args[0], args[1], ' '.join(args[2:]) except IndexError: irc.reply( 'Error: Not enough arguments. Needs 3: source nick, target, text.') return sourceuid = utils.nickToUid(irc, msgsource) if not sourceuid: irc.reply('Error: Unknown user %r.' % msgsource) return if not utils.isChannel(target): real_target = utils.nickToUid(irc, target) if real_target is None: irc.reply('Error: Unknown user %r.' % target) return else: real_target = target if not text: irc.reply('Error: No text given.') return irc.proto.messageClient(sourceuid, real_target, text) irc.callHooks([ sourceuid, 'PYLINK_BOTSPLUGIN_MSG', { 'target': real_target, 'text': text, 'parse_as': 'PRIVMSG' } ])
def identify(irc, source, args): """<username> <password> Logs in to PyLink using the configured administrator account.""" if utils.isChannel(irc.called_by): irc.reply('Error: This command must be sent in private. ' '(Would you really type a password inside a channel?)') return try: username, password = args[0], args[1] except IndexError: irc.msg(source, 'Error: Not enough arguments.') return # Usernames are case-insensitive, passwords are NOT. if username.lower() == irc.conf['login']['user'].lower() and password == irc.conf['login']['password']: realuser = irc.conf['login']['user'] irc.users[source].identified = realuser irc.msg(source, 'Successfully logged in as %s.' % realuser) log.info("(%s) Successful login to %r by %s", irc.name, username, utils.getHostmask(irc, source)) else: irc.msg(source, 'Error: Incorrect credentials.') u = irc.users[source] log.warning("(%s) Failed login to %r from %s", irc.name, username, utils.getHostmask(irc, source))
def part(irc, source, args): """<target> <channel1>,[<channel2>],... [<reason>] Admin-only. Parts <target>, the nick of a PyLink client, from a comma-separated list of channels.""" utils.checkAuthenticated(irc, source, allowOper=False) try: nick = args[0] clist = args[1].split(',') reason = ' '.join(args[2:]) except IndexError: irc.reply( "Error: Not enough arguments. Needs 2: nick, comma separated list of channels." ) return u = utils.nickToUid(irc, nick) if not utils.isManipulatableClient(irc, u): irc.reply( "Error: Cannot force part a protected PyLink services client.") return for channel in clist: if not utils.isChannel(channel): irc.reply("Error: Invalid channel name %r." % channel) return irc.proto.partClient(u, channel, reason) irc.callHooks([ u, 'PYLINK_BOTSPLUGIN_PART', { 'channels': clist, 'text': reason, 'parse_as': 'PART' } ])
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 _getNick(self, target): """Converts a nick argument to its matching UID. This differs from utils.nickToUid() in that it returns the original text instead of None, if no matching nick is found.""" target = utils.nickToUid(self.irc, target) or target if target not in self.irc.users and not utils.isChannel(target): log.debug( "(%s) Possible desync? Got command target %s, who " "isn't in our user list!", self.irc.name, target) return target
def knockClient(self, numeric, target, text): """Sends a KNOCK from a PyLink client.""" # KNOCKs in UnrealIRCd are actually just specially formatted NOTICEs, # sent to all ops in a channel. # <- :unreal.midnight.vpn NOTICE @#test :[Knock] by GL|!gl@hidden-1C620195 (test) assert utils.isChannel(target), "Can only knock on channels!" sender = utils.clientToServer(self.irc, numeric) s = '[Knock] by %s (%s)' % (utils.getHostmask(self.irc, numeric), text) self._send(sender, 'NOTICE @%s :%s' % (target, s))
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]}
def handle_fantasy(irc, source, command, args): """Fantasy command handler.""" try: # First, try to fetch the config-defined prefix. prefixes = [irc.botdata["prefix"]] except KeyError: # Config option is missing. prefixes = [] if irc.botdata.get("respondtonick"): # If responding to nick is enabled, add variations of the current nick # to the prefix list: "<nick>," and "<nick>:" nick = irc.pseudoclient.nick prefixes += [nick + ',', nick + ':'] if not prefixes: # We finished with an empty prefixes list, meaning fantasy is misconfigured! log.warning( "(%s) Fantasy prefix was not set in configuration - " "fantasy commands will not work!", irc.name) return channel = args['target'] text = args['text'] for prefix in prefixes: # Cycle through the prefixes list we finished with. # The following conditions must be met for an incoming message for # fantasy to trigger: # 1) The message target is a channel. # 2) The message starts with one of our fantasy prefixes. # 3) The main PyLink client is in the channel where the command was # called. # 4) The sender is NOT a PyLink client (this prevents infinite # message loops). if utils.isChannel(channel) and text.startswith(prefix) and \ irc.pseudoclient.uid in irc.channels[channel].users and not \ utils.isInternalClient(irc, source): # Cut off the length of the prefix from the text. text = text[len(prefix):] # Set the "place last command was called in" variable to the # channel in question, so that replies from fantasy-supporting # plugins get forwarded to it. irc.called_by = channel # Finally, call the bot command and break. irc.callCommand(source, text) break
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 _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 testIsChannel(self): self.assertFalse(utils.isChannel('')) self.assertFalse(utils.isChannel('lol')) self.assertTrue(utils.isChannel('#channel')) self.assertTrue(utils.isChannel('##ABCD'))