Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-limnoria for openSUSE:Factory 
checked in at 2026-05-19 17:48:43
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-limnoria (Old)
 and      /work/SRC/openSUSE:Factory/.python-limnoria.new.1966 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-limnoria"

Tue May 19 17:48:43 2026 rev:36 rq:1353882 version:2026.5.8

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-limnoria/python-limnoria.changes  
2025-11-19 14:59:22.293931065 +0100
+++ 
/work/SRC/openSUSE:Factory/.python-limnoria.new.1966/python-limnoria.changes    
    2026-05-19 17:49:01.805289631 +0200
@@ -1,0 +2,13 @@
+Mon May 18 17:23:17 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 2026.5.8:
+  * Google: fix double-escaping of text argument
+  * Fix _rlimit_min
+  * ChannelUserDB: Close FD after reading
+  * Remove 'return' statements from 'finally' clauses
+  * registry: Escape default values with newlines and linewrap long ones
+  * SedRegex: Split _replacer_process into smaller functions
+  * Service: Fix 'Holding JOIN to' log
+  * Exclude CTCP and formatting characters from URL regexp
+
+-------------------------------------------------------------------

Old:
----
  limnoria-2025.11.2.tar.gz

New:
----
  limnoria-2026.5.8.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-limnoria.spec ++++++
--- /var/tmp/diff_new_pack.Ba7raD/_old  2026-05-19 17:49:02.697326531 +0200
+++ /var/tmp/diff_new_pack.Ba7raD/_new  2026-05-19 17:49:02.701326696 +0200
@@ -1,7 +1,7 @@
 #
 # spec file for package python-limnoria
 #
-# Copyright (c) 2025 SUSE LLC and contributors
+# Copyright (c) 2026 SUSE LLC and contributors
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -22,7 +22,7 @@
 %bcond_with libalternatives
 %endif
 Name:           python-limnoria
-Version:        2025.11.2
+Version:        2026.5.8
 Release:        0
 Summary:        A modified version of Supybot (an IRC bot and framework)
 License:        BSD-3-Clause
@@ -33,15 +33,14 @@
 BuildRequires:  %{python_module PySocks}
 BuildRequires:  %{python_module chardet}
 BuildRequires:  %{python_module cryptography}
-BuildRequires:  %{python_module ecdsa}
 BuildRequires:  %{python_module feedparser}
 BuildRequires:  %{python_module pip}
 BuildRequires:  %{python_module python-dateutil}
 BuildRequires:  %{python_module python-gnupg}
-BuildRequires:  %{python_module pytzdata}
 # pyxmpp2-scram not available, the code actually covers the non-availability
 #BuildRequires:  %%{python_module pyxmpp2-scram}
 BuildRequires:  %{python_module setuptools}
+BuildRequires:  %{python_module tzdata}
 BuildRequires:  %{python_module wheel}
 # full python for sqlite3 module
 BuildRequires:  %{pythons}
@@ -54,11 +53,10 @@
 Requires:       python-PySocks
 Requires:       python-chardet
 Requires:       python-cryptography
-Requires:       python-ecdsa
 Requires:       python-feedparser
 Requires:       python-python-dateutil
 Requires:       python-python-gnupg
-Requires:       python-pytzdata
+Requires:       python-tzdata
 #Requires:       python-pyxmpp2-scram
 Provides:       Supybot = %{version}
 Obsoletes:      Supybot < 1.0

++++++ limnoria-2025.11.2.tar.gz -> limnoria-2026.5.8.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/limnoria-2025.11.2/PKG-INFO 
new/limnoria-2026.5.8/PKG-INFO
--- old/limnoria-2025.11.2/PKG-INFO     2025-11-02 21:38:00.720120400 +0100
+++ new/limnoria-2026.5.8/PKG-INFO      2026-05-08 07:12:08.882426500 +0200
@@ -1,11 +1,12 @@
 Metadata-Version: 2.4
 Name: limnoria
-Version: 2025.11.2
+Version: 2026.5.8
 Summary: A multipurpose Python IRC bot, designed for flexibility and 
robustness , while being easy to install, set up, and maintain.
 Home-page: https://limnoria.net/
 Download-URL: https://pypi.python.org/pypi/limnoria
 Author: Valentin Lorentz
 Author-email: [email protected]
+License: BSD-3-Clause
 Platform: linux
 Platform: linux2
 Platform: win32
@@ -16,7 +17,6 @@
 Classifier: Environment :: No Input/Output (Daemon)
 Classifier: Intended Audience :: End Users/Desktop
 Classifier: Intended Audience :: Developers
-Classifier: License :: OSI Approved :: BSD License
 Classifier: Natural Language :: English
 Classifier: Natural Language :: Finnish
 Classifier: Natural Language :: French
@@ -40,6 +40,7 @@
 Dynamic: description
 Dynamic: download-url
 Dynamic: home-page
+Dynamic: license
 Dynamic: license-file
 Dynamic: platform
 Dynamic: provides
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/limnoria-2025.11.2/limnoria.egg-info/PKG-INFO 
new/limnoria-2026.5.8/limnoria.egg-info/PKG-INFO
--- old/limnoria-2025.11.2/limnoria.egg-info/PKG-INFO   2025-11-02 
21:38:00.000000000 +0100
+++ new/limnoria-2026.5.8/limnoria.egg-info/PKG-INFO    2026-05-08 
07:12:08.000000000 +0200
@@ -1,11 +1,12 @@
 Metadata-Version: 2.4
 Name: limnoria
-Version: 2025.11.2
+Version: 2026.5.8
 Summary: A multipurpose Python IRC bot, designed for flexibility and 
robustness , while being easy to install, set up, and maintain.
 Home-page: https://limnoria.net/
 Download-URL: https://pypi.python.org/pypi/limnoria
 Author: Valentin Lorentz
 Author-email: [email protected]
+License: BSD-3-Clause
 Platform: linux
 Platform: linux2
 Platform: win32
@@ -16,7 +17,6 @@
 Classifier: Environment :: No Input/Output (Daemon)
 Classifier: Intended Audience :: End Users/Desktop
 Classifier: Intended Audience :: Developers
-Classifier: License :: OSI Approved :: BSD License
 Classifier: Natural Language :: English
 Classifier: Natural Language :: Finnish
 Classifier: Natural Language :: French
@@ -40,6 +40,7 @@
 Dynamic: description
 Dynamic: download-url
 Dynamic: home-page
+Dynamic: license
 Dynamic: license-file
 Dynamic: platform
 Dynamic: provides
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/limnoria-2025.11.2/plugins/Config/plugin.py 
new/limnoria-2026.5.8/plugins/Config/plugin.py
--- old/limnoria-2025.11.2/plugins/Config/plugin.py     2025-11-02 
21:37:50.000000000 +0100
+++ new/limnoria-2026.5.8/plugins/Config/plugin.py      2026-05-08 
07:11:55.000000000 +0200
@@ -72,7 +72,8 @@
     while parts:
         part = parts.pop(0)
         group = group.get(part)
-        if not getattr(group, '_opSettable', True):
+        op_settable = getattr(group, '_opSettable', True)
+        if not utils.force(op_settable):
             return 'owner'
         if irc.isChannel(part):
             # If a registry value has a channel in it, it requires a
@@ -82,31 +83,9 @@
         ### Do more later, for specific capabilities/sections.
     return capability
 
-def isReadOnly(name):
-    """Prevents changing certain config variables to gain shell access via
-    a vulnerable IRC network."""
-    parts = registry.split(name.lower())
-    if parts[0] != 'supybot':
-        parts.insert(0, 'supybot')
-    if parts == ['supybot', 'commands', 'allowshell'] and \
-            not conf.supybot.commands.allowShell():
-        # allow setting supybot.commands.allowShell from True to False,
-        # but not from False to True.
-        # Otherwise an IRC network could overwrite it.
-        return True
-    elif parts[0:2] == ['supybot', 'directories'] and \
-            not conf.supybot.commands.allowShell():
-        # Setting plugins directory allows for arbitrary code execution if
-        # an attacker can both use the IRC network to MITM and upload files
-        # on the server (eg. with a web CMS).
-        # Setting other directories allows writing data at arbitrary
-        # locations.
-        return True
-    else:
-        return False
-
 def checkCanSetValue(irc, msg, group):
-    if isReadOnly(group._name):
+    settable = getattr(group, '_settable', True)
+    if not utils.force(settable):
         irc.error(_("This configuration variable is not writeable "
             "via IRC. To change it you have to: 1) use the 'flush' command 2) 
edit "
             "the config file 3) use the 'config reload' command."), Raise=True)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/limnoria-2025.11.2/plugins/Fediverse/activitypub.py 
new/limnoria-2026.5.8/plugins/Fediverse/activitypub.py
--- old/limnoria-2025.11.2/plugins/Fediverse/activitypub.py     2025-11-02 
21:37:50.000000000 +0100
+++ new/limnoria-2026.5.8/plugins/Fediverse/activitypub.py      2026-05-08 
07:07:49.000000000 +0200
@@ -87,7 +87,7 @@
                 f,
                 *args,
                 timeout=20,
-                heap_size=300 * 1024 * 1024,
+                heap_size=1024 * 1024 * 1024,
                 pn="Fediverse",
                 cn=f.__name__,
                 **kwargs
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/limnoria-2025.11.2/plugins/Fediverse/test.py 
new/limnoria-2026.5.8/plugins/Fediverse/test.py
--- old/limnoria-2025.11.2/plugins/Fediverse/test.py    2025-11-02 
21:37:50.000000000 +0100
+++ new/limnoria-2026.5.8/plugins/Fediverse/test.py     2026-05-08 
07:07:49.000000000 +0200
@@ -33,9 +33,8 @@
 import json
 import functools
 import contextlib
