diff --git a/sopel/bot.py b/sopel/bot.py index 5354602b14..b40bdd68bb 100644 --- a/sopel/bot.py +++ b/sopel/bot.py @@ -14,7 +14,6 @@ import logging import re import threading -import time from types import MappingProxyType from typing import ( Any, @@ -27,9 +26,8 @@ Union, ) -from sopel import db, irc, logger, plugin, plugins, tools +from sopel import db, irc, logger, plugins, tools from sopel.irc import modes -from sopel.lifecycle import deprecated from sopel.plugins import ( capabilities as plugin_capabilities, jobs as plugin_jobs, @@ -58,21 +56,6 @@ def __init__(self, config, daemon=False): self._cap_requests_manager = plugin_capabilities.Manager() self._scheduler = plugin_jobs.Scheduler(self) - self._url_callbacks = tools.SopelMemory() - """Tracking of manually registered URL callbacks. - - Should be manipulated only by use of :meth:`register_url_callback` and - :meth:`unregister_url_callback` methods, which are deprecated. - - Remove in Sopel 9, along with the above related methods. - """ - - self._times = {} - """ - A dictionary mapping lowercased nicks to dictionaries which map - function names to the time which they were last used by that nick. - """ - self.modeparser = modes.ModeParser() """A mode parser used to parse ``MODE`` messages and modestrings.""" @@ -656,112 +639,6 @@ def call_rule( except Exception as error: self.error(trigger, exception=error) - def call( - self, - func: Any, - sopel: 'SopelWrapper', - trigger: Trigger, - ) -> None: - """Call a function, applying any rate limits or other restrictions. - - :param func: the function to call - :type func: :term:`function` - :param sopel: a SopelWrapper instance - :type sopel: :class:`SopelWrapper` - :param Trigger trigger: the Trigger object for the line from the server - that triggered this call - """ - nick = trigger.nick - current_time = time.time() - if nick not in self._times: - self._times[nick] = dict() - if self.nick not in self._times: - self._times[self.nick] = dict() - if not trigger.is_privmsg and trigger.sender not in self._times: - self._times[trigger.sender] = dict() - - if not trigger.admin and not func.unblockable: - if func in self._times[nick]: - usertimediff = current_time - self._times[nick][func] - if func.user_rate > 0 and usertimediff < func.user_rate: - LOGGER.info( - "%s prevented from using %s in %s due to user limit: %d < %d", - trigger.nick, func.__name__, trigger.sender, usertimediff, - func.user_rate - ) - return - if func in self._times[self.nick]: - globaltimediff = current_time - self._times[self.nick][func] - if func.global_rate > 0 and globaltimediff < func.global_rate: - LOGGER.info( - "%s prevented from using %s in %s due to global limit: %d < %d", - trigger.nick, func.__name__, trigger.sender, globaltimediff, - func.global_rate - ) - return - - if not trigger.is_privmsg and func in self._times[trigger.sender]: - chantimediff = current_time - self._times[trigger.sender][func] - if func.channel_rate > 0 and chantimediff < func.channel_rate: - LOGGER.info( - "%s prevented from using %s in %s due to channel limit: %d < %d", - trigger.nick, func.__name__, trigger.sender, chantimediff, - func.channel_rate - ) - return - - # if channel has its own config section, check for excluded plugins/plugin methods, - # but only if the source plugin is NOT coretasks, because we NEED those handlers. - # Normal, whole-bot configuration will not let you disable coretasks either. - if trigger.sender in self.config and func.plugin_name != 'coretasks': - channel_config = self.config[trigger.sender] - LOGGER.debug( - "Evaluating configuration for %s.%s in channel %s", - func.plugin_name, func.__name__, trigger.sender - ) - - # disable listed plugins completely on provided channel - if 'disable_plugins' in channel_config: - disabled_plugins = channel_config.disable_plugins.split(',') - - # if "*" is used, we are disabling all plugins on provided channel - if '*' in disabled_plugins: - LOGGER.debug( - "All plugins disabled in %s; skipping execution of %s.%s", - trigger.sender, func.plugin_name, func.__name__ - ) - return - if func.plugin_name in disabled_plugins: - LOGGER.debug( - "Plugin %s is disabled in %s; skipping execution of %s", - func.plugin_name, trigger.sender, func.__name__ - ) - return - - # disable chosen methods from plugins - if 'disable_commands' in channel_config: - disabled_commands = literal_eval(channel_config.disable_commands) - - if func.plugin_name in disabled_commands: - if func.__name__ in disabled_commands[func.plugin_name]: - LOGGER.debug( - "Skipping execution of %s.%s in %s: disabled_commands matched", - func.plugin_name, func.__name__, trigger.sender - ) - return - - try: - exit_code = func(sopel, trigger) - except Exception as error: # TODO: Be specific - exit_code = None - self.error(trigger, exception=error) - - if exit_code != plugin.NOLIMIT: - self._times[nick][func] = current_time - self._times[self.nick][func] = current_time - if not trigger.is_privmsg: - self._times[trigger.sender][func] = current_time - def _is_pretrigger_blocked( self, pretrigger: PreTrigger, @@ -1076,148 +953,6 @@ def _shutdown(self) -> None: # Avoid calling shutdown methods if we already have. self.shutdown_methods = [] - # URL callbacks management - - @deprecated( - reason='Issues with @url decorator have been fixed. Simply use that.', - version='7.1', - warning_in='8.0', - removed_in='9.0', - ) - def register_url_callback(self, pattern, callback): - """Register a ``callback`` for URLs matching the regex ``pattern``. - - :param pattern: compiled regex pattern to register - :type pattern: :ref:`re.Pattern ` - :param callback: callable object to handle matching URLs - :type callback: :term:`function` - - .. versionadded:: 7.0 - - This method replaces manual management of ``url_callbacks`` in - Sopel's plugins, so instead of doing this in ``setup()``:: - - if 'url_callbacks' not in bot.memory: - bot.memory['url_callbacks'] = tools.SopelMemory() - - regex = re.compile(r'http://example.com/path/.*') - bot.memory['url_callbacks'][regex] = callback - - use this much more concise pattern:: - - regex = re.compile(r'http://example.com/path/.*') - bot.register_url_callback(regex, callback) - - It's recommended you completely avoid manual management of URL - callbacks through the use of :func:`sopel.plugin.url`. - - .. deprecated:: 7.1 - - Made obsolete by fixes to the behavior of - :func:`sopel.plugin.url`. Will be removed in Sopel 9. - - .. versionchanged:: 8.0 - - Stores registered callbacks in an internal property instead of - ``bot.memory['url_callbacks']``. - - """ - if isinstance(pattern, str): - pattern = re.compile(pattern) - - self._url_callbacks[pattern] = callback - - @deprecated( - reason='Issues with @url decorator have been fixed. Simply use that.', - version='7.1', - warning_in='8.0', - removed_in='9.0', - ) - def unregister_url_callback(self, pattern, callback): - """Unregister the callback for URLs matching the regex ``pattern``. - - :param pattern: compiled regex pattern to unregister callback - :type pattern: :ref:`re.Pattern ` - :param callback: callable object to remove - :type callback: :term:`function` - - .. versionadded:: 7.0 - - This method replaces manual management of ``url_callbacks`` in - Sopel's plugins, so instead of doing this in ``shutdown()``:: - - regex = re.compile(r'http://example.com/path/.*') - try: - del bot.memory['url_callbacks'][regex] - except KeyError: - pass - - use this much more concise pattern:: - - regex = re.compile(r'http://example.com/path/.*') - bot.unregister_url_callback(regex, callback) - - It's recommended you completely avoid manual management of URL - callbacks through the use of :func:`sopel.plugin.url`. - - .. deprecated:: 7.1 - - Made obsolete by fixes to the behavior of - :func:`sopel.plugin.url`. Will be removed in Sopel 9. - - .. versionchanged:: 8.0 - - Deletes registered callbacks from an internal property instead of - ``bot.memory['url_callbacks']``. - - """ - if isinstance(pattern, str): - pattern = re.compile(pattern) - - try: - del self._url_callbacks[pattern] - except KeyError: - pass - - def search_url_callbacks(self, url): - """Yield callbacks whose regex pattern matches the ``url``. - - :param str url: URL found in a trigger - :return: yield 2-value tuples of ``(callback, match)`` - - For each pattern that matches the ``url`` parameter, it yields a - 2-value tuple of ``(callable, match)`` for that pattern. - - The ``callable`` is the one registered with - :meth:`register_url_callback`, and the ``match`` is the result of - the regex pattern's ``search`` method. - - .. versionadded:: 7.0 - - .. versionchanged:: 8.0 - - Searches for registered callbacks in an internal property instead - of ``bot.memory['url_callbacks']``. - - .. deprecated:: 8.0 - - Made obsolete by fixes to the behavior of - :func:`sopel.plugin.url`. Will be removed in Sopel 9. - - .. seealso:: - - The Python documentation for the `re.search`__ function and - the `match object`__. - - .. __: https://docs.python.org/3.7/library/re.html#re.search - .. __: https://docs.python.org/3.7/library/re.html#match-objects - - """ - for regex, function in self._url_callbacks.items(): - match = regex.search(url) - if match: - yield function, match - class SopelWrapper: """Wrapper around a Sopel instance and a Trigger. diff --git a/sopel/coretasks.py b/sopel/coretasks.py index 8805f8a305..35d77dc3c3 100644 --- a/sopel/coretasks.py +++ b/sopel/coretasks.py @@ -26,7 +26,6 @@ import collections import copy import datetime -import functools import logging import re import time @@ -1579,24 +1578,3 @@ def track_topic(bot, trigger): return bot.channels[channel].topic = trigger.args[-1] LOGGER.info("Channel's topic updated: %s", channel) - - -@plugin.rule(r'(?u).*(.+://\S+).*') -def handle_url_callbacks(bot, trigger): - """Dispatch callbacks on URLs - - For each URL found in the trigger, trigger the URL callback registered by - the ``@url`` decorator. - """ - # find URLs in the trigger - for url in trigger.urls: - # find callbacks for said URL - for function, match in bot.search_url_callbacks(url): - # trigger callback defined by the `@url` decorator - if hasattr(function, 'url_regex'): - # bake the `match` argument in before passing the callback on - @functools.wraps(function) - def decorated(bot, trigger): - return function(bot, trigger, match=match) - - bot.call(decorated, bot, trigger) diff --git a/sopel/modules/url.py b/sopel/modules/url.py index 7f16d18797..dd0b3747b2 100644 --- a/sopel/modules/url.py +++ b/sopel/modules/url.py @@ -473,7 +473,6 @@ def check_callbacks(bot: SopelWrapper, url: str, use_excludes: bool = True) -> b ) return ( excluded or - any(bot.search_url_callbacks(url)) or bot.rules.check_url_callback(bot, url) ) diff --git a/test/modules/test_modules_url.py b/test/modules/test_modules_url.py index d8a6d087c1..f590a4a644 100644 --- a/test/modules/test_modules_url.py +++ b/test/modules/test_modules_url.py @@ -60,13 +60,6 @@ def url_callback_http(bot, trigger, match): # register callables sopel.register_urls(callables) - # manually register URL Callback - pattern = re.escape('https://help.example.com/') + r'(.+)' - - def callback(bot, trigger, match): - pass - - sopel.register_url_callback(pattern, callback) return sopel @@ -80,7 +73,6 @@ def test_check_callbacks(mockbot): """Test that check_callbacks works with both new & legacy URL callbacks.""" assert url.check_callbacks(mockbot, 'https://example.com/test') assert url.check_callbacks(mockbot, 'http://example.com/test') - assert url.check_callbacks(mockbot, 'https://help.example.com/test') assert not url.check_callbacks(mockbot, 'https://not.example.com/test') diff --git a/test/test_bot.py b/test/test_bot.py index 8530bd82a7..55ff556ff3 100644 --- a/test/test_bot.py +++ b/test/test_bot.py @@ -10,7 +10,7 @@ from sopel import bot, loader, plugin, plugins, trigger from sopel.plugins import rules from sopel.tests import rawlist -from sopel.tools import Identifier, SopelMemory, target +from sopel.tools import Identifier, target if typing.TYPE_CHECKING: @@ -1140,218 +1140,6 @@ def test_has_channel_privilege_operator(ircfactory, botfactory, tmpconfig): assert sopel.has_channel_privilege('#adminchannel', plugin.OPER) -# ----------------------------------------------------------------------------- -# URL Callbacks - -def test_search_url_callbacks(tmpconfig): - """Test search_url_callbacks for a registered URL.""" - sopel = bot.Sopel(tmpconfig, daemon=False) - - def url_handler(*args, **kwargs): - return None - - sopel.register_url_callback(r'https://example\.com', url_handler) - results = list(sopel.search_url_callbacks('https://example.com')) - assert len(results) == 1, 'Expected 1 handler; found %d' % len(results) - assert url_handler in results[0], 'Once registered, handler must be found' - - -def test_search_url_callbacks_pattern(tmpconfig): - """Test search_url_callbacks for a registered regex pattern.""" - sopel = bot.Sopel(tmpconfig, daemon=False) - - def url_handler(*args, **kwargs): - return None - - sopel.register_url_callback(r'https://(www\.)?example\.com', url_handler) - results = list(sopel.search_url_callbacks('https://example.com')) - assert len(results) == 1, 'Expected 1 handler; found %d' % len(results) - assert url_handler in results[0], 'Once registered, handler must be found' - - results = list(sopel.search_url_callbacks('https://www.example.com')) - assert len(results) == 1, 'Regex pattern must match both URLs' - assert url_handler in results[0] - - -def test_search_url_callbacks_compiled_pattern(tmpconfig): - """Test search_url_callbacks for a registered compiled regex pattern.""" - sopel = bot.Sopel(tmpconfig, daemon=False) - url_regex = re.compile(r'https://(www\.)?example\.com') - - def url_handler(*args, **kwargs): - return None - - sopel.register_url_callback(url_regex, url_handler) - results = list(sopel.search_url_callbacks('https://example.com')) - assert len(results) == 1, 'Expected 1 handler; found %d' % len(results) - assert url_handler in results[0], 'Once registered, handler must be found' - - results = list(sopel.search_url_callbacks('https://www.example.com')) - assert len(results) == 1, 'Regex pattern must match both URLs' - assert url_handler in results[0] - - -def test_search_url_callbacks_not_found(tmpconfig): - """Test search_url_callbacks when pattern does not match.""" - sopel = bot.Sopel(tmpconfig, daemon=False) - results = sopel.search_url_callbacks('https://example.com') - assert not list(results), 'No handler registered; must return an empty list' - - def url_handler(*args, **kwargs): - return None - - sopel.register_url_callback(r'https://(www\.)?example\.com', url_handler) - - results = sopel.search_url_callbacks('https://not-example.com') - assert not list(results), 'URL must not match any pattern' - - -def test_register_url_callback_multiple(tmpconfig): - """Test register_url_callback replace URL callbacks for a pattern.""" - test_pattern = r'https://(www\.)?example\.com' - - def url_handler(*args, **kwargs): - return None - - def url_handler_replacement(*args, **kwargs): - return None - - sopel = bot.Sopel(tmpconfig, daemon=False) - sopel.register_url_callback(test_pattern, url_handler) - - results = list(sopel.search_url_callbacks('https://www.example.com')) - assert url_handler in results[0] - - sopel.register_url_callback(test_pattern, url_handler_replacement) - - results = list(sopel.search_url_callbacks('https://www.example.com')) - assert len(results) == 1, 'There must be one and only one callback' - assert url_handler_replacement in results[0], ( - 'Handler must have been replaced') - - -def test_unregister_url_callback(tmpconfig): - """Test unregister_url_callback removes URL callback for a pattern.""" - test_pattern = r'https://(www\.)?example\.com' - - def url_handler(*args, **kwargs): - return None - - sopel = bot.Sopel(tmpconfig, daemon=False) - - # now register a pattern, make sure it still work - sopel.register_url_callback(test_pattern, url_handler) - assert list(sopel.search_url_callbacks('https://www.example.com')) - - # unregister this pattern - sopel.unregister_url_callback(test_pattern, url_handler) - - # now it is not possible to find a callback for this pattern - results = list(sopel.search_url_callbacks('https://www.example.com')) - assert not results, 'Unregistered URL callback must not work anymore' - - -def test_unregister_url_callback_no_memory(tmpconfig): - """Test unregister_url_callback behavior when bot.memory empty""" - test_pattern = r'https://(www\.)?example\.com' - - def url_handler(*args, **kwargs): - return None - - sopel = bot.Sopel(tmpconfig, daemon=False) - sopel.unregister_url_callback(test_pattern, url_handler) - # no exception implies success - - -def test_unregister_url_callback_unknown_pattern(tmpconfig): - """Test unregister_url_callback pass when pattern is unknown.""" - test_pattern = r'https://(www\.)?example\.com' - - def url_handler(*args, **kwargs): - return None - - sopel = bot.Sopel(tmpconfig, daemon=False) - - # now register a pattern, make sure it still work - sopel.register_url_callback(test_pattern, url_handler) - assert list(sopel.search_url_callbacks('https://www.example.com')) - - # unregister another pattern (that doesn't exist) - sopel.unregister_url_callback(r'http://localhost', url_handler) - - # the existing pattern still work - assert list(sopel.search_url_callbacks('https://www.example.com')) - - -def test_unregister_url_callback_compiled_pattern(tmpconfig): - """Test unregister_url_callback works with a compiled regex.""" - test_pattern = r'https://(www\.)?example\.com' - url_regex = re.compile(test_pattern) - - def url_handler(*args, **kwargs): - return None - - sopel = bot.Sopel(tmpconfig, daemon=False) - - # now register a pattern, make sure it still work - sopel.register_url_callback(test_pattern, url_handler) - assert list(sopel.search_url_callbacks('https://www.example.com')) - - # unregister using the compiled version - sopel.unregister_url_callback(url_regex, url_handler) - - assert not list(sopel.search_url_callbacks('https://www.example.com')) - - -def test_multiple_url_callback(tmpconfig): - """Test multiple URL callbacks for the same URL.""" - test_pattern_example = r'https://(www\.)?example\.com' - test_pattern_global = r'https://.*\.com' - - def url_handler(*args, **kwargs): - return None - - def url_handler_global(*args, **kwargs): - return None - - sopel = bot.Sopel(tmpconfig, daemon=False) - sopel.register_url_callback(test_pattern_example, url_handler) - sopel.register_url_callback(test_pattern_global, url_handler_global) - - results = list(sopel.search_url_callbacks('https://example.com')) - assert len(results) == 2 - handlers = [result[0] for result in results] - - assert url_handler in handlers - assert url_handler_global in handlers - - # now unregister one of them: the other must still work - sopel.unregister_url_callback(test_pattern_example, url_handler) - - results = list(sopel.search_url_callbacks('https://example.com')) - assert len(results) == 1, 'Exactly one handler must remain' - assert url_handler_global in results[0], 'Wrong remaining handler' - - -# Added for Sopel 8; can be removed in Sopel 9 -def test_manual_url_callback_not_found(tmpconfig): - """Test that the bot now ignores manually registered URL callbacks.""" - # Sopel 8.0 no longer supports `bot.memory['url_callbacks'], and this test - # is to make sure that it *really* no longer works. - test_pattern = r'https://(www\.)?example\.com' - - def url_handler(*args, **kwargs): - return None - - sopel = bot.Sopel(tmpconfig, daemon=False) - sopel.memory['url_callbacks'] = SopelMemory() - - # register a callback manually - sopel.memory['url_callbacks'][re.compile(test_pattern)] = url_handler - results = list(sopel.search_url_callbacks("https://www.example.com")) - assert not results, "Manually registered callback must not be found" - - # ----------------------------------------------------------------------------- # Test various message handling