def handle_privmsg(self, source, command, args): # Convert nicks to UIDs, where they exist. target = self._getUid(args[0]) # We use lowercase channels internally, but uppercase UIDs. if utils.isChannel(target): target = self.irc.toLower(target) return {'target': target, 'text': args[1]}
def identify(irc, source, args): """<username> <password> Logs in to PyLink using the configured administrator account.""" if utils.isChannel(irc.called_in): 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.reply('Error: Not enough arguments.') return # Process new-style accounts. if login.checkLogin(username, password): _login(irc, source, username) return # Process legacy logins (login:user). if username.lower() == conf.conf['login'].get('user', '').lower() and password == conf.conf['login'].get('password'): realuser = conf.conf['login']['user'] _login(irc, source, realuser) else: # Username not found. _loginfail(irc, source, username)
def mode(self, source, channel, modes, ts=None): """Sends channel MODE changes.""" if utils.isChannel(channel): extmodes = [] # Re-parse all channel modes locally to eliminate anything invalid, such as unbanning # things that were never banned. This prevents the bot from getting caught in a loop # with IRCd MODE acknowledgements. # FIXME: More related safety checks should be added for this. log.debug('(%s) mode: re-parsing modes %s', self.irc.name, modes) joined_modes = self.irc.joinModes(modes) for modepair in self.irc.parseModes(channel, joined_modes): log.debug('(%s) mode: checking if %s a prefix mode: %s', self.irc.name, modepair, self.irc.prefixmodes) if modepair[0][-1] in self.irc.prefixmodes: if self.irc.isInternalClient(modepair[1]): # Ignore prefix modes for virtual internal clients. log.debug( '(%s) mode: skipping virtual client prefixmode change %s', self.irc.name, modepair) continue else: # For other clients, change the mode argument to nick instead of PUID. nick = self.irc.getFriendlyName(modepair[1]) log.debug('(%s) mode: coersing mode %s argument to %s', self.irc.name, modepair, nick) modepair = (modepair[0], nick) extmodes.append(modepair) log.debug('(%s) mode: filtered modes for %s: %s', self.irc.name, channel, extmodes) if extmodes: self.irc.send('MODE %s %s' % (channel, self.irc.joinModes(extmodes)))
def mode(self, numeric, target, modes, ts=None): """ Sends mode changes from a PyLink client/server. The mode list should be a list of (mode, arg) tuples, i.e. the format of utils.parseModes() output. """ # <- :unreal.midnight.vpn MODE #test +ntCo GL 1444361345 if (not self.irc.isInternalClient(numeric)) and \ (not self.irc.isInternalServer(numeric)): raise LookupError('No such PyLink client/server exists.') self.irc.applyModes(target, modes) joinedmodes = self.irc.joinModes(modes) if utils.isChannel(target): # The MODE command is used for channel mode changes only ts = ts or self.irc.channels[self.irc.toLower(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 self.irc.isInternalClient(target): raise ProtocolError( 'Cannot force mode change on external clients!') self._send(target, 'UMODE2 %s' % joinedmodes)
def handle_mode(self, source, command, args): """Handles MODE changes.""" # <- :[email protected] MODE #dev +v ice # <- :ice MODE ice :+Zi target = args[0] if utils.isChannel(target): target = self.irc.toLower(target) oldobj = self.irc.channels[target].deepcopy() else: target = self.irc.nickToUid(target) oldobj = None modes = args[1:] changedmodes = self.irc.parseModes(target, modes) self.irc.applyModes(target, changedmodes) if self.irc.isInternalClient(target): log.debug( '(%s) Suppressing MODE change hook for internal client %s', self.irc.name, target) return if changedmodes: # Prevent infinite loops: don't send MODE hooks if the sender is US. # Note: this is not the only check in Clientbot to prevent mode loops: if our nick # somehow gets desynced, this may not catch everything it's supposed to. if (self.irc.pseudoclient and source != self.irc.pseudoclient.uid ) or not self.irc.pseudoclient: return { 'target': target, 'modes': changedmodes, 'channeldata': oldobj }
def mode(self, numeric, target, modes, ts=None): """Sends mode changes from a PyLink client/server.""" # c <- :0UYAAAAAA TMODE 0 #a +o 0T4AAAAAC # u <- :0UYAAAAAA MODE 0UYAAAAAA :-Facdefklnou if (not self.irc.isInternalClient(numeric)) and \ (not self.irc.isInternalServer(numeric)): raise LookupError('No such PyLink client/server exists.') self.irc.applyModes(target, modes) modes = list(modes) if utils.isChannel(target): ts = ts or self.irc.channels[self.irc.toLower(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[:10]: # Seriously, though. If you send more than 10 mode parameters in # a line, charybdis will silently REJECT the entire command! joinedmodes = self.irc.joinModes(modes = [m for m in modes[:10] if m[0] not in self.irc.cmodes['*A']]) modes = modes[10:] self._send(numeric, 'TMODE %s %s %s' % (ts, target, joinedmodes)) else: joinedmodes = self.irc.joinModes(modes) self._send(numeric, 'MODE %s %s' % (target, joinedmodes))
def handle_privmsg(self, source, command, args): """Handles incoming PRIVMSG/NOTICE.""" # TS6: # <- :70MAAAAAA PRIVMSG #dev :afasfsa # <- :70MAAAAAA NOTICE 0ALAAAAAA :afasfsa # P10: # <- ABAAA P AyAAA :privmsg text # <- ABAAA O AyAAA :notice text target = self._getUid(args[0]) # Coerse =#channel from Charybdis op moderated +z to @#channel. if target.startswith('='): target = '@' + target[1:] # We use lowercase channels internally, but uppercase UIDs. # Strip the target of leading prefix modes (for targets like @#channel) # before checking whether it's actually a channel. split_channel = target.split('#', 1) if len(split_channel) >= 2 and utils.isChannel('#' + split_channel[1]): # Note: don't mess with the case of the channel prefix, or ~#channel # messages will break on RFC1459 casemapping networks (it becomes ^#channel # instead). target = '#'.join( (split_channel[0], self.irc.toLower(split_channel[1]))) log.debug('(%s) Normalizing channel target %s to %s', self.irc.name, args[0], target) return {'target': target, 'text': args[1]}
def getChannelPair(irc, source, chanpair, perm=None): """ Fetches the network and channel given a channel pair, also optionally checking the caller's permissions. """ log.debug('(%s) Looking up chanpair %s', irc.name, chanpair) try: network, channel = chanpair.split('#') except ValueError: raise ValueError("Invalid channel pair %r" % chanpair) channel = '#' + channel channel = irc.toLower(channel) assert utils.isChannel(channel), "Invalid channel name %s." % channel if network: ircobj = world.networkobjects.get(network) else: ircobj = irc assert ircobj, "Unknown network %s" % network if perm is not None: # Only check for permissions if we're told to and the irc object exists. if ircobj.name != irc.name: perm = 'remote' + perm checkAccess(irc, source, channel, perm) return (ircobj, channel)
def mode(self, numeric, target, modes, ts=None): """Sends mode changes from a PyLink client/server.""" # c <- :0UYAAAAAA TMODE 0 #a +o 0T4AAAAAC # u <- :0UYAAAAAA MODE 0UYAAAAAA :-Facdefklnou if (not self.irc.isInternalClient(numeric)) and \ (not self.irc.isInternalServer(numeric)): raise LookupError('No such PyLink client/server exists.') self.irc.applyModes(target, modes) modes = list(modes) if utils.isChannel(target): ts = ts or self.irc.channels[self.irc.toLower(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. msgprefix = ':%s TMODE %s %s ' % (numeric, ts, target) bufsize = S2S_BUFSIZE - len(msgprefix) for modestr in self.irc.wrapModes(modes, bufsize, max_modes_per_msg=10): self.irc.send(msgprefix + modestr) else: joinedmodes = self.irc.joinModes(modes) self._send(numeric, 'MODE %s %s' % (target, joinedmodes))
def identify(irc, source, args): """<username> <password> Logs in to PyLink using the configured administrator account.""" if utils.isChannel(irc.called_in): 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.reply('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.reply('Successfully logged in as %s.' % realuser) log.info("(%s) Successful login to %r by %s", irc.name, username, irc.getHostmask(source)) else: irc.reply('Error: Incorrect credentials.') u = irc.users[source] log.warning("(%s) Failed login to %r from %s", irc.name, username, irc.getHostmask(source))
def mimic(irc, source, args): """<channel> <log glob> Echoes chatlogs matching the log glob to the given channel. Home folders ("~") and environment variables ($HOME, etc.) are expanded in THAT order.""" irc.checkAuthenticated(source, allowOper=False) try: channel, logs = args[:2] except ValueError: irc.reply("Error: Not enough arguments. Needs 2: channel, log glob.") return else: assert utils.isChannel(channel), "Invalid channel %s" % channel channel = irc.toLower(channel) # Expand variables in the path glob logs = os.path.expandvars(os.path.expanduser(logs)) mysid = irc.proto.spawnServer(HOSTNAME) logs = sorted(glob.glob(logs)) def talk(): userdict = {} for item in logs: irc.proto.notice(irc.pseudoclient.uid, channel, 'Beginning mimic of log file %s' % item) with open(item, errors='replace' ) as f: # errors='replace' ignores UnicodeDecodeError for line in f.readlines(): action = False # Marks whether line is an action match = CHAT_REGEX.search(line) actionmatch = ACTION_REGEX.search(line) if actionmatch: action = True # Only one of the two matches need to exist for the check below match = match or actionmatch if match: sender, text = match.group(1, 2) # Update the user dict returned by _sayit(), which automatically spawns users # as they're seen. userdict = _sayit(irc, mysid, userdict, channel, sender, text, action=action) # Pause for a bit to prevent excess flood. time.sleep(LINEDELAY) else: # Once we're done, SQUIT everyone to clean up automagically. irc.proto.notice(irc.pseudoclient.uid, channel, 'Finished mimic of %s items' % len(logs)) irc.proto.squit(irc.sid, mysid) threading.Thread(target=talk).start()
def knock(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 = self.irc.getServer(numeric) s = '[Knock] by %s (%s)' % (self.irc.getHostmask(numeric), text) self._send(sender, 'NOTICE @%s :%s' % (target, s))
def mode(self, numeric, target, modes, ts=None): """ Sends mode changes from a PyLink client/server. The mode list should be a list of (mode, arg) tuples, i.e. the format of utils.parseModes() output. """ # <- :unreal.midnight.vpn MODE #test +ntCo GL 1444361345 if (not self.irc.isInternalClient(numeric)) and \ (not self.irc.isInternalServer(numeric)): raise LookupError('No such PyLink client/server exists.') self.irc.applyModes(target, modes) if utils.isChannel(target): # Make sure we expand any PUIDs when sending outgoing modes... for idx, mode in enumerate(modes): if mode[0][-1] in self.irc.prefixmodes: log.debug('(%s) mode: expanding PUID of mode %s', self.irc.name, str(mode)) modes[idx] = (mode[0], self._expandPUID(mode[1])) # The MODE command is used for channel mode changes only ts = ts or self.irc.channels[self.irc.toLower(target)].ts # 7 characters for "MODE", the space between MODE and the target, the space between the # target and mode list, and the space between the mode list and TS. bufsize = S2S_BUFSIZE - 7 # Subtract the length of the TS and channel arguments bufsize -= len(str(ts)) bufsize -= len(target) # Subtract the prefix (":SID " for servers or ":SIDAAAAAA " for servers) bufsize -= (5 if self.irc.isInternalServer(numeric) else 11) # There is also an (undocumented) 15 args per line limit for MODE. The target, mode # characters, and TS take up three args, so we're left with 12 spaces for parameters. # Any lines that go over 15 args/line has the potential of corrupting a channel's TS # pretty badly, as the last argument gets mangled into a number: # * *** Warning! Possible desynch: MODE for channel #test ('+bbbbbbbbbbbb *!*@0.1 *!*@1.1 *!*@2.1 *!*@3.1 *!*@4.1 *!*@5.1 *!*@6.1 *!*@7.1 *!*@8.1 *!*@9.1 *!*@10.1 *!*@11.1') has fishy timestamp (12) (from pylink.local/pylink.local) # Thanks to kevin and Jobe for helping me debug this! for modestring in self.irc.wrapModes(modes, bufsize, max_modes_per_msg=12): self._send(numeric, 'MODE %s %s %s' % (target, modestring, 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 self.irc.isInternalClient(target): raise ProtocolError('Cannot force mode change on external clients!') # XXX: I don't expect usermode changes to ever get cut off, but length # checks could be added just to be safe... joinedmodes = self.irc.joinModes(modes) self._send(target, 'UMODE2 %s' % joinedmodes)
def hook_privmsg(irc, source, command, args): channel = args['target'] text = args['text'] # irc.pseudoclient stores the IrcUser object of the main PyLink client. # (i.e. the user defined in the bot: section of the config) if utils.isChannel(channel) and irc.pseudoclient.nick in text: irc.msg(channel, 'hi there!') # log.debug, log.info, log.warning, log.error, log.exception (within except: clauses) # and log.critical are supported here. log.info('%s said my name on channel %s (PRIVMSG hook caught)' % (source, channel))
def msg(irc, source, args): """[<source>] <target> <text> Admin-only. Sends message <text> from <source>, where <source> is the nick of a PyLink client. If <source> is not given, it defaults to the main PyLink client.""" permissions.checkPermissions(irc, source, ['bots.msg']) # Because we want the source nick to be optional, this argument parsing gets a bit tricky. try: msgsource = args[0] target = args[1] text = ' '.join(args[2:]) # First, check if the first argument is an existing PyLink client. If it is not, # then assume that the first argument was actually the message TARGET. sourceuid = irc.nickToUid(msgsource) if not irc.isInternalClient( sourceuid): # First argument isn't one of our clients raise IndexError if not text: raise IndexError except IndexError: try: sourceuid = irc.pseudoclient.uid target = args[0] text = ' '.join(args[1:]) except IndexError: irc.error( 'Not enough arguments. Needs 2-3: source nick (optional), target, text.' ) return if not text: irc.error('No text given.') return if not utils.isChannel(target): # Convert nick of the message target to a UID, if the target isn't a channel real_target = irc.nickToUid(target) if real_target is None: # Unknown target user, if target isn't a valid channel name irc.error('Unknown user %r.' % target) return else: real_target = target irc.proto.message(sourceuid, real_target, text) irc.reply("Done.") irc.callHooks([ sourceuid, 'PYLINK_BOTSPLUGIN_MSG', { 'target': real_target, 'text': text, 'parse_as': 'PRIVMSG' } ])
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. If <target> is not given, it defaults to the main PyLink client.""" irc.checkAuthenticated(source, allowOper=False) try: nick = args[0] clist = args[1] # For the part message, join all remaining arguments into one text string reason = ' '.join(args[2:]) # First, check if the first argument is an existing PyLink client. If it is not, # then assume that the first argument was actually the channels being parted. u = irc.nickToUid(nick) if not irc.isInternalClient( u): # First argument isn't one of our clients raise IndexError except IndexError: # No nick was given; shift arguments one to the left. u = irc.pseudoclient.uid try: clist = args[0] except IndexError: irc.reply( "Error: Not enough arguments. Needs 1-2: nick (optional), comma separated list of channels." ) return reason = ' '.join(args[1:]) clist = clist.split(',') if not clist: irc.reply("Error: No valid channels given.") return if not irc.isManipulatableClient(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.part(u, channel, reason) irc.callHooks([ u, 'PYLINK_BOTSPLUGIN_PART', { 'channels': clist, 'text': reason, 'parse_as': 'PART' } ])
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. stripped_target = target.lstrip(''.join(self.irc.prefixmodes.values())) if utils.isChannel(stripped_target): target = self.irc.toLower(target) return {'target': target, 'text': args[1]}
def mode(self, numeric, target, modes, ts=None): """Sends mode changes from a PyLink client/server.""" # -> :9PYAAAAAA FMODE #pylink 1433653951 +os 9PYAAAAAA # -> :9PYAAAAAA MODE 9PYAAAAAA -i+w if (not self.irc.isInternalClient(numeric)) and \ (not self.irc.isInternalServer(numeric)): raise LookupError('No such PyLink client/server exists.') 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/inspircd/inspircd/blob/master/src/modules/m_spanningtree/opertype.cpp#L26-L28 # Servers need a special command to set umode +o on people. self._operUp(target) self.irc.applyModes(target, modes) joinedmodes = self.irc.joinModes(modes) if utils.isChannel(target): ts = ts or self.irc.channels[self.irc.toLower(target)].ts self._send(numeric, 'FMODE %s %s %s' % (target, ts, joinedmodes)) else: self._send(numeric, 'MODE %s %s' % (target, joinedmodes))
def handle_fantasy(irc, source, command, args): """Fantasy command handler.""" if not irc.connected.is_set(): # Break if the IRC network isn't ready. return respondtonick = irc.botdata.get("respondtonick") channel = args['target'] orig_text = args['text'] if utils.isChannel(channel) and not irc.isInternalClient(source): # The following conditions must be met for an incoming message for # fantasy to trigger: # 1) The message target is a channel. # 2) A PyLink service client exists in the channel. # 3) The message starts with one of our fantasy prefixes. # 4) The sender is NOT a PyLink client (this prevents infinite # message loops). for botname, sbot in world.services.items(): log.debug('(%s) fantasy: checking bot %s', irc.name, botname) servuid = sbot.uids.get(irc.name) if servuid in irc.channels[channel].users: # Try to look up a prefix specific for this bot in # bot: prefixes: <botname>, falling back to the default prefix if not # specified. prefixes = [irc.botdata.get('prefixes', {}).get(botname) or irc.botdata.get('prefix')] # If responding to nick is enabled, add variations of the current nick # to the prefix list: "<nick>," and "<nick>:" nick = irc.users[servuid].nick if respondtonick: prefixes += [nick+',', nick+':'] if not any(prefixes): # We finished with an empty prefixes list, meaning fantasy is misconfigured! log.warning("(%s) Fantasy prefix for bot %s was not set in configuration - " "fantasy commands will not work!", irc.name, botname) continue for prefix in prefixes: # Cycle through the prefixes list we finished with. if prefix and orig_text.startswith(prefix): # Cut off the length of the prefix from the text. text = orig_text[len(prefix):] # Finally, call the bot command and loop to the next bot. sbot.call_cmd(irc, source, text, called_in=channel) continue
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. If <target> is not given, it defaults to the main PyLink client.""" irc.checkAuthenticated(source, allowOper=False) try: # Check if the first argument is an existing PyLink client. If it is not, # then assume that the first argument was actually the channels being joined. u = irc.nickToUid(args[0]) if not irc.isInternalClient( u): # First argument isn't one of our clients raise IndexError clist = args[1] except IndexError: # No nick was given; shift arguments one to the left. u = irc.pseudoclient.uid try: clist = args[0] except IndexError: irc.reply( "Error: Not enough arguments. Needs 1-2: nick (optional), comma separated list of channels." ) return clist = clist.split(',') if not clist: irc.reply("Error: No valid channels given.") return if not irc.isManipulatableClient(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.join(u, channel) # Call a join hook manually so other plugins like relay can understand it. irc.callHooks([ u, 'PYLINK_BOTSPLUGIN_JOIN', { 'channel': channel, 'users': [u], 'modes': irc.channels[channel].modes, 'parse_as': 'JOIN' } ])
def mimic(irc, source, args): """<channel> <log glob> Echoes chatlogs matching the log glob to the given channel. Home folders ("~") and environment variables ($HOME, etc.) are expanded in THAT order.""" irc.checkAuthenticated(source, allowOper=False) try: channel, logs = args[:2] except ValueError: irc.reply("Error: Not enough arguments. Needs 2: channel, log glob.") return else: assert utils.isChannel(channel), "Invalid channel %s" % channel channel = irc.toLower(channel) # Expand variables in the path glob logs = os.path.expandvars(os.path.expanduser(logs)) mysid = irc.proto.spawnServer(HOSTNAME) logs = sorted(glob.glob(logs)) def talk(): userdict = {} for item in logs: irc.proto.notice(irc.pseudoclient.uid, channel, 'Beginning mimic of log file %s' % item) with open(item, errors='replace') as f: # errors='replace' ignores UnicodeDecodeError for line in f.readlines(): action = False # Marks whether line is an action match = CHAT_REGEX.search(line) actionmatch = ACTION_REGEX.search(line) if actionmatch: action = True # Only one of the two matches need to exist for the check below match = match or actionmatch if match: sender, text = match.group(1, 2) # Update the user dict returned by _sayit(), which automatically spawns users # as they're seen. userdict = _sayit(irc, mysid, userdict, channel, sender, text, action=action) # Pause for a bit to prevent excess flood. time.sleep(LINEDELAY) else: # Once we're done, SQUIT everyone to clean up automagically. irc.proto.notice(irc.pseudoclient.uid, channel, 'Finished mimic of %s items' % len(logs)) irc.proto.squit(irc.sid, mysid) threading.Thread(target=talk).start()
def handle_mode(self, numeric, command, args): # <- :unreal.midnight.vpn MODE #test +bb test!*@* *!*@bad.net # <- :unreal.midnight.vpn MODE #test +q GL 1444361345 # <- :unreal.midnight.vpn MODE #test +ntCo GL 1444361345 # <- :unreal.midnight.vpn MODE #test +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 = self.irc.toLower(args[0]) oldobj = self.irc.channels[channel].deepcopy() modes = [arg for arg in args[1:] if arg] # normalize whitespace parsedmodes = self.irc.parseModes(channel, modes) if parsedmodes: if parsedmodes[0][0] == '+&': # UnrealIRCd uses a & virtual mode to denote mode bounces, meaning that an # attempt to set modes by us was rejected for some reason (usually due to # timestamps). Drop the mode change to prevent mode floods. log.debug( "(%s) Received mode bounce %s in channel %s! Our TS: %s", self.irc.name, modes, channel, self.irc.channels[channel].ts) return self.irc.applyModes(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(numeric, channel, their_ts) return { 'target': channel, 'modes': parsedmodes, 'channeldata': oldobj } else: # User mode change: pass those on to handle_umode2() self.handle_umode2(numeric, 'MODE', args[1:])
def handle_404(self, source, command, args): """ Handles ERR_CANNOTSENDTOCHAN and other similar numerics. """ # <- :some.server 404 james #test :Cannot send to channel if len(args) >= 2 and utils.isChannel(args[1]): channel = args[1] f = log.warning # Don't sent the warning multiple times to prevent flood if the target # is a log chan. if hasattr(self.irc.channels[channel], '_clientbot_cannot_send_warned'): f = log.debug f('(%s) Failed to send message to %s: %s', self.irc.name, channel, args[-1]) self.irc.channels[channel]._clientbot_cannot_send_warned = True
def handle_privmsg(self, source, command, args): """Handles incoming PRIVMSG/NOTICE.""" # <- :sender PRIVMSG #dev :afasfsa # <- :sender NOTICE somenick :afasfsa target = args[0] if self.irc.isInternalClient(source) or self.irc.isInternalServer( source): log.warning('(%s) Received %s to %s being routed the wrong way!', self.irc.name, command, target) return # We use lowercase channels internally. if utils.isChannel(target): target = self.irc.toLower(target) else: target = self.irc.nickToUid(target) if target: return {'target': target, 'text': args[1]}
def setacc(irc, source, args): """<channel> <mask> <mode list> Assigns the given prefix mode characters to the given mask for the channel given. Extended targets are supported for masks - use this to your advantage! Examples: SET #channel *!*@localhost ohv SET #channel $account v SET #channel $oper:Network?Administrator qo SET #staffchan $channel:#mainchan:op o """ irc.checkAuthenticated(source, allowOper=False) try: channel, mask, modes = args except ValueError: reply( irc, "Error: Invalid arguments given. Needs 3: channel, mask, mode list." ) return else: if not utils.isChannel(channel): reply(irc, "Error: Invalid channel name %s." % channel) return # Store channels case insensitively channel = irc.toLower(channel) # Database entries for any network+channel pair are automatically created using # defaultdict. Note: string keys are used here instead of tuples so they can be # exported easily as JSON. dbentry = db[irc.name + channel] # Otherwise, update the modes as is. dbentry[mask] = modes reply( irc, "Done. \x02%s\x02 now has modes \x02%s\x02 in \x02%s\x02." % (mask, modes, channel))
def joinclient(irc, source, args): """[<target>] <channel1>[,<channel2>,<channel3>,...] Admin-only. Joins <target>, the nick of a PyLink client, to a comma-separated list of channels. If <target> is not given, it defaults to the main PyLink client. For the channel arguments, prefixes can also be specified to join the given client with (e.g. @#channel will join the client with op, while ~@#channel will join it with +qo. """ permissions.checkPermissions(irc, source, ['bots.joinclient']) try: # Check if the first argument is an existing PyLink client. If it is not, # then assume that the first argument was actually the channels being joined. u = irc.nickToUid(args[0]) if not irc.isInternalClient( u): # First argument isn't one of our clients raise IndexError clist = args[1] except IndexError: # No nick was given; shift arguments one to the left. u = irc.pseudoclient.uid try: clist = args[0] except IndexError: irc.error( "Not enough arguments. Needs 1-2: nick (optional), comma separated list of channels." ) return clist = clist.split(',') if not clist: irc.error("No valid channels given.") return if not (irc.isManipulatableClient(u) or irc.getServiceBot(u)): irc.error("Cannot force join a protected PyLink services client.") return prefix_to_mode = {v: k for k, v in irc.prefixmodes.items()} for channel in clist: real_channel = channel.lstrip(''.join(prefix_to_mode)) # XXX we need a better way to do this, but only the other option I can think of is regex... prefixes = channel[:len(channel) - len(real_channel)] joinmodes = ''.join(prefix_to_mode[prefix] for prefix in prefixes) if not utils.isChannel(real_channel): irc.error("Invalid channel name %r." % real_channel) return # join() doesn't support prefixes. if prefixes: irc.proto.sjoin(irc.sid, real_channel, [(joinmodes, u)]) else: irc.proto.join(u, real_channel) # Call a join hook manually so other plugins like relay can understand it. irc.callHooks([ u, 'PYLINK_BOTSPLUGIN_JOIN', { 'channel': real_channel, 'users': [u], 'modes': irc.channels[real_channel].modes, 'parse_as': 'JOIN' } ]) irc.reply("Done.")
def handle_fantasy(irc, source, command, args): """Fantasy command handler.""" if not irc.connected.is_set(): # Break if the IRC network isn't ready. return channel = args['target'] orig_text = args['text'] if utils.isChannel(channel) and not irc.isInternalClient(source): # The following conditions must be met for an incoming message for # fantasy to trigger: # 1) The message target is a channel. # 2) A PyLink service client exists in the channel. # 3) The message starts with one of our fantasy prefixes. # 4) The sender is NOT a PyLink client (this prevents infinite # message loops). for botname, sbot in world.services.copy().items(): if botname not in world.services: # Bot was removed during iteration continue # Check respond to nick options in this order: # 1) The service specific "respond_to_nick" option # 2) The global "pylink::respond_to_nick" option # 3) The (deprecated) global "bot::respondtonick" option. respondtonick = conf.conf.get(botname, {}).get( 'respond_to_nick', conf.conf['pylink'].get("respond_to_nick", conf.conf['bot'].get("respondtonick"))) log.debug('(%s) fantasy: checking bot %s', irc.name, botname) servuid = sbot.uids.get(irc.name) if servuid in irc.channels[channel].users: # Look up a string prefix for this bot in either its own configuration block, or # in bot::prefixes::<botname>. prefixes = [ conf.conf.get(botname, {}).get( 'prefix', conf.conf['bot'].get('prefixes', {}).get(botname)) ] # If responding to nick is enabled, add variations of the current nick # to the prefix list: "<nick>," and "<nick>:" nick = irc.toLower(irc.users[servuid].nick) nick_prefixes = [nick + ',', nick + ':'] if respondtonick: prefixes += nick_prefixes if not any(prefixes): # No prefixes were set, so skip. continue lowered_text = irc.toLower(orig_text) for prefix in filter( None, prefixes ): # Cycle through the prefixes list we finished with. if lowered_text.startswith(prefix): # Cut off the length of the prefix from the text. text = orig_text[len(prefix):] # HACK: don't trigger on commands like "& help" to prevent false positives. # Weird spacing like "PyLink: help" and "/msg PyLink help" should still # work though. if text.startswith( ' ') and prefix not in nick_prefixes: log.debug( '(%s) fantasy: skipping trigger with text prefix followed by space', irc.name) continue # Finally, call the bot command and loop to the next bot. sbot.call_cmd(irc, source, text, called_in=channel) continue
def spawn_service(irc, source, command, args): """Handles new service bot introductions.""" if not irc.connected.is_set(): return # Service name name = args['name'] # Get the ServiceBot object. sbot = world.services[name] # Look up the nick or ident in the following order: # 1) Network specific nick/ident settings for this service (servers::irc.name::servicename_nick) # 2) Global settings for this service (servicename::nick) # 3) The preferred nick/ident combination defined by the plugin (sbot.nick / sbot.ident) # 4) The literal service name. # settings, and then falling back to the literal service name. nick = irc.serverdata.get("%s_nick" % name) or irc.conf.get( name, {}).get('nick') or sbot.nick or name ident = irc.serverdata.get("%s_ident" % name) or irc.conf.get( name, {}).get('ident') or sbot.ident or name # TODO: make this configurable? host = irc.serverdata["hostname"] # Spawning service clients with these umodes where supported. servprotect usage is a # configuration option. preferred_modes = ['oper', 'hideoper', 'hidechans', 'invisible', 'bot'] modes = [] if conf.conf['bot'].get('protect_services'): preferred_modes.append('servprotect') for mode in preferred_modes: mode = irc.umodes.get(mode) if mode: modes.append((mode, None)) # Track the service's UIDs on each network. userobj = irc.proto.spawnClient(nick, ident, host, modes=modes, opertype="PyLink Service", manipulatable=sbot.manipulatable) sbot.uids[irc.name] = u = userobj.uid # Special case: if this is the main PyLink client being spawned, # assign this as irc.pseudoclient. if name == 'pylink': irc.pseudoclient = userobj # TODO: channels should be tracked in a central database, not hardcoded # in conf. channels = set(irc.serverdata.get( 'channels', [])) | sbot.extra_channels.get(irc.name, set()) for chan in channels: if utils.isChannel(chan): irc.proto.join(u, chan) irc.callHooks([ irc.sid, 'PYLINK_SERVICE_JOIN', { 'channel': chan, 'users': [u] } ]) else: log.warning('(%s) Ignoring invalid autojoin channel %r.', irc.name, chan)