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 2022-11-01 13:42:23
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-limnoria (Old)
 and      /work/SRC/openSUSE:Factory/.python-limnoria.new.2275 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-limnoria"

Tue Nov  1 13:42:23 2022 rev:27 rq:1032514 version:2022.09.27

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-limnoria/python-limnoria.changes  
2022-07-15 13:52:43.903568340 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-limnoria.new.2275/python-limnoria.changes    
    2022-11-01 13:42:34.387897647 +0100
@@ -1,0 +2,6 @@
+Mon Oct 31 09:57:34 UTC 2022 - Atri Bhattacharya <badshah...@gmail.com>
+
+- Update to version 2022-09-27:
+  * utils/web: Add <br/> to the list of block elements.
+
+-------------------------------------------------------------------

Old:
----
  limnoria-2022.07.03.tar.gz

New:
----
  limnoria-2022.09.27.tar.gz

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

Other differences:
------------------
++++++ python-limnoria.spec ++++++
--- /var/tmp/diff_new_pack.zvtK2G/_old  2022-11-01 13:42:34.875900244 +0100
+++ /var/tmp/diff_new_pack.zvtK2G/_new  2022-11-01 13:42:34.879900264 +0100
@@ -18,9 +18,9 @@
 
 %define skip_python2 1
 %define appname limnoria
-%define srcver 2022-07-03
+%define srcver 2022-09-27
 Name:           python-limnoria
-Version:        2022.07.03
+Version:        2022.09.27
 Release:        0
 Summary:        A modified version of Supybot (an IRC bot and framework)
 License:        BSD-3-Clause

++++++ limnoria-2022.07.03.tar.gz -> limnoria-2022.09.27.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2022-07-03/plugins/Aka/README.rst 
new/Limnoria-master-2022-09-27/plugins/Aka/README.rst
--- old/Limnoria-master-2022-07-03/plugins/Aka/README.rst       2022-06-23 
22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/plugins/Aka/README.rst       2022-09-20 
07:51:46.000000000 +0200
@@ -46,7 +46,7 @@
 Trout
 ^^^^^
 
-Add an aka, trout, which expects a word as an argument::
+Add an aka, ``trout``, which expects a word as an argument::
 
     <jamessan> @aka add trout "reply action slaps $1 with a large trout"
     <bot> jamessan: The operation succeeded.
@@ -56,23 +56,19 @@
 This ``trout`` aka requires the plugin ``Reply`` to be loaded since it
 provides the ``action`` command.
 
-LastFM
-^^^^^^
+Random percentage
+^^^^^^^^^^^^^^^^^
 
-Add an aka, ``lastfm``, which expects a last.fm username and replies with
-their most recently played item::
+Add an aka, ``randpercent``, which returns a random percentage value::
 
-    @aka add lastfm "rss [format concat http://ws.audioscrobbler.com/1.0/user/ 
[format concat [web urlquote $1] /recenttracks.rss]]"
+    @aka add randpercent "squish [dice 1d100]%"
 
-This ``lastfm`` aka requires the following plugins to be loaded: ``RSS``,
-``Format`` and ``Web``.
+This requires the ``Filter`` and ``Games`` plugins to be loaded.
 
-``RSS`` provides ``rss``, ``Format`` provides ``concat`` and ``Web`` provides
-``urlquote``.
-
-Note that if the nested commands being aliased hadn't been quoted, then
-those commands would have been run immediately, and ``@lastfm`` would always
-reply with the same information, the result of those commands.
+Note that nested commands in an alias should be quoted, or they will only
+run once when you create the alias, and not each time the alias is
+called. (In this case, not quoting the nested command would mean that
+``@randpercent`` always responds with the same value!)
 
 .. _commands-Aka:
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2022-07-03/plugins/Aka/plugin.py 
new/Limnoria-master-2022-09-27/plugins/Aka/plugin.py
--- old/Limnoria-master-2022-07-03/plugins/Aka/plugin.py        2022-06-23 
22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/plugins/Aka/plugin.py        2022-09-20 
07:51:46.000000000 +0200
@@ -532,7 +532,7 @@
     Trout
     ^^^^^
 
-    Add an aka, trout, which expects a word as an argument::
+    Add an aka, ``trout``, which expects a word as an argument::
 
         <jamessan> @aka add trout "reply action slaps $1 with a large trout"
         <bot> jamessan: The operation succeeded.
@@ -542,23 +542,19 @@
     This ``trout`` aka requires the plugin ``Reply`` to be loaded since it
     provides the ``action`` command.
 
-    LastFM
-    ^^^^^^
+    Random percentage
+    ^^^^^^^^^^^^^^^^^
 
-    Add an aka, ``lastfm``, which expects a last.fm username and replies with
-    their most recently played item::
+    Add an aka, ``randpercent``, which returns a random percentage value::
 
-        @aka add lastfm "rss [format concat 
http://ws.audioscrobbler.com/1.0/user/ [format concat [web urlquote $1] 
/recenttracks.rss]]"
+        @aka add randpercent "squish [dice 1d100]%"
 
-    This ``lastfm`` aka requires the following plugins to be loaded: ``RSS``,
-    ``Format`` and ``Web``.
+    This requires the ``Filter`` and ``Games`` plugins to be loaded.
 
-    ``RSS`` provides ``rss``, ``Format`` provides ``concat`` and ``Web`` 
provides
-    ``urlquote``.
-
-    Note that if the nested commands being aliased hadn't been quoted, then
-    those commands would have been run immediately, and ``@lastfm`` would 
always
-    reply with the same information, the result of those commands.
+    Note that nested commands in an alias should be quoted, or they will only
+    run once when you create the alias, and not each time the alias is
+    called. (In this case, not quoting the nested command would mean that
+    ``@randpercent`` always responds with the same value!)
     """
 
     def __init__(self, irc):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2022-07-03/plugins/Alias/README.rst 
new/Limnoria-master-2022-09-27/plugins/Alias/README.rst
--- old/Limnoria-master-2022-07-03/plugins/Alias/README.rst     2022-06-23 
22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/plugins/Alias/README.rst     2022-09-20 
07:51:46.000000000 +0200
@@ -18,21 +18,23 @@
 built-in Aka plugin instead (you can migrate your existing aliases using
 the 'importaliasdatabase' command.
 
-To add an alias, `trout`, which expects a word as an argument::
+To add an alias, ``trout``, which expects a word as an argument::
 
     <jamessan> @alias add trout "action slaps $1 with a large trout"
     <bot> jamessan: The operation succeeded.
     <jamessan> @trout me
     * bot slaps me with a large trout
 
-To add an alias, `lastfm`, which expects a last.fm user and replies with
-their recently played items::
+Add an alias, ``randpercent``, which returns a random percentage value::
 
-    @alias add lastfm "rss [format concat 
http://ws.audioscrobbler.com/1.0/user/ [format concat [urlquote $1] 
/recenttracks.rss]]"
+    @alias add randpercent "squish [dice 1d100]%"
 
-Note that if the nested commands being aliased hadn't been quoted, then
-those commands would have been run immediately, and `@lastfm` would always
-reply with the same information, the result of those commands.
+This requires the ``Filter`` and ``Games`` plugins to be loaded.
+
+Note that nested commands in an alias should be quoted, or they will only
+run once when you create the alias, and not each time the alias is
+called. (In this case, not quoting the nested command would mean that
+``@randpercent`` always responds with the same value!)
 
 .. _commands-Alias:
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2022-07-03/plugins/Alias/plugin.py 
new/Limnoria-master-2022-09-27/plugins/Alias/plugin.py
--- old/Limnoria-master-2022-07-03/plugins/Alias/plugin.py      2022-06-23 
22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/plugins/Alias/plugin.py      2022-09-20 
07:51:46.000000000 +0200
@@ -243,21 +243,23 @@
     built-in Aka plugin instead (you can migrate your existing aliases using
     the 'importaliasdatabase' command.
 
-    To add an alias, `trout`, which expects a word as an argument::
+    To add an alias, ``trout``, which expects a word as an argument::
 
         <jamessan> @alias add trout "action slaps $1 with a large trout"
         <bot> jamessan: The operation succeeded.
         <jamessan> @trout me
         * bot slaps me with a large trout
 
-    To add an alias, `lastfm`, which expects a last.fm user and replies with
-    their recently played items::
+    Add an alias, ``randpercent``, which returns a random percentage value::
 
-        @alias add lastfm "rss [format concat 
http://ws.audioscrobbler.com/1.0/user/ [format concat [urlquote $1] 
/recenttracks.rss]]"
+        @alias add randpercent "squish [dice 1d100]%"
 
-    Note that if the nested commands being aliased hadn't been quoted, then
-    those commands would have been run immediately, and `@lastfm` would always
-    reply with the same information, the result of those commands.
+    This requires the ``Filter`` and ``Games`` plugins to be loaded.
+
+    Note that nested commands in an alias should be quoted, or they will only
+    run once when you create the alias, and not each time the alias is
+    called. (In this case, not quoting the nested command would mean that
+    ``@randpercent`` always responds with the same value!)
     """
     def __init__(self, irc):
         self.__parent = super(Alias, self)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/Limnoria-master-2022-07-03/plugins/Autocomplete/__init__.py 