-from multiprocessing import Manager
 
-from supybot import conf, log, utils
+from supybot import conf, log, utils, world
 from supybot.test import ChannelPluginTestCase, network
 
 from . import activitypub as ap
@@ -104,7 +103,7 @@
 
 
 class NetworklessFediverseTestCase(BaseFediverseTestCase):
-    timeout = 1.0
+    timeout = 5.0
 
     @contextlib.contextmanager
     def mockWebfingerSupport(self, value):
@@ -125,7 +124,7 @@
 
     @contextlib.contextmanager
     def mockRequests(self, expected_requests):
-        with Manager() as m:
+        with world.SUPYPROCESS_MULTIPROCESSING_CONTEXT.Manager() as m:
             expected_requests = m.list(list(expected_requests))
             original_getUrlContent = utils.web.getUrlContent
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/limnoria-2025.11.2/plugins/Google/plugin.py 
new/limnoria-2026.5.8/plugins/Google/plugin.py
--- old/limnoria-2025.11.2/plugins/Google/plugin.py     2025-11-02 
21:37:50.000000000 +0100
+++ new/limnoria-2026.5.8/plugins/Google/plugin.py      2026-05-08 
07:07:49.000000000 +0200
@@ -60,8 +60,6 @@
         sourceLang = utils.web.urlquote(sourceLang)
         targetLang = utils.web.urlquote(targetLang)
 
-        text = utils.web.urlquote(text)
-
         url = 'https://translate.googleapis.com/translate_a/single?' + \
             utils.web.urlencode({
                 'client': 'gtx',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/limnoria-2025.11.2/plugins/Misc/plugin.py 
new/limnoria-2026.5.8/plugins/Misc/plugin.py
--- old/limnoria-2025.11.2/plugins/Misc/plugin.py       2025-11-02 
21:37:50.000000000 +0100
+++ new/limnoria-2026.5.8/plugins/Misc/plugin.py        2026-05-08 
07:07:49.000000000 +0200
@@ -47,6 +47,7 @@
 from supybot.commands import *
 from supybot.commands import ProcessTimeoutError
 import supybot.ircdb as ircdb
+import supybot.world as world
 import supybot.irclib as irclib
 import supybot.ircmsgs as ircmsgs
 import supybot.ircutils as ircutils
@@ -71,6 +72,19 @@
 class RegexpTimeout(Exception):
     pass
 
+
+def _matchMessages(messages, arg, q):
+    reobj = re.compile(arg)
+
+    for m in messages:
+        if ircmsgs.isAction(m):
+            s = ircmsgs.unAction(m)
+        else:
+            s = m.args[1]
+        if reobj.search(s):
+            q.put(m)
+
+
 class Misc(callbacks.Plugin):
     """Miscellaneous commands to access Supybot core. This is a core
     Supybot plugin that should not be removed!"""
@@ -495,22 +509,13 @@
                 predicates.setdefault('without', []).append(f)
             elif option == 'regexp':
                 def f(messages, arg=arg):
-                    reobj = re.compile(arg)
-
                     # using a queue to return results, so we can return at
                     # least some results in case of timeout
-                    q = multiprocessing.Queue()
+                    q = world.SUPYPROCESS_MULTIPROCESSING_CONTEXT.Queue()
 
-                    def p(messages):
-                        for m in messages:
-                            if ircmsgs.isAction(m):
-                                s = ircmsgs.unAction(m)
-                            else:
-                                s = m.args[1]
-                            if reobj.search(s):
-                                q.put(m)
                     try:
-                        process(p, messages, timeout=3.,
+                        process(_matchMessages, messages, arg, q,
+                                timeout=3., load_plugin_modules=['Misc'],
                                 pn=self.name(), cn='last')
                     except ProcessTimeoutError:
                         pass
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/limnoria-2025.11.2/plugins/Network/plugin.py 
new/limnoria-2026.5.8/plugins/Network/plugin.py
--- old/limnoria-2025.11.2/plugins/Network/plugin.py    2025-11-02 
21:37:50.000000000 +0100
+++ new/limnoria-2026.5.8/plugins/Network/plugin.py     2026-05-08 
07:07:49.000000000 +0200
@@ -59,10 +59,11 @@
         """[--nossl] <network> [<host[:port]>] [<password>]
 
         Connects to another network (which will be represented by the name
-        provided in <network>) at <host:port>.  If port is not provided, it
-        defaults to 6697, the default port for IRC with SSL.  If password is
-        provided, it will be sent to the server in a PASS command.  If --nossl 
is
-        provided, an SSL connection will not be attempted, and the port will
+        provided in <network>) at <host:port>.
+        The bot will then connect to the network automatically when it 
restarts.
+        If port is not provided, it defaults to 6697, the default port for IRC 
with SSL.
+        If <password> is provided, it will be sent to the server in a PASS 
command. 
+        If --nossl is provided, an SSL connection will not be attempted, and 
the port will
         default to 6667.
         """
         if '.' in network:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/limnoria-2025.11.2/plugins/Note/plugin.py 
new/limnoria-2026.5.8/plugins/Note/plugin.py
--- old/limnoria-2025.11.2/plugins/Note/plugin.py       2025-11-02 
21:37:50.000000000 +0100
+++ new/limnoria-2026.5.8/plugins/Note/plugin.py        2026-05-08 
07:07:49.000000000 +0200
@@ -290,6 +290,7 @@
         the notes.  If --sent is specified, only search sent notes.
         """
         criteria = []
+        regex_arg = None
         def to(note):
             return note.to == user.id
         def frm(note):
@@ -297,10 +298,7 @@
         own = to
         for (option, arg) in optlist:
             if option == 'regexp':
-                criteria.append(lambda s:
-                                regexp_wrapper(s, reobj=arg, timeout=0.1,
-                                               plugin_name=self.name(),
-                                               fcn_name='search'))
+                regex_arg = arg
             elif option == 'sent':
                 own = frm
         if glob:
@@ -312,6 +310,13 @@
                     return False
             return True
         notes = list(self.db.select(lambda n: match(n) and own(n)))
+        if regex_arg is not None and notes:
+            texts = [n.text for n in notes]
+            matches = batch_regexp_wrapper(
+                texts, reobj=regex_arg, timeout=1,
+                plugin_name=self.name(), fcn_name='search')
+            if matches is not None:
+                notes = [n for n, m in zip(notes, matches) if m]
         if not notes:
             irc.reply('No matching notes were found.')
         else:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/limnoria-2025.11.2/plugins/Owner/plugin.py 
new/limnoria-2026.5.8/plugins/Owner/plugin.py
--- old/limnoria-2025.11.2/plugins/Owner/plugin.py      2025-11-02 
21:37:50.000000000 +0100
+++ new/limnoria-2026.5.8/plugins/Owner/plugin.py       2026-05-08 
07:07:49.000000000 +0200
@@ -156,8 +156,7 @@
                     ircquote_response["irc"].reply(str(msg).strip())
         except Exception as e:
             self.log.exception("Errored while sending ircquote response")
-        finally:
-            return ret
+        return ret
 
     def outFilter(self, irc, msg):
         if msg.command == 'PRIVMSG' and not world.testing:
@@ -587,6 +586,8 @@
         Loads the plugin <plugin> from any of the directories in
         conf.supybot.directories.plugins; usually this includes the main
         installed directory and 'plugins' in the current directory.
+        The <plugin> will then be loaded automatically every time the bot
+        restarts.
         --deprecated is necessary if you wish to load deprecated plugins.
         """
         ignoreDeprecation = False
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/limnoria-2025.11.2/plugins/Plugin/plugin.py 
new/limnoria-2026.5.8/plugins/Plugin/plugin.py
--- old/limnoria-2025.11.2/plugins/Plugin/plugin.py     2025-11-02 
21:37:50.000000000 +0100
+++ new/limnoria-2026.5.8/plugins/Plugin/plugin.py      2026-05-08 
07:07:49.000000000 +0200
@@ -29,8 +29,9 @@
 # POSSIBILITY OF SUCH DAMAGE.
 ###
 
-import supybot
+import sys
 
+import supybot
 import supybot.utils as utils
 from supybot.commands import *
 import supybot.plugins as plugins
@@ -42,7 +43,7 @@
 
 class Plugin(callbacks.Plugin):
     """
-    This plugin exists to help users manage their plugins.  Use 'plugin
+    This plugin exists to help users manage their plugins.  Use 'misc
     list' to list the loaded plugins; use 'plugin help' to get the description
     of a plugin; use the 'plugin' command itself to determine what plugin a
     command exists in.
@@ -122,9 +123,21 @@
                 irc.reply(format(_('The %q command is available in the %L 
%s.'),
                                  command, L, plugin))
         else:
-            irc.error(format('There is no command %q.', command))
+            irc.error(format(_('There is no command %q.'), command))
     plugins = wrap(plugins, [many('something')])
 
+    @internationalizeDocstring
+    def path(self, irc, msg, args, name):
+        """<name>
+
+        Returns the path where a plugin is loaded from.
+        """
+        cb = irc.getCallback(name)
+        if cb is None:
+            irc.error(format(_('There is no plugin %s'), name), Raise=True)
+        irc.reply(sys.modules[cb.__module__].__file__)
+    path = wrap(path, ['owner', 'somethingWithoutSpaces'])
+
     def author(self, irc, msg, args, cb):
         """<plugin>
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/limnoria-2025.11.2/plugins/PluginDownloader/plugin.py 
new/limnoria-2026.5.8/plugins/PluginDownloader/plugin.py
--- old/limnoria-2025.11.2/plugins/PluginDownloader/plugin.py   2025-11-02 
21:37:50.000000000 +0100
+++ new/limnoria-2026.5.8/plugins/PluginDownloader/plugin.py    2026-05-08 
07:07:49.000000000 +0200
@@ -233,6 +233,11 @@
                                                    'nelluk',
                                                    'Limnoria-Plugins',
                                                    ),
+               'Alcheri':          GithubRepository(
+                                                   'Alcheri',
+                                                   'Limnoria-plugins',
+                                                   'plugins',
+                                                   ),
                })
 
 class PluginDownloader(callbacks.Plugin):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/limnoria-2025.11.2/plugins/SedRegex/plugin.py 
