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 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 {} at {}".format(nick, timestamp) if Identifier(channel) == trigger.sender: msg = msg + " in here, saying " + message else: msg += " in another channel." bot.say(str(trigger.nick) + ': ' + msg) else: bot.say("Sorry, I haven't seen %s around." % nick)
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] = WillieMemory() 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 receive_info(bot, trigger): if trigger.sender != 'NickServ': return account = Identifier(trigger.group(2)) nick = Identifier(trigger.group(1)) try: bot.db.alias_nick(account, nick) except ValueError as e: try: bot.db.alias_nick(nick, account) except ValueError as e: if nick in force: bot.db.merge_nick_groups(account, nick) first_id = bot.db.get_nick_id(Identifier(account)) second_id = bot.db.get_nick_id(Identifier(nick)) bot.db.execute( 'UPDATE nicknames SET nick_id = ? WHERE nick_id = ?', [first_id, second_id]) bot.msg(nick, 'Merged {0} and {1}. If conflicting values were found' \ ' between accounts, values from {0} were used.'.format(account, nick)) del force[nick] else: extra = '' if nick.lower() != account.lower(): extra = 'If you wish to merge data' \ ' from {0} to {1}, you may do so by using `.alias merge`. Please note that doing so' \ ' will overwrite conflicting values with those found in {0}. '.format(account, nick) bot.msg(nick, 'Sorry, I was unable to alias your nick' \ ' to your account -- it might have already been aliased. {1}({0})'.format(e.message, extra)) return bot.msg(nick, 'Successfully aliased ' + nick + ' to account ' + account)
def get_count(bot, user, chan): chan_count = bot.memory['pls_count'] # only do something if there is conversation to work with if chan not in chan_count: return 0 if Identifier(user) not in chan_count[chan]: return 0 return chan_count[chan][Identifier(user)]
def alias_nick(self, nick, alias): """Create an alias for a nick. Raises ValueError if the alias already exists. If nick does not already exist, it will be added along with the alias.""" nick = Identifier(nick) alias = Identifier(alias) nick_id = self.get_nick_id(nick) sql = 'INSERT INTO nicknames (nick_id, slug, canonical) VALUES (?, ?, ?)' values = [nick_id, alias.lower(), alias] try: self.execute(sql, values) except sqlite3.IntegrityError as e: raise ValueError('Alias already exists.')
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("%s whom?" % verb) return tellee = trigger.group(3).rstrip('.,:;') msg = trigger.group(2).lstrip(tellee).lstrip() if not msg: bot.reply("%s %s what?" % (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 = willie.tools.get_timezone(bot.db, bot.config, None, tellee) timenow = willie.tools.format_time(bot.db, bot.config, tz, tellee) 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 %s is around." % tellee bot.reply(response) elif Identifier(teller) == tellee: bot.say('You can %s yourself that.' % 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 watch(bot, trigger): """Receive a notification when a user speaks. Use * on the end of [nick] to match multiple nicks (e.g Tell*)""" if trigger.is_privmsg is False: return bot.reply("This command only works in PMs.") teller = trigger.nick verb = trigger.group(1) if not trigger.group(3): bot.reply("%s whom?" % verb) return tellee = trigger.group(3).rstrip('.,:;') tellee = Identifier(tellee) if len(tellee) > 20: return bot.reply('That nickname is too long.') if tellee == bot.nick or tellee == 'Cashy': return bot.reply("[-_-]") if not tellee in (Identifier(teller), bot.nick, 'me'): timenow = time.time() bot.memory['tell_lock'].acquire() try: if not tellee in bot.memory['tell_dict']: bot.memory['tell_dict'][tellee] = [(teller, verb, timenow, '')] else: found = False for (_teller, _verb, _datetime, _msg) in bot.memory['tell_dict'][tellee]: if verb.lower()=='watch': if _teller == teller: found = True break if found: return bot.say("You already have me watching for %s." % tellee) else: bot.memory['tell_dict'][tellee].append((teller, verb, timenow, '')) finally: bot.memory['tell_lock'].release() bot.reply("I'll let you know when I see %s." % tellee) elif Identifier(teller) == tellee: bot.say('[-_-]') else: bot.say("[-_-]") storage.put('tell',bot.memory['tell_dict'])
def check_alias(bot, trigger): if not trigger.group(3): bot.reply('alias usage: .alias <add|merge|list>') return if (trigger.group(3).lower() == 'add'): bot.write(['PRIVMSG', 'NickServ', ':info', trigger.nick]) bot.reply('Fetching NickServ info... I will get back to you in a PM') elif (trigger.group(3).lower() == 'merge'): force[trigger.nick] = True bot.write(['PRIVMSG', 'NickServ', ':info', trigger.nick]) bot.reply('Fetching NickServ info... I will get back to you in a PM') elif (trigger.group(3).lower() == 'list'): try: alias = Identifier(trigger.nick) nick_id = bot.db.get_nick_id(alias, False) nicks = bot.db.execute( 'SELECT DISTINCT canonical FROM nicknames WHERE nick_id = ?', [nick_id]).fetchall() bot.say('{}, your aliases are: {}'.format( trigger.nick, ' '.join([nick[0] for nick in nicks]))) except: bot.say( 'Something went wrong, perhaps you haven\'t aliased any nicks?' )
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 = trigger.group(2).strip() timestamp = bot.db.get_nick_value(nick, 'seen_timestamp') if timestamp: channel = bot.db.get_nick_value(nick, 'seen_channel') message = bot.db.get_nick_value(nick, 'seen_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 {} at {}".format(nick, timestamp) if Identifier(channel) == trigger.sender: msg = msg + " in here, saying " + message else: msg += " in another channel." bot.say(str(trigger.nick) + ': ' + msg) else: bot.say("Sorry, I haven't seen {} around.".format(nick))
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][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 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 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 delete_nick_group(self, nick): """Removes a nickname, and all associated aliases and settings. """ nick = Identifier(nick) nick_id = self.get_nick_id(nick, False) self.execute('DELETE FROM nicknames WHERE nick_id = ?', [nick_id]) self.execute('DELETE FROM nick_values WHERE nick_id = ?', [nick_id])
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 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 set_nick_value(self, nick, key, value): """Sets the value for a given key to be associated with the nick.""" nick = Identifier(nick) value = json.dumps(value, ensure_ascii=False) nick_id = self.get_nick_id(nick) self.execute('INSERT OR REPLACE INTO nick_values VALUES (?, ?, ?)', [nick_id, key, value])
def get_nick_or_channel_value(self, name, key): """Gets the value `key` associated to the nick or channel `name`. """ name = Identifier(name) if name.is_nick(): return self.get_nick_value(name, key) else: return self.get_channel_value(name, key)
def tell(bot, trigger): """Give someone a message the next time they're seen. Use * on the end of [recipient] to match multiple nicks (e.g Tell*)""" if trigger.is_privmsg is False: return bot.reply("This command only works in PMs.") teller = trigger.nick verb = trigger.group(1) if not trigger.group(3): bot.reply("%s whom?" % verb) return tellee = trigger.group(3).rstrip('.,:;') msg = trigger.group(2).lstrip(tellee).lstrip() if not msg: bot.reply("%s %s what?" % (verb, tellee)) return tellee = Identifier(tellee) if len(tellee) > 20: return bot.reply('That nickname is too long.') if tellee == bot.nick or tellee == 'Cashy': return bot.reply("I'm right here.") if not tellee in (Identifier(teller), bot.nick, 'me'): timenow = time.time() bot.memory['tell_lock'].acquire() try: if not tellee in bot.memory['tell_dict']: bot.memory['tell_dict'][tellee] = [(teller, verb, timenow, msg)] else: bot.memory['tell_dict'][tellee].append((teller, verb, timenow, msg)) finally: bot.memory['tell_lock'].release() bot.reply("I'll pass that on when %s is around." % tellee) elif Identifier(teller) == tellee: bot.say('You can %s yourself that.' % verb) else: bot.say("[-_-]") storage.put('tell',bot.memory['tell_dict'])
def get_nick_value(self, nick, key): """Retrieves the value for a given key associated with a nick.""" nick = Identifier(nick) result = self.execute( 'SELECT value FROM nicknames, nick_values WHERE slug = ? AND key = ?', [nick.lower(), key]).fetchone() if result is not None: result = result[0] return _deserialize(result)
def get_channel_value(self, channel, key): """Retrieves the value for a given key associated with a channel.""" channel = Identifier(channel).lower() result = self.execute( 'SELECT value FROM channel_values WHERE channel = ? AND key = ?', [channel, key]).fetchone() if result is not None: result = result[0] return _deserialize(result)
def promote_karma(bot, trigger): """ Update karma status for specify IRC user if get '++' message. """ if (trigger.is_privmsg): return bot.say('People like it when you tell them good things.') if (bot.db.get_nick_id(Identifier(trigger.group(1))) == bot.db.get_nick_id( Identifier(trigger.nick))): return bot.say('You may not give yourself karma!') current_karma = bot.db.get_nick_value(trigger.group(1), 'karma') if not current_karma: current_karma = 0 else: current_karma = int(current_karma) current_karma += 1 bot.db.set_nick_value(trigger.group(1), 'karma', current_karma) bot.say(trigger.group(1) + ' == ' + str(current_karma))
def demote_karma(bot, trigger): """ Update karma status for specify IRC user if get '--' message. """ if (trigger.is_privmsg): return bot.say('Say it to their face!') if (bot.db.get_nick_id(Identifier(trigger.group(1))) == bot.db.get_nick_id( Identifier(trigger.nick))): return bot.say('You may not reduce your own karma!') current_karma = bot.db.get_nick_value(trigger.group(1), 'karma') if not current_karma: current_karma = 0 else: current_karma = int(current_karma) current_karma -= 1 bot.db.set_nick_value(trigger.group(1), 'karma', current_karma) bot.say(trigger.group(1) + ' == ' + str(current_karma))
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 mapping = { 'v': willie.module.VOICE, 'h': willie.module.HALFOP, 'o': willie.module.OP, 'a': willie.module.ADMIN, 'q': willie.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
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 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 merge_nick_groups(self, first_nick, second_nick): """Merges the nick groups for the specified nicks. Takes two nicks, which may or may not be registered. Unregistered nicks will be registered. Keys which are set for only one of the given nicks will be preserved. Where multiple nicks have values for a given key, the value set for the first nick will be used. Note that merging of data only applies to the native key-value store. If modules define their own tables which rely on the nick table, they will need to have their merging done separately.""" first_id = self.get_nick_id(Identifier(first_nick)) second_id = self.get_nick_id(Identifier(second_nick)) self.execute( 'UPDATE OR IGNORE nick_values SET nick_id = ? WHERE nick_id = ?', [first_id, second_id]) self.execute('DELETE FROM nick_values WHERE nick_id = ?', [second_id]) self.execute('UPDATE nicknames SET nick_id = ? WHERE nick_id = ?', [first_id, second_id])
def format_count(bot, trigger): if trigger.is_privmsg: return user = Identifier(trigger.group(2) or trigger.nick) user = Identifier(user.strip()) count = get_count(bot, user, trigger.sender) since = bot.memory['pls_count_time'] timezone = get_timezone(bot.db, bot.config, None, trigger.nick) if not timezone: timezone = 'UTC' time = format_time(bot.db, bot.config, timezone, trigger.nick, trigger.sender, datetime.datetime.fromtimestamp(since)) bot.say('{} has said pls in {} {} time(s) since {}'.format( user, trigger.sender, count, time))
def handle_names(bot, trigger): """Handle NAMES response, happens when joining to channels.""" names = trigger.split() #TODO specific to one channel type. See issue 281. 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 = { '+': willie.module.VOICE, '%': willie.module.HALFOP, '@': willie.module.OP, '&': willie.module.ADMIN, '~': willie.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_nicks(bot, trigger): """Track nickname changes and maintain our chanops list accordingly.""" old = trigger.nick new = Identifier(trigger) # 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)