new/Limnoria-master-2022-09-27/plugins/Autocomplete/__init__.py
--- old/Limnoria-master-2022-07-03/plugins/Autocomplete/__init__.py     
2022-06-23 22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/plugins/Autocomplete/__init__.py     
2022-09-20 07:51:46.000000000 +0200
@@ -61,6 +61,7 @@
 from . import plugin
 
 from importlib import reload
+
 # In case we're being reloaded.
 reload(config)
 reload(plugin)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2022-07-03/plugins/Channel/plugin.py 
new/Limnoria-master-2022-09-27/plugins/Channel/plugin.py
--- old/Limnoria-master-2022-07-03/plugins/Channel/plugin.py    2022-06-23 
22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/plugins/Channel/plugin.py    2022-09-20 
07:51:46.000000000 +0200
@@ -381,8 +381,12 @@
                              msg.prefix, bannedNick)
             raise callbacks.ArgumentError
         elif bannedNick == irc.nick:
-            self.log.warning('%q tried to make me kban myself.', msg.prefix)
-            irc.error(_('I cowardly refuse to kickban myself.'))
+            if kick:
+                self.log.warning('%q tried to make me kban myself.', 
msg.prefix)
+                irc.error(_('I cowardly refuse to kickban myself.'))
+            else:
+                self.log.warning('%q tried to make me ban myself.', msg.prefix)
+                irc.error(_('I cowardly refuse to ban myself.'))
             return
         if not reason:
             reason = msg.nick
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2022-07-03/plugins/Config/test.py 
new/Limnoria-master-2022-09-27/plugins/Config/test.py
--- old/Limnoria-master-2022-07-03/plugins/Config/test.py       2022-06-23 
22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/plugins/Config/test.py       2022-09-20 
07:51:46.000000000 +0200
@@ -1,7 +1,7 @@
 ###
 # Copyright (c) 2002-2005, Jeremiah Fincher
 # Copyright (c) 2009, James McCoy
-# Copyright (c) 2010-2021, Valentin Lorentz
+# Copyright (c) 2010-2022, Valentin Lorentz
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -33,11 +33,20 @@
 
 from supybot.test import *
 import supybot.conf as conf
+import supybot.registry as registry
 
 _letters = 'abcdefghijklmnopqrstuvwxyz'
 def random_string():
     return ''.join(random.choice(_letters) for _ in range(16))
 
+class Fruit(registry.OnlySomeStrings):
+    validStrings = ('Apple', 'Orange')
+
+group = conf.registerGroup(conf.supybot.plugins.Config, 'test')
+conf.registerGlobalValue(group, 'fruit',
+    Fruit('Orange', '''Must be a fruit'''))
+
+
 class ConfigTestCase(ChannelPluginTestCase):
     # We add utilities so there's something in supybot.plugins.
     plugins = ('Config', 'User', 'Utilities', 'Web')
@@ -50,6 +59,16 @@
         self.assertNotRegexp('config get supybot.reply', r'registry\.Group')
         self.assertResponse('config supybot.protocols.irc.throttleTime', '0.0')
 
+    def testSetOnlysomestrings(self):
+        self.assertResponse('config supybot.plugins.Config.test.fruit Apple',
+                            'The operation succeeded.')
+        self.assertResponse('config supybot.plugins.Config.test.fruit orange',
+                            'The operation succeeded.')
+        self.assertResponse('config supybot.plugins.Config.test.fruit Tomatoe',
+                            "Error: Valid values include 'Apple' and "
+                            "'Orange', not 'Tomatoe'.")
+
+
     def testList(self):
         self.assertError('config list asldfkj')
         self.assertError('config list supybot.asdfkjsldf')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2022-07-03/plugins/Ctcp/plugin.py 
new/Limnoria-master-2022-09-27/plugins/Ctcp/plugin.py
--- old/Limnoria-master-2022-07-03/plugins/Ctcp/plugin.py       2022-06-23 
22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/plugins/Ctcp/plugin.py       2022-09-20 
07:51:46.000000000 +0200
@@ -64,7 +64,7 @@
     def callCommand(self, command, irc, msg, *args, **kwargs):
         if conf.supybot.abuse.flood.ctcp():
             now = time.time()
-            for (ignore, expiration) in self.ignores.items():
+            for (ignore, expiration) in list(self.ignores.items()):
                 if expiration < now:
                     del self.ignores[ignore]
                 elif ircutils.hostmaskPatternEqual(ignore, msg.prefix):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/Limnoria-master-2022-07-03/plugins/Fediverse/__init__.py 
new/Limnoria-master-2022-09-27/plugins/Fediverse/__init__.py
--- old/Limnoria-master-2022-07-03/plugins/Fediverse/__init__.py        
2022-06-23 22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/plugins/Fediverse/__init__.py        
2022-09-20 07:51:46.000000000 +0200
@@ -53,6 +53,7 @@
 from . import plugin
 
 from importlib import reload
+
 # In case we're being reloaded.
 reload(config)
 reload(plugin)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/Limnoria-master-2022-07-03/plugins/Fediverse/plugin.py 
new/Limnoria-master-2022-09-27/plugins/Fediverse/plugin.py
--- old/Limnoria-master-2022-07-03/plugins/Fediverse/plugin.py  2022-06-23 
22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/plugins/Fediverse/plugin.py  2022-09-20 
07:51:46.000000000 +0200
@@ -38,6 +38,7 @@
 from supybot.i18n import PluginInternationalization
 
 from . import activitypub as ap
+from .utils import parse_xsd_duration
 
 
 importlib.reload(ap)
@@ -49,6 +50,10 @@
 _username_regexp = re.compile("@(?P<localuser>[^@ ]+)@(?P<hostname>[^@ ]+)")
 
 
+def html_to_text(html):
+    return utils.web.htmlToText(html).split("\n", 1)[0].strip()
+
+
 class FediverseHttp(httpserver.SupyHTTPServerCallback):
     name = "minimal ActivityPub server"
     defaultResponse = _(
@@ -222,18 +227,40 @@
         name = actor.get("name", username)
         return "\x02%s\x02 (@%s@%s)" % (name, username, hostname)
 
+    def _format_author(self, irc, author):
+        if isinstance(author, str):
+            # it's an URL
+            try:
+                author = self._get_actor(irc, author)
+            except ap.ActivityPubError as e:
+                return _("<error: %s>") % str(e)
+            else:
+                return self._format_actor_fullname(author)
+        elif isinstance(author, dict):
+            if author.get("type") == "Group":
+                # Typically, there is an actor named "Default <username> 
channel"
+                # on PeerTube, which we do not want to show.
+                return None
+            if author.get("id"):
+                return self._format_author(irc, author["id"])
+        elif isinstance(author, list):
+            return format(
+                "%L",
+                filter(
+                    bool, [self._format_author(irc, item) for item in author]
+                ),
+            )
+        else:
+            return "<unknown>"
+
     def _format_status(self, irc, msg, status):
         if status["type"] == "Create":
             return self._format_status(irc, msg, status["object"])
         elif status["type"] == "Note":
-            author_url = status["attributedTo"]
-            try:
-                author = self._get_actor(irc, author_url)
-            except ap.ActivityPubError as e:
-                author_fullname = _("<error: %s>") % str(e)
-            else:
-                author_fullname = self._format_actor_fullname(author)
             cw = status.get("summary")
+            author_fullname = self._format_author(
+                irc, status.get("attributedTo")
+            )
             if cw:
                 if self.registryValue(
                     "format.statuses.showContentWithCW",
@@ -246,7 +273,7 @@
                         % (
                             author_fullname,
                             cw,
-                            utils.web.htmlToText(status["content"]),
+                            html_to_text(status["content"]),
                         )
                     ]
                 else:
@@ -258,7 +285,7 @@
                     _("%s: %s")
                     % (
                         author_fullname,
-                        utils.web.htmlToText(status["content"]),
+                        html_to_text(status["content"]),
                     )
                 ]
 