new/limnoria-2026.5.8/plugins/SedRegex/plugin.py
--- old/limnoria-2025.11.2/plugins/SedRegex/plugin.py   2025-11-02 
21:37:50.000000000 +0100
+++ new/limnoria-2026.5.8/plugins/SedRegex/plugin.py    2026-05-08 
07:07:49.000000000 +0200
@@ -1,7 +1,7 @@
 ###
 # Copyright (c) 2015, Michael Daniel Telatynski <[email protected]>
-# Copyright (c) 2015-2020, James Lu <[email protected]>
-# Copyright (c) 2020-2021, Valentin Lorentz
+# Copyright (c) 2015-2025, James Lu <[email protected]>
+# Copyright (c) 2020-2026, Valentin Lorentz
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -30,6 +30,8 @@
 
 ###
 
+import functools
+import multiprocessing
 from supybot.commands import *
 from supybot.commands import ProcessTimeoutError
 import supybot.plugins as plugins
@@ -38,6 +40,7 @@
 import supybot.ircutils as ircutils
 import supybot.ircdb as ircdb
 import supybot.utils as utils
+import supybot.world as world
 
 import re
 
@@ -58,6 +61,71 @@
 class SearchNotFoundError(Exception):
     pass
 
+
+def filter_messages(network, msg, target, messages, ignoreRegex, sedRegex):
+    """Applies all filters but the user-provided regexp, so it is safe to
+    run in the main process."""
+    # short-lived cache for the duration of this request.
+    checkIgnored = functools.cache(ircdb.checkIgnored)
+
+    for m in messages:
+        if m.command in ('PRIVMSG', 'NOTICE') and \
+                ircutils.strEqual(m.args[0], msg.args[0]) and \
+                m.tagged('receivedBy') is not None and \
+                m.tagged('receivedBy').network == network :
+            if target and m.nick != target:
+                continue
+            # Don't snarf ignored users' messages unless specifically
+            # told to.
+            if ircdb.checkIgnored(m.prefix) and not target:
+                continue
+
+            # Test messages sent before SedRegex was activated. Mark them all 
as seen
+            # so we only need to do this check once per message.
+            if not m.tagged(TAG_SEEN):
+                m.tag(TAG_SEEN)
+                if sedRegex.match(m.args[1]):
+                    m.tag(TAG_IS_REGEX)
+            # Ignore messages containing a regexp if ignoreRegex is on.
+            if ignoreRegex and m.tagged(TAG_IS_REGEX):
+                continue
+
+            yield m
+
+def get_first_matching_message(pattern, messages):
+    for m in messages:
+        # When running substitutions, ignore the "* nick" part of any actions.
+        action = ircmsgs.isAction(m)
+        if action:
+            text = ircmsgs.unAction(m)
+        else:
+            text = m.args[1]
+
+        replace_result = pattern.search(text)
+        if replace_result:
+            return m
+
+def apply_substitution(pattern, replacement, m, count):
+    action = ircmsgs.isAction(m)
+    if action:
+        text = ircmsgs.unAction(m)
+    else:
+        text = m.args[1]
+
+    subst = pattern.sub(replacement, text, count)
+    if action:  # If the message was an ACTION, prepend the nick back.
+        subst = '* %s %s' % (m.nick, subst)
+
+    return axe_spaces(subst)
+
+def apply_substitution_to_first_matching_message(pattern, replacement,
+                                                 messages, count):
+    m = get_first_matching_message(pattern, messages)
+    if m:
+        subst = apply_substitution(pattern, replacement, m, count)
+        return (m, subst)
+
+
 class SedRegex(callbacks.Plugin):
     """
     Enable SedRegex on the desired channels:
@@ -175,10 +243,25 @@
             return
 
         regex_timeout = self.registryValue('processTimeout')
+        if self.registryValue('boldReplacementText', msg.channel, irc.network):
+            replacement = ircutils.bold(replacement)
         try:
-            message = process(self._replacer_process, irc, msg,
-                    target, pattern, replacement, count, iterable, sedRegex,
-                    timeout=regex_timeout, pn=self.name(), cn='replacer')
+            if hasattr(multiprocessing.context, "ForkContext") and \
+                    isinstance(world.SUPYPROCESS_MULTIPROCESSING_CONTEXT,
+                               multiprocessing.context.ForkContext):
+                # global state is shared with child processes, so the child
+                # process has access to history and can lazily filter it
+                message = process(self._replacer_process, irc, msg,
+                        target, pattern, replacement, count, iterable, 
sedRegex,
+                        timeout=regex_timeout, pn=self.name(), cn='replacer')
+            else:
+                # SpawnContext (or possibly ForkServerContext in future
+                # versions of Limnoria): global state is not shared with child
+                # processes, so we have to filter the message list in the main
+                # process and pass it to the child.
+                message = self._replacer(irc, msg,
+                        target, pattern, replacement, count, iterable, 
sedRegex,
+                        timeout=regex_timeout, pn=self.name(), cn='replacer')
         except ProcessTimeoutError:
             irc.error(_("Search timed out."))
         except SearchNotFoundError:
@@ -192,63 +275,57 @@
         else:
             irc.reply(message, prefixNick=False)
 
-    def _replacer_process(self, irc, msg, target, pattern, replacement, count, 
messages, sedRegex):
-        for m in messages:
-            if m.command in ('PRIVMSG', 'NOTICE') and \
-                    ircutils.strEqual(m.args[0], msg.args[0]) and 
m.tagged('receivedBy') == irc:
-                if target and m.nick != target:
-                    continue
-                # Don't snarf ignored users' messages unless specifically
-                # told to.
-                if ircdb.checkIgnored(m.prefix) and not target:
-                    continue
+    def _format_result(self, irc, msg, m, subst):
+        if m.nick == msg.nick:
+            fmt = self.registryValue('format', msg.channel, irc.network)
+            env = {'replacement': subst}
+        else:
+            fmt = self.registryValue('format.other', msg.channel, irc.network)
+            env = {'otherNick': msg.nick, 'replacement': subst}
 
-                # When running substitutions, ignore the "* nick" part of any 
actions.
-                action = ircmsgs.isAction(m)
-                if action:
-                    text = ircmsgs.unAction(m)
-                else:
-                    text = m.args[1]
-
-                # Test messages sent before SedRegex was activated. Mark them 
all as seen
-                # so we only need to do this check once per message.
-                if not m.tagged(TAG_SEEN):
-                    m.tag(TAG_SEEN)
-                    if sedRegex.match(m.args[1]):
-                        m.tag(TAG_IS_REGEX)
-                # Ignore messages containing a regexp if ignoreRegex is on.
-                if self.registryValue('ignoreRegex', msg.channel, irc.network) 
and m.tagged(TAG_IS_REGEX):
-                    self.log.debug("Skipping message %s because it is tagged 
as isRegex", m.args[1])
-                    continue
+        return ircutils.standardSubstitute(irc, m, fmt, env)
 
-                try:
-                    replace_result = pattern.search(text)
-                    if replace_result:
-                        if self.registryValue('boldReplacementText',
-                                              msg.channel, irc.network):
-                            replacement = ircutils.bold(replacement)
-                        subst = pattern.sub(replacement, text, count)
-                        if action:  # If the message was an ACTION, prepend 
the nick back.
-                            subst = '* %s %s' % (m.nick, subst)
-
-                        subst = axe_spaces(subst)
-
-                        if m.nick == msg.nick:
-                            fmt = self.registryValue('format', msg.channel, 
irc.network)
-                            env = {'replacement': subst}
-                        else:
-                            fmt = self.registryValue('format.other', 
msg.channel, irc.network)
-                            env = {'otherNick': msg.nick, 'replacement': subst}
-
-                        return ircutils.standardSubstitute(irc, m, fmt, env)
-
-                except Exception as e:
-                    self.log.warning(_("SedRegex error: %s"), e, exc_info=True)
-                    raise
+    def _replacer_process(self, irc, msg, target, pattern, replacement, count,
+                          messages, sedRegex):
+        ignoreRegex = self.registryValue('ignoreRegex', msg.channel, 
irc.network)
+        messages = filter_messages(irc.network, msg, target, messages,
+                                   ignoreRegex, sedRegex)
+
+        try:
+            m = get_first_matching_message(pattern, messages)
+            if m:
+                subst = apply_substitution(pattern, replacement, m, count)
+                return self._format_result(irc, msg, m, subst)
+        except Exception as e:
+            self.log.warning(_("SedRegex error: %s"), e, exc_info=True)
+            raise
+
+        self.log.debug(_("SedRegex: Search %r not found in the last %i 
messages of %s."),
+                         msg.args[1], len(irc.state.history), msg.args[0])
+        raise SearchNotFoundError()
+
+    def _replacer(self, irc, msg, target, pattern, replacement, count,
+                          messages, sedRegex, **kwargs):
+        ignoreRegex = self.registryValue('ignoreRegex', msg.channel, 
irc.network)
+        messages = filter_messages(irc.network, msg, target, messages,
+                                   ignoreRegex, sedRegex)
+        messages = list(messages)  # materialize the iterator to pickle it
+
+        try:
+            result = process(apply_substitution_to_first_matching_message,
+                             pattern, replacement, messages, count,
+                             load_plugin_modules=["SedRegex"], **kwargs)
+            if result:
+                (m, subst) = result
+                return self._format_result(irc, msg, m, subst)
+        except Exception as e:
+            self.log.warning(_("SedRegex error: %s"), e, exc_info=True)
+            raise
 
         self.log.debug(_("SedRegex: Search %r not found in the last %i 
messages of %s."),
                          msg.args[1], len(irc.state.history), msg.args[0])
         raise SearchNotFoundError()
+
     doNotice = doPrivmsg
 
 Class = SedRegex
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/limnoria-2025.11.2/plugins/SedRegex/test.py 
new/limnoria-2026.5.8/plugins/SedRegex/test.py
--- old/limnoria-2025.11.2/plugins/SedRegex/test.py     2025-11-02 
21:37:50.000000000 +0100
+++ new/limnoria-2026.5.8/plugins/SedRegex/test.py      2026-05-08 
07:07:49.000000000 +0200
@@ -1,6 +1,6 @@
 ###
-# Copyright (c) 2017-2020, James Lu <[email protected]>
-# Copyright (c) 2020-2021, Valentin Lorentz
+# Copyright (c) 2017-2025, James Lu <[email protected]>
+# Copyright (c) 2020-2026, Valentin Lorentz
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -31,7 +31,11 @@
 
 from __future__ import print_function
 import unittest
+import contextlib
+import multiprocessing
 from supybot.test import *
+import supybot.world as world
+import supybot.callbacks as callbacks
 
 class SedRegexTestCase(ChannelPluginTestCase):
     other = "[email protected]"
@@ -324,4 +328,31 @@
 
     # TODO: test ignores
 
+
+    @unittest.skipIf(
+        world.disableMultiprocessing,
+        "Test requires multiprocessing to be enabled"
+    )
+    def testSpawnContext(self):
+        """Test that SedRegex works with 'spawn' multiprocessing context
+        (Windows compatibility)."""
+        with useSpawnContext():
+            self.feedMsg('hello world')
+            self.feedMsg('s/world/everyone/')
+            m = self.getMsg(' ')
+            self.assertIn('hello everyone', str(m))
+
+    @unittest.skipIf(
+        world.disableMultiprocessing,
+        "Test requires multiprocessing to be enabled"
+    )
+    def testSpawnReDoSTimeout(self):
+        """Test that ReDoS protection works with 'spawn' context."""
+        with useSpawnContext():
+            for idx in range(500):
+                self.feedMsg("ACCCCCCCCCCCCCCCCCCCCCCCCCCCCX")
+            self.feedMsg(r"s/A(B|C+)+D/this should abort/")
+            m = self.getMsg(' ', timeout=1)
+            self.assertIn('timed out', str(m))
+
 # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/limnoria-2025.11.2/plugins/Services/config.py 
new/limnoria-2026.5.8/plugins/Services/config.py
--- old/limnoria-2025.11.2/plugins/Services/config.py   2025-11-02 
21:37:50.000000000 +0100
+++ new/limnoria-2026.5.8/plugins/Services/config.py    2026-05-08 
07:07:49.000000000 +0200
@@ -47,14 +47,6 @@
 def configure(advanced):
     from supybot.questions import something
     conf.registerPlugin('Services', True)
-    nick = something(_('What is your registered nick?'))
-    password = something(_('What is your password for that nick?'))
-    chanserv = something(_('What is your ChanServ named?'), default='ChanServ')
-    nickserv = something(_('What is your NickServ named?'), default='NickServ')
-    conf.supybot.plugins.Services.nicks.setValue([nick])
-    conf.supybot.plugins.Services.NickServ.setValue(nickserv)
-    registerNick(nick, password)
-    conf.supybot.plugins.Services.ChanServ.setValue(chanserv)
 
 class ValidNickOrEmptyString(registry.String):
     def setValue(self, v):
@@ -69,7 +61,8 @@
 Services = conf.registerPlugin('Services')
 conf.registerNetworkValue(Services, 'nicks',
     ValidNickSet([], _("""Space-separated list of nicks the bot will use with
