def bomb_glue(bot, trigger): old = trigger.nick new = Identifier(trigger) with lock: if old.lower() in BOMBS: BOMBS[new.lower()] = BOMBS.pop(old.lower()) bot.notice(STRINGS['BOMB_STILL'] % new, new)
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] channel = trigger.sender 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 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 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 and new != 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 for channel in bot.channels.values(): channel.rename_user(old, new) if old in bot.users: bot.users[new] = bot.users.pop(old)
def cutwire(bot, trigger): """ Tells sopel to cut a wire when you've been bombed. """ global bombs, colors target = Identifier(trigger.nick) if target.lower() != bot.nick.lower() and target.lower() not in bombs: bot.say('You can\'t cut a wire till someone bombs you') return if not trigger.group(2): bot.say('You have to choose a wire to cut.') return color, code = bombs.pop(target.lower()) # remove target from bomb list wirecut = trigger.group(2).rstrip(' ') if wirecut.lower() in ('all', 'all!'): sch.cancel(code) # defuse timer, execute premature detonation kmsg = ('KICK %s %s : Cutting ALL the wires! *boom* (You should\'ve picked the %s wire.)' % (trigger.sender, target, color)) bot.write([kmsg]) elif wirecut.capitalize() not in colors: bot.say('I can\'t seem to find that wire, ' + target + '! You sure you\'re picking the right one? It\'s not here!') bombs[target.lower()] = (color, code) # Add the target back onto the bomb list, elif wirecut.capitalize() == color: bot.say('You did it, ' + target + '! I\'ll be honest, I thought you were dead. But nope, you did it. You picked the right one. Well done.') sch.cancel(code) # defuse bomb else: sch.cancel(code) # defuse timer, execute premature detonation kmsg = 'KICK ' + trigger.sender + ' ' + target + \ ' : No! No, that\'s the wrong one. Aww, you\'ve gone and killed yourself. Oh, that\'s... that\'s not good. No good at all, really. Wow. Sorry. (You should\'ve picked the ' + color + ' wire.)' bot.write([kmsg])
def start(bot, trigger): """ Put a bomb in the specified user's pants. They will be kicked if they don't guess the right wire fast enough. """ if not trigger.group(3): bot.say('Who do you want to Bomb?') return if not trigger.sender.startswith('#'): bot.say('Tell me this in a channel') return global bombs global sch target = Identifier(trigger.group(3)) if target == bot.nick: bot.say('I will NOT BOMB MYSELF!') return if target.lower() in bombs: bot.say('I can\'t fit another bomb in ' + target + '\'s pants!') return if target == trigger.nick: bot.say('I will not LET YOU BOMB YOURSELF!') return if target.lower() not in bot.privileges[trigger.sender.lower()]: bot.say('Please Bomb someone WHO IS HERE!') return message = 'Hey, ' + target + '! Don\'t look but, I think there\'s a bomb in your pants. 2 minute timer, 5 wires: Red, Yellow, Blue, White and Black. Which wire should I cut? Don\'t worry, I know what I\'m doing! (respond with .cutwire color)' bot.say(message) color = choice(colors) bot.msg(trigger.nick, "Hey, don\'t tell %s, but the %s wire? Yeah, that\'s the one." " But shh! Don\'t say anything!" % (target, color)) code = sch.enter(fuse, 1, explode, (bot, trigger)) bombs[target.lower()] = (color, code) sch.run()
def cancel_bomb(bot, trigger): """ Lets a bomber disarm the bomb they set on the specified user. Does not reset the cooldown timer. (Bot admins can cancel bombs on any player in the channel.) """ target = trigger.group(3) or None if not target: for bomb in BOMBS: if trigger.nick == BOMBS[bomb]['bomber']: target = BOMBS[bomb]['target'] break if not target: return bot.reply(STRINGS['CANCEL_WHOM']) target = Identifier(target) # issue #24 with lock: if target.lower() not in BOMBS: bot.reply(STRINGS['CANCEL_NO_BOMB'] % target) return if trigger.nick != BOMBS[target.lower()]['bomber'] and not trigger.admin: bot.reply(STRINGS['CANCEL_NO_PERMISSION'] % target) return bomber = BOMBS[target.lower()]['bomber'] bombs_planted = bot.db.get_nick_value(bomber, 'bombs_planted') or 0 bot.db.set_nick_value(bomber, 'bombs_planted', bombs_planted - 1) BOMBS.pop(target.lower())['timer'].cancel() bot.say(STRINGS['CANCEL_DONE'] % target)
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': sopel.module.VOICE, 'h': sopel.module.HALFOP, 'o': sopel.module.OP, 'a': sopel.module.ADMIN, 'q': sopel.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.channels[channel].privileges.get(arg, 0) # Log a warning if the two privilege-tracking data structures # get out of sync. That should never happen. # This is a good place to verify that bot.channels is doing # what it's supposed to do before ultimately removing the old, # deprecated bot.privileges structure completely. ppriv = bot.privileges[channel].get(arg, 0) if priv != ppriv: LOGGER.warning("Privilege data error! Please share Sopel's" "raw log with the developers, if enabled. " "(Expected {} == {} for {} in {}.)" .format(priv, ppriv, arg, channel)) 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 bot.channels[channel].privileges[arg] = priv
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 JOIN nick_values ' 'ON nicknames.nick_id = nick_values.nick_id ' 'WHERE slug = ? AND key = ?', [nick.lower(), key] ).fetchone() if result is not None: result = result[0] return _deserialize(result)
def unalias_nick(self, alias): """Removes an alias. Raises ValueError if there is not at least one other nick in the group. To delete an entire group, use `delete_group`. """ alias = Identifier(alias) nick_id = self.get_nick_id(alias, False) count = self.execute('SELECT COUNT(*) FROM nicknames WHERE nick_id = ?', [nick_id]).fetchone()[0] if count == 0: raise ValueError('Given alias is the only entry in its group.') self.execute('DELETE FROM nicknames WHERE slug = ?', [alias.lower()])
def explode(bot, trigger): target = Identifier(trigger.group(3)) orig_target = target with lock: if target.lower() not in BOMBS: # nick change happened for nick in BOMBS.keys(): if BOMBS[nick]['target'] == target: target = Identifier(nick) break bot.say(STRINGS['NEVER_TRIED'] % (target, BOMBS[target.lower()]['color'])) kickboom(bot, trigger, target) BOMBS.pop(target.lower()) timeouts = bot.db.get_nick_value(orig_target, 'bomb_timeouts') or 0 bot.db.set_nick_value(orig_target, 'bomb_timeouts', timeouts + 1)
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. {0}'.format(e))
def votemode(bot, trigger, mode): make_user_active(bot, trigger) channel = trigger.sender account = trigger.account if account is None: bot.say("You must be authed to use this command") return if bot.privileges[trigger.sender][bot.nick] < OP: return bot.reply("I'm not a channel operator!") quota = calculate_quota(bot, trigger, bot.memory['mode_threshold'][mode]) # This isn't per user but it's probably an OK heuristic if datetime.now() - bot.memory['last_vote'] > timedelta(minutes=5): clear_votes(bot) # Quota is 50% of active users plus one if trigger.group(2): target = Identifier(str(trigger.group(2)).split()[0].strip().lower()) if not target.is_nick(): return bot.reply("That is not a valid nick") if target not in bot.privileges[channel]: return bot.reply("I don't see %s." % target) target_privs = bot.privileges[channel][target] if target_privs > 0: return bot.reply("You cannot vote" + mode + " privileged users") if target in bot.memory['votes'][mode]: if str(account) not in bot.memory['votes'][mode][target]: bot.memory['votes'][mode][target].append(str(account)) else: bot.memory['votes'][mode][target] = [str(account)] bot.reply("Vote recorded. (%s more votes for action)" % str(max(0, quota - len(bot.memory['votes'][mode][target])+1))) if len(bot.memory['votes'][mode][target]) > quota: bot.memory['vote_methods'][mode](bot, channel, target) bot.memory['last_vote'] = datetime.now() elif mode == "registered" or mode == "moderated": if str(account) not in bot.memory['votes'][mode]: bot.memory['votes'][mode].append(str(account)) else: bot.memory['votes'][mode] = [str(account)] bot.reply("Vote recorded. (%s more votes for action)" % str(max(0, quota - len(bot.memory['votes'][mode])+1))) if len(bot.memory['votes'][mode]) > quota: bot.memory['vote_methods'][mode](bot, channel) bot.memory['last_vote'] = datetime.now() else: bot.say("Current active vote%s (%s needed to %s): " % (mode, str(quota + 1), mode)) for ballot in bot.memory['votes'][mode]: bot.say("%s has %s %s votes." % (ballot, len(bot.memory['votes'][mode][ballot]), mode)) return
def write_log(bot, event, channel): if bot.config.chanlogs2.allow_toggle: if not bot.db.get_channel_value(channel, 'logging'): return if not isinstance(channel, Identifier): channel = Identifier(channel) if channel.is_nick() and not bot.config.chanlogs2.privmsg: return # Don't log if we are configured not to log PMs if bot.config.chanlogs2.backend == 'postgres': write_db_line(bot, event, channel) else: write_log_line(bot, event, channel)
def get_nick_value(self, nick, key): """Retrieves the value for a given key associated with a nick.""" nick = Identifier(nick) session = self.ssession() try: result = session.query(NickValues) \ .filter(Nicknames.nick_id == NickValues.nick_id) \ .filter(Nicknames.slug == nick.lower()) \ .filter(NickValues.key == key) \ .one_or_none() if result is not None: result = result.value return _deserialize(result) except SQLAlchemyError: session.rollback() raise finally: session.close()
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": sopel.module.VOICE, "h": sopel.module.HALFOP, "o": sopel.module.OP, "a": sopel.module.ADMIN, "q": sopel.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 cancel_bomb(bot, trigger): """ Cancel the bomb placed on the specified player (can also be used by admins). """ target = trigger.group(3) or None if not target: bot.reply(STRINGS['CANCEL_WHOM']) return target = Identifier(target) # issue #24 with lock: if target.lower() not in BOMBS: bot.reply(STRINGS['CANCEL_NO_BOMB'] % target) return if trigger.nick != BOMBS[target.lower()]['bomber'] and not trigger.admin: bot.reply(STRINGS['CANCEL_NO_PERMISSION'] % target) return bomber = BOMBS[target.lower()]['bomber'] bombs_planted = bot.db.get_nick_value(bomber, 'bombs_planted') or 0 bot.db.set_nick_value(bomber, 'bombs_planted', bombs_planted - 1) BOMBS.pop(target.lower())['timer'].cancel() bot.say(STRINGS['CANCEL_DONE'] % target)
def test_bot_mixed_mode_types(mockbot, ircfactory): """Ensure mixed argument-required and -not-required modes are handled. Sopel 6.6.6 and older did not behave well. .. seealso:: GitHub issue #1575 (https://github.com/sopel-irc/sopel/pull/1575). """ irc = ircfactory(mockbot) irc.bot._isupport = isupport.ISupport(chanmodes=("be", "", "", "mn", tuple())) irc.bot.modeparser.chanmodes = irc.bot.isupport.CHANMODES irc.channel_joined( '#test', ['Uvoice', 'Uop', 'Uadmin', 'Uvoice2', 'Uop2', 'Uadmin2']) irc.mode_set('#test', '+amovn', ['Uadmin', 'Uop', 'Uvoice']) assert mockbot.channels["#test"].privileges[Identifier("Uadmin")] == ADMIN assert mockbot.channels["#test"].modes["m"] assert mockbot.channels["#test"].privileges[Identifier("Uop")] == OP assert mockbot.channels["#test"].privileges[Identifier("Uvoice")] == VOICE assert mockbot.channels["#test"].modes["n"] irc.mode_set('#test', '+above', ['Uadmin2', 'x!y@z', 'Uop2', 'Uvoice2', 'a!b@c']) assert mockbot.channels["#test"].privileges[Identifier("Uadmin2")] == ADMIN assert "x!y@z" in mockbot.channels["#test"].modes["b"] assert mockbot.channels["#test"].privileges[Identifier("Uop2")] == OP assert mockbot.channels["#test"].privileges[Identifier("Uvoice2")] == VOICE assert "a!b@c" in mockbot.channels["#test"].modes["e"]
def test_bot_mixed_mode_types(mockbot, ircfactory): """Ensure mixed argument-required and -not-required modes are handled. Sopel 6.6.6 and older did not behave well. .. seealso:: GitHub issue #1575 (https://github.com/sopel-irc/sopel/pull/1575). """ irc = ircfactory(mockbot) irc.channel_joined( '#test', ['Uvoice', 'Uop', 'Uadmin', 'Uvoice2', 'Uop2', 'Uadmin2']) irc.mode_set('#test', '+amov', ['Uadmin', 'Uop', 'Uvoice']) assert mockbot.channels["#test"].privileges[Identifier("Uadmin")] == ADMIN assert mockbot.channels["#test"].privileges[Identifier("Uop")] == OP assert mockbot.channels["#test"].privileges[Identifier("Uvoice")] == VOICE irc.mode_set('#test', '+abov', ['Uadmin2', 'x!y@z', 'Uop2', 'Uvoice2']) assert mockbot.channels["#test"].privileges[Identifier("Uadmin2")] == 0 assert mockbot.channels["#test"].privileges[Identifier("Uop2")] == 0 assert mockbot.channels["#test"].privileges[Identifier("Uvoice2")] == 0 assert mockbot.backend.message_sent == rawlist('WHO #test'), ( 'Upon finding an unexpected nick, the bot must send a WHO request.')
def test_call_rule_rate_limited_user(mockbot): items = [] # setup def testrule(bot, trigger): bot.say('hi') items.append(1) return "Return Value" rule_hello = rules.Rule( [re.compile(r'(hi|hello|hey|sup)')], plugin='testplugin', label='testrule', handler=testrule, rate_limit=100, threaded=False, ) # trigger line = ':[email protected] PRIVMSG #channel :hello' pretrigger = trigger.PreTrigger(mockbot.nick, line) # match matches = list(rule_hello.match(mockbot, pretrigger)) match = matches[0] # trigger and wrapper rule_trigger = trigger.Trigger(mockbot.settings, pretrigger, match, account=None) wrapper = bot.SopelWrapper(mockbot, rule_trigger) # call rule mockbot.call_rule(rule_hello, wrapper, rule_trigger) # assert the rule has been executed assert mockbot.backend.message_sent == rawlist('PRIVMSG #channel :hi') assert items == [1] # assert the rule is now rate limited assert rule_hello.is_rate_limited(Identifier('Test')) assert not rule_hello.is_channel_rate_limited('#channel') assert not rule_hello.is_global_rate_limited() # call rule again mockbot.call_rule(rule_hello, wrapper, rule_trigger) # assert no new message assert mockbot.backend.message_sent == rawlist( 'PRIVMSG #channel :hi'), 'There must not be any new message sent' assert items == [1], 'There must not be any new item'
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 try: del bot.privileges[trigger.sender][nick] except KeyError: pass
def adjust_channel_value(self, channel, key, value): """Adjusts the value for a given key to be associated with the 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] current_value = _deserialize(result) value = current_value + value value = json.dumps(value, ensure_ascii=False) self.execute('INSERT OR REPLACE INTO channel_values VALUES (?, ?, ?)', [channel, key, value])
def test_basic_pretrigger(nick): line = ':[email protected] PRIVMSG #Sopel :Hello, world' pretrigger = PreTrigger(nick, line) assert pretrigger.tags == {} assert pretrigger.hostmask == '[email protected]' assert pretrigger.line == line assert pretrigger.args == ['#Sopel', 'Hello, world'] assert pretrigger.text == 'Hello, world' assert pretrigger.event == 'PRIVMSG' assert pretrigger.nick == Identifier('Foo') assert pretrigger.user == 'foo' assert pretrigger.host == 'example.com' assert pretrigger.sender == '#Sopel'
def test_quit_pretrigger(nick): line = ':[email protected] QUIT :quit message text' pretrigger = PreTrigger(nick, line) assert pretrigger.tags == {} assert pretrigger.hostmask == '[email protected]' assert pretrigger.line == line assert pretrigger.args == ['quit message text'] assert pretrigger.text == 'quit message text' assert pretrigger.event == 'QUIT' assert pretrigger.nick == Identifier('Foo') assert pretrigger.user == 'foo' assert pretrigger.host == 'example.com' assert pretrigger.sender is None
def unexclude(bot, trigger): """ Re-enable bombing yourself (admins: or another user) """ if not trigger.group(3): target = trigger.nick else: target = Identifier(trigger.group(3)) if not trigger.admin and target != trigger.nick: bot.say(STRINGS['ADMINS_MARK_BOMBABLE']) return bot.db.set_nick_value(target, 'unbombable', False) bot.say(STRINGS['MARKED_BOMBABLE'] % target)
def test_bot_unknown_mode(mockbot, ircfactory): """Ensure modes not in PREFIX or CHANMODES trigger a WHO.""" irc = ircfactory(mockbot) irc.bot._isupport = isupport.ISupport(chanmodes=("b", "", "", "mnt", tuple())) irc.bot.modeparser.chanmodes = irc.bot.isupport.CHANMODES irc.channel_joined("#test", ["Alex", "Bob", "Cheryl"]) irc.mode_set("#test", "+te", ["Alex"]) assert mockbot.channels["#test"].privileges[Identifier("Alex")] == 0 assert mockbot.backend.message_sent == rawlist( "WHO #test" ), "Upon finding an unknown mode, the bot must send a WHO request."
def kick(bot, trigger): """Kick a user from the channel.""" if bot.channels[trigger.sender].privileges[bot.nick] < plugin.HALFOP: bot.reply(ERROR_MESSAGE_NOT_OP) return 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.core.nick: bot.kick(nick, channel, reason)
def unalias_nick(self, alias): """Removes an alias. Raises ValueError if there is not at least one other nick in the group. To delete an entire group, use `delete_group`. """ alias = Identifier(alias) nick_id = self.get_nick_id(alias, False) session = self.ssession() try: count = session.query(Nicknames) \ .filter(Nicknames.nick_id == nick_id) \ .count() if count <= 1: raise ValueError('Given alias is the only entry in its group.') session.query(Nicknames).filter(Nicknames.slug == alias.lower()).delete() session.commit() except SQLAlchemyError: session.rollback() raise finally: session.close()
def fighterStatus(bot, trigger): if not trigger.group(2): bot.say('fighter status for who?') return targetNick = Identifier(trigger.group(2).strip()) hitpoints = bot.db.get_nick_value(targetNick,'hitPoints') xl = bot.db.get_nick_value(targetNick,'xl') if not hitpoints: bot.say('I can''t find stats for {nick}'.format(nick=targetNick)) return else: bot.say('{nick} has {hp} hit points / {max} @ Level {xl} with {xp} xp until the next level'.format(nick=targetNick, hp=hitpoints,max=100 + (xl*3), xl=xl,xp=xlMap[bot.db.get_nick_value(targetNick,'xl') + 1] - bot.db.get_nick_value(targetNick,'xp')))
def rpl_names(self, 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(r'(#\S*)', trigger.raw) if not channels: return channel = Identifier(channels.group(1)) self.add_channel(channel) mapping = { '+': sopel.module.VOICE, '%': sopel.module.HALFOP, '@': sopel.module.OP, '&': sopel.module.ADMIN, '~': sopel.module.OWNER } for name in names: nick = Identifier(name.lstrip(''.join(mapping.keys()))) self.add_to_channel(channel, nick)
def cutwire(bot, trigger): """ Tells sopel to cut a wire when you've been bombed. """ global BOMBS target = Identifier(trigger.nick) if target == bot.nick: # a parallel bot behind a bouncer (e.g. Bucket) can trigger this function (see #16) return with lock: if target.lower() != bot.nick.lower() and target.lower() not in BOMBS: bot.say(STRINGS['CUT_NO_BOMB'] % target) return if not trigger.group(3): bot.say(STRINGS['CUT_NO_WIRE']) return # Remove target from bomb list temporarily bomb = BOMBS.pop(target.lower()) wirecut = trigger.group(3) if wirecut.lower() in ('all', 'all!'): bomb['timer'].cancel() # defuse timer, execute premature detonation bot.say(STRINGS['CUT_ALL_WIRES'] % bomb['color']) kickboom(bot, trigger, target) alls = bot.db.get_nick_value(bomb['target'], 'bomb_alls') or 0 bot.db.set_nick_value(bomb['target'], 'bomb_alls', alls + 1) elif wirecut.capitalize() not in bomb['wires']: bot.say(STRINGS['CUT_IMAGINARY'] % target) # Add the target back onto the bomb list BOMBS[target.lower()] = bomb elif wirecut.capitalize() == bomb['color']: bot.say(STRINGS['CUT_CORRECT'] % target) bomb['timer'].cancel() # defuse bomb defuses = bot.db.get_nick_value(bomb['target'], 'bomb_defuses') or 0 bot.db.set_nick_value(bomb['target'], 'bomb_defuses', defuses + 1) else: bomb['timer'].cancel() # defuse timer, execute premature detonation bot.say(STRINGS['CUT_WRONG'] % bomb['color']) kickboom(bot, trigger, target) wrongs = bot.db.get_nick_value(bomb['target'], 'bomb_wrongs') or 0 bot.db.set_nick_value(bomb['target'], 'bomb_wrongs', wrongs + 1)
def unalias_nick(self, alias): """Removes an alias. Raises ValueError if there is not at least one other nick in the group. To delete an entire group, use `delete_group`. """ alias = Identifier(alias) nick_id = self.get_nick_id(alias, False) session = self.ssession() try: count = session.query(Nicknames) \ .filter(Nicknames.nick_id == nick_id) \ .count() if count <= 1: raise ValueError('Given alias is the only entry in its group.') session.query(Nicknames).filter(Nicknames.slug == alias.lower()).delete() session.commit() except SQLAlchemyError: session.rollback() raise finally: self.ssession.remove()
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)) session = self.ssession() try: # Get second_id's values res = session.query(NickValues).filter( NickValues.nick_id == second_id).all() # Update first_id with second_id values if first_id doesn't have that key for row in res: first_res = session.query(NickValues) \ .filter(NickValues.nick_id == first_id) \ .filter(NickValues.key == row.key) \ .one_or_none() if not first_res: self.set_nick_value(first_nick, row.key, _deserialize(row.value)) session.query(NickValues).filter( NickValues.nick_id == second_id).delete() session.query(Nicknames) \ .filter(Nicknames.nick_id == second_id) \ .update({'nick_id': first_id}) session.commit() except SQLAlchemyError: session.rollback() raise finally: session.close()
def kick(self, bot, trigger): targetnick = Identifier(str(trigger.args[1])) # bot block if targetnick == bot.nick: self.remove_all_from_channel(trigger.sender) self.lock.acquire() self.chandict[trigger.sender]["joined"] = False self.chandict[trigger.sender]["reason"] = "kicked" self.lock.release() return # Identify nick_id = self.whois_ident(targetnick) # Verify nick is not in the channel list self.remove_from_channel(trigger.sender, targetnick, nick_id)
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) session = self.ssession() try: session.query(Nicknames).filter(Nicknames.nick_id == nick_id).delete() session.query(NickValues).filter(NickValues.nick_id == nick_id).delete() session.commit() except SQLAlchemyError: session.rollback() raise finally: self.ssession.remove()
def _record_who(bot, channel, user, host, nick, account=None, away=None, modes=None): nick = Identifier(nick) channel = Identifier(channel) if nick not in bot.users: bot.users[nick] = User(nick, user, host) user = bot.users[nick] if account == '0': user.account = None else: user.account = account user.away = away priv = 0 if modes: mapping = {'+': sopel.module.VOICE, '%': sopel.module.HALFOP, '@': sopel.module.OP, '&': sopel.module.ADMIN, '~': sopel.module.OWNER} for c in modes: priv = priv | mapping[c] if channel not in bot.channels: bot.channels[channel] = Channel(channel) bot.channels[channel].add_user(user, privs=priv)
def _nick_blocked(self, nick): """Check if a nickname is blocked. :param str nick: the nickname to check """ bad_nicks = self.config.core.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 kick(bot, trigger): """ Kick a user from the channel. """ 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.core.nick: bot.write(['KICK', channel, nick, reason])
def test_tags_pretrigger(nick): line = '@foo=bar;baz;sopel.chat/special=value :[email protected] PRIVMSG #Sopel :Hello, world' pretrigger = PreTrigger(nick, line) assert pretrigger.tags == {'baz': None, 'foo': 'bar', 'sopel.chat/special': 'value'} assert pretrigger.hostmask == '[email protected]' assert pretrigger.line == line assert pretrigger.args == ['#Sopel', 'Hello, world'] assert pretrigger.event == 'PRIVMSG' assert pretrigger.nick == Identifier('Foo') assert pretrigger.user == 'foo' assert pretrigger.host == 'example.com' assert pretrigger.sender == '#Sopel'
def ban(bot, trigger): """Ban a user from the channel The bot must be a channel operator for this command to work. """ if bot.channels[trigger.sender].privileges[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 test_bot_mixed_mode_removal(mockbot, ircfactory): """Ensure mixed mode types like ``-h+a`` are handled. Sopel 6.6.6 and older did not handle this correctly. .. seealso:: GitHub issue #1575 (https://github.com/sopel-irc/sopel/pull/1575). """ irc = ircfactory(mockbot) irc.channel_joined('#test', ['Uvoice', 'Uop']) irc.mode_set('#test', '+qao', ['Uvoice', 'Uvoice', 'Uvoice']) assert mockbot.channels["#test"].privileges[Identifier("Uop")] == 0 assert mockbot.channels["#test"].privileges[Identifier("Uvoice")] == ( ADMIN + OWNER + OP), 'Uvoice got +q, +a, and +o modes' irc.mode_set('#test', '-o+o-qa+v', ['Uvoice', 'Uop', 'Uvoice', 'Uvoice', 'Uvoice']) assert mockbot.channels["#test"].privileges[Identifier("Uop")] == OP, ( 'OP got +o only') assert mockbot.channels["#test"].privileges[Identifier( "Uvoice")] == VOICE, ('Uvoice got -o, -q, -a, then +v')
def unquiet(bot, trigger): """Unquiet a user The bot must be a channel operator for this command to work. """ if bot.channels[trigger.sender].privileges[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', channel, '-q', quietmask])
def _send_who(bot, channel): if 'WHOX' in bot.isupport: # WHOX syntax, see http://faerion.sourceforge.net/doc/irc/whox.var # Needed for accounts in WHO replies. The `CORE_QUERYTYPE` parameter # for WHO is used to identify the reply from the server and confirm # that it has the requested format. WHO replies with different # querytypes in the response were initiated elsewhere and will be # ignored. bot.write(['WHO', channel, 'a%nuachtf,' + CORE_QUERYTYPE]) else: # We might be on an old network, but we still care about keeping our # user list updated bot.write(['WHO', channel]) bot.channels[Identifier(channel)].last_who = datetime.datetime.utcnow()
def rpl_who(self, bot, trigger): if len(trigger.args) < 2 or trigger.args[1] not in self.who_reqs: # Ignored, some module probably called WHO return if len(trigger.args) != 8: return _, _, channel, user, host, nick, status, account = trigger.args nick = Identifier(nick) channel = Identifier(channel) # Identify nick_id = self.whois_ident(nick) # Verify nick is in the all list self.add_to_all(nick, nick_id) # Verify nick is in the all list self.add_to_current(nick, nick_id) # set current nick self.mark_current_nick(nick, nick_id) # add joined channel to nick list self.add_channel(channel, nick_id) # mark user as online self.mark_user_online(nick_id) # check if nick is registered self.whois_send(bot, nick)
def check_user_import(self, nick, nick_id=None): if not nick_id: nick = Identifier(nick) nick_id = botusers.get_nick_id(nick, True) if nick_id not in list(self.dict["sessioncache"].keys()): self.dict["sessioncache"][nick_id] = botdb.get_nick_value(nick, 'botai') or {} for predicate in list(self.dict["sessioncache"][nick_id].keys()): predval = self.dict["sessioncache"][nick_id][predicate] self.aiml_kernel.setPredicate(predicate, predval, nick_id) # defaults if "nick" not in list(self.dict["sessioncache"][nick_id].keys()): self.dict["sessioncache"][nick_id]["nick"] = nick self.aiml_kernel.setPredicate("nick", nick, nick_id)
def on_message(self, bot, trigger, message): nick = Identifier(trigger.nick) nick_id = botusers.get_nick_id(nick, True) self.check_user_import(nick, nick_id) message = self.bot_message_precipher(bot, trigger, message) aiml_response = self.aiml_kernel.respond(message, nick_id) if aiml_response: aiml_response = self.bot_message_decipher(bot, trigger, aiml_response) self.save_nick_session(nick, nick_id) self.save_brain() return aiml_response
def kick(bot, trigger): """ Kick a user from the channel. """ 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.core.nick: bot.write(['KICK', channel, nick], reason)
def test_ctcp_action_pretrigger(nick): line = ':[email protected] PRIVMSG #Sopel :\x01ACTION Hello, world\x01' pretrigger = PreTrigger(nick, line) assert pretrigger.tags == {'intent': 'ACTION'} assert pretrigger.hostmask == '[email protected]' assert pretrigger.line == line assert pretrigger.args == ['#Sopel', 'Hello, world'] assert pretrigger.text == '\x01ACTION Hello, world\x01' assert pretrigger.plain == 'Hello, world' assert pretrigger.event == 'PRIVMSG' assert pretrigger.nick == Identifier('Foo') assert pretrigger.user == 'foo' assert pretrigger.host == 'example.com' assert pretrigger.sender == '#Sopel'
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][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', channel, '-q', quietmask])
def get_channel_slug(self, chan): """Return the case-normalized representation of ``channel``. :param str channel: the channel name to normalize, with prefix (required) :return str: the case-normalized channel name (or "slug" representation) This is useful to make sure that a channel name is stored consistently in both the bot's own database and third-party plugins' databases/files, without regard for variation in case between different clients and/or servers on the network. """ chan = Identifier(chan) slug = chan.lower() session = self.ssession() try: count = session.query(ChannelValues) \ .filter(ChannelValues.channel == slug) \ .count() if count == 0: # see if it needs case-mapping migration old_rows = session.query(ChannelValues) \ .filter(ChannelValues.channel == Identifier._lower_swapped(chan)) old_count = old_rows.count() if old_count > 0: # it does! old_rows.update({ChannelValues.channel: slug}) session.commit() return slug except SQLAlchemyError: session.rollback() raise finally: self.ssession.remove()
def _record_who(bot, channel, user, host, nick, account=None, away=None, modes=None): nick = Identifier(nick) channel = Identifier(channel) if nick not in bot.users: usr = target.User(nick, user, host) bot.users[nick] = usr else: usr = bot.users[nick] # check for & fill in sparse User added by handle_names() if usr.host is None and host: usr.host = host if usr.user is None and user: usr.user = user if account == '0': usr.account = None else: usr.account = account if away is not None: usr.away = away priv = 0 if modes: mapping = { "+": module.VOICE, "%": module.HALFOP, "@": module.OP, "&": module.ADMIN, "~": module.OWNER, "!": module.OPER, } for c in modes: priv = priv | mapping[c] if channel not in bot.channels: bot.channels[channel] = target.Channel(channel) bot.channels[channel].add_user(usr, privs=priv) if channel not in bot.privileges: bot.privileges[channel] = dict() bot.privileges[channel][nick] = priv
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) session = self.ssession() try: result = session.query(Nicknames) \ .filter(Nicknames.slug == alias.lower()) \ .filter(Nicknames.canonical == alias) \ .one_or_none() if result: raise ValueError('Given alias is the only entry in its group.') nickname = Nicknames(nick_id=nick_id, slug=alias.lower(), canonical=alias) session.add(nickname) session.commit() except SQLAlchemyError: session.rollback() raise finally: session.close()
def luv_h8(bot, trigger, target, which, warn_nonexistent=True): target = Identifier(target) which = which.lower() # issue #18 pfx = change = selfreply = None # keep PyCharm & other linters happy if target.lower() not in bot.privileges[trigger.sender.lower()]: if warn_nonexistent: bot.reply("You can only %s someone who is here." % which) return if rep_too_soon(bot, trigger.nick): return if which == 'luv': selfreply = "No narcissism allowed!" pfx, change = 'in', 1 if which == 'h8': selfreply = "Go to 4chan if you really hate yourself!" pfx, change = 'de', -1 if not (pfx and change and selfreply): # safeguard against leaving something in the above mass-None assignment bot.say("Logic error! Please report this to %s." % bot.config.core.owner) return if is_self(bot, trigger.nick, target): bot.reply(selfreply) return rep = mod_rep(bot, trigger.nick, target, change) bot.say("%s has %screased %s's reputation score to %d" % (trigger.nick, pfx, target, rep))
def explode(bot, trigger): target = Identifier(trigger.group(3)) kmsg = 'KICK ' + trigger.sender + ' ' + target + \ ' : Oh, come on, ' + target + '! You could\'ve at least picked one! Now you\'re dead. Guts, all over the place. You see that? Guts, all over YourPants. (You should\'ve picked the ' + bombs[target.lower()][0] + ' wire.)' bot.write([kmsg]) bombs.pop(target.lower())
def track_modes(bot, trigger): """Track usermode changes and keep our lists of ops up to date.""" # Mode message format: <channel> *( ( "-" / "+" ) *<modes> *<modeparams> ) if len(trigger.args) < 3: # We need at least [channel, mode, nickname] to do anything useful # MODE messages with fewer args won't help us LOGGER.info("Received an apparently useless MODE message: {}" .format(trigger.raw)) return # Our old MODE parsing code checked if any of the args was empty. # Somewhere around here would be a good place to re-implement that if it's # actually necessary to guard against some non-compliant IRCd. But for now # let's just log malformed lines to the debug log. if not all(trigger.args): LOGGER.debug("The server sent a possibly malformed MODE message: {}" .format(trigger.raw)) # From here on, we will make a (possibly dangerous) assumption that the # received MODE message is more-or-less compliant channel = Identifier(trigger.args[0]) # 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. # TODO: Handle CHANTYPES from ISUPPORT numeric (005) # (Actually, most of this function should be rewritten again when we parse # ISUPPORT...) if channel.is_nick(): return modestring = trigger.args[1] nicks = [Identifier(nick) for nick in trigger.args[2:]] mapping = {'v': sopel.module.VOICE, 'h': sopel.module.HALFOP, 'o': sopel.module.OP, 'a': sopel.module.ADMIN, 'q': sopel.module.OWNER} # Parse modes before doing anything else modes = [] sign = '' for char in modestring: # There was a comment claiming IRC allows e.g. MODE +aB-c foo, but it # doesn't seem to appear in any RFCs. But modern.ircdocs.horse shows # it, so we'll leave in the extra parsing for now. if char in '+-': sign = char elif char in mapping: # Filter out unexpected modes and hope they don't have parameters modes.append(sign + char) # Try to map modes to arguments, after sanity-checking if len(modes) != len(nicks) or not all([nick.is_nick() for nick in nicks]): # Something fucky happening, like unusual batching of non-privilege # modes together with the ones we expect. Way easier to just re-WHO # than try to account for non-standard parameter-taking modes. _send_who(bot, channel) return pairs = dict(zip(modes, nicks)) for (mode, nick) in pairs.items(): priv = bot.channels[channel].privileges.get(nick, 0) # Log a warning if the two privilege-tracking data structures # get out of sync. That should never happen. # This is a good place to verify that bot.channels is doing # what it's supposed to do before ultimately removing the old, # deprecated bot.privileges structure completely. ppriv = bot.privileges[channel].get(nick, 0) if priv != ppriv: LOGGER.warning("Privilege data error! Please share Sopel's" "raw log with the developers, if enabled. " "(Expected {} == {} for {} in {}.)" .format(priv, ppriv, nick, channel)) value = mapping.get(mode[1]) if value is not None: if mode[0] == '+': priv = priv | value else: priv = priv & ~value bot.privileges[channel][nick] = priv bot.channels[channel].privileges[nick] = priv
def start(bot, trigger): """ Put a bomb in the specified user's pants. If they take too long or guess wrong, they die (and get kicked from the channel, if enabled). """ if not trigger.group(3): bot.say(STRINGS['TARGET_MISSING']) return NOLIMIT if bot.db.get_channel_value(trigger.sender, 'bombs_disabled'): bot.notice(STRINGS['CHANNEL_DISABLED'] % trigger.sender, trigger.nick) return NOLIMIT since_last = time_since_bomb(bot, trigger.nick) if since_last < TIMEOUT and not trigger.admin: bot.notice(STRINGS['TIMEOUT_REMAINING'] % (TIMEOUT - since_last), trigger.nick) return global BOMBS target = Identifier(trigger.group(3)) target_unbombable = bot.db.get_nick_value(target, 'unbombable') if target == bot.nick: bot.say(STRINGS['TARGET_BOT']) return NOLIMIT if is_self(bot, trigger.nick, target): bot.say(STRINGS['TARGET_SELF'] % trigger.nick) return NOLIMIT if target.lower() not in bot.privileges[trigger.sender.lower()]: bot.say(STRINGS['TARGET_IMAGINARY']) return NOLIMIT if target_unbombable and not trigger.admin: bot.say(STRINGS['TARGET_DISABLED'] % target) return NOLIMIT if bot.db.get_nick_value(trigger.nick, 'unbombable'): bot.say(STRINGS['NOT_WHILE_DISABLED'] % trigger.nick) return NOLIMIT with lock: if target.lower() in BOMBS: bot.say(STRINGS['TARGET_FULL'] % target) return NOLIMIT wires = [COLORS[i] for i in sorted(sample(xrange(len(COLORS)), randrange(3, 5)))] num_wires = len(wires) wires_list = [formatting.color(str(wire), str(wire)) for wire in wires] wires_list = ", ".join(wires_list[:-2] + [" and ".join(wires_list[-2:])]).replace('Light_', '') wires = [wire.replace('Light_', '') for wire in wires] color = choice(wires) bot.say( choice(STRINGS['BOMB_PLANTED']) % {'target': target, 'fuse_time': STRINGS['FUSE'], 'wire_num': num_wires, 'wire_list': wires_list, 'prefix': bot.config.core.help_prefix or '.' }) bot.notice(STRINGS['BOMB_ANSWER'] % (target, color), trigger.nick) if target_unbombable: bot.notice(STRINGS['TARGET_DISABLED_FYI'] % target, trigger.nick) timer = Timer(FUSE, explode, (bot, trigger)) BOMBS[target.lower()] = {'wires': wires, 'color': color, 'timer': timer, 'target': target, 'bomber': trigger.nick } timer.start() bombs_planted = bot.db.get_nick_value(trigger.nick, 'bombs_planted') or 0 bot.db.set_nick_value(trigger.nick, 'bombs_planted', bombs_planted + 1) bot.db.set_nick_value(trigger.nick, 'bomb_last_planted', time.time())