@@ -275,6 +302,17 @@
                 return self._format_status(irc, msg, status)
             except ap.ActivityPubProtocolError as e:
                 return "<Could not fetch status: %s>" % e.args[0]
+        elif status["type"] == "Video":
+            author_fullname = self._format_author(
+                irc, status.get("attributedTo")
+            )
+            return format(
+                _("\x02%s\x02 (%T) by %s: %s"),
+                status["name"],
+                abs(parse_xsd_duration(status["duration"]).total_seconds()),
+                author_fullname,
+                html_to_text(status["content"]),
+            )
         else:
             assert False, "Unknown status type %s: %r" % (
                 status["type"],
@@ -292,14 +330,14 @@
             _("%s: %s")
             % (
                 self._format_actor_fullname(actor),
-                utils.web.htmlToText(actor["summary"]),
+                html_to_text(actor["summary"]),
             )
         )
 
     def _format_profile(self, irc, msg, actor):
         return _("%s: %s") % (
             self._format_actor_fullname(actor),
-            utils.web.htmlToText(actor["summary"]),
+            html_to_text(actor["summary"]),
         )
 
     def usernameSnarfer(self, irc, msg, match):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2022-07-03/plugins/Fediverse/test.py 
new/Limnoria-master-2022-09-27/plugins/Fediverse/test.py
--- old/Limnoria-master-2022-07-03/plugins/Fediverse/test.py    2022-06-23 
22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/plugins/Fediverse/test.py    2022-09-20 
07:51:46.000000000 +0200
@@ -60,6 +60,10 @@
     BOOSTED_DATA,
     BOOSTED_ACTOR_URL,
     BOOSTED_ACTOR_DATA,
+    PEERTUBE_VIDEO_URL,
+    PEERTUBE_VIDEO_DATA,
+    PEERTUBE_ACTOR_URL,
+    PEERTUBE_ACTOR_DATA,
 )
 
 
@@ -430,6 +434,20 @@
                     + 
"<https://example.net/system/media_attachments/image.png>",
                 )
 
+    def testVideo(self):
+        expected_requests = [
+            (PEERTUBE_VIDEO_URL, PEERTUBE_VIDEO_DATA),
+            (PEERTUBE_ACTOR_URL, PEERTUBE_ACTOR_DATA),
+        ]
+
+        with self.mockRequests(expected_requests):
+            self.assertResponse(
+                "status https://example.org/w/gABde9e210FGHre";,
+                "\x02name of video\x02 (1 hour, 26 minutes, and 0 seconds) "
+                "by \x02chocobozzz\x02 (@chocobo...@peertube.cpy.re): "
+                "description of the video with a second line",
+            )
+
     def testStatusUrlSnarferDisabled(self):
         with self.mockWebfingerSupport("not called"), self.mockRequests([]):
             self.assertSnarfNoResponse(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/Limnoria-master-2022-07-03/plugins/Fediverse/test_data.py 
new/Limnoria-master-2022-09-27/plugins/Fediverse/test_data.py
--- old/Limnoria-master-2022-07-03/plugins/Fediverse/test_data.py       
2022-06-23 22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/plugins/Fediverse/test_data.py       
2022-09-20 07:51:46.000000000 +0200
@@ -384,3 +384,124 @@
     "endpoints": {"sharedInbox": "https://example.net/inbox"},
 }
 BOOSTED_ACTOR_DATA = json.dumps(BOOSTED_ACTOR_VALUE).encode()
+
+PEERTUBE_ACTOR_VALUE = {
+    "@context": [
+        "https://www.w3.org/ns/activitystreams";,
+        "https://w3id.org/security/v1";,
+        {"RsaSignature2017": "https://w3id.org/security#RsaSignature2017"},
+        {
+            "pt": "https://joinpeertube.org/ns#";,
+            "sc": "http://schema.org/";,
+            "playlists": {"@id": "pt:playlists", "@type": "@id"},
+        },
+    ],
+    "type": "Person",
+    "id": "https://peertube.cpy.re/accounts/chocobozzz";,
+    "following": "https://peertube.cpy.re/accounts/chocobozzz/following";,
+    "followers": "https://peertube.cpy.re/accounts/chocobozzz/followers";,
+    "playlists": "https://peertube.cpy.re/accounts/chocobozzz/playlists";,
+    "inbox": "https://peertube.cpy.re/accounts/chocobozzz/inbox";,
+    "outbox": "https://peertube.cpy.re/accounts/chocobozzz/outbox";,
+    "preferredUsername": "chocobozzz",
+    "url": "https://peertube.cpy.re/accounts/chocobozzz";,
+    "name": "chocobozzz",
+    "published": "2017-11-28T08:48:24.271Z",
+    "summary": None,
+}
+PEERTUBE_ACTOR_DATA = json.dumps(PEERTUBE_ACTOR_VALUE).encode()
+PEERTUBE_ACTOR_URL = "https://peertube.cpy.re/accounts/chocobozzz";
+
+
+PEERTUBE_VIDEO_VALUE = {
+    "@context": [
+        "https://www.w3.org/ns/activitystreams";,
+        "https://w3id.org/security/v1";,
+        {"RsaSignature2017": "https://w3id.org/security#RsaSignature2017"},
+        {
+            "pt": "https://joinpeertube.org/ns#";,
+            "sc": "http://schema.org/";,
+            "Hashtag": "as:Hashtag",
+            "uuid": "sc:identifier",
+            "category": "sc:category",
+            "licence": "sc:license",
+            "subtitleLanguage": "sc:subtitleLanguage",
+            "sensitive": "as:sensitive",
+            "language": "sc:inLanguage",
+            "icons": "as:icon",
+            "isLiveBroadcast": "sc:isLiveBroadcast",
+            "liveSaveReplay": {
+                "@type": "sc:Boolean",
+                "@id": "pt:liveSaveReplay",
+            },
+            "permanentLive": {
+                "@type": "sc:Boolean",
+                "@id": "pt:permanentLive",
+            },
+            "latencyMode": {"@type": "sc:Number", "@id": "pt:latencyMode"},
+            "Infohash": "pt:Infohash",
+            "originallyPublishedAt": "sc:datePublished",
+            "views": {"@type": "sc:Number", "@id": "pt:views"},
+            "state": {"@type": "sc:Number", "@id": "pt:state"},
+            "size": {"@type": "sc:Number", "@id": "pt:size"},
+            "fps": {"@type": "sc:Number", "@id": "pt:fps"},
+            "commentsEnabled": {
+                "@type": "sc:Boolean",
+                "@id": "pt:commentsEnabled",
+            },
+            "downloadEnabled": {
+                "@type": "sc:Boolean",
+                "@id": "pt:downloadEnabled",
+            },
+            "waitTranscoding": {
+                "@type": "sc:Boolean",
+                "@id": "pt:waitTranscoding",
+            },
+            "support": {"@type": "sc:Text", "@id": "pt:support"},
+            "likes": {"@id": "as:likes", "@type": "@id"},
+            "dislikes": {"@id": "as:dislikes", "@type": "@id"},
+            "shares": {"@id": "as:shares", "@type": "@id"},
+            "comments": {"@id": "as:comments", "@type": "@id"},
+        },
+    ],
+    "to": ["https://www.w3.org/ns/activitystreams#Public";],
+    "type": "Video",
+    "name": "name of video",
+    "duration": "PT5160S",
+    "tag": [{"type": "Hashtag", "name": "vostfr"}],
+    "category": {"identifier": "2", "name": "Films"},
+    "licence": {"identifier": "4", "name": "Attribution - Non Commercial"},
+    "language": {"identifier": "en", "name": "English"},
+    "views": 13718,
+    "sensitive": False,
+    "waitTranscoding": False,
+    "state": 1,
+    "commentsEnabled": True,
+    "downloadEnabled": True,
+    "published": "2017-10-23T07:54:38.155Z",
+    "originallyPublishedAt": None,
+    "updated": "2022-07-13T07:03:12.373Z",
+    "mediaType": "text/markdown",
+    "content": "description of <strong>the</strong> video\r\nwith a second 
line",
+    "support": None,
+    "subtitleLanguage": [],
+    "icon": [
+        # redacted
+    ],
+    "url": [
+        # redacted
+    ],
+    "attributedTo": [
+        {"type": "Person", "id": PEERTUBE_ACTOR_URL},
+        {
+            "type": "Group",
+            "id": ACTOR_URL,
+        },
+    ],
+    "isLiveBroadcast": False,
+    "liveSaveReplay": None,
+    "permanentLive": None,
+    "latencyMode": None,
+}
+PEERTUBE_VIDEO_DATA = json.dumps(PEERTUBE_VIDEO_VALUE).encode()
+PEERTUBE_VIDEO_URL = "https://example.org/w/gABde9e210FGHre";
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/Limnoria-master-2022-07-03/plugins/Fediverse/utils.py 
new/Limnoria-master-2022-09-27/plugins/Fediverse/utils.py
--- old/Limnoria-master-2022-07-03/plugins/Fediverse/utils.py   1970-01-01 
01:00:00.000000000 +0100
+++ new/Limnoria-master-2022-09-27/plugins/Fediverse/utils.py   2022-09-20 
07:51:46.000000000 +0200
@@ -0,0 +1,63 @@
+###
+# Copyright (c) 2022, Valentin Lorentz
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+#   * Redistributions of source code must retain the above copyright notice,
+#     this list of conditions, and the following disclaimer.
+#   * Redistributions in binary form must reproduce the above copyright notice,
+#     this list of conditions, and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution.
+#   * Neither the name of the author of this software nor the name of
+#     contributors to this software may be used to endorse or promote products
+#     derived from this software without specific prior written consent.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+###
+
+import re
+import datetime
+
+# Credits for the regexp and function: 
https://stackoverflow.com/a/2765366/539465
+_XSD_DURATION_RE = re.compile(
+    "(?P<sign>-?)P"
+    "(?:(?P<years>\d+)Y)?"
+    "(?:(?P<months>\d+)M)?"
+    "(?:(?P<days>\d+)D)?"
+    "(?:T(?:(?P<hours>\d+)H)?(?:(?P<minutes>\d+)M)?(?:(?P<seconds>\d+)S)?)?"
+)
+
+
+def parse_xsd_duration(s):
+    """Parses this format to a timedelta:
+    https://www.w3.org/TR/xmlschema11-2/#duration""";
+    # Fetch the match groups with default value of 0 (not None)
+    duration = _XSD_DURATION_RE.match(s).groupdict(0)
+
+    # Create the timedelta object from extracted groups
+    delta = datetime.timedelta(
+        days=int(duration["days"])
+        + (int(duration["months"]) * 30)
+        + (int(duration["years"]) * 365),
+        hours=int(duration["hours"]),
+        minutes=int(duration["minutes"]),
+        seconds=int(duration["seconds"]),
+    )
+
+    if duration["sign"] == "-":
+        delta *= -1
+
+    return delta
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/Limnoria-master-2022-07-03/plugins/Geography/plugin.py 
new/Limnoria-master-2022-09-27/plugins/Geography/plugin.py
--- old/Limnoria-master-2022-07-03/plugins/Geography/plugin.py  2022-06-23 
22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/plugins/Geography/plugin.py  2022-09-20 
07:51:46.000000000 +0200
@@ -150,7 +150,8 @@
                 continue
 
             offset_seconds = int(
-                datetime.datetime.now(tz=timezone).utcoffset().total_seconds())
+                datetime.datetime.now(tz=timezone).utcoffset().total_seconds()
+            )
             offset = self._format_utc_offset(offset_seconds)
 
             # Extract a human-friendly name, depending on the type of
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2022-07-03/plugins/Geography/test.py 
new/Limnoria-master-2022-09-27/plugins/Geography/test.py
--- old/Limnoria-master-2022-07-03/plugins/Geography/test.py    2022-06-23 
22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/plugins/Geography/test.py    2022-09-20 
07:51:46.000000000 +0200
@@ -83,7 +83,7 @@
         with patch.object(wikidata, "timezone_from_uri", return_value=tz):
             self.assertRegexp(
                 "timezone Newfoundland",
-                r"Canada/Newfoundland \(currently UTC-[23]:30\)"
+                r"Canada/Newfoundland \(currently UTC-[23]:30\)",
             )
 
         tz = pytz.timezone("Asia/Kolkata")