-    services.""")))
+    services. Don't change this config value yourself, use the 'services 
password'
+    command instead.""")))
 
 class Networks(registry.SpaceSeparatedSetOfStrings):
     List = ircutils.IrcSet
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/limnoria-2025.11.2/plugins/Services/plugin.py 
new/limnoria-2026.5.8/plugins/Services/plugin.py
--- old/limnoria-2025.11.2/plugins/Services/plugin.py   2025-11-02 
21:37:50.000000000 +0100
+++ new/limnoria-2026.5.8/plugins/Services/plugin.py    2026-05-08 
07:07:49.000000000 +0200
@@ -40,6 +40,7 @@
 import supybot.irclib as irclib
 import supybot.ircmsgs as ircmsgs
 import supybot.ircutils as ircutils
+import supybot.registry as registry
 import supybot.callbacks as callbacks
 from supybot.i18n import PluginInternationalization, internationalizeDocstring
 _ = PluginInternationalization('Services')
@@ -69,9 +70,7 @@
     def __init__(self, irc):
         self.__parent = super(Services, self)
         self.__parent.__init__(irc)
-        network = irc.network if irc else None
-        for nick in self.registryValue('nicks', network=network):
-            config.registerNick(nick)
+        self._initializedNetworks = set()
         self.reset()
 
     def reset(self):
@@ -93,7 +92,7 @@
             if not state.identified:
                 if self.registryValue('noJoinsUntilIdentified', 
network=irc.network):
                     self.log.info('Holding JOIN to %s @ %s until identified.',
-                                  msg.channel, irc.network)
+                                  msg.args[0], irc.network)
                     state.waitingJoins.append(msg)
                     return None
         return msg
@@ -108,7 +107,18 @@
     def _getNickServPassword(self, nick, network):
         # This should later be nick-specific.
         assert nick in self.registryValue('nicks', network=network)
-        return self.registryValue('NickServ.password.%s' % nick, 
network=network)
+        try:
+            return self.registryValue(f'NickServ.password.{nick}', 
network=network)
+        except registry.NonExistentRegistryEntry:
+            msg = (
+                f'Nick {nick} is listed in supybot.plugins.Services.nicks, '
+                f'supybot.plugins.NickServ.password.{nick} does not exist. '
+                f'If you manually changed the value, undo it and use the '
+                f'"services password" command instead.'
+            )
+            self.log.error(msg)
+            raise callbacks.Error(msg)
+
 
     def _setNickServPassword(self, nick, password, network):
         # This also should be nick-specific.
@@ -174,6 +184,12 @@
         self.__parent.__call__(irc, msg)
         if self.disabled(irc):
             return
+
+        if irc.network not in self._initializedNetworks:
+            self._initializedNetworks.add(irc.network)
+            for nick in self.registryValue('nicks', network=irc.network):
+                config.registerNick(nick)
+
         state = self._getState(irc)
         nick = self._getNick(irc.network)
         if nick not in self.registryValue('nicks', network=irc.network):
@@ -237,10 +253,25 @@
                 return
             self._doGhost(irc)
 
+    def do477(self, irc, msg):
+        # DALnet: Cannot join channel (+R, need to be identified)
+        state = self._getState(irc)
+        channel = msg.args[1]
+        self.log.debug(
+            'Got 477 (need identified) for %s on %s, will retry after 
identify.',
+            channel, irc.network
+        )
+        state.channels.append(channel)
+
     def do515(self, irc, msg):
         # Can't join this channel, it's +r (we must be identified).
         state = self._getState(irc)
-        state.channels.append(msg.args[1])
+        channel = msg.args[1]
+        self.log.debug(
+            'Got 515 (need identified) for %s on %s, will retry after 
identify.',
+            channel, irc.network
+        )
+        state.channels.append(channel)
 
     def doNick(self, irc, msg):
         nick = self._getNick(irc.network)
@@ -258,7 +289,9 @@
     def doNotice(self, irc, msg):
         if irc.afterConnect:
             nickserv = self.registryValue('NickServ', network=irc.network)
+            nickserv = nickserv.split('@')[0]
             chanserv = self.registryValue('ChanServ', network=irc.network)
+            chanserv = chanserv.split('@')[0]
             if nickserv and ircutils.strEqual(msg.nick, nickserv):
                 self.doNickservNotice(irc, msg)
             elif chanserv and ircutils.strEqual(msg.nick, chanserv):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/limnoria-2025.11.2/plugins/Todo/plugin.py 
