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):