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",
)