new/limnoria-2026.5.8/plugins/Todo/plugin.py
--- old/limnoria-2025.11.2/plugins/Todo/plugin.py       2025-11-02 
21:37:50.000000000 +0100
+++ new/limnoria-2026.5.8/plugins/Todo/plugin.py        2026-05-08 
07:07:49.000000000 +0200
@@ -239,20 +239,28 @@
         if not optlist and not globs:
             raise callbacks.ArgumentError
         criteria = []
+        regex_arg = None
         for (option, arg) in optlist:
             if option == 'regexp':
-                criteria.append(lambda s:
-                                regexp_wrapper(s, reobj=arg, timeout=0.1,
-                                               plugin_name=self.name(),
-                                               fcn_name='search'))
+                regex_arg = arg
         for glob in globs:
             glob = utils.python.glob2re(glob)
             criteria.append(re.compile(glob).search)
         try:
-            tasks = self.db.select(user.id, criteria)
+            tasks = list(self.db.select(user.id, criteria))
+        except dbi.NoRecordError:
+            tasks = []
+        if regex_arg is not None and tasks:
+            texts = [t.task for t in tasks]
+            matches = batch_regexp_wrapper(
+                texts, reobj=regex_arg, timeout=1,
+                plugin_name=self.name(), fcn_name='search')
+            if matches is not None:
+                tasks = [t for t, m in zip(tasks, matches) if m]
+        if tasks:
             L = [format('#%i: %s', t.id, self._shrink(t.task)) for t in tasks]
             irc.reply(format('%L', L))
-        except dbi.NoRecordError:
+        else:
             irc.reply(_('No tasks matched that query.'))
     search = wrap(search,
                   ['user', getopts({'regexp': 'regexpMatcher'}), any('glob')])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/limnoria-2025.11.2/plugins/Unix/config.py 
new/limnoria-2026.5.8/plugins/Unix/config.py
--- old/limnoria-2025.11.2/plugins/Unix/config.py       2025-11-02 
21:37:50.000000000 +0100
+++ new/limnoria-2026.5.8/plugins/Unix/config.py        2026-05-08 
07:07:49.000000000 +0200
@@ -72,7 +72,8 @@
 conf.registerGroup(Unix, 'fortune')
 conf.registerGlobalValue(Unix.fortune, 'command',
     registry.String(utils.findBinaryInPath('fortune') or '', _("""Determines
-    what command will be called for the fortune command.""")))
+    what command will be called for the fortune command.""")),
+    settable=lambda: conf.supybot.commands.allowShell())
 conf.registerChannelValue(Unix.fortune, 'short',
     registry.Boolean(True, _("""Determines whether only short fortunes will be
     used if possible.  This sends the -s option to the fortune program.""")))
@@ -95,7 +96,8 @@
 conf.registerGlobalValue(Unix.spell, 'command',
     registry.String(utils.findBinaryInPath('aspell') or
                     utils.findBinaryInPath('ispell') or '', _("""Determines
-    what command will be called for the spell command.""")))
+    what command will be called for the spell command.""")),
+    settable=lambda: conf.supybot.commands.allowShell())
 conf.registerGlobalValue(Unix.spell, 'language',
     registry.String('en', _("""Determines what aspell dictionary will be used
     for spell checking.""")))
@@ -103,28 +105,33 @@
 conf.registerGroup(Unix, 'wtf')
 conf.registerGlobalValue(Unix.wtf, 'command',
     registry.String(utils.findBinaryInPath('wtf') or '', _("""Determines what
-    command will be called for the wtf command.""")))
+    command will be called for the wtf command.""")),
+    settable=lambda: conf.supybot.commands.allowShell())
 
 conf.registerGroup(Unix, 'ping')
-conf.registerGlobalValue(Unix.ping, 'command', 
-    registry.String(utils.findBinaryInPath('ping') or '', """Determines what 
-    command will be called for the ping command."""))
+conf.registerGlobalValue(Unix.ping, 'command',
+    registry.String(utils.findBinaryInPath('ping') or '', """Determines what
+    command will be called for the ping command."""),
+    settable=lambda: conf.supybot.commands.allowShell())
 conf.registerGlobalValue(Unix.ping, 'defaultCount',
     registry.PositiveInteger(5, """Determines what ping and ping6 counts (-c) 
will default to."""))
 
 conf.registerGroup(Unix, 'ping6')
-conf.registerGlobalValue(Unix.ping6, 'command', 
-    registry.String(utils.findBinaryInPath('ping6') or '', """Determines what 
-    command will be called for the ping6 command."""))
+conf.registerGlobalValue(Unix.ping6, 'command',
+    registry.String(utils.findBinaryInPath('ping6') or '', """Determines what
+    command will be called for the ping6 command."""),
+    settable=lambda: conf.supybot.commands.allowShell())
 
 conf.registerGroup(Unix, 'sysuptime')
 conf.registerGlobalValue(Unix.sysuptime, 'command',
     registry.String(utils.findBinaryInPath('uptime') or '', """Determines what
-    command will be called for the uptime command."""))
+    command will be called for the uptime command."""),
+    settable=lambda: conf.supybot.commands.allowShell())
 
 conf.registerGroup(Unix, 'sysuname')
 conf.registerGlobalValue(Unix.sysuname, 'command',
     registry.String(utils.findBinaryInPath('uname') or '', """Determines what
-    command will be called for the uname command."""))
+    command will be called for the uname command."""),
+    settable=lambda: conf.supybot.commands.allowShell())
 
 # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/limnoria-2025.11.2/plugins/Web/test.py 
new/limnoria-2026.5.8/plugins/Web/test.py
--- old/limnoria-2025.11.2/plugins/Web/test.py  2025-11-02 21:37:50.000000000 
+0100
+++ new/limnoria-2026.5.8/plugins/Web/test.py   2026-05-08 07:07:49.000000000 
+0200
@@ -102,6 +102,16 @@
             finally:
                 conf.supybot.plugins.Web.titleSnarfer.setValue(False)
 
+        def testTitleSnarferCtcp(self):
+            try:
+                conf.supybot.plugins.Web.titleSnarfer.setValue(True)
+                self.assertSnarfRegexp(
+                    '\x01ACTION is browsing https://microsoft.com/\x01',
+                    'Microsoft'
+                )
+            finally:
+                conf.supybot.plugins.Web.titleSnarfer.setValue(False)
+
         def testMultipleTitleSnarfer(self):
             try:
                 conf.supybot.plugins.Web.titleSnarfer.setValue(True)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/limnoria-2025.11.2/plugins/__init__.py 
new/limnoria-2026.5.8/plugins/__init__.py
--- old/limnoria-2025.11.2/plugins/__init__.py  2025-11-02 21:37:50.000000000 
+0100
+++ new/limnoria-2026.5.8/plugins/__init__.py   2026-05-08 07:07:49.000000000 
+0200
@@ -233,30 +233,33 @@
         except EnvironmentError as e:
             log.warning('Couldn\'t open %s: %s.', self.filename, e)
             return
-        reader = csv.reader(fd)
         try:
-            lineno = 0
-            for t in reader:
-                lineno += 1
-                try:
-                    channel = t.pop(0)
-                    id = t.pop(0)
+            reader = csv.reader(fd)
+            try:
+                lineno = 0
+                for t in reader:
+                    lineno += 1
                     try:
-                        id = int(id)
-                    except ValueError:
-                        # We'll skip over this so, say, nicks can be kept here.
-                        pass
-                    channel = sys.intern(channel)
-                    v = self.deserialize(channel, id, t)
-                    self[channel, id] = v
-                except Exception as e:
-                    log.warning('Invalid line #%s in %s.',
-                                lineno, self.__class__.__name__)
-                    log.debug('Exception: %s', utils.exnToString(e))
-        except Exception as e: # This catches exceptions from csv.reader.
-            log.warning('Invalid line #%s in %s.',
-                        lineno, self.__class__.__name__)
-            log.debug('Exception: %s', utils.exnToString(e))
+                        channel = t.pop(0)
+                        id = t.pop(0)
+                        try:
+                            id = int(id)
+                        except ValueError:
+                            # We'll skip over this so, say, nicks can be kept 
here.
+                            pass
+                        channel = sys.intern(channel)
+                        v = self.deserialize(channel, id, t)
+                        self[channel, id] = v
+                    except Exception as e:
+                        log.warning('Invalid line #%s in %s.',
+                                    lineno, self.__class__.__name__)
+                        log.debug('Exception: %s', utils.exnToString(e))
+            except Exception as e: # This catches exceptions from csv.reader.
+                log.warning('Invalid line #%s in %s.',
+                            lineno, self.__class__.__name__)
+                log.debug('Exception: %s', utils.exnToString(e))
+        finally:
+            fd.close()
 
     def flush(self):
         mode = 'wb' if utils.minisix.PY2 else 'w'
@@ -412,6 +415,7 @@
         Searches for $types matching the criteria given.
         """
         predicates = []
+        regex_arg = None
         def p(record):
             for predicate in predicates:
                 if not predicate(record):
@@ -428,14 +432,21 @@
                     # https://github.com/progval/Limnoria/issues/855 for 
details
                     irc.errorNoCapability('trusted')
 
-                predicates.append(lambda r: regexp_wrapper(r.text, reobj=arg,
-                        timeout=0.1, plugin_name=self.name(), 
fcn_name='search'))
+                regex_arg = arg
         if glob:
             def globP(r, glob=glob.lower()):
                 return fnmatch.fnmatch(r.text.lower(), glob)
             predicates.append(globP)
