def toggle_safety(bot: SopelWrapper, trigger: Trigger): """Set safety setting for channel.""" if not trigger.admin and bot.channels[trigger.sender].privileges[ trigger.nick] < plugin.OP: bot.reply('Only channel operators can change safety settings') return new_mode = None if trigger.group(2): new_mode = trigger.group(2).lower() if not new_mode or (new_mode != "default" and new_mode not in SAFETY_MODES): bot.reply( "Current mode: {}. Available modes: {}, or default ({})".format( bot.db.get_channel_value( trigger.sender, "safety", "default", ), ", ".join(SAFETY_MODES), bot.settings.safety.default_mode, )) return if new_mode == "default": bot.db.delete_channel_value(trigger.sender, "safety") else: bot.db.set_channel_value(trigger.sender, "safety", new_mode) bot.say('Safety is now set to "%s" for this channel' % new_mode)
def test_bot_legacy_permissions(sopel_bot): """ Make sure permissions match after being updated from both RPL_NAMREPLY and RPL_WHOREPLY, #1482 """ nick = Identifier("Admin") # RPL_NAMREPLY pretrigger = PreTrigger("Foo", ":test.example.com 353 Foo = #test :Foo ~@Admin") trigger = Trigger(sopel_bot.config, pretrigger, None) coretasks.handle_names(sopel_bot, trigger) assert (sopel_bot.channels["#test"].privileges[nick] == sopel_bot.privileges["#test"][nick]) # RPL_WHOREPLY pretrigger = PreTrigger( "Foo", ":test.example.com 352 Foo #test ~Admin adminhost test.example.com Admin Hr~ :0 Admin", ) trigger = Trigger(sopel_bot.config, pretrigger, None) coretasks.recv_who(sopel_bot, trigger) assert (sopel_bot.channels["#test"].privileges[nick] == sopel_bot.privileges["#test"][nick]) assert sopel_bot.users.get(nick) is not None
def test_intents_trigger(nick): line = '@intent=ACTION :[email protected] PRIVMSG #Sopel :Hello, world' pretrigger = PreTrigger(nick, line) config = MockConfig() config.core.owner = 'Foo' config.core.admins = ['Bar'] fakematch = re.match('.*', line) trigger = Trigger(config, pretrigger, fakematch) assert trigger.sender == '#Sopel' assert trigger.raw == line assert trigger.is_privmsg is False assert trigger.hostmask == '[email protected]' assert trigger.user == 'foo' assert trigger.nick == Identifier('Foo') assert trigger.host == 'example.com' assert trigger.event == 'PRIVMSG' assert trigger.match == fakematch assert trigger.group == fakematch.group assert trigger.groups == fakematch.groups assert trigger.args == ['#Sopel', 'Hello, world'] assert trigger.tags == {'intent': 'ACTION'} assert trigger.admin is True assert trigger.owner is True
def trigger_account(bot): line = '@account=egg :[email protected] PRIVMSG #Sopel :Hello, world' return Trigger( bot.config, PreTrigger(tools.Identifier('egg'), line), None, 'egg')
def test_ircv3_extended_join_trigger(nick): line = ':[email protected] JOIN #Sopel bar :Real Name' pretrigger = PreTrigger(nick, line) config = MockConfig() config.core.owner_account = 'bar' fakematch = re.match('.*', line) trigger = Trigger(config, pretrigger, fakematch) assert trigger.sender == '#Sopel' assert trigger.raw == line assert trigger.is_privmsg is False assert trigger.hostmask == '[email protected]' assert trigger.user == 'foo' assert trigger.nick == Identifier('Foo') assert trigger.host == 'example.com' assert trigger.event == 'JOIN' assert trigger.match == fakematch assert trigger.group == fakematch.group assert trigger.groups == fakematch.groups assert trigger.args == ['#Sopel', 'bar', 'Real Name'] assert trigger.account == 'bar' assert trigger.tags == {'account': 'bar'} assert trigger.owner is True assert trigger.admin is True
def test_ircv3_extended_join_trigger(nick, configfactory): line = ':[email protected] JOIN #Sopel bar :Real Name' pretrigger = PreTrigger(nick, line) config = configfactory('default.cfg', TMP_CONFIG) fakematch = re.match('.*', line) trigger = Trigger(config, pretrigger, fakematch) assert trigger.sender == '#Sopel' assert trigger.raw == line assert trigger.is_privmsg is False assert trigger.hostmask == '[email protected]' assert trigger.user == 'foo' assert trigger.nick == Identifier('Foo') assert trigger.host == 'example.com' assert trigger.event == 'JOIN' assert trigger.match == fakematch assert trigger.group == fakematch.group assert trigger.groups == fakematch.groups assert trigger.args == ['#Sopel', 'bar', 'Real Name'] assert trigger.plain == 'Real Name' assert trigger.account == 'bar' assert trigger.tags == {'account': 'bar'} assert trigger.ctcp is None assert trigger.owner is True assert trigger.admin is True
def test_ircv3_intents_trigger(nick, configfactory): line = '@intent=ACTION :[email protected] PRIVMSG #Sopel :Hello, world' pretrigger = PreTrigger(nick, line) config = configfactory('default.cfg', TMP_CONFIG) fakematch = re.match('.*', line) trigger = Trigger(config, pretrigger, fakematch) assert trigger.sender == '#Sopel' assert trigger.raw == line assert trigger.is_privmsg is False assert trigger.hostmask == '[email protected]' assert trigger.user == 'bar' assert trigger.nick == Identifier('Foo') assert trigger.host == 'example.com' assert trigger.event == 'PRIVMSG' assert trigger.match == fakematch assert trigger.group == fakematch.group assert trigger.groups == fakematch.groups assert trigger.groupdict == fakematch.groupdict assert trigger.args == ['#Sopel', 'Hello, world'] assert trigger.plain == 'Hello, world' assert trigger.tags == {'intent': 'ACTION'} assert trigger.ctcp == 'ACTION' assert trigger.account is None assert trigger.admin is True assert trigger.owner is True
def dispatch(self, pretrigger): args = pretrigger.args event, args, text = pretrigger.event, args, args[-1] if args else '' if self.config.core.nick_blocks or self.config.core.host_blocks: nick_blocked = self._nick_blocked(pretrigger.nick) host_blocked = self._host_blocked(pretrigger.host) else: nick_blocked = host_blocked = None list_of_blocked_functions = [] for priority in ('high', 'medium', 'low'): items = list(self._callables[priority].items()) for regexp, funcs in items: match = regexp.match(text) if not match: continue user_obj = self.users.get(pretrigger.nick) account = user_obj.account if user_obj else None trigger = Trigger(self.config, pretrigger, match, account) wrapper = self.SopelWrapper(self, trigger) for func in funcs: if (not trigger.admin and not func.unblockable and (nick_blocked or host_blocked)): function_name = "%s.%s" % ( func.__module__, func.__name__ ) list_of_blocked_functions.append(function_name) continue if event not in func.event: continue if (hasattr(func, 'intents') and trigger.tags.get('intent') not in func.intents): continue if func.thread: targs = (func, wrapper, trigger) t = threading.Thread(target=self.call, args=targs) t.start() else: self.call(func, wrapper, trigger) if list_of_blocked_functions: if nick_blocked and host_blocked: block_type = 'both' elif nick_blocked: block_type = 'nick' else: block_type = 'host' LOGGER.info( "[%s]%s prevented from using %s.", block_type, trigger.nick, ', '.join(list_of_blocked_functions) )
def test_mode_colon(sopel): """ Ensure mode messages with colons are parsed properly """ # RPL_NAMREPLY to create Users and (zeroed) privs for user in set("Uvoice Uadmin".split(" ")): pretrigger = PreTrigger( "Foo", ":test.example.com 353 Foo = #test :Foo %s" % user) trigger = Trigger(sopel.config, pretrigger, None) coretasks.handle_names(MockSopelWrapper(sopel, trigger), trigger) pretrigger = PreTrigger("Foo", "MODE #test +av Uadmin :Uvoice") trigger = Trigger(sopel.config, pretrigger, None) coretasks.track_modes(MockSopelWrapper(sopel, trigger), trigger) assert sopel.channels["#test"].privileges[Identifier("Uvoice")] == VOICE assert sopel.channels["#test"].privileges[Identifier("Uadmin")] == ADMIN
def test_ircv3_account_tag_trigger(nick, configfactory): line = '@account=bar :[email protected] PRIVMSG #Sopel :Hello, world' pretrigger = PreTrigger(nick, line) config = configfactory('default.cfg', TMP_CONFIG_ACCOUNT) fakematch = re.match('.*', line) trigger = Trigger(config, pretrigger, fakematch) assert trigger.admin is True assert trigger.owner is True
def get_weather(bot: SopelWrapper, trigger: Trigger) -> None: """ Gets the weather at a given location and returns the first result """ location_lookup_args = trigger.group(2) if location_lookup_args is None: location_lookup_args = bot.db.get_nick_value(trigger.nick, 'place_id') if not location_lookup_args: bot.reply("I don't know where you live. " "Give me a location, like {pfx}{command} London, " "or tell me where you live by saying {pfx}setlocation " "London, for example.".format( command=trigger.group(1), pfx=bot.config.core.help_prefix)) return plugin.NOLIMIT api = get_owm_api(bot) location = parse_location_args(location_lookup_args) message = get_weather_message(api, location) bot.reply(message)
def url_unban(bot: SopelWrapper, trigger: Trigger): """Allow a URL for auto title. Use ``urlpallow`` to allow a pattern instead of a URL. """ url = trigger.group(2) if not url: bot.reply('This command requires a URL to allow.') return if trigger.group(1) in ['urlpallow', 'urlpunban']: # validate regex pattern try: re.compile(url) except re.error as err: bot.reply('Invalid regex pattern: %s' % err) return else: # escape the URL to ensure a valid pattern url = re.escape(url) patterns = bot.settings.url.exclude if url not in patterns: bot.reply('This URL was not excluded from auto title.') return # update settings patterns.remove(url) bot.settings.url.exclude = patterns # set the config option bot.settings.save() LOGGER.info('%s allowed the URL pattern "%s"', trigger.nick, url) # re-compile bot.memory['url_exclude'] = [re.compile(s) for s in patterns] # tell the user bot.reply('This URL is not excluded from auto title anymore.')
def test_ircv3_account_tag_trigger(nick): line = '@account=Foo :[email protected] PRIVMSG #Sopel :Hello, world' pretrigger = PreTrigger(nick, line) config = MockConfig() config.core.owner_account = 'Foo' config.core.admins = ['Bar'] fakematch = re.match('.*', line) trigger = Trigger(config, pretrigger, fakematch) assert trigger.admin is True assert trigger.owner is True
def test_ircv3_server_time_trigger(nick, configfactory): line = '@time=2016-01-09T03:15:42.000Z :[email protected] PRIVMSG #Sopel :Hello, world' pretrigger = PreTrigger(nick, line) config = configfactory('default.cfg', TMP_CONFIG) fakematch = re.match('.*', line) trigger = Trigger(config, pretrigger, fakematch) assert trigger.time == datetime.datetime(2016, 1, 9, 3, 15, 42, 0) # Spec-breaking string line = '@time=2016-01-09T04:20 :[email protected] PRIVMSG #Sopel :Hello, world' pretrigger = PreTrigger(nick, line) assert pretrigger.time is not None
def test_bot_mixed_mode_types(sopel): """ Ensure mixed argument- and non-argument- modes are handled Sopel 6.6.6 and older did not behave well. #1575 """ # RPL_NAMREPLY to create Users and (zeroed) privs for user in set("Uvoice Uop Uadmin Uvoice2 Uop2 Uadmin2".split(" ")): pretrigger = PreTrigger( "Foo", ":test.example.com 353 Foo = #test :Foo %s" % user) trigger = Trigger(sopel.config, pretrigger, None) coretasks.handle_names(MockSopelWrapper(sopel, trigger), trigger) # Non-attribute-requiring non-permission mode pretrigger = PreTrigger("Foo", "MODE #test +amov Uadmin Uop Uvoice") trigger = Trigger(sopel.config, pretrigger, None) coretasks.track_modes(MockSopelWrapper(sopel, trigger), trigger) assert sopel.channels["#test"].privileges[Identifier("Uvoice")] == VOICE assert sopel.channels["#test"].privileges[Identifier("Uop")] == OP assert sopel.channels["#test"].privileges[Identifier("Uadmin")] == ADMIN # Attribute-requiring non-permission modes # This results in a _send_who, which isn't supported in MockSopel or this # test, so we just make sure it results in an exception instead of privesc. pretrigger = PreTrigger("Foo", "MODE #test +abov Uadmin2 x!y@z Uop2 Uvoice2") trigger = Trigger(sopel.config, pretrigger, None) try: coretasks.track_modes(MockSopelWrapper(sopel, trigger), trigger) except AttributeError as e: if e.args[ 0] == "'MockSopel' object has no attribute 'enabled_capabilities'": return assert sopel.channels["#test"].privileges[Identifier("Uvoice2")] == VOICE assert sopel.channels["#test"].privileges[Identifier("Uop2")] == OP assert sopel.channels["#test"].privileges[Identifier("Uadmin2")] == ADMIN
def vt_command(bot: SopelWrapper, trigger: Trigger): """Look up VT results on demand.""" if not bot.settings.safety.vt_api_key: bot.reply("Sorry, I don't have a VirusTotal API key configured.") return url = trigger.group(2) safe_url = safeify_url(url) result = virustotal_lookup(bot, url, max_cache_age=timedelta(minutes=1)) if not result: bot.reply("Sorry, an error occurred while looking that up.") return analysis = result["virustotal_data"]["last_analysis_stats"] result_types = { "malicious": colors.RED, "suspicious": colors.YELLOW, "harmless": colors.GREEN, "undetected": colors.GREY, } result_strs = [] for result_type, result_color in result_types.items(): if analysis[result_type] == 0: result_strs.append("0 " + result_type) else: result_strs.append( bold( color( str(analysis[result_type]) + " " + result_type, result_color))) results_str = ", ".join(result_strs) vt_scan_time = datetime.fromtimestamp( result["virustotal_data"]["last_analysis_date"], timezone.utc, ) bot.reply("Results: {} at {} for {}".format( results_str, tools.time.format_time( bot.db, bot.config, nick=trigger.nick, channel=trigger.sender, time=vt_scan_time, ), safe_url, ))
def test_bot_mixed_mode_removal(sopel): """ Ensure mixed mode types like -h+a are handled Sopel 6.6.6 and older did not handle this correctly. #1575 """ # RPL_NAMREPLY to create Users and (zeroed) privs for user in set("Uvoice Uop".split(" ")): pretrigger = PreTrigger( "Foo", ":test.example.com 353 Foo = #test :Foo %s" % user) trigger = Trigger(sopel.config, pretrigger, None) coretasks.handle_names(MockSopelWrapper(sopel, trigger), trigger) pretrigger = PreTrigger("Foo", "MODE #test +qao Uvoice Uvoice Uvoice") trigger = Trigger(sopel.config, pretrigger, None) coretasks.track_modes(MockSopelWrapper(sopel, trigger), trigger) pretrigger = PreTrigger( "Foo", "MODE #test -o+o-qa+v Uvoice Uop Uvoice Uvoice Uvoice") trigger = Trigger(sopel.config, pretrigger, None) coretasks.track_modes(MockSopelWrapper(sopel, trigger), trigger) assert sopel.channels["#test"].privileges[Identifier("Uvoice")] == VOICE assert sopel.channels["#test"].privileges[Identifier("Uop")] == OP
def test_bot_mixed_modes(sopel): """ Ensure mixed modes like +vha are tracked correctly. Sopel 6.6.6 and older would assign all modes to all users. #1575 """ # RPL_NAMREPLY to create Users and (zeroed) privs for user in set("Unothing Uvoice Uhalfop Uop Uadmin Uowner".split(" ")): pretrigger = PreTrigger( "Foo", ":test.example.com 353 Foo = #test :Foo %s" % user) trigger = Trigger(sopel.config, pretrigger, None) coretasks.handle_names(MockSopelWrapper(sopel, trigger), trigger) pretrigger = PreTrigger( "Foo", "MODE #test +qvhao Uowner Uvoice Uhalfop Uadmin Uop") trigger = Trigger(sopel.config, pretrigger, None) coretasks.track_modes(MockSopelWrapper(sopel, trigger), trigger) assert sopel.channels["#test"].privileges[Identifier("Unothing")] == 0 assert sopel.channels["#test"].privileges[Identifier("Uvoice")] == VOICE assert sopel.channels["#test"].privileges[Identifier("Uhalfop")] == HALFOP assert sopel.channels["#test"].privileges[Identifier("Uop")] == OP assert sopel.channels["#test"].privileges[Identifier("Uadmin")] == ADMIN assert sopel.channels["#test"].privileges[Identifier("Uowner")] == OWNER
def test_ircv3_server_time_trigger(nick): line = '@time=2016-01-09T03:15:42.000Z :[email protected] PRIVMSG #Sopel :Hello, world' pretrigger = PreTrigger(nick, line) config = MockConfig() config.core.owner = 'Foo' config.core.admins = ['Bar'] fakematch = re.match('.*', line) trigger = Trigger(config, pretrigger, fakematch) assert trigger.time == datetime.datetime(2016, 1, 9, 3, 15, 42, 0) # Spec-breaking string line = '@time=2016-01-09T04:20 :[email protected] PRIVMSG #Sopel :Hello, world' pretrigger = PreTrigger(nick, line) assert pretrigger.time is not None
def title_command(bot: SopelWrapper, trigger: Trigger): """ Show the title or URL information for the given URL, or the last URL seen in this channel. """ result_count = 0 if not trigger.group(2): if trigger.sender not in bot.memory['last_seen_url']: return urls = [bot.memory["last_seen_url"][trigger.sender]] else: # needs to be a list so len() can be checked later urls = list(web.search_urls(trigger)) for url, title, domain, tinyurl, dispatched in process_urls( bot, trigger, urls, requested=True): if dispatched: result_count += 1 continue message = "%s | %s" % (title, domain) if tinyurl: message += ' ( %s )' % tinyurl bot.reply(message) bot.memory['last_seen_url'][trigger.sender] = url result_count += 1 expected_count = len(urls) if result_count < expected_count: if expected_count == 1: bot.reply( "Sorry, fetching that title failed. Make sure the site is working." ) elif result_count == 0: bot.reply("Sorry, I couldn't fetch titles for any of those.") else: bot.reply( "I couldn't get all of the titles, but I fetched what I could!" )
def dispatch(self, pretrigger): """Dispatch a parsed message to any registered callables. :param PreTrigger pretrigger: a parsed message from the server """ args = pretrigger.args text = args[-1] if args else '' event = pretrigger.event intent = pretrigger.tags.get('intent') nick = pretrigger.nick is_echo_message = nick.lower() == self.nick.lower() user_obj = self.users.get(nick) account = user_obj.account if user_obj else None if self.config.core.nick_blocks or self.config.core.host_blocks: nick_blocked = self._nick_blocked(pretrigger.nick) host_blocked = self._host_blocked(pretrigger.host) else: nick_blocked = host_blocked = None blocked = bool(nick_blocked or host_blocked) list_of_blocked_functions = [] for priority in ('high', 'medium', 'low'): for regexp, funcs in self._callables[priority].items(): match = regexp.match(text) if not match: continue for func in funcs: trigger = Trigger(self.config, pretrigger, match, account) # check blocked nick/host if blocked and not func.unblockable and not trigger.admin: function_name = "%s.%s" % (func.__module__, func.__name__) list_of_blocked_functions.append(function_name) continue # check event if event not in func.event: continue # check intents if hasattr(func, 'intents'): if not intent: continue match = any( func_intent.match(intent) for func_intent in func.intents) if not match: continue # check echo-message feature if is_echo_message and not func.echo: continue # call triggered function wrapper = SopelWrapper(self, trigger) if func.thread: targs = (func, wrapper, trigger) t = threading.Thread(target=self.call, args=targs) t.start() else: self.call(func, wrapper, trigger) if list_of_blocked_functions: if nick_blocked and host_blocked: block_type = 'both' elif nick_blocked: block_type = 'nick' else: block_type = 'host' LOGGER.info("[%s]%s prevented from using %s.", block_type, nick, ', '.join(list_of_blocked_functions))
def dispatch(self, pretrigger): """Dispatch a parsed message to any registered callables. :param pretrigger: a parsed message from the server :type pretrigger: :class:`~sopel.trigger.PreTrigger` The ``pretrigger`` (a parsed message) is used to find matching rules; it will retrieve them by order of priority, and execute them. It runs triggered rules in separate threads, unless they are marked otherwise. However, it won't run triggered blockable rules at all when they can't be executed for blocked nickname or hostname. .. seealso:: The pattern matching is done by the :class:`Rules Manager<sopel.plugins.rules.Manager>`. """ # list of commands running in separate threads for this dispatch running_triggers = [] # nickname/hostname blocking nick_blocked, host_blocked = self._is_pretrigger_blocked(pretrigger) blocked = bool(nick_blocked or host_blocked) list_of_blocked_rules = set() # account info nick = pretrigger.nick user_obj = self.users.get(nick) account = user_obj.account if user_obj else None for rule, match in self._rules_manager.get_triggered_rules( self, pretrigger): trigger = Trigger(self.settings, pretrigger, match, account) is_unblockable = trigger.admin or rule.is_unblockable() if blocked and not is_unblockable: list_of_blocked_rules.add(str(rule)) continue wrapper = SopelWrapper(self, trigger, output_prefix=rule.get_output_prefix()) if rule.is_threaded(): # run in a separate thread targs = (rule, wrapper, trigger) t = threading.Thread(target=self.call_rule, args=targs) plugin_name = rule.get_plugin_name() rule_label = rule.get_rule_label() t.name = '%s-%s-%s' % (t.name, plugin_name, rule_label) t.start() running_triggers.append(t) else: # direct call self.call_rule(rule, wrapper, trigger) # update currently running triggers self._update_running_triggers(running_triggers) if list_of_blocked_rules: if nick_blocked and host_blocked: block_type = 'both blocklists' elif nick_blocked: block_type = 'nick blocklist' else: block_type = 'host blocklist' LOGGER.debug( "%s prevented from using %s by %s.", pretrigger.nick, ', '.join(list_of_blocked_rules), block_type, )
def get_triggered_callables(self, priority, pretrigger, blocked): """Get triggered callables by priority. :param str priority: priority to retrieve callables :param pretrigger: Sopel pretrigger object :type pretrigger: :class:`~sopel.trigger.PreTrigger` :param bool blocked: true if nick or channel is blocked from triggering callables :return: a tuple with the callable, the trigger, and if it's blocked :rtype: :class:`tuple` This methods retrieves all triggered callables for this ``priority`` level. It yields each function with its :class:`trigger <sopel.trigger.Trigger>` object and a boolean flag to tell if this callable is blocked or not. To be triggered, a callable must match the ``pretrigger`` using a regex pattern. Then it must comply with other criteria (if any) such as event, intents, and echo-message filters. A triggered callable won't actually be invoked by Sopel if the nickname or hostname is ``blocked``, *unless* the nickname is an admin or the callable is marked as "unblockable". .. seealso:: Sopel invokes triggered callables in its :meth:`~.dispatch` method. The priority of a callable can be set with the :func:`sopel.module.priority` decorator. Other decorators from :mod:`sopel.module` provide additional criteria and conditions. """ args = pretrigger.args text = args[-1] if args else '' event = pretrigger.event intent = pretrigger.tags.get('intent') nick = pretrigger.nick is_echo_message = (nick.lower() == self.nick.lower() and event in ["PRIVMSG", "NOTICE"]) user_obj = self.users.get(nick) account = user_obj.account if user_obj else None # get a copy of the list of (regex, function) to prevent race-condition # e.g. when a callable wants to remove callable (other or itself) # from the bot, Python won't (and must not) allow to modify a dict # while it iterates over this very same dict. items = list(self._callables[priority].items()) for regexp, funcs in items: match = regexp.match(text) if not match: continue for func in funcs: trigger = Trigger(self.settings, pretrigger, match, account) # check event if event not in func.event: continue # check intents if hasattr(func, 'intents'): if not intent: continue match = any( func_intent.match(intent) for func_intent in func.intents) if not match: continue # check echo-message feature if is_echo_message and not func.echo: continue is_unblockable = func.unblockable or trigger.admin is_blocked = blocked and not is_unblockable yield (func, trigger, is_blocked)
def trigger_owner(bot): line = ':[email protected] PRIVMSG #Sopel :Hello, world' return Trigger(bot.config, PreTrigger(tools.Identifier('Bar'), line), None)
def trigger(bot, pretrigger): return Trigger(bot.config, pretrigger, None)
def trigger_pm(bot, pretrigger_pm): return Trigger(bot.config, pretrigger_pm, None)