@@ -111,7 +111,7 @@
         with patch.object(wikidata, "timezone_from_uri", return_value=tz):
             self.assertRegexp(
                 "timezone Newfoundland",
-                r"Canada/Newfoundland \(currently UTC-[23]:30\)"
+                r"Canada/Newfoundland \(currently UTC-[23]:30\)",
             )
 
         tz = zoneinfo.ZoneInfo("Asia/Kolkata")
@@ -144,9 +144,7 @@
         self.assertRegexp(
             "timezone Delhi", r"Asia/Kolkata \(currently UTC\+5:30\)"
         )
-        self.assertRegexp(
-            "timezone Newfoundland", r"UTC-[23]:30"
-        )
+        self.assertRegexp("timezone Newfoundland", r"UTC-[23]:30")
 
 
 class GeographyLocaltimeTestCase(PluginTestCase):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/Limnoria-master-2022-07-03/plugins/Geography/wikidata.py 
new/Limnoria-master-2022-09-27/plugins/Geography/wikidata.py
--- old/Limnoria-master-2022-07-03/plugins/Geography/wikidata.py        
2022-06-23 22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/plugins/Geography/wikidata.py        
2022-09-20 07:51:46.000000000 +0200
@@ -134,10 +134,12 @@
     """Returns a :class:datetime.tzinfo object, given a Wikidata Q-ID.
     eg. ``"Q60"`` for New York City."""
     for tztype in [
-        "http://www.wikidata.org/entity/Q17272692";, # IANA timezones first
-        "http://www.wikidata.org/entity/Q12143";, # any timezone as a fallback
+        "http://www.wikidata.org/entity/Q17272692";,  # IANA timezones first
+        "http://www.wikidata.org/entity/Q12143";,  # any timezone as a fallback
     ]:
-        data = _query_sparql(TIMEZONE_QUERY.substitute(subject=location_uri, 
tztype=tztype))
+        data = _query_sparql(
+            TIMEZONE_QUERY.substitute(subject=location_uri, tztype=tztype)
+        )
         results = data["results"]["bindings"]
         for result in results:
             if "tzid" in result:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2022-07-03/plugins/Math/plugin.py 