+        candidates = list(self.db.select(channel, p))
+        if regex_arg is not None and candidates:
+            texts = [r.text for r in candidates]
+            matches = batch_regexp_wrapper(
+                texts, reobj=regex_arg, timeout=1,
+                plugin_name=self.name(), fcn_name='search')
+            if matches is not None:
+                candidates = [r for r, m in zip(candidates, matches) if m]
         L = []
-        for record in self.db.select(channel, p):
+        for record in candidates:
             L.append(self.searchSerializeRecord(record))
         if L:
             L.sort()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/limnoria-2025.11.2/setup.py 
new/limnoria-2026.5.8/setup.py
--- old/limnoria-2025.11.2/setup.py     2025-11-02 21:37:50.000000000 +0100
+++ new/limnoria-2026.5.8/setup.py      2026-05-08 07:07:49.000000000 +0200
@@ -192,7 +192,6 @@
         'Environment :: No Input/Output (Daemon)',
         'Intended Audience :: End Users/Desktop',
         'Intended Audience :: Developers',
-        'License :: OSI Approved :: BSD License',
         'Natural Language :: English',
         'Natural Language :: Finnish',
         'Natural Language :: French',
@@ -209,6 +208,7 @@
         'Topic :: Communications :: Chat :: Internet Relay Chat',
         'Topic :: Software Development :: Libraries :: Python Modules',
         ],
+    license="BSD-3-Clause",
 
     # Installation data
     packages=packages,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/limnoria-2025.11.2/src/commands.py 
new/limnoria-2026.5.8/src/commands.py
--- old/limnoria-2025.11.2/src/commands.py      2025-11-02 21:37:50.000000000 
+0100
+++ new/limnoria-2026.5.8/src/commands.py       2026-05-08 07:07:49.000000000 
+0200
@@ -1,7 +1,7 @@
 ###
 # Copyright (c) 2002-2005, Jeremiah Fincher
 # Copyright (c) 2009-2010,2015, James McCoy
-# Copyright (c) 2010-2021, Valentin Lorentz
+# Copyright (c) 2010-2026, Valentin Lorentz
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -87,7 +87,7 @@
     elif b == resource.RLIM_INFINITY:
         return a
     else:
-        return min(soft, heap_size)
+        return min(a, b)
 
 def _process_target(f, q, heap_size, *args, **kwargs):
     """Called by :func:`process`"""
@@ -103,6 +103,17 @@
     except Exception as e:
         q.put([True, e])
 
+def _process_queued_target(f_q, q, heap_size, *args, load_plugin_modules, 
**kwargs):
+    """Called by :func:`process` on non-``fork`` multiprocessing contexts"""
+    from .plugin import loadPluginModule
+
+    for plugin in load_plugin_modules:
+        loadPluginModule(plugin, ignoreDeprecation=True)
+
+    f = f_q.get()
+    _process_target(f, q, heap_size, *args, **kwargs)
+
+
 def process(f, *args, **kwargs):
     """Runs a function <f> in a subprocess.
     
@@ -121,13 +132,14 @@
     if world.disableMultiprocessing:
         pn = kwargs.pop('pn', 'Unknown')
         cn = kwargs.pop('cn', 'unknown')
+        kwargs.pop('load_plugin_modules', None)
         try:
             return f(*args, **kwargs)
         except Exception as e:
             raise e
     
     try:
-        q = multiprocessing.Queue()
+        q = world.SUPYPROCESS_MULTIPROCESSING_CONTEXT.Queue()
     except OSError:
         log.error('Using multiprocessing.Queue raised an OSError.\n'
                 'This is probably caused by your system denying semaphore\n'
@@ -137,9 +149,23 @@
                 '(See https://github.com/travis-ci/travis-core/issues/187\n'
                 'for more information about this bug.)\n')
         raise
-    targetArgs = (f, q, heap_size) + args
-    p = callbacks.CommandProcess(target=_process_target,
-                                 args=targetArgs, kwargs=kwargs)
+    if hasattr(multiprocessing.context, "ForkContext") and \
+            isinstance(world.SUPYPROCESS_MULTIPROCESSING_CONTEXT,
+                       multiprocessing.context.ForkContext):
+        # no need to pickle f
+        targetArgs = (f, q, heap_size) + args
+        kwargs.pop('load_plugin_modules', None)
+        p = callbacks.CommandProcess(target=_process_target,
+                                     args=targetArgs, kwargs=kwargs)
+    else:
+        # f must be picklable, but it probably comes from a plugin module,
+        # so we need to load the plugin module first, before unpickling it.
+        # so we put it on the queue instead of an argument.
+        f_q = world.SUPYPROCESS_MULTIPROCESSING_CONTEXT.Queue()
+        f_q.put(f)
+        targetArgs = (f_q, q, heap_size) + args
+        p = callbacks.CommandProcess(target=_process_queued_target,
+                                     args=targetArgs, kwargs=kwargs)
     try:
         p.start()
     except OSError as e:
@@ -172,20 +198,47 @@
     '''A convenient wrapper to stuff regexp search queries through a 
subprocess.
 
     This is used because specially-crafted regexps can use exponential time
-    and hang the bot.'''
-    def re_bool(s, reobj):
-        """Since we can't enqueue match objects into the multiprocessing queue,
-        we'll just wrap the function to return bools."""
-        if reobj.search(s) is not None:
-            return True
-        else:
-            return False
+    and hang the bot.
+
+    Use :`batch_regexp_wrapper` if you need to check multiple strings.'''
     try:
-        v = process(re_bool, s, reobj, timeout=timeout, pn=plugin_name, 
cn=fcn_name)
+        v = process(_re_bool, s, reobj, timeout=timeout, pn=plugin_name, 
cn=fcn_name)
         return v
     except ProcessTimeoutError:
         return None
 
+def _re_bool(s, reobj):
+    """(helper for :func:`regexp_wrapper`)
+
+    _Since we can't enqueue match objects into the multiprocessing queue,
+    we'll just wrap the function to return bools."""
+    if reobj.search(s) is not None:
+        return True
+    else:
+        return False
+
+def batch_regexp_wrapper(strings, reobj, timeout, plugin_name, fcn_name):
+    """Like :func:`regexp_wrapper`, but checks all strings in a single 
subprocess.
+
+    Returns a list of bools parallel to ``strings`` indicating matches.
+    Returns None on timeout."""
+    if not strings:
+        return []
+    try:
+        return process(
+            _re_filter_batch, strings, reobj,
+            timeout=timeout, pn=plugin_name, cn=fcn_name
+        )
+    except ProcessTimeoutError:
+        return None
+
+def _re_filter_batch(strings, reobj):
+    """(helper for batch_regexp_wrapper)
+
+    Returns a list of bools, one per string, indicating whether the regex
+    matched."""
+    return [reobj.search(s) is not None for s in strings]
+
 class UrlSnarfThread(world.SupyThread):
     def __init__(self, *args, **kwargs):
         assert 'url' in kwargs
@@ -1231,7 +1284,7 @@
     # Decorators.
     'urlSnarfer', 'thread',
     # Functions.
-    'wrap', 'process', 'regexp_wrapper',
+    'wrap', 'process', 'regexp_wrapper', 'batch_regexp_wrapper',
     # Stuff for testing.
     'Spec',
 ]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/limnoria-2025.11.2/src/conf.py 
new/limnoria-2026.5.8/src/conf.py
--- old/limnoria-2025.11.2/src/conf.py  2025-11-02 21:37:50.000000000 +0100
+++ new/limnoria-2026.5.8/src/conf.py   2026-05-08 07:11:55.000000000 +0200
@@ -85,15 +85,17 @@
         group = registry.Group(**kwargs)
     return Group.register(name, group)
 
-def registerGlobalValue(group, name, value):
+def registerGlobalValue(group, name, value, settable=True):
     value._networkValue = False
     value._channelValue = False
+    value._settable = settable
     return group.register(name, value)
 
-def registerNetworkValue(group, name, value):
+def registerNetworkValue(group, name, value, settable=True):
     value._supplyDefault = True
     value._networkValue = True
     value._channelValue = False
+    value._settable = settable
     g = group.register(name, value)
     gname = g._name.lower()
     for name in registry._cache.keys():
@@ -105,11 +107,12 @@
                 g.get(parts[0])()
     return g
 
-def registerChannelValue(group, name, value, opSettable=True):
+def registerChannelValue(group, name, value, opSettable=True, settable=True):
     value._supplyDefault = True
     value._networkValue = True
     value._channelValue = True
     value._opSettable = opSettable
+    value._settable = settable
     g = group.register(name, value)
     gname = g._name.lower()
     for name in registry._cache.keys():
@@ -868,7 +871,8 @@
     to prevent MITM from the IRC network itself (vulnerable IRCd or IRCops)
     from gaining shell access to the bot's server by impersonating the owner.
     Setting this to False also disables plugins and commands that can be
-    used to indirectly gain shell access.""")))
+    used to indirectly gain shell access.""")),
+    settable=lambda: supybot.commands.allowShell())
 
 # supybot.commands.disabled moved to callbacks for canonicalName.
 
@@ -991,22 +995,28 @@
 registerGroup(supybot, 'directories')
 registerGlobalValue(supybot.directories, 'conf',
     Directory('conf', _("""Determines what directory configuration data is
-    put into.""")))
+    put into.""")),
+    settable=lambda: supybot.commands.allowShell())
 registerGlobalValue(supybot.directories, 'data',
-    Directory('data', _("""Determines what directory data is put into.""")))
+    Directory('data', _("""Determines what directory data is put into.""")),
+    settable=lambda: supybot.commands.allowShell())
 registerGlobalValue(supybot.directories, 'backup',
     Directory('backup', _("""Determines what directory backup data is put
     into. Set it to /dev/null to disable backup (it is a special value,
-    so it also works on Windows and systems without /dev/null).""")))
+    so it also works on Windows and systems without /dev/null).""")),
+    settable=lambda: supybot.commands.allowShell())
 registerGlobalValue(supybot.directories, 'log',
-    Directory('logs', """Determines what directory the bot will store its
-    logfiles in."""))
+    Directory('logs', _("""Determines what directory the bot will store its
+    logfiles in.""")),
+    settable=lambda: supybot.commands.allowShell())
 registerGlobalValue(supybot.directories.data, 'tmp',
     DataFilenameDirectory('tmp', _("""Determines what directory temporary files
-    are put into.""")))
+    are put into.""")),
+    settable=lambda: supybot.commands.allowShell())
 registerGlobalValue(supybot.directories.data, 'web',
     DataFilenameDirectory('web', _("""Determines what directory files of the
-    web server (templates, custom images, ...) are put into.""")))
+    web server (templates, custom images, ...) are put into.""")),
+    settable=lambda: supybot.commands.allowShell())
 
 def _update_tmp():
     utils.file.AtomicFile.default.tmpDir = supybot.directories.data.tmp
@@ -1023,7 +1033,8 @@
     strings.
     This means that to add another directory, you can nest the former value and
     add a new one.  E.g. you can say: bot: 'config supybot.directories.plugins
-    [config supybot.directories.plugins], newPluginDirectory'.""")))
+    [config supybot.directories.plugins], newPluginDirectory'.""")),
+    settable=lambda: supybot.commands.allowShell())
 
 registerGlobalValue(supybot, 'plugins',
     registry.SpaceSeparatedSetOfStrings([], _("""List of all plugins that were
@@ -1036,7 +1047,8 @@
     regardless of what their configured state is.  Generally, if these plugins
     are configured not to load, you didn't do it on purpose, and you still
     want them to load.  Users who don't want to load these plugins are smart
-    enough to change the value of this variable appropriately :)""")))
+    enough to change the value of this variable appropriately :)""")),
+    settable=lambda: supybot.commands.allowShell())
 
 ###
 # supybot.databases.  For stuff relating to Supybot's databases (duh!)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/limnoria-2025.11.2/src/registry.py 
new/limnoria-2026.5.8/src/registry.py
--- old/limnoria-2025.11.2/src/registry.py      2025-11-02 21:37:50.000000000 
+0100
+++ new/limnoria-2026.5.8/src/registry.py       2026-05-08 07:11:55.000000000 
+0200
@@ -84,7 +84,7 @@
     fd = utils.file.nonCommentNonEmptyLines(_fd)
     acc = ''
     slashEnd = re.compile(r'\\*$')
-    for line in fd:
+    for (lineno, line) in enumerate(fd):
         line = line.rstrip('\r\n')
         # XXX There should be some way to determine whether or not we're
         #     starting a new variable or not.  As it is, if there's a backslash
@@ -106,7 +106,9 @@
             value = decoder(value)[0]
             acc = ''
         except ValueError:
-            raise InvalidRegistryFile('Error unpacking line %r' % acc)
+            raise InvalidRegistryFile(
+                f'Error unpacking {filename} line {lineno+1}: {acc!r}'
+            )
         _cache[key] = value
     _lastModified = monotonic_time()
     _fd.close()
@@ -154,7 +156,10 @@
                         exception('Exception instantiating default for %s:' %
                                   value._name)
                     try:
-                        lines.append('# Default value: %s\n' % x)
+                        lines_ = iter(textwrap.wrap(str(x)))
+                        lines.append('# Default value: %s\n' % next(lines_, 
""))
+                        for line in lines_:
+                            lines.append('# %s\n' % line)
                     except Exception:
                         exception('Exception printing default value of %s:' %
                                   value._name)
@@ -334,7 +339,8 @@
     """Invalid registry value.  If you're getting this message, report it,
     because we forgot to put a proper help string here."""
     __slots__ = ('__parent', '_default', '_showDefault', '_help', '_callbacks',
-            'value', '_networkValue', '_channelValue', '_opSettable')
+            'value', '_networkValue', '_channelValue', '_opSettable',
+            '_settable')
     def __init__(self, default, help, setDefault=True,
                  showDefault=True, **kwargs):
         self.__parent = super(Value, self)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/limnoria-2025.11.2/src/schedule.py 
new/limnoria-2026.5.8/src/schedule.py
--- old/limnoria-2025.11.2/src/schedule.py      2025-11-02 21:37:50.000000000 
+0100
+++ new/limnoria-2026.5.8/src/schedule.py       2026-05-08 07:07:49.000000000 
+0200
@@ -117,12 +117,13 @@
             nonlocal count
             try:
                 f(*args, **kwargs)
-            finally:
+            except Exception:
+                log.exception('Uncaught exception in scheduled function:')
                 # Even if it raises an exception, let's schedule it.
-                if count is not None:
-                    count -= 1
-                if count is None or count > 0:
-                    return self.addEvent(wrapper, time.time() + t, name)
+            if count is not None:
+                count -= 1
+            if count is None or count > 0:
+                return self.addEvent(wrapper, time.time() + t, name)
         return wrapper
 
     def addPeriodicEvent(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/limnoria-2025.11.2/src/test.py 
new/limnoria-2026.5.8/src/test.py
--- old/limnoria-2025.11.2/src/test.py  2025-11-02 21:37:50.000000000 +0100
+++ new/limnoria-2026.5.8/src/test.py   2026-05-08 07:07:49.000000000 +0200
@@ -1,7 +1,7 @@
 ###
 # Copyright (c) 2002-2005, Jeremiah Fincher
 # Copyright (c) 2011, James McCoy
-# Copyright (c) 2010-2021, Valentin Lorentz
+# Copyright (c) 2010-2026, Valentin Lorentz
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -37,8 +37,11 @@
 import shutil
 import urllib
 import unittest
+import unittest.mock
 import functools
 import threading
+import contextlib
+import multiprocessing
 
 from . import (callbacks, conf, drivers, httpserver, i18n, ircdb, irclib,
         ircmsgs, ircutils, log, plugin, registry, utils, world)
@@ -660,5 +663,53 @@
     def setUp(self):
         ChannelPluginTestCase.setUp(self, forceSetup=True)
 
-# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
 
+# allows plugins to test the 'spawn' and 'forkserver' methods when
+# the global multiprocessing context is 'fork'.
+#
+# This has to be here because plugins can't define it in their test.py,
+# because Python unpickles the Process before we get a change to load the
+# plugin.
+try:
+    _SPAWN_MULTIPROCESSING_CONTEXT = multiprocessing.get_context('spawn')
+except ValueError:
+    @contextlib.contextmanager
+    def useSpawnContext():
+        raise unittest.SkipTest("'spawn' start method is not supported")
+else:
+    class _SpawnSupyProcess(_SPAWN_MULTIPROCESSING_CONTEXT.Process):
+        def __init__(self, *args, **kwargs):
+            world.processesSpawned += 1
+            super(world.SupyProcess, self).__init__(*args, **kwargs)
+            log.debug('Spawning process %q.', self.name)
+
+    class _SpawnCommandProcess(_SpawnSupyProcess):
+        """Just does some extra logging and error-recovery for commands that 
need
+        to run in processes.
+        """
+        def __init__(self, target=None, args=(), kwargs={}):
+            pn = kwargs.pop('pn', 'Unknown')
+            cn = kwargs.pop('cn', 'unknown')
+            procName = 'Process #%s (for %s.%s)' % (world.processesSpawned,
+                                                     pn,
+                                                     cn)
+            log.debug('Spawning process %s (args: %r)', procName, args)
+            super().__init__(target=target, name=procName,
+                             args=args, kwargs=kwargs)
+
+    @contextlib.contextmanager
+    def useSpawnContext():
+        """Temporarily redefine SupyProcess and CommandProcess to use the
+        'spawn' multiprocessing context, simulating Windows behaviour on POSIX
+        systems."""
+        with contextlib.ExitStack() as stack:
+            stack.enter_context(unittest.mock.patch(
+                "supybot.world.SUPYPROCESS_MULTIPROCESSING_CONTEXT",
+                _SPAWN_MULTIPROCESSING_CONTEXT))
+            stack.enter_context(unittest.mock.patch(
+                "supybot.world.SupyProcess", _SpawnSupyProcess))
+            stack.enter_context(unittest.mock.patch(
+                "supybot.callbacks.CommandProcess", _SpawnCommandProcess))
+            yield
+
+# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/limnoria-2025.11.2/src/utils/str.py 
new/limnoria-2026.5.8/src/utils/str.py
--- old/limnoria-2025.11.2/src/utils/str.py     2025-11-02 21:37:50.000000000 
+0100
+++ new/limnoria-2026.5.8/src/utils/str.py      2026-05-08 07:07:49.000000000 
+0200
@@ -278,9 +278,13 @@
     """
     (r, g) = perlReToPythonRe(s, allowG=True)
     if g:
-        return lambda s: r.findall(s)
+        return r.findall
     else:
-        return lambda s: r.search(s) and r.search(s).group(0) or ''
+        return functools.partial(_matchFindall, r=r)
+
+def _matchFindall(s, r):
+    match = r.search(s)
+    return match and match.group(0) or ''
 
 def perlReToReplacer(s):
     """Converts a string representation of a Perl regular expression (i.e.,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/limnoria-2025.11.2/src/utils/web.py 
new/limnoria-2026.5.8/src/utils/web.py
--- old/limnoria-2025.11.2/src/utils/web.py     2025-11-02 21:37:50.000000000 
+0100
+++ new/limnoria-2026.5.8/src/utils/web.py      2026-05-08 07:07:49.000000000 
+0200
@@ -88,7 +88,7 @@
 _urlRe = r'(%s://(?:\S+@)?(?:%s|%s)(?::\d+)?(?:/[^\])>\s]*)?)' % (
         _scheme, _domain, _ipAddr)
 urlRe = re.compile(_urlRe, re.I)
-_httpUrlRe = r'(https?://(?:\S+@)?(?:%s|%s)(?::\d+)?(?:/[^\]>\s]*)?)' % \
+_httpUrlRe = r'(https?://(?:\S+@)?(?:%s|%s)(?::\d+)?(?:/[^\]>\s\x00-\x1f]*)?)' 
% \
              (_domain, _ipAddr)
 httpUrlRe = re.compile(_httpUrlRe, re.I)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/limnoria-2025.11.2/src/version.py 
new/limnoria-2026.5.8/src/version.py
--- old/limnoria-2025.11.2/src/version.py       2025-11-02 21:38:00.000000000 
+0100
+++ new/limnoria-2026.5.8/src/version.py        2026-05-08 07:12:08.000000000 
+0200
@@ -1,4 +1,4 @@
-version = '2025.11.02'
+version = '2026.05.08'
 try: # For import from setup.py
     import supybot.utils.python
     supybot.utils.python._debug_software_version = version
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/limnoria-2025.11.2/src/world.py 
new/limnoria-2026.5.8/src/world.py
--- old/limnoria-2025.11.2/src/world.py 2025-11-02 21:37:50.000000000 +0100
+++ new/limnoria-2026.5.8/src/world.py  2026-05-08 07:07:49.000000000 +0200
@@ -64,8 +64,25 @@
         super(SupyThread, self).__init__(*args, **kwargs)
         log.debug('Spawning thread %q.', self.getName())
 
+try:
+    SUPYPROCESS_MULTIPROCESSING_CONTEXT = multiprocessing.get_context('fork')
+    """
+    Which :mod:`multiprocessing` is used to run :class:`SupyProcess`
+
+    Currently this is (unfortunately) ``fork`` when possible (non-Windows)
+    because functions running in it often need to read the global state.
+    """
+except ValueError:
+    SUPYPROCESS_MULTIPROCESSING_CONTEXT = multiprocessing.get_context()
+    """
+    Which :mod:`multiprocessing` is used to run :class:`SupyProcess`
+
+    Currently this is (unfortunately) ``fork`` when possible (non-Windows)
+    because functions running in it often need to read the global state.
+    """
+
 processesSpawned = 1 # Starts at one for the initial process.
-class SupyProcess(multiprocessing.get_context('fork').Process):
+class SupyProcess(SUPYPROCESS_MULTIPROCESSING_CONTEXT.Process):
     def __init__(self, *args, **kwargs):
         global processesSpawned
         processesSpawned += 1
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/limnoria-2025.11.2/test/test_registry.py 
new/limnoria-2026.5.8/test/test_registry.py
--- old/limnoria-2025.11.2/test/test_registry.py        2025-11-02 
21:37:50.000000000 +0100
+++ new/limnoria-2026.5.8/test/test_registry.py 2026-05-08 07:07:49.000000000 
+0200
@@ -35,6 +35,11 @@
 import supybot.conf as conf
 import supybot.registry as registry
 
+conf.registerGlobalValue(conf.supybot, 'testRegexp1',
+    registry.Regexp(r'm/(example\.org)/', 'this is a test registry variable'))
+conf.registerGlobalValue(conf.supybot, 'testRegexp2',
+    registry.Regexp(r'm/(example\.org)/', 'this is a test registry variable'))
+
 join = registry.join
 split = registry.split
 escape = registry.escape
@@ -280,11 +285,14 @@
             .getSpecific(network='testreloadnet',
                 channel='#testreloadchan')(),
             '#')
+        self.assertEqual(conf.supybot.testRegexp1().pattern, r'(example\.org)')
+        self.assertEqual(conf.supybot.testRegexp2().pattern, r'(example\.org)')
 
         filename = conf.supybot.directories.conf.dirize('reload.conf')
         registry.close(conf.supybot, filename)
         with open(filename, 'at') as fd:
-            fd.write('supybot.reply.whenAddressedBy.chars: !')
+            fd.write('supybot.reply.whenAddressedBy.chars: !\n')
+            fd.write('supybot.testRegexp1: m/example\\\\.com/\n')
 
         registry.open_registry(filename)
 
@@ -299,6 +307,7 @@
         self.assertEqual(conf.supybot.reply.whenAddressedBy.chars
             .getSpecific(channel='#testchan')(),
             '!')
+        self.assertEqual(conf.supybot.testRegexp1().pattern, r'example\.com')
 
         # remain unchanged
         self.assertEqual(conf.supybot.reply.whenAddressedBy.chars
@@ -308,6 +317,7 @@
             .getSpecific(network='testreloadnet',
                 channel='#testreloadchan')(),
             '#')
+        self.assertEqual(conf.supybot.testRegexp2().pattern, r'(example\.org)')
 
     def testWith(self):
         v = registry.String('foo', 'help')

++++++ skip-fediverse-profile-tests.patch ++++++
--- /var/tmp/diff_new_pack.Ba7raD/_old  2026-05-19 17:49:03.677367071 +0200
+++ /var/tmp/diff_new_pack.Ba7raD/_new  2026-05-19 17:49:03.689367568 +0200
@@ -1,16 +1,16 @@
-Index: limnoria-2025.11.2/plugins/Fediverse/test.py
+Index: limnoria-2026.5.8/plugins/Fediverse/test.py
 ===================================================================
---- limnoria-2025.11.2.orig/plugins/Fediverse/test.py
-+++ limnoria-2025.11.2/plugins/Fediverse/test.py
-@@ -33,6 +33,7 @@ import copy
+--- limnoria-2026.5.8.orig/plugins/Fediverse/test.py
++++ limnoria-2026.5.8/plugins/Fediverse/test.py
+@@ -31,6 +31,7 @@
+ import os
+ import copy
  import json
++import unittest
  import functools
  import contextlib
-+import unittest
- from multiprocessing import Manager
  
- from supybot import conf, log, utils
-@@ -155,6 +156,7 @@ class NetworklessFediverseTestCase(BaseF
+@@ -154,6 +155,7 @@ class NetworklessFediverseTestCase(BaseF
                  list(expected_requests), [], "Less requests than expected."
              )
  
@@ -18,7 +18,7 @@
      def testFeaturedNone(self):
          featured = {
              "@context": "https://www.w3.org/ns/activitystreams";,
-@@ -176,6 +178,7 @@ class NetworklessFediverseTestCase(BaseF
+@@ -175,6 +177,7 @@ class NetworklessFediverseTestCase(BaseF
                  "featured @[email protected]", "No featured statuses."
              )
  
@@ -26,7 +26,7 @@
      def testFeaturedSome(self):
          featured = {
              "@context": [
-@@ -242,6 +245,7 @@ class NetworklessFediverseTestCase(BaseF
+@@ -241,6 +244,7 @@ class NetworklessFediverseTestCase(BaseF
                  "featured @[email protected]", "This is a pinned toot"
              )
  
@@ -34,7 +34,7 @@
      def testProfile(self):
          expected_requests = [
              (HOSTMETA_URL, HOSTMETA_DATA),
-@@ -255,6 +259,7 @@ class NetworklessFediverseTestCase(BaseF
+@@ -254,6 +258,7 @@ class NetworklessFediverseTestCase(BaseF
                  "\x02someuser\x02 (@[email protected]): My Biography",
              )
  
@@ -42,7 +42,7 @@
      def testProfileNoHostmeta(self):
          expected_requests = [
              (HOSTMETA_URL, utils.web.Error("blah")),
-@@ -268,6 +273,7 @@ class NetworklessFediverseTestCase(BaseF
+@@ -267,6 +272,7 @@ class NetworklessFediverseTestCase(BaseF
                  "\x02someuser\x02 (@[email protected]): My Biography",
              )
  
@@ -50,7 +50,7 @@
      def testProfileSnarfer(self):
          with self.mockWebfingerSupport("not called"), self.mockRequests([]):
              self.assertSnarfNoResponse("aaa @[email protected] bbb")
-@@ -343,6 +349,7 @@ class NetworklessFediverseTestCase(BaseF
+@@ -342,6 +348,7 @@ class NetworklessFediverseTestCase(BaseF
                      "\x02someuser\x02 (@[email protected]): My Biography",
                  )
  
@@ -58,7 +58,7 @@
      def testProfileUnknown(self):
          expected_requests = [
              (HOSTMETA_URL, HOSTMETA_DATA),
-@@ -404,6 +411,7 @@ class NetworklessFediverseTestCase(BaseF
+@@ -403,6 +410,7 @@ class NetworklessFediverseTestCase(BaseF
                  "@FirstAuthor I am replying to you",
              )
  

Reply via email to