def kickban(bot, trigger): """ This gives admins the ability to kickban a user. The bot must be a Channel Operator for this command to work. .kickban [#chan] user1 user!*@* get out of here """ if bot.privileges[trigger.sender][trigger.nick] < OP: return if bot.privileges[trigger.sender][bot.nick] < HALFOP: return bot.reply("I'm not a channel operator!") text = trigger.group().split() argc = len(text) if argc < 4: return opt = Identifier(text[1]) nick = opt mask = text[2] reasonidx = 3 if not opt.is_nick(): if argc < 5: return channel = opt nick = text[2] mask = text[3] reasonidx = 4 reason = ' '.join(text[reasonidx:]) mask = configureHostMask(mask) if mask == '': return bot.write(['MODE', channel, '+b', mask]) bot.write(['KICK', channel, nick, ' :', reason])
def collectlines(bot, trigger): """Create a temporary log of what people say""" # Don't log things in PM if trigger.is_privmsg: return # Add a log for the channel and nick, if there isn't already one if trigger.sender not in bot.memory['find_lines']: bot.memory['find_lines'][trigger.sender] = lpbotMemory() if Identifier( trigger.nick) not in bot.memory['find_lines'][trigger.sender]: bot.memory['find_lines'][trigger.sender][Identifier( trigger.nick)] = list() # Create a temporary list of the user's lines in a channel templist = bot.memory['find_lines'][trigger.sender][Identifier( trigger.nick)] line = trigger.group() if line.startswith("s/"): # Don't remember substitutions return elif line.startswith("\x01ACTION"): # For /me messages line = line[:-1] templist.append(line) else: templist.append(line) del templist[:-10] # Keep the log to 10 lines per person bot.memory['find_lines'][trigger.sender][Identifier( trigger.nick)] = templist
def unquiet(bot, trigger): """ This gives admins the ability to unquiet a user. The bot must be a Channel Operator for this command to work. """ if bot.privileges[trigger.sender][trigger.nick] < OP: return if bot.privileges[trigger.sender][bot.nick] < OP: return bot.reply("I'm not a channel operator!") text = trigger.group().split() argc = len(text) if argc < 2: return opt = Identifier(text[1]) quietmask = opt channel = trigger.sender if not opt.is_nick(): if argc < 3: return quietmask = text[2] channel = opt quietmask = configureHostMask(quietmask) if quietmask == '': return bot.write(['MODE', opt, '-q', quietmask])
def kick(bot, trigger): """ Kick a user from the channel. """ if bot.privileges[trigger.sender][trigger.nick] < OP: return if bot.privileges[trigger.sender][bot.nick] < HALFOP: return bot.reply("I'm not a channel operator!") text = trigger.group().split() argc = len(text) if argc < 2: return opt = Identifier(text[1]) nick = opt channel = trigger.sender reasonidx = 2 if not opt.is_nick(): if argc < 3: return nick = text[2] channel = opt reasonidx = 3 reason = ' '.join(text[reasonidx:]) if nick != bot.config.nick: bot.write(['KICK', channel, nick, reason])
def test_get_nick_id(db): conn = sqlite3.connect(db_filename) tests = [ [None, 'embolalia', Identifier('Embolalia')], # Ensures case conversion is handled properly [None, '[][]', Identifier('[]{}')], # Unicode, just in case [None, 'embölaliå', Identifier('EmbölaliÅ')], ] for test in tests: test[0] = db.get_nick_id(test[2]) nick_id, slug, nick = test with conn: cursor = conn.cursor() registered = cursor.execute( 'SELECT nick_id, slug, canonical FROM nicknames WHERE canonical IS ?', [nick]).fetchall() assert len(registered) == 1 assert registered[0][1] == slug and registered[0][2] == nick # Check that each nick ended up with a different id assert len(set(test[0] for test in tests)) == len(tests) # Check that the retrieval actually is idempotent for test in tests: nick_id = test[0] new_id = db.get_nick_id(test[2]) assert nick_id == new_id # Even if the case is different for test in tests: nick_id = test[0] new_id = db.get_nick_id(Identifier(test[2].upper())) assert nick_id == new_id
def ban(bot, trigger): """ This give admins the ability to ban a user. The bot must be a Channel Operator for this command to work. """ if bot.privileges[trigger.sender][trigger.nick] < OP: return if bot.privileges[trigger.sender][bot.nick] < HALFOP: return bot.reply("I'm not a channel operator!") text = trigger.group().split() argc = len(text) if argc < 2: return opt = Identifier(text[1]) banmask = opt channel = trigger.sender if not opt.is_nick(): if argc < 3: return channel = opt banmask = text[2] banmask = configureHostMask(banmask) if banmask == '': return bot.write(['MODE', channel, '+b', banmask])
def f_remind(bot, trigger): """Give someone a message the next time they're seen""" teller = trigger.nick verb = trigger.group(1) if not trigger.group(3): bot.reply("{} whom?".format(verb)) return tellee = trigger.group(3).rstrip('.,:;') msg = trigger.group(2).lstrip(tellee).lstrip() if not msg: bot.reply("{} {} what?".format(verb, tellee)) return tellee = Identifier(tellee) if not os.path.exists(bot.tell_filename): return if len(tellee) > 20: return bot.reply('That nickname is too long.') if tellee == bot.nick: return bot.reply("I'm here now, you can tell me whatever you want!") if not tellee in (Identifier(teller), bot.nick, 'me'): tz = get_timezone(bot.db, bot.config, None, tellee) timenow = format_time(bot.db, bot.config, tz, tellee, channel=trigger.sender) bot.memory['tell_lock'].acquire() try: if not tellee in bot.memory['reminders']: bot.memory['reminders'][tellee] = [(teller, verb, timenow, msg) ] else: bot.memory['reminders'][tellee].append( (teller, verb, timenow, msg)) finally: bot.memory['tell_lock'].release() response = "I'll pass that on when {} is around.".format(tellee) bot.reply(response) elif Identifier(teller) == tellee: bot.say('You can {} yourself that.'.format(verb)) else: bot.say("Hey, I'm not as stupid as Monty you know!") dumpReminders(bot.tell_filename, bot.memory['reminders'], bot.memory['tell_lock']) # @@ tell
def test_merge_nick_groups(db): conn = sqlite3.connect(db_filename) aliases = ['Embolalia', 'Embo'] for nick_id, alias in enumerate(aliases): conn.execute('INSERT INTO nicknames VALUES (?, ?, ?)', [nick_id, Identifier(alias).lower(), alias]) conn.commit() finals = (('foo', 'bar'), ('bar', 'blue'), ('spam', 'eggs')) db.set_nick_value(aliases[0], finals[0][0], finals[0][1]) db.set_nick_value(aliases[0], finals[1][0], finals[1][1]) db.set_nick_value(aliases[1], 'foo', 'baz') db.set_nick_value(aliases[1], finals[2][0], finals[2][1]) db.merge_nick_groups(aliases[0], aliases[1]) nick_id = conn.execute('SELECT nick_id FROM nicknames').fetchone()[0] alias_id = conn.execute('SELECT nick_id FROM nicknames').fetchone()[0] assert nick_id == alias_id for key, value in finals: found = conn.execute( 'SELECT value FROM nick_values WHERE nick_id = ? AND key = ?', [nick_id, key]).fetchone()[0] assert json.loads(unicode(found)) == value
def __init__(self, config): ca_certs = '/etc/pki/tls/cert.pem' if config.ca_certs is not None: ca_certs = config.ca_certs elif not os.path.isfile(ca_certs): ca_certs = '/etc/ssl/certs/ca-certificates.crt' if not os.path.isfile(ca_certs): stderr('Could not open CA certificates file. SSL will not ' 'work properly.') if config.log_raw is None: # Default is to log raw data, can be disabled in config config.log_raw = True asynchat.async_chat.__init__(self) self.set_terminator(b'\n') self.buffer = '' self.nick = Identifier(config.nick) """lpbot's current ``Identifier``. Changing this while lpbot is running is untested.""" self.user = config.user """lpbot's user/ident.""" self.name = config.name """lpbot's "real name", as used for whois.""" self.channels = [] """The list of channels lpbot is currently in.""" self.stack = {} self.ca_certs = ca_certs self.hasquit = False self.sending = threading.RLock() self.writing_lock = threading.Lock() self.raw = None # Right now, only accounting for two op levels. # This might be expanded later. # These lists are filled in startup.py, as of right now. self.ops = dict() """ A dictionary mapping channels to a ``Identifier`` list of their operators. """ self.halfplus = dict() """ A dictionary mapping channels to a ``Identifier`` list of their half-ops and ops. """ self.voices = dict() """ A dictionary mapping channels to a ``Identifier`` list of their voices, half-ops and ops. """ # We need this to prevent error loops in handle_error self.error_count = 0 self.connection_registered = False """ Set to True when a server has accepted the client connection and
def track_nicks(bot, trigger): """Track nickname changes and maintain our chanops list accordingly.""" old = trigger.nick new = Identifier(trigger) if old == bot.config.core.owner: bot.memory['owner_auth'] = False # Give debug mssage, and PM the owner, if the bot's own nick changes. if old == bot.nick: privmsg = "Hi, I'm your bot, %s." + \ " Something has made my nick change." + \ " This can cause some problems for me," + \ " and make me do weird things." + \ " You'll probably want to restart me," + \ " and figure out what made that happen" + \ " so you can stop it happening again." + \ " (Usually, it means you tried to give me a nick" + \ " that's protected by NickServ.)" % bot.nick debug_msg = "Nick changed by server." + \ " This can cause unexpected behavior. Please restart the bot." LOGGER.critical(debug_msg) bot.msg(bot.config.core.owner, privmsg) return for channel in bot.privileges: channel = Identifier(channel) if old in bot.privileges[channel]: value = bot.privileges[channel].pop(old) bot.privileges[channel][new] = value # Old privilege maintenance for channel in bot.halfplus: if old in bot.halfplus[channel]: bot.del_halfop(channel, old) bot.add_halfop(channel, new) for channel in bot.ops: if old in bot.ops[channel]: bot.del_op(channel, old) bot.add_op(channel, new) for channel in bot.voices: if old in bot.voices[channel]: bot.del_voice(channel, old) bot.add_voice(channel, new)
def test_unalias_nick(db): conn = sqlite3.connect(db_filename) nick = 'Embolalia' nick_id = 42 conn.execute('INSERT INTO nicknames VALUES (?, ?, ?)', [nick_id, Identifier(nick).lower(), nick]) aliases = ['EmbölaliÅ', 'Embo`work', 'Embo'] for alias in aliases: conn.execute('INSERT INTO nicknames VALUES (?, ?, ?)', [nick_id, Identifier(alias).lower(), alias]) conn.commit() for alias in aliases: db.unalias_nick(alias) for alias in aliases: found = conn.execute('SELECT * FROM nicknames WHERE nick_id = ?', [nick_id]).fetchall() assert len(found) == 1
def _nick_blocked(self, nick): bad_nicks = self.config.core.get_list('nick_blocks') for bad_nick in bad_nicks: bad_nick = bad_nick.strip() if not bad_nick: continue if (re.match(bad_nick + '$', nick, re.IGNORECASE) or Identifier(bad_nick) == nick): return True return False
def handle_names(bot, trigger): """Handle NAMES response, happens when joining to channels.""" names = trigger.split() channels = re.search('(#\S*)', trigger.raw) if not channels: return channel = Identifier(channels.group(1)) if channel not in bot.privileges: bot.privileges[channel] = dict() bot.init_ops_list(channel) # This could probably be made flexible in the future, but I don't think # it'd be worth it. mapping = { '+': lpbot.module.VOICE, '%': lpbot.module.HALFOP, '@': lpbot.module.OP, '&': lpbot.module.ADMIN, '~': lpbot.module.OWNER } for name in names: priv = 0 for prefix, value in iteritems(mapping): if prefix in name: priv = priv | value nick = Identifier(name.lstrip(''.join(mapping.keys()))) bot.privileges[channel][nick] = priv # Old op list maintenance is down here, and should be removed at some # point if '@' in name or '~' in name or '&' in name: bot.add_op(channel, name.lstrip('@&%+~')) bot.add_halfop(channel, name.lstrip('@&%+~')) bot.add_voice(channel, name.lstrip('@&%+~')) elif '%' in name: bot.add_halfop(channel, name.lstrip('@&%+~')) bot.add_voice(channel, name.lstrip('@&%+~')) elif '+' in name: bot.add_voice(channel, name.lstrip('@&%+~'))
def track_kick(bot, trigger): nick = Identifier(trigger.args[1]) if nick == bot.nick: bot.channels.remove(trigger.sender) del bot.privileges[trigger.sender] else: # Temporary fix to stop KeyErrors from being sent to channel # The privileges dict may not have all nicks stored at all times # causing KeyErrors if trigger.nick == bot.config.core.owner: bot.memory['owner_auth'] = False try: del bot.privileges[trigger.sender][nick] except KeyError: pass
def test_delete_nick_group(db): conn = sqlite3.connect(db_filename) aliases = ['Embolalia', 'Embo'] nick_id = 42 for alias in aliases: conn.execute('INSERT INTO nicknames VALUES (?, ?, ?)', [nick_id, Identifier(alias).lower(), alias]) conn.commit() db.set_nick_value(aliases[0], 'foo', 'bar') db.set_nick_value(aliases[1], 'spam', 'eggs') db.delete_nick_group(aliases[0]) # Nothing else has created values, so we know the tables are empty nicks = conn.execute('SELECT * FROM nicknames').fetchall() assert len(nicks) == 0 data = conn.execute('SELECT * FROM nick_values').fetchone() assert data is None
def seen(bot, trigger): """Reports when and where the user was last seen.""" if not trigger.group(2): bot.say(".seen <nick> - Reports when <nick> was last seen.") return nick = Identifier(trigger.group(2).strip()) if nick in seen_dict: timestamp = seen_dict[nick]['timestamp'] channel = seen_dict[nick]['channel'] message = seen_dict[nick]['message'] tz = get_timezone(bot.db, bot.config, None, trigger.nick, trigger.sender) saw = datetime.datetime.utcfromtimestamp(timestamp) timestamp = format_time(bot.db, bot.config, tz, trigger.nick, trigger.sender, saw) msg = "I last saw %s at %s on %s, saying \"%s\"" % (nick, timestamp, channel, message) bot.say(str(trigger.nick) + ': ' + msg) else: bot.say("Sorry, I haven't seen %s around." % nick)
def del_halfop(self, channel, name): self.halfplus[channel].discard(Identifier(name))
def track_modes(bot, trigger): """Track usermode changes and keep our lists of ops up to date.""" # Mode message format: <channel> *( ( "-" / "+" ) *<modes> *<modeparams> ) channel = Identifier(trigger.args[0]) line = trigger.args[1:] # If the first character of where the mode is being set isn't a # # then it's a user mode, not a channel mode, so we'll ignore it. if channel.is_nick(): return def handle_old_modes(nick, mode): #Old mode maintenance. Drop this crap in 5.0. if mode[1] == 'o' or mode[1] == 'q' or mode[1] == 'a': if mode[0] == '+': bot.add_op(channel, nick) else: bot.del_op(channel, nick) elif mode[1] == 'h': # Halfop if mode[0] == '+': bot.add_halfop(channel, nick) else: bot.del_halfop(channel, nick) elif mode[1] == 'v': if mode[0] == '+': bot.add_voice(channel, nick) else: bot.del_voice(channel, nick) mapping = {'v': lpbot.module.VOICE, 'h': lpbot.module.HALFOP, 'o': lpbot.module.OP, 'a': lpbot.module.ADMIN, 'q': lpbot.module.OWNER} modes = [] for arg in line: if len(arg) == 0: continue if arg[0] in '+-': # There was a comment claiming IRC allows e.g. MODE +aB-c foo, but # I don't see it in any RFCs. Leaving in the extra parsing for now. sign = '' modes = [] for char in arg: if char == '+' or char == '-': sign = char else: modes.append(sign + char) else: arg = Identifier(arg) for mode in modes: priv = bot.privileges[channel].get(arg, 0) value = mapping.get(mode[1]) if value is not None: if mode[0] == '+': priv = priv | value else: priv = priv & ~value bot.privileges[channel][arg] = priv handle_old_modes(arg, mode)
def findandreplace(bot, trigger): # Don't bother in PM if trigger.is_privmsg: return # Correcting other person vs self. rnick = Identifier(trigger.group(1) or trigger.nick) search_dict = bot.memory['find_lines'] # only do something if there is conversation to work with if trigger.sender not in search_dict: return if Identifier(rnick) not in search_dict[trigger.sender]: return #TODO rest[0] is find, rest[1] is replace. These should be made variables of #their own at some point. rest = [trigger.group(2), trigger.group(3)] rest[0] = rest[0].replace(r'\/', '/') rest[1] = rest[1].replace(r'\/', '/') me = False # /me command flags = (trigger.group(4) or '') # If g flag is given, replace all. Otherwise, replace once. if 'g' in flags: count = -1 else: count = 1 # repl is a lambda function which performs the substitution. i flag turns # off case sensitivity. re.U turns on unicode replacement. if 'i' in flags: regex = re.compile(re.escape(rest[0]), re.U | re.I) repl = lambda s: re.sub(regex, rest[1], s, count == 1) else: repl = lambda s: s.replace(rest[0], rest[1], count) # Look back through the user's lines in the channel until you find a line # where the replacement works for line in reversed(search_dict[trigger.sender][rnick]): if line.startswith("\x01ACTION"): me = True # /me command line = line[8:] else: me = False new_phrase = repl(line) if new_phrase != line: # we are done break if not new_phrase or new_phrase == line: return # Didn't find anything # Save the new "edited" message. action = (me and '\x01ACTION ') or '' # If /me message, prepend \x01ACTION templist = search_dict[trigger.sender][rnick] templist.append(action + new_phrase) search_dict[trigger.sender][rnick] = templist bot.memory['find_lines'] = search_dict # output if not me: new_phrase = '%s to say: %s' % (bold('meant'), new_phrase) if trigger.group(1): phrase = '%s thinks %s %s' % (trigger.nick, rnick, new_phrase) else: phrase = '%s %s' % (trigger.nick, new_phrase) bot.say(phrase)
def add_halfop(self, channel, name): if isinstance(name, Identifier): self.halfplus[channel].add(name) else: self.halfplus[channel].add(Identifier(name))
def msg(self, recipient, text, max_messages=1): #TODO: it is not obvious how self.stack works, #add explanation or make code more self-documenting # We're arbitrarily saying that the max is 400 bytes of text when # messages will be split. Otherwise, we'd have to acocunt for the bot's # hostmask, which is hard. max_text_length = 400 # Encode to bytes, for proper length calculation if isinstance(text, str): encoded_text = text.encode('utf-8') else: encoded_text = text excess = '' if max_messages > 1 and len(encoded_text) > max_text_length: last_space = encoded_text.rfind(' '.encode('utf-8'), 0, max_text_length) if last_space == -1: excess = encoded_text[max_text_length:] encoded_text = encoded_text[:max_text_length] else: excess = encoded_text[last_space + 1:] encoded_text = encoded_text[:last_space] # We'll then send the excess at the end # Back to str again, so we don't screw things up later. text = encoded_text.decode('utf-8') try: self.sending.acquire() # No messages within the last 3 seconds? Go ahead! # Otherwise, wait so it's been at least 0.8 seconds + penalty recipient_id = Identifier(recipient) if recipient_id not in self.stack: self.stack[recipient_id] = [] elif self.stack[recipient_id]: elapsed = time.time() - self.stack[recipient_id][-1][0] if elapsed < 3: penalty = float(max(0, len(text) - 50)) / 70 wait = 0.7 + penalty if elapsed < wait: time.sleep(wait - elapsed) # Loop detection messages = [m[1] for m in self.stack[recipient_id][-8:]] # If what we are about to send repeated at least 5 times in the # last 2 minutes, replace with '...' if messages.count(text) >= 5 and elapsed < 120: text = '...' if messages.count('...') >= 3: # If we said '...' 3 times, discard message return self.write(('PRIVMSG', recipient), text) self.stack[recipient_id].append((time.time(), self.safe(text))) self.stack[recipient_id] = self.stack[recipient_id][-10:] finally: self.sending.release() # Now that we've sent the first part, we need to send the rest. Doing # this recursively seems easier to me than iteratively if excess: self.msg(recipient, excess, max_messages - 1)
def add_voice(self, channel, name): if isinstance(name, Identifier): self.voices[channel].add(name) else: self.voices[channel].add(Identifier(name))
def track_modes(bot, trigger): """Track usermode changes and keep our lists of ops up to date.""" # Mode message format: <channel> *( ( "-" / "+" ) *<modes> *<modeparams> ) channel = Identifier(trigger.args[0]) line = trigger.args[1:] # If the first character of where the mode is being set isn't a # # then it's a user mode, not a channel mode, so we'll ignore it. if channel.is_nick(): return def handle_old_modes(nick, mode): #Old mode maintenance. Drop this crap in 5.0. if mode[1] == 'o' or mode[1] == 'q' or mode[1] == 'a': if mode[0] == '+': bot.add_op(channel, nick) else: bot.del_op(channel, nick) elif mode[1] == 'h': # Halfop if mode[0] == '+': bot.add_halfop(channel, nick) else: bot.del_halfop(channel, nick) elif mode[1] == 'v': if mode[0] == '+': bot.add_voice(channel, nick) else: bot.del_voice(channel, nick) mapping = { 'v': lpbot.module.VOICE, 'h': lpbot.module.HALFOP, 'o': lpbot.module.OP, 'a': lpbot.module.ADMIN, 'q': lpbot.module.OWNER } modes = [] for arg in line: if len(arg) == 0: continue if arg[0] in '+-': # There was a comment claiming IRC allows e.g. MODE +aB-c foo, but # I don't see it in any RFCs. Leaving in the extra parsing for now. sign = '' modes = [] for char in arg: if char == '+' or char == '-': sign = char else: modes.append(sign + char) else: arg = Identifier(arg) for mode in modes: priv = bot.privileges[channel].get(arg, 0) value = mapping.get(mode[1]) if value is not None: if mode[0] == '+': priv = priv | value else: priv = priv & ~value bot.privileges[channel][arg] = priv handle_old_modes(arg, mode)
def del_op(self, channel, name): self.ops[channel].discard(Identifier(name))
def note(bot, trigger): if not trigger.is_privmsg: nick = Identifier(trigger.nick) seen_dict[nick]['timestamp'] = time.time() seen_dict[nick]['channel'] = trigger.sender seen_dict[nick]['message'] = trigger
def del_voice(self, channel, name): self.voices[channel].discard(Identifier(name))