new/Limnoria-master-2022-09-27/plugins/Math/plugin.py
--- old/Limnoria-master-2022-07-03/plugins/Math/plugin.py       2022-06-23 
22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/plugins/Math/plugin.py       2022-09-20 
07:51:46.000000000 +0200
@@ -166,8 +166,8 @@
         """
         try:
             self.log.info('evaluating %q from %s', text, msg.prefix)
-            x = safe_eval(text, allow_ints=True)
-            irc.reply(str(x))
+            result = safe_eval(text, allow_ints=True)
+            float(result)  # fail early if it is too large to be displayed
         except OverflowError:
             maxFloat = math.ldexp(0.9999999999999999, 1024)
             irc.error(_('The answer exceeded %s or so.') % maxFloat)
@@ -177,6 +177,17 @@
             irc.error(_('%s is not a defined function.') % str(e).split()[1])
         except Exception as e:
             irc.error(utils.exnToString(e))
+        else:
+            try:
+                result_str = str(result)
+            except ValueError as e:
+                # Probably too large to be converted to string; go through
+                # floats instead.
+                # 
https://docs.python.org/3/library/stdtypes.html#int-max-str-digits
+                # (Depending on configuration, this may be dead code because it
+                # is caught by the float() check above.
+                result_str = str(float(result))
+            irc.reply(result_str)
     icalc = wrap(icalc, [('checkCapability', 'trusted'), 'text'])
 
     _rpnEnv = {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2022-07-03/plugins/Math/test.py 
new/Limnoria-master-2022-09-27/plugins/Math/test.py
--- old/Limnoria-master-2022-07-03/plugins/Math/test.py 2022-06-23 
22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/plugins/Math/test.py 2022-09-20 
07:51:46.000000000 +0200
@@ -112,7 +112,10 @@
         self.assertNotError('calc (1600 * 1200) - 2*(1024*1280)')
         self.assertNotError('calc 3-2*4')
         self.assertNotError('calc (1600 * 1200)-2*(1024*1280)')
-        self.assertError('calc factorial(20000)')
+        self.assertResponse('calc factorial(20000)',
+            'Error: factorial argument too large')
+        self.assertResponse('calc factorial(20000) / factorial(19999)',
+            'Error: factorial argument too large')
 
     def testCalcNoNameError(self):
         self.assertRegexp('calc foobar(x)', 'foobar is not a defined function')
@@ -147,7 +150,10 @@
         self.assertResponse('icalc 1^1', '0')
         self.assertResponse('icalc 10**24', '1' + '0'*24)
         self.assertRegexp('icalc 49/6', '8.16')
-        self.assertNotError('icalc factorial(20000)')
+        self.assertRegexp('icalc factorial(20000)',
+            'Error: The answer exceeded')
+        self.assertResponse('icalc factorial(20000) / factorial(19999)',
+            '20000.0')
 
     def testRpn(self):
         self.assertResponse('rpn 5 2 +', '7')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/Limnoria-master-2022-07-03/plugins/MessageParser/plugin.py 
new/Limnoria-master-2022-09-27/plugins/MessageParser/plugin.py
--- old/Limnoria-master-2022-07-03/plugins/MessageParser/plugin.py      
2022-06-23 22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/plugins/MessageParser/plugin.py      
2022-09-20 07:51:46.000000000 +0200
@@ -163,6 +163,20 @@
         channel = msg.channel
         if not channel:
             return
+
+        if 'batch' in msg.server_tags:
+            parent_batches = irc.state.getParentBatches(msg)
+            parent_batch_types = [batch.type for batch in parent_batches]
+            if 'chathistory' in parent_batch_types:
+                # Either sent automatically by the server upon join,
+                # or triggered by a plugin (why?!)
+                # Either way, replying to messages from the history would
+                # look weird, because they may have been sent a while ago,
+                # and we may have already answered them.
+                # (this is the same behavior as in Owner.doPrivmsg and
+                # PluginRegexp.doPrivmsg)
+                return
+
         if self.registryValue('enable', channel, irc.network):
             actions = []
             results = []
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/Limnoria-master-2022-07-03/plugins/MessageParser/test.py 
new/Limnoria-master-2022-09-27/plugins/MessageParser/test.py
--- old/Limnoria-master-2022-07-03/plugins/MessageParser/test.py        
2022-06-23 22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/plugins/MessageParser/test.py        
2022-09-20 07:51:46.000000000 +0200
@@ -116,10 +116,31 @@
 
     def testTrigger(self):
         self.assertNotError('messageparser add "stuff" "echo i saw some 
stuff"')
-        self.feedMsg('this message has some stuff in it')
+        self.irc.feedMsg(ircmsgs.IrcMsg(
+            prefix=self.prefix,
+            command='PRIVMSG',
+            args=(self.channel, 'this message has some stuff in it')))
         m = self.getMsg(' ')
         self.assertTrue(str(m).startswith('PRIVMSG #test :i saw some stuff'))
 
+    def testIgnoreChathistory(self):
+        self.assertNotError('messageparser add "stuff" "echo i saw some 
stuff"')
+
+        self.irc.feedMsg(ircmsgs.IrcMsg(
+            command='BATCH',
+            args=('+123', 'chathistory', self.channel)))
+        self.irc.feedMsg(ircmsgs.IrcMsg(
+            server_tags={'batch': '123'},
+            prefix=self.prefix,
+            command='PRIVMSG',
+            args=(self.channel, 'this message has some stuff in it')))
+        self.irc.feedMsg(ircmsgs.IrcMsg(
+            command='BATCH',
+            args=('-123',)))
+
+        m = self.getMsg(' ')
+        self.assertFalse(m)
+
     def testMaxTriggers(self):
         self.assertNotError('messageparser add "stuff" "echo i saw some 
stuff"')
         self.assertNotError('messageparser add "sbd" "echo i saw somebody"')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/Limnoria-master-2022-07-03/plugins/MoobotFactoids/plugin.py 
new/Limnoria-master-2022-09-27/plugins/MoobotFactoids/plugin.py
--- old/Limnoria-master-2022-07-03/plugins/MoobotFactoids/plugin.py     
2022-06-23 22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/plugins/MoobotFactoids/plugin.py     
2022-09-20 07:51:46.000000000 +0200
@@ -293,12 +293,23 @@
     ``@something is something`` And when you call ``@something`` the bot says
     ``something is something``.
 
-    If you want factoid to be in different format say (for example):
+    If you want the factoid to be in different format say (for example):
     ``@Hi is <reply> Hello`` And when you call ``@hi`` the bot says ``Hello.``
 
     If you want the bot to use /mes with Factoids, that is possible too.
     ``@test is <action> tests.`` and everytime when someone calls for
     ``test`` the bot answers ``* bot tests.``
+
+    If you want the factoid to have random answers say (for example):
+    ``@fruit is <reply> (orange|apple|banana)``. So when ``@fruit`` is called
+    the bot will reply with one of the listed fruits (random): ``orange``.
+    
+    If you want to replace the value of the factoid, for example:
+    ``@no Hi is <reply> Hey`` when you call ``@hi`` the bot says ``Hey``.
+
+    If you want to append to the current value of a factoid say:
+    ``@Hi is also Hello``, so that when you call ``@hi`` the
+    bot says ``Hey, or Hello.`` 
     """
     callBefore = ['Dunno']
     def __init__(self, irc):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2022-07-03/plugins/Owner/plugin.py 
new/Limnoria-master-2022-09-27/plugins/Owner/plugin.py
--- old/Limnoria-master-2022-07-03/plugins/Owner/plugin.py      2022-06-23 
22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/plugins/Owner/plugin.py      2022-09-20 
07:51:46.000000000 +0200
@@ -309,8 +309,9 @@
                 # Either sent automatically by the server upon join,
                 # or triggered by a plugin (why?!)
                 # Either way, replying to commands from the history would
-                # look weird, because it may have been sent a while ago,
-                # and we may have already answered to it.
+                # look weird, because they may have been sent a while ago,
+                # and we may have already answered to them.
+                # (this is the same behavior as in PluginRegexp.doPrivmsg)
                 return
 
         self._doPrivmsgs(irc, msg)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2022-07-03/plugins/Poll/__init__.py 
new/Limnoria-master-2022-09-27/plugins/Poll/__init__.py
--- old/Limnoria-master-2022-07-03/plugins/Poll/__init__.py     2022-06-23 
22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/plugins/Poll/__init__.py     2022-09-20 
07:51:46.000000000 +0200
@@ -53,6 +53,7 @@
 from . import plugin
 
 from importlib import reload
+
 # In case we're being reloaded.
 reload(config)
 reload(plugin)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2022-07-03/plugins/Poll/plugin.py 
new/Limnoria-master-2022-09-27/plugins/Poll/plugin.py
--- old/Limnoria-master-2022-07-03/plugins/Poll/plugin.py       2022-06-23 
22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/plugins/Poll/plugin.py       2022-09-20 
07:51:46.000000000 +0200
@@ -131,7 +131,7 @@
 
         poll_id = max(self._polls[(irc.network, channel)], default=0) + 1
 
-        answers = [(answer.split()[0], answer) for answer in answers]
+        answers = [(answer.split()[0].casefold(), answer) for answer in 
answers]
 
         answer_id_counts = collections.Counter(
             id_ for (id_, _) in answers
@@ -149,7 +149,10 @@
             )
 
         self._polls[(irc.network, channel)][poll_id] = Poll(
-            question=question, answers=dict(answers), votes={}, open=True
+            question=question,
+            answers=dict(answers),
+            votes=ircutils.IrcDict(),
+            open=True,
         )
 
         irc.replySuccess(_("Poll # %d created.") % poll_id)
@@ -191,6 +194,8 @@
         if msg.nick in poll.votes:
             irc.error(_("You already voted on this poll."), Raise=True)
 
+        answer_id = answer_id.casefold()
+
         if answer_id not in poll.answers:
             irc.error(
                 format(
@@ -218,11 +223,32 @@
         counts.update({answer_id: 0 for answer_id in poll.answers})
 
         results = [
-            format(_("%n for %s"), (v, "vote"), k)
+            format(_("%n for %s"), (v, _("vote")), poll.answers[k].split()[0])
             for (k, v) in counts.most_common()
         ]
 
         irc.replies(results)
 
+    @wrap(["channel"])
+    def list(self, irc, msg, args, channel):
+        """[<channel>]
+
+        Lists open polls in the <channel>."""
+        results = [
+            format(
+                _("%i: %s (%n)"),
+                poll_id,
+                poll.question,
+                (len(poll.votes), _("vote")),
+            )
+            for (poll_id, poll) in self._polls[(irc.network, channel)].items()
+            if poll.open
+        ]
+
+        if results:
+            irc.replies(results)
+        else:
+            irc.reply(_("There are no open polls."))
+
 
 Class = Poll_
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2022-07-03/plugins/Poll/test.py 
new/Limnoria-master-2022-09-27/plugins/Poll/test.py
--- old/Limnoria-master-2022-07-03/plugins/Poll/test.py 2022-06-23 
22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/plugins/Poll/test.py 2022-09-20 
07:51:46.000000000 +0200
@@ -49,6 +49,17 @@
             "2 votes for No, 1 vote for Yes, and 0 votes for Maybe",
         )
 
+    def testNoResults(self):
+        self.assertResponse(
+            'poll add "Is this a test?" "Yes" "No" "Maybe"',
+            "The operation succeeded.  Poll # 1 created.",
+        )
+
+        self.assertResponse(
+            "results 1",
+            "0 votes for Yes, 0 votes for No, and 0 votes for Maybe",
+        )
+
     def testDoubleVoting(self):
         self.assertResponse(
             'poll add "Is this a test?" "Yes" "No" "Maybe"',
@@ -62,6 +73,11 @@
             "voter1: Error: You already voted on this poll.",
             frm="voter1!foo@bar",
         )
+        self.assertResponse(
+            "vote 1 Yes",
+            "VOTER1: Error: You already voted on this poll.",
+            frm="VOTER1!foo@bar",
+        )
 
         self.assertRegexp(
             "results 1",
@@ -115,10 +131,58 @@
     def testDuplicateId(self):
         self.assertResponse(
             'poll add "Is this a test?" "Yes" "Yes" "Maybe"',
-            "Error: Duplicate answer identifier(s): Yes",
+            "Error: Duplicate answer identifier(s): yes",
         )
 
         self.assertResponse(
             'poll add "Is this a test?" "Yes totally" "Yes and no" "Maybe"',
-            "Error: Duplicate answer identifier(s): Yes",
+            "Error: Duplicate answer identifier(s): yes",
+        )
+
+    def testCaseInsensitive(self):
+        self.assertResponse(
+            'poll add "Is this a test?" "Ye??" "No" "Maybe"',
+            "The operation succeeded.  Poll # 1 created.",
+        )
+
+        self.assertNotError("vote 1 Ye??", frm="voter1!foo@bar")
+        self.assertNotError("vote 1 yESS", frm="voter2!foo@bar")
+        self.assertNotError("vote 1 no", frm="voter3!foo@bar")
+
+        self.assertResponse(
+            "results 1",
+            "2 votes for Ye??, 1 vote for No, and 0 votes for Maybe",
+        )
+
+    def testList(self):
+        self.assertResponse("poll list", "There are no open polls.")
+
+        self.assertResponse(
+            'poll add "Is this a test?" "Yes" "No" "Maybe"',
+            "The operation succeeded.  Poll # 1 created.",
+        )
+        self.assertResponse("poll list", "1: Is this a test? (0 votes)")
+
+        self.assertNotError("vote 1 Yes", frm="voter1!foo@bar")
+        self.assertResponse("poll list", "1: Is this a test? (1 vote)")
+
+        self.assertNotError("vote 1 No", frm="voter2!foo@bar")
+        self.assertResponse("poll list", "1: Is this a test? (2 votes)")
+
+        self.assertResponse(
+            'poll add "Is this another test?" "Yes" "No" "Maybe"',
+            "The operation succeeded.  Poll # 2 created.",
+        )
+        self.assertResponse(
+            "poll list",
+            "1: Is this a test? (2 votes) and 2: Is this another test? (0 
votes)",
         )
+
+        self.assertNotError("poll close 1")
+        self.assertResponse(
+            "poll list",
+            "2: Is this another test? (0 votes)",
+        )
+
+        self.assertNotError("poll close 2")
+        self.assertResponse("poll list", "There are no open polls.")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2022-07-03/plugins/RSS/plugin.py 
new/Limnoria-master-2022-09-27/plugins/RSS/plugin.py
--- old/Limnoria-master-2022-07-03/plugins/RSS/plugin.py        2022-06-23 
22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/plugins/RSS/plugin.py        2022-09-20 
07:51:46.000000000 +0200
@@ -356,8 +356,17 @@
             handlers.append(ProxyHandler(
                 {'https': utils.force(utils.web.proxy())}))
         with feed.lock:
-            d = feedparser.parse(feed.url, etag=feed.etag,
-                    modified=feed.modified, handlers=handlers)
+            try:
+                d = feedparser.parse(feed.url, etag=feed.etag,
+                        modified=feed.modified, handlers=handlers)
+            except socket.error as e:
+                self.log.warning("Network error while fetching <%s>: %s",
+                               feed.url, e)
+                feed.last_exception = e
+                return
+            except Exception as e:
+                self.log.error("Failed to fetch <%s>: %s", feed.url, e)
+                raise  # reraise so @log.firewall prints the traceback
             if 'status' not in d or d.status != 304: # Not modified
                 if 'etag' in d:
                     feed.etag = d.etag
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2022-07-03/plugins/RSS/test.py 
new/Limnoria-master-2022-09-27/plugins/RSS/test.py
--- old/Limnoria-master-2022-07-03/plugins/RSS/test.py  2022-06-23 
22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/plugins/RSS/test.py  2022-09-20 
07:51:46.000000000 +0200
@@ -31,6 +31,7 @@
 
 import functools
 from unittest.mock import patch
+import socket
 import sys
 
 import feedparser
@@ -362,7 +363,22 @@
         timeFastForward(1.1)
         mock._data = not_well_formed
         self.assertRegexp('rss http://example.com/',
-                          'Parser error')
+                          'Parser error: .*mismatch')
+
+    def testSocketError(self):
+        class MockResponse:
+            headers = {}
+            url = ''
+            def read(self):
+                raise socket.error("oh no")
+
+            def close(self):
+                pass
+        mock = MockResponse()
+        with patch("urllib.request.OpenerDirector.open", return_value=mock):
+            timeFastForward(1.1)
+            self.assertRegexp('rss http://example.com/',
+                              'Parser error: .*oh no')
 
     if network:
         timeout = 5  # Note this applies also to the above tests
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2022-07-03/plugins/Time/plugin.py 
new/Limnoria-master-2022-09-27/plugins/Time/plugin.py
--- old/Limnoria-master-2022-07-03/plugins/Time/plugin.py       2022-06-23 
22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/plugins/Time/plugin.py       2022-09-20 
07:51:46.000000000 +0200
@@ -72,10 +72,16 @@
 except ImportError:
     tzlocal = None
 
+
+# Note: Python 3.6 does not support empty pattern matches, see:
+# https://docs.python.org/3/library/re.html#re.split
+_SECONDS_SPLIT_RE = re.compile('(?<=[a-z]) ?')
+
+
 class Time(callbacks.Plugin):
     """This plugin allows you to use different time-related functions."""
     @internationalizeDocstring
-    def seconds(self, irc, msg, args):
+    def seconds(self, irc, msg, args, text):
         """[<years>y] [<weeks>w] [<days>d] [<hours>h] [<minutes>m] [<seconds>s]
 
         Returns the number of seconds in the number of <years>, <weeks>,
@@ -84,11 +90,13 @@
         Useful for scheduling events at a given number of seconds in the
         future.
         """
-        if not args:
-            raise callbacks.ArgumentError
         seconds = 0
-        for arg in args:
-            if not arg or arg[-1] not in 'ywdhms':
+        if not text:
+            raise callbacks.ArgumentError
+        for arg in _SECONDS_SPLIT_RE.split(text):
+            if not arg:
+                continue
+            if arg[-1] not in 'ywdhms':
                 raise callbacks.ArgumentError
             (s, kind) = arg[:-1], arg[-1]
             try:
@@ -108,6 +116,7 @@
             elif kind == 's':
                 seconds += i
         irc.reply(str(seconds))
+    seconds = wrap(seconds, ['text'])
 
     @internationalizeDocstring
     def at(self, irc, msg, args, s=None):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2022-07-03/plugins/Time/test.py 
new/Limnoria-master-2022-09-27/plugins/Time/test.py
--- old/Limnoria-master-2022-07-03/plugins/Time/test.py 2022-06-23 
22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/plugins/Time/test.py 2022-09-20 
07:51:46.000000000 +0200
@@ -88,6 +88,17 @@
         self.assertResponse('seconds 1y 1s', '31536001')
         self.assertResponse('seconds 1w 1s', '604801')
 
+    @skipIf(sys.version_info < (3, 7, 0),
+            "Python 3.6 does not support empty pattern matches, see: "
+            "https://docs.python.org/3/library/re.html#re.split";)
+    def testSecondsNoSpace(self):
+        self.assertResponse('seconds 1m1s', '61')
+        self.assertResponse('seconds 1h1s', '3601')
+        self.assertResponse('seconds 1d1s', '86401')
+        self.assertResponse('seconds 2d2h2m2s', '180122')
+        self.assertResponse('seconds 1y1s', '31536001')
+        self.assertResponse('seconds 1w1s', '604801')
+
     def testNoErrors(self):
         self.assertNotError('ctime')
         self.assertNotError('time %Y')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2022-07-03/requirements.txt 
new/Limnoria-master-2022-09-27/requirements.txt
--- old/Limnoria-master-2022-07-03/requirements.txt     2022-06-23 
22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/requirements.txt     2022-09-20 
07:51:46.000000000 +0200
@@ -1,10 +1,17 @@
+# mandatory:
+
 setuptools
-chardet
-pytz;python_version<'3.9'
-python-dateutil
-python-gnupg
-feedparser
-PySocks
-mock
-cryptography
-pyxmpp2-scram
+
+# optional core dependencies:
+
+chardet                     # to detect encoding of incoming IRC lines, if 
they are not in UTF-8
+python-gnupg                # for authenticated based on GPG tokens
+PySocks                     # for SOCKS proxy (typically used to connect to 
IRC via Tor)
+pyxmpp2-scram               # for the scram-sha-256 SASL mechanism
+
+# optional plugin dependencies:
+
+cryptography                # required to load the Fediverse plugin (used to 
implement HTTP signatures to support Mastodon instances with 
AUTHORIZED_FETCH=true)
+feedparser                  # required to load the RSS plugin
+pytz;python_version<'3.9'   # enables timezone manipulation in the Time and 
Geography plugins. On Python >=3.9, the standard library is used instead
+python-dateutil             # enable fancy time string parsing in the Time 
plugin
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/Limnoria-master-2022-07-03/scripts/supybot-plugin-doc 
new/Limnoria-master-2022-09-27/scripts/supybot-plugin-doc
--- old/Limnoria-master-2022-07-03/scripts/supybot-plugin-doc   2022-06-23 
22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/scripts/supybot-plugin-doc   2022-09-20 
07:51:46.000000000 +0200
@@ -297,7 +297,7 @@
                       'with the plugin\'s name and "$format" with the value '
                       'if --format.')
     parser.add_option('-f', '--format', dest='format', choices=['rst', 'stx'],
-                      default='stx', help='Specifies which output format to '
+                      default='rst', help='Specifies which output format to '
                       'use.')
     parser.add_option('--plugins-dir',
                       action='append', dest='pluginsDirs', default=[],
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2022-07-03/src/callbacks.py 
new/Limnoria-master-2022-09-27/src/callbacks.py
--- old/Limnoria-master-2022-07-03/src/callbacks.py     2022-06-23 
22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/src/callbacks.py     2022-09-20 
07:51:46.000000000 +0200
@@ -1804,6 +1804,19 @@
     def doPrivmsg(self, irc, msg):
         if msg.isError:
             return
+
+        if 'batch' in msg.server_tags:
+            parent_batches = irc.state.getParentBatches(msg)
+            parent_batch_types = [batch.type for batch in parent_batches]
+            if 'chathistory' in parent_batch_types:
+                # Either sent automatically by the server upon join,
+                # or triggered by a plugin (why?!)
+                # Either way, replying to messages from the history would
+                # look weird, because they may have been sent a while ago,
+                # and we may have already answered them.
+                # (this is the same behavior as in Owner.doPrivmsg)
+                return
+
         proxy = self.Proxy(irc, msg)
         if not msg.addressed:
             for (r, name) in self.unaddressedRes:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2022-07-03/src/commands.py 
new/Limnoria-master-2022-09-27/src/commands.py
--- old/Limnoria-master-2022-07-03/src/commands.py      2022-06-23 
22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/src/commands.py      2022-09-20 
07:51:46.000000000 +0200
@@ -50,6 +50,13 @@
 from .i18n import PluginInternationalization, internationalizeDocstring
 _ = PluginInternationalization()
 
+LOG_CONVERTERS = world.testing
+"""Defines whether converters and contexts should log the argument stack
+while parsing it.
+Disabled by default (unless running via supybot-test) as it is very noisy
+and rarely needs to be debugged.
+"""
+
 ###
 # Non-arg wrappers -- these just change the behavior of a command without
 # changing the arguments given to it.
@@ -509,7 +516,8 @@
     elif msg.channel:
         channel = msg.channel
     else:
-        state.log.debug('Raising ArgumentError because there is no channel.')
+        if LOG_CONVERTERS:
+            state.log.debug('Raising ArgumentError because there is no 
channel.')
         raise callbacks.ArgumentError
     state.channel = channel
     state.args.append(channel)
@@ -520,7 +528,8 @@
     elif msg.channel:
         channels = [msg.channel]
     else:
-        state.log.debug('Raising ArgumentError because there is no channel.')
+        if LOG_CONVERTERS:
+            state.log.debug('Raising ArgumentError because there is no 
channel.')
         raise callbacks.ArgumentError
     state.args.append(channels)
 
@@ -898,9 +907,11 @@
             self.converter = spec
 
     def __call__(self, irc, msg, args, state):
-        log.debug('args before %r: %r', self, args)
+        if LOG_CONVERTERS:
+            log.debug('args before %r: %r', self, args)
         self.converter(irc, msg, args, state, *self.args)
-        log.debug('args after %r: %r', self, args)
+        if LOG_CONVERTERS:
+            log.debug('args after %r: %r', self, args)
 
     def __repr__(self):
         return '<%s for %s>' % (self.__class__.__name__, self.spec)
@@ -929,7 +940,8 @@
         try:
             self.__parent.__call__(irc, msg, args, state)
         except IndexError:
-            log.debug('Got IndexError, returning default.')
+            if LOG_CONVERTERS:
+                log.debug('Got IndexError, returning default.')
             setDefault(state, self.default)
 
 # optional means: Look for this, but if it's not the type I'm expecting or
@@ -939,7 +951,8 @@
         try:
             super(optional, self).__call__(irc, msg, args, state)
         except (callbacks.ArgumentError, callbacks.Error) as e:
-            log.debug('Got %s, returning default.', utils.exnToString(e))
+            if LOG_CONVERTERS:
+                log.debug('Got %s, returning default.', utils.exnToString(e))
             state.errored = False
             setDefault(state, self.default)
 
@@ -960,7 +973,8 @@
             if not self.continueOnError:
                 raise
             else:
-                log.debug('Got %s, returning default.', utils.exnToString(e))
+                if LOG_CONVERTERS:
+                    log.debug('Got %s, returning default.', 
utils.exnToString(e))
                 pass
         state.args.append(st.args)
 
@@ -1041,11 +1055,13 @@
                     self.getopts[name] = contextify(spec)
                 self.getoptL.append(name + '=')
                 self.getopts[name] = contextify(spec)
-        log.debug('getopts: %r', self.getopts)
-        log.debug('getoptL: %r', self.getoptL)
+        if LOG_CONVERTERS:
+            log.debug('getopts: %r', self.getopts)
+            log.debug('getoptL: %r', self.getoptL)
 
     def __call__(self, irc, msg, args, state):
-        log.debug('args before %r: %r', self, args)
+        if LOG_CONVERTERS:
+            log.debug('args before %r: %r', self, args)
         (optlist, rest) = getopt.getopt(args, self.getoptLs, self.getoptL)
         getopts = []
         for (opt, arg) in optlist:
@@ -1053,7 +1069,8 @@
                 opt = opt[2:] # Strip --
             else:
                 opt = opt[1:]
-            log.debug('opt: %r, arg: %r', opt, arg)
+            if LOG_CONVERTERS:
+                log.debug('opt: %r, arg: %r', opt, arg)
             context = self.getopts[opt]
             if context is not None:
                 st = state.essence()
@@ -1064,7 +1081,8 @@
                 getopts.append((opt, True))
         state.args.append(getopts)
         args[:] = rest
-        log.debug('args after %r: %r', self, args)
+        if LOG_CONVERTERS:
+            log.debug('args after %r: %r', self, args)
 
 ###
 # This is our state object, passed to converters along with irc, msg, and args.
@@ -1123,7 +1141,8 @@
             except IndexError:
                 raise callbacks.ArgumentError
         if args and not state.allowExtra:
-            log.debug('args and not self.allowExtra: %r', args)
+            if LOG_CONVERTERS:
+                log.debug('args and not self.allowExtra: %r', args)
             raise callbacks.ArgumentError
         return state
 
@@ -1134,9 +1153,11 @@
     spec = Spec(specList, **kw)
     def newf(self, irc, msg, args, **kwargs):
         state = spec(irc, msg, args, stateAttrs={'cb': self, 'log': self.log})
-        self.log.debug('State before call: %s', state)
+        if LOG_CONVERTERS:
+            self.log.debug('State before call: %s', state)
         if state.errored:
-            self.log.debug('Refusing to call %s due to state.errored.', f)
+            if LOG_CONVERTERS:
+                self.log.debug('Refusing to call %s due to state.errored.', f)
         else:
             try:
                 f(self, irc, msg, args, *state.args, **state.kwargs)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2022-07-03/src/irclib.py 
new/Limnoria-master-2022-09-27/src/irclib.py
--- old/Limnoria-master-2022-07-03/src/irclib.py        2022-06-23 
22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/src/irclib.py        2022-09-20 
07:51:46.000000000 +0200
@@ -1731,20 +1731,41 @@
         self.sasl_current_mechanism = None
 
         for mechanism in network_config.sasl.mechanisms():
-            if mechanism == 'ecdsa-nist256p-challenge' and \
-                    crypto and self.sasl_username and \
-                    self.sasl_ecdsa_key:
-                self.sasl_next_mechanisms.append(mechanism)
-            elif mechanism == 'external' and (
-                    network_config.certfile() or
-                    conf.supybot.protocols.irc.certfile()):
-                self.sasl_next_mechanisms.append(mechanism)
-            elif mechanism.startswith('scram-') and scram and \
-                    self.sasl_username and self.sasl_password:
-                self.sasl_next_mechanisms.append(mechanism)
-            elif mechanism == 'plain' and \
-                    self.sasl_username and self.sasl_password:
-                self.sasl_next_mechanisms.append(mechanism)
+            if mechanism == 'ecdsa-nist256p-challenge':
+                if not crypto:
+                    log.debug('Skipping SASL %s, crypto module '
+                              'is not available',
+                              mechanism)
+                elif not self.sasl_username or not self.sasl_ecdsa_key:
+                    log.debug('Skipping SASL %s, missing username and/or key',
+                              mechanism)
+                else:
+                    self.sasl_next_mechanisms.append(mechanism)
+            elif mechanism == 'external':
+                if not network_config.certfile() and \
+                        not conf.supybot.protocols.irc.certfile():
+                    log.debug('Skipping SASL %s, missing cert file',
+                              mechanism)
+                else:
+                    self.sasl_next_mechanisms.append(mechanism)
+            elif mechanism.startswith('scram-'):
+                if not scram:
+                    log.debug('Skipping SASL %s, scram module '
+                              'is not available',
+                              mechanism)
+                elif not self.sasl_username or not self.sasl_password:
+                    log.debug('Skipping SASL %s, missing username and/or '
+                              'password',
+                              mechanism)
+                else:
+                    self.sasl_next_mechanisms.append(mechanism)
+            elif mechanism == 'plain':
+                if not self.sasl_username or not self.sasl_password:
+                    log.debug('Skipping SASL %s, missing username and/or '
+                              'password',
+                              mechanism)
+                else:
+                    self.sasl_next_mechanisms.append(mechanism)
 
         if self.sasl_next_mechanisms:
             self.REQUEST_CAPABILITIES.add('sasl')
@@ -1862,6 +1883,7 @@
             IrcStateFsm.States.INIT_SASL,
             IrcStateFsm.States.CONNECTED_SASL,
         ])
+        log.debug('Next SASL mechanisms: %s', self.sasl_next_mechanisms)
         if self.sasl_next_mechanisms:
             self.sasl_current_mechanism = self.sasl_next_mechanisms.pop(0)
             self.sendMsg(ircmsgs.IrcMsg(command='AUTHENTICATE',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2022-07-03/src/registry.py 
new/Limnoria-master-2022-09-27/src/registry.py
--- old/Limnoria-master-2022-07-03/src/registry.py      2022-06-23 
22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/src/registry.py      2022-09-20 
07:51:46.000000000 +0200
@@ -710,7 +710,7 @@
 
     def setValue(self, s):
         v = self.normalize(s)
-        if s in self.validStrings:
+        if v in self.validStrings:
             self.__parent.setValue(v)
         else:
             self.error(v)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2022-07-03/src/utils/web.py 
new/Limnoria-master-2022-09-27/src/utils/web.py
--- old/Limnoria-master-2022-07-03/src/utils/web.py     2022-06-23 
22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/src/utils/web.py     2022-09-20 
07:51:46.000000000 +0200
@@ -226,6 +226,8 @@
 # From beautifulsoup (version 4.10.0, bs4/builder/__init__.py, line 391)
 _block_elements = set(["address", "article", "aside", "blockquote", "canvas", 
"dd", "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer", "form", 
"h1", "h2", "h3", "h4", "h5", "h6", "header", "hr", "li", "main", "nav", 
"noscript", "ol", "output", "p", "pre", "section", "table", "tfoot", "ul", 
"video"])
 
+_block_elements.update({"br"})
+
 class HtmlToText(HTMLParser, object):
     """Taken from some eff-bot code on c.l.p."""
     entitydefs = entitydefs.copy()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2022-07-03/test/test_callbacks.py 
new/Limnoria-master-2022-09-27/test/test_callbacks.py
--- old/Limnoria-master-2022-07-03/test/test_callbacks.py       2022-06-23 
22:31:17.000000000 +0200
+++ new/Limnoria-master-2022-09-27/test/test_callbacks.py       2022-09-20 
07:51:46.000000000 +0200
@@ -975,16 +975,50 @@
                 '-' + batch_name,)))
 
 
-class PluginRegexpTestCase(PluginTestCase):
+class PluginRegexpTestCase(ChannelPluginTestCase):
     plugins = ()
     class PCAR(callbacks.PluginRegexp):
+        regexps = ("test", "test2")
+
         def test(self, irc, msg, args):
             "<foo>"
             raise callbacks.ArgumentError
-    def testNoEscapingArgumentError(self):
+
+        def test2(self, irc, msg, args):
+            "<bar>"
+            irc.reply("hello")
+
+    def setUp(self):
+        super().setUp()
         self.irc.addCallback(self.PCAR(self.irc))
+
+    def testNoEscapingArgumentError(self):
         self.assertResponse('test', 'test <foo>')
 
+    def testReply(self):
+        self.irc.feedMsg(ircmsgs.IrcMsg(
+            prefix=self.prefix,
+            command='PRIVMSG',
+            args=(self.channel, 'foo <bar> baz')))
+        self.assertResponse(' ', 'hello')
+
+    def testIgnoreChathistory(self):
+        self.irc.feedMsg(ircmsgs.IrcMsg(
+            command='BATCH',
+            args=('+123', 'chathistory', self.channel)))
+
+        self.irc.feedMsg(ircmsgs.IrcMsg(
+            server_tags={'batch': '123'},
+            prefix=self.prefix,
+            command='PRIVMSG',
+            args=(self.channel, 'foo <bar> baz')))
+
+        self.irc.feedMsg(ircmsgs.IrcMsg(
+            command='BATCH',
+            args=('-123',)))
+
+        self.assertNoResponse(' ')
+
 class RichReplyMethodsTestCase(PluginTestCase):
     plugins = ('Config',)
     class NoCapability(callbacks.Plugin):

Reply via email to