From d04e8161d5eef565c9abc0fb73207c1a26c0efe6 Mon Sep 17 00:00:00 2001 From: SamStrongTalks <33920407+SamStrongTalks@users.noreply.github.com> Date: Fri, 17 Jun 2022 10:44:12 -0400 Subject: [PATCH 01/71] Add ability to exclude channel from self censoring (#1508) --- plugins/BadWords/config.py | 3 +++ plugins/BadWords/messages.pot | 10 ++++++++-- plugins/BadWords/plugin.py | 5 +++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/plugins/BadWords/config.py b/plugins/BadWords/config.py index bea4b0a33..9c9fb31ee 100644 --- a/plugins/BadWords/config.py +++ b/plugins/BadWords/config.py @@ -113,6 +113,9 @@ conf.registerGlobalValue(BadWords, 'stripFormatting', filtering. If it's True, however, it will interact poorly with other plugins that do coloring or bolding of text."""))) +conf.registerChannelValue(BadWords, 'selfCensor', + registry.Boolean(True, _("""Determines whether the bot will filter its own + messages."""))) conf.registerChannelValue(BadWords, 'kick', registry.Boolean(False, _("""Determines whether the bot will kick people with a warning when they use bad words."""))) diff --git a/plugins/BadWords/messages.pot b/plugins/BadWords/messages.pot index a2fb4818f..b67ae4adc 100644 --- a/plugins/BadWords/messages.pot +++ b/plugins/BadWords/messages.pot @@ -81,18 +81,24 @@ msgstr "" #: config.py:117 msgid "" +"Determines whether the bot will filter its own\n" +" messages." +msgstr "" + +#: config.py:120 +msgid "" "Determines whether the bot will kick people with\n" " a warning when they use bad words." msgstr "" -#: config.py:120 +#: config.py:123 msgid "" "You have been kicked for using a word\n" " prohibited in the presence of this bot. Please use more appropriate\n" " language in the future." msgstr "" -#: config.py:122 +#: config.py:125 msgid "" "Determines the kick message used by the\n" " bot when kicking users for saying bad words." diff --git a/plugins/BadWords/plugin.py b/plugins/BadWords/plugin.py index aa9a6c703..a98e59320 100644 --- a/plugins/BadWords/plugin.py +++ b/plugins/BadWords/plugin.py @@ -104,9 +104,10 @@ class BadWords(callbacks.Privmsg): self.lastModified = time.time() def outFilter(self, irc, msg): + channel = msg.channel if self.filtering and msg.command == 'PRIVMSG' \ - and (self.words() or self.phrases()): - channel = msg.channel + and (self.words() or self.phrases()) \ + and self.registryValue('selfCensor', channel, irc.network): self.updateRegexp(channel, irc.network) s = msg.args[1] if self.registryValue('stripFormatting'): From b3443a5a4ced54ac14d5cece95cdc067f5db197d Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Wed, 22 Jun 2022 20:31:53 +0200 Subject: [PATCH 02/71] setup: Fix install of subpackages when pip-installed from git repositories It seems setuptools needs to be explicitly told to include subpackages in this case. --- src/setup.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/setup.py b/src/setup.py index ace3ce0f7..63248be83 100644 --- a/src/setup.py +++ b/src/setup.py @@ -79,7 +79,14 @@ if setuptools: break module_name = kwargs['name'].replace('-', '_') - kwargs.setdefault('packages', [module_name]) + + if 'packages' not in kwargs: + kwargs["packages"] = [module_name] + [ + "%s.%s" % (module_name, package_name.replace('-', '_')) + for package_name + in setuptools.find_packages(where=".") + ] + kwargs.setdefault('package_dir', {module_name: '.'}) kwargs.setdefault('entry_points', { 'limnoria.plugins': '%s = %s' % (capitalized_name, module_name)}) From 8ccf2c717553d45b9115604705ef955bd7e10db4 Mon Sep 17 00:00:00 2001 From: James Lu Date: Thu, 23 Jun 2022 13:09:23 -0700 Subject: [PATCH 03/71] PluginDownloader: drop legacy Python 2-only repos Most of these haven't been updated in ~10 years and are unlikely to work out of the box today (obsolete web APIs, etc.) --- plugins/PluginDownloader/plugin.py | 66 ------------------------------ plugins/PluginDownloader/test.py | 25 +---------- 2 files changed, 1 insertion(+), 90 deletions(-) diff --git a/plugins/PluginDownloader/plugin.py b/plugins/PluginDownloader/plugin.py index e88eb1e45..2c8d9155c 100644 --- a/plugins/PluginDownloader/plugin.py +++ b/plugins/PluginDownloader/plugin.py @@ -201,65 +201,11 @@ repositories = utils.InsensitivePreservingDict({ 'progval', 'Supybot-plugins' ), - 'quantumlemur': GithubRepository( - 'quantumlemur', - 'Supybot-plugins', - ), - 'stepnem': GithubRepository( - 'stepnem', - 'supybot-plugins', - ), - 'code4lib-snapshot':GithubRepository( - 'code4lib', - 'supybot-plugins', - 'Supybot-plugins-20060723', - ), - 'code4lib-edsu': GithubRepository( - 'code4lib', - 'supybot-plugins', - 'edsu-plugins', - ), - 'code4lib': GithubRepository( - 'code4lib', - 'supybot-plugins', - 'plugins', - ), - 'nanotube-bitcoin': GithubRepository( - 'nanotube', - 'supybot-bitcoin-' - 'marketmonitor', - ), - 'mtughan-weather': GithubRepository( - 'mtughan', - 'Supybot-Weather', - ), 'SpiderDave': GithubRepository( 'SpiderDave', 'spidey-supybot-plugins', 'Plugins', ), - 'doorbot': GithubRepository( - 'hacklab', - 'doorbot', - ), - 'boombot': GithubRepository( - 'nod', - 'boombot', - 'plugins', - ), - 'mailed-notifier': GithubRepository( - 'tbielawa', - 'supybot-mailed-notifier', - ), - 'pingdom': GithubRepository( - 'rynop', - 'supyPingdom', - 'plugins', - ), - 'scrum': GithubRepository( - 'amscanne', - 'supybot-scrum', - ), 'Hoaas': GithubRepository( 'Hoaas', 'Supybot-plugins' @@ -268,23 +214,11 @@ repositories = utils.InsensitivePreservingDict({ 'nyuszika7h', 'limnoria-plugins' ), - 'nyuszika7h-old': GithubRepository( - 'nyuszika7h', - 'Supybot-plugins' - ), - 'resistivecorpse': GithubRepository( - 'resistivecorpse', - 'supybot-plugins' - ), 'frumious': GithubRepository( 'frumiousbandersnatch', 'sobrieti-plugins', 'plugins', ), - 'jonimoose': GithubRepository( - 'Jonimoose', - 'Supybot-plugins', - ), 'skgsergio': GithubRepository( 'skgsergio', 'Limnoria-plugins', diff --git a/plugins/PluginDownloader/test.py b/plugins/PluginDownloader/test.py index e6d889bd3..e2b78bac2 100644 --- a/plugins/PluginDownloader/test.py +++ b/plugins/PluginDownloader/test.py @@ -62,7 +62,7 @@ class PluginDownloaderTestCase(PluginTestCase): def testRepolist(self): self.assertRegexp('repolist', '(.*, )?progval(, .*)?') - self.assertRegexp('repolist', '(.*, )?quantumlemur(, .*)?') + self.assertRegexp('repolist', '(.*, )?jlu5(, .*)?') self.assertRegexp('repolist progval', '(.*, )?AttackProtector(, .*)?') def testInstallprogval(self): @@ -76,29 +76,6 @@ class PluginDownloaderTestCase(PluginTestCase): self.assertRegexp('plugindownloader install progval Darcs', 'Error:.*not available.*supybot.commands.allowShell') - def testInstallQuantumlemur(self): - self.assertError('plugindownloader install quantumlemur AttackProtector') - self.assertNotError('plugindownloader install quantumlemur Listener') - self.assertError('plugindownloader install quantumlemur AttackProtector') - self._testPluginInstalled('Listener') - - def testInstallStepnem(self): - self.assertNotError('plugindownloader install stepnem Freenode') - self._testPluginInstalled('Freenode') - - def testInstallNanotubeBitcoin(self): - self.assertNotError('plugindownloader install nanotube-bitcoin GPG') - self._testPluginInstalled('GPG') - - def testInstallMtughanWeather(self): - self.assertNotError('plugindownloader install mtughan-weather ' - 'WunderWeather') - self._testPluginInstalled('WunderWeather') - - def testInstallSpiderDave(self): - self.assertNotError('plugindownloader install SpiderDave Pastebin') - self._testPluginInstalled('Pastebin') - def testInstallNonAsciiInit(self): self.assertNotError('plugindownloader install Hoaas DuckDuckGo') self._testPluginInstalled('DuckDuckGo') From d00113e92dcc7f4cb34a99abdd757f519caf3ffd Mon Sep 17 00:00:00 2001 From: James Lu Date: Thu, 23 Jun 2022 13:12:13 -0700 Subject: [PATCH 04/71] PluginDownloader: replace automatic 2to3 step with a simple warning The previous heuristic runs into false positives when imports are merged in __init__.py More broadly though, it's unlikely automatic 2to3 is particularly useful in 2022 - plugins that were written ~10 years ago are unlikely to work even if syntax errors are fixed. --- plugins/PluginDownloader/plugin.py | 40 ++++++------------------------ plugins/PluginDownloader/test.py | 12 +++------ 2 files changed, 12 insertions(+), 40 deletions(-) diff --git a/plugins/PluginDownloader/plugin.py b/plugins/PluginDownloader/plugin.py index 2c8d9155c..35755b0fd 100644 --- a/plugins/PluginDownloader/plugin.py +++ b/plugins/PluginDownloader/plugin.py @@ -121,11 +121,11 @@ class GithubRepository(GitRepository): assert directory is not None, \ 'No valid directory in supybot.directories.plugins.' + possibly_incompatible = False try: assert archive.getmember(prefix + dirname).isdir(), \ 'This is not a valid plugin (it is a file, not a directory).' - run_2to3 = minisix.PY3 for file in archive.getmembers(): if file.name.startswith(prefix + dirname): extractedFile = archive.extractfile(file) @@ -140,42 +140,18 @@ class GithubRepository(GitRepository): os.mkdir(newFileName) else: with open(newFileName, 'ab') as fd: - reload_imported = False for line in extractedFile.readlines(): - if minisix.PY3: - if b'import reload' in line: - reload_imported = True - elif not reload_imported and \ - b'reload(' in line: - fd.write(b'from importlib import reload\n') - reload_imported = True + if file.name.endswith('__init__.py') and \ + line.startswith((b'import config', b'import plugin')): + possibly_incompatible = True fd.write(line) - if newFileName.endswith('__init__.py'): - with open(newFileName) as fd: - lines = list(filter(lambda x:'import plugin' in x, - fd.readlines())) - if lines and lines[0].startswith('from . import'): - # This should be already Python 3-compatible - run_2to3 = False finally: archive.close() del archive - if run_2to3: - try: - import lib2to3 - except ImportError: - return _('Plugin is probably not compatible with your ' - 'Python version (3.x) and could not be converted ' - 'because 2to3 is not installed.') - import subprocess - fixers = [] - subprocess.Popen(['2to3', '-wn', os.path.join(directory, plugin)]) \ - .wait() - return _('Plugin was designed for Python 2, but an attempt to ' - 'convert it to Python 3 has been made. There is no ' - 'guarantee it will work, though.') - else: - return _('Plugin successfully installed.') + if possibly_incompatible: + return _('Plugin installed. However, it may be incompatible with ' + 'Python 3 and require manual code changes to work correctly.') + return _('Plugin successfully installed.') def getInfo(self, plugin): archive = self._download(plugin) diff --git a/plugins/PluginDownloader/test.py b/plugins/PluginDownloader/test.py index e2b78bac2..3164c06d1 100644 --- a/plugins/PluginDownloader/test.py +++ b/plugins/PluginDownloader/test.py @@ -29,11 +29,9 @@ ### import os -import sys import shutil from supybot.test import * -import supybot.utils.minisix as minisix pluginsPath = '%s/test-plugins' % os.getcwd() @@ -80,17 +78,15 @@ class PluginDownloaderTestCase(PluginTestCase): self.assertNotError('plugindownloader install Hoaas DuckDuckGo') self._testPluginInstalled('DuckDuckGo') + def testInstallLegacyWarning(self): + self.assertRegexp('plugindownloader install frumious Codepoints', + 'may be incompatible') + def testInfo(self): self.assertResponse('plugindownloader info progval Twitter', 'Advanced Twitter plugin for Supybot, with capabilities ' 'handling, and per-channel user account.') - if minisix.PY3: - def test_2to3(self): - self.assertRegexp('plugindownloader install SpiderDave Pastebin', - 'convert') - self.assertNotError('load Pastebin') - if not network: class PluginDownloaderTestCase(PluginTestCase): pass From 6a943b834282d2195955fec400a55689038fffad Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Wed, 6 Jul 2022 22:04:33 +0200 Subject: [PATCH 05/71] test_callbacks: Fix PluginRegexpTestCase to actually check regexp callbacks --- test/test_callbacks.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/test_callbacks.py b/test/test_callbacks.py index 38e03555d..4c2bdba57 100644 --- a/test/test_callbacks.py +++ b/test/test_callbacks.py @@ -978,9 +978,12 @@ class MultilinePrivmsgTestCase(ChannelPluginTestCase): class PluginRegexpTestCase(PluginTestCase): plugins = () class PCAR(callbacks.PluginRegexp): + regexps = ("test",) + def test(self, irc, msg, args): "" raise callbacks.ArgumentError + def testNoEscapingArgumentError(self): self.irc.addCallback(self.PCAR(self.irc)) self.assertResponse('test', 'test ') From 3ecb37de102c7daca6485586fe38dbaa923ccfea Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Wed, 6 Jul 2022 22:05:30 +0200 Subject: [PATCH 06/71] test_callbacks: Add PluginRegexpTestCase.testReply to check basic behavior --- test/test_callbacks.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/test/test_callbacks.py b/test/test_callbacks.py index 4c2bdba57..4fac79c59 100644 --- a/test/test_callbacks.py +++ b/test/test_callbacks.py @@ -975,19 +975,33 @@ class MultilinePrivmsgTestCase(ChannelPluginTestCase): '-' + batch_name,))) -class PluginRegexpTestCase(PluginTestCase): +class PluginRegexpTestCase(ChannelPluginTestCase): plugins = () class PCAR(callbacks.PluginRegexp): - regexps = ("test",) + regexps = ("test", "test2") def test(self, irc, msg, args): "" raise callbacks.ArgumentError - def testNoEscapingArgumentError(self): + def test2(self, irc, msg, args): + "" + irc.reply("hello") + + def setUp(self): + super().setUp() self.irc.addCallback(self.PCAR(self.irc)) + + def testNoEscapingArgumentError(self): self.assertResponse('test', 'test ') + def testReply(self): + self.irc.feedMsg(ircmsgs.IrcMsg( + prefix=self.prefix, + command='PRIVMSG', + args=(self.channel, 'foo baz'))) + self.assertResponse(' ', 'hello') + class RichReplyMethodsTestCase(PluginTestCase): plugins = ('Config',) class NoCapability(callbacks.Plugin): From 96b7f51e7162f540475ed9dd8763dff38a256e64 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Wed, 6 Jul 2022 22:07:37 +0200 Subject: [PATCH 07/71] callbacks: Ignore chathistory batches in PluginRegexp This is consistent with what we already do with commands; and generally makes sense, as we don't want to re-send titles and others when cycling on UnrealIRCd (which includes a chathistory batch when joining when chmode +H is set, despite umode +B) --- plugins/Owner/plugin.py | 5 +++-- src/callbacks.py | 13 +++++++++++++ test/test_callbacks.py | 17 +++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/plugins/Owner/plugin.py b/plugins/Owner/plugin.py index ff89c99c7..afe174033 100644 --- a/plugins/Owner/plugin.py +++ b/plugins/Owner/plugin.py @@ -309,8 +309,9 @@ class Owner(callbacks.Plugin): # 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 --git a/src/callbacks.py b/src/callbacks.py index 38969008b..dcccab3c5 100644 --- a/src/callbacks.py +++ b/src/callbacks.py @@ -1804,6 +1804,19 @@ class PluginRegexp(Plugin): 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 --git a/test/test_callbacks.py b/test/test_callbacks.py index 4fac79c59..613d546a6 100644 --- a/test/test_callbacks.py +++ b/test/test_callbacks.py @@ -1002,6 +1002,23 @@ class PluginRegexpTestCase(ChannelPluginTestCase): args=(self.channel, 'foo 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 baz'))) + + self.irc.feedMsg(ircmsgs.IrcMsg( + command='BATCH', + args=('-123',))) + + self.assertNoResponse(' ') + class RichReplyMethodsTestCase(PluginTestCase): plugins = ('Config',) class NoCapability(callbacks.Plugin): From 988fe089457ceec679d6b38f1d22526126025a11 Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 9 Jul 2022 14:15:33 -0700 Subject: [PATCH 08/71] .gitignore: add doc-* paths from supybot-plugin-doc --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index ae0ab4221..95eb2762a 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,9 @@ supybot.egg-info/ test-conf/ test-data/ test-logs/ +doc-conf/ +doc-data/ +doc-logs/ src/version.py INSTALL README.txt From 95f6b1698ea92b5df466927da50522bcdc4855eb Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 9 Jul 2022 14:15:57 -0700 Subject: [PATCH 09/71] Aka, Alias: replace obsolete LastFM example --- plugins/Aka/README.rst | 24 ++++++++++-------------- plugins/Aka/plugin.py | 24 ++++++++++-------------- plugins/Alias/README.rst | 16 +++++++++------- plugins/Alias/plugin.py | 16 +++++++++------- 4 files changed, 38 insertions(+), 42 deletions(-) diff --git a/plugins/Aka/README.rst b/plugins/Aka/README.rst index 25ed71a79..6a7d29083 100644 --- a/plugins/Aka/README.rst +++ b/plugins/Aka/README.rst @@ -46,7 +46,7 @@ Now you can use Aka as you used Alias before. Trout ^^^^^ -Add an aka, trout, which expects a word as an argument:: +Add an aka, ``trout``, which expects a word as an argument:: @aka add trout "reply action slaps $1 with a large trout" jamessan: The operation succeeded. @@ -56,23 +56,19 @@ Add an aka, trout, which expects a word as an argument:: 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 --git a/plugins/Aka/plugin.py b/plugins/Aka/plugin.py index ff457eac9..2c8ee9caa 100644 --- a/plugins/Aka/plugin.py +++ b/plugins/Aka/plugin.py @@ -532,7 +532,7 @@ class Aka(callbacks.Plugin): Trout ^^^^^ - Add an aka, trout, which expects a word as an argument:: + Add an aka, ``trout``, which expects a word as an argument:: @aka add trout "reply action slaps $1 with a large trout" jamessan: The operation succeeded. @@ -542,23 +542,19 @@ class Aka(callbacks.Plugin): 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 --git a/plugins/Alias/README.rst b/plugins/Alias/README.rst index d2eff2c62..bb4fc4576 100644 --- a/plugins/Alias/README.rst +++ b/plugins/Alias/README.rst @@ -18,21 +18,23 @@ This plugin is only kept for backward compatibility, you should use the 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:: @alias add trout "action slaps $1 with a large trout" jamessan: The operation succeeded. @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 --git a/plugins/Alias/plugin.py b/plugins/Alias/plugin.py index 1350c88c0..7b39998c9 100644 --- a/plugins/Alias/plugin.py +++ b/plugins/Alias/plugin.py @@ -243,21 +243,23 @@ class Alias(callbacks.Plugin): 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:: @alias add trout "action slaps $1 with a large trout" jamessan: The operation succeeded. @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) From 65d88440c2896aa89f64f48c9cf0baee032310bf Mon Sep 17 00:00:00 2001 From: James Lu Date: Sat, 9 Jul 2022 14:24:13 -0700 Subject: [PATCH 10/71] supybot-plugin-doc: default to RST None of the current maintainers are sure what stx is?? --- scripts/supybot-plugin-doc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/supybot-plugin-doc b/scripts/supybot-plugin-doc index 224b11dbd..c27b5d109 100644 --- a/scripts/supybot-plugin-doc +++ b/scripts/supybot-plugin-doc @@ -297,7 +297,7 @@ if __name__ == '__main__': '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=[], From 964acac058b93c090a93c108abdc1cb72ed8015d Mon Sep 17 00:00:00 2001 From: Pratyush Desai Date: Wed, 13 Jul 2022 11:50:44 +0530 Subject: [PATCH 11/71] for #1490 extend usage examples --- plugins/MoobotFactoids/plugin.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/plugins/MoobotFactoids/plugin.py b/plugins/MoobotFactoids/plugin.py index 8d93eeaa6..0e46d9e6a 100755 --- a/plugins/MoobotFactoids/plugin.py +++ b/plugins/MoobotFactoids/plugin.py @@ -293,12 +293,23 @@ class MoobotFactoids(callbacks.Plugin): ``@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 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 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 (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 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): From 5d8f59bf80e6b0ff291c76b7b3106698a9f56a0e Mon Sep 17 00:00:00 2001 From: pratyushd Date: Thu, 14 Jul 2022 00:32:28 +0530 Subject: [PATCH 12/71] add conditional to respond acc to kick being true or not (#1512) Co-authored-by: Pratyush Desai --- plugins/Channel/plugin.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/Channel/plugin.py b/plugins/Channel/plugin.py index a5b82428a..d6332f9d4 100644 --- a/plugins/Channel/plugin.py +++ b/plugins/Channel/plugin.py @@ -381,8 +381,12 @@ class Channel(callbacks.Plugin): 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 From d67fb2a8b2862bc0babf5d722cb55063214d5d34 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Wed, 20 Jul 2022 17:52:40 +0200 Subject: [PATCH 13/71] Autocomplete, Fediverse, Geography, Poll: Run Black --- plugins/Autocomplete/__init__.py | 1 + plugins/Fediverse/__init__.py | 1 + plugins/Geography/plugin.py | 3 ++- plugins/Geography/test.py | 8 +++----- plugins/Geography/wikidata.py | 8 +++++--- plugins/Poll/__init__.py | 1 + 6 files changed, 13 insertions(+), 9 deletions(-) diff --git a/plugins/Autocomplete/__init__.py b/plugins/Autocomplete/__init__.py index afc1573ef..bd3597b4e 100644 --- a/plugins/Autocomplete/__init__.py +++ b/plugins/Autocomplete/__init__.py @@ -61,6 +61,7 @@ from . import config from . import plugin from importlib import reload + # In case we're being reloaded. reload(config) reload(plugin) diff --git a/plugins/Fediverse/__init__.py b/plugins/Fediverse/__init__.py index c31057f6e..c96ff1db4 100644 --- a/plugins/Fediverse/__init__.py +++ b/plugins/Fediverse/__init__.py @@ -53,6 +53,7 @@ from . import config from . import plugin from importlib import reload + # In case we're being reloaded. reload(config) reload(plugin) diff --git a/plugins/Geography/plugin.py b/plugins/Geography/plugin.py index 2578fe18f..d8af1075d 100644 --- a/plugins/Geography/plugin.py +++ b/plugins/Geography/plugin.py @@ -150,7 +150,8 @@ class Geography(callbacks.Plugin): 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 --git a/plugins/Geography/test.py b/plugins/Geography/test.py index 2685bb74e..cc88a47fd 100644 --- a/plugins/Geography/test.py +++ b/plugins/Geography/test.py @@ -83,7 +83,7 @@ class GeographyTimezoneTestCase(PluginTestCase): 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 @@ class GeographyTimezoneTestCase(PluginTestCase): 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 @@ class GeographyTimezoneTestCase(PluginTestCase): 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 --git a/plugins/Geography/wikidata.py b/plugins/Geography/wikidata.py index 712fe01a1..2256691ee 100644 --- a/plugins/Geography/wikidata.py +++ b/plugins/Geography/wikidata.py @@ -134,10 +134,12 @@ def timezone_from_uri(location_uri): """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 --git a/plugins/Poll/__init__.py b/plugins/Poll/__init__.py index e11be72f7..6d6aa4381 100644 --- a/plugins/Poll/__init__.py +++ b/plugins/Poll/__init__.py @@ -53,6 +53,7 @@ from . import config from . import plugin from importlib import reload + # In case we're being reloaded. reload(config) reload(plugin) From 2df2bc28d08dee5cc0864fd2e874a9e43035b3ae Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Wed, 20 Jul 2022 17:53:00 +0200 Subject: [PATCH 14/71] Fediverse: Add support for videos --- plugins/Fediverse/plugin.py | 35 +++++-- plugins/Fediverse/test.py | 19 ++++ plugins/Fediverse/test_data.py | 177 +++++++++++++++++++++++++++++++++ plugins/Fediverse/utils.py | 63 ++++++++++++ 4 files changed, 287 insertions(+), 7 deletions(-) create mode 100644 plugins/Fediverse/utils.py diff --git a/plugins/Fediverse/plugin.py b/plugins/Fediverse/plugin.py index 0dea17aee..134569f31 100644 --- a/plugins/Fediverse/plugin.py +++ b/plugins/Fediverse/plugin.py @@ -38,6 +38,7 @@ from supybot.commands import urlSnarfer, wrap from supybot.i18n import PluginInternationalization from . import activitypub as ap +from .utils import parse_xsd_duration importlib.reload(ap) @@ -222,18 +223,29 @@ class Fediverse(callbacks.PluginRegexp): 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 _("") % str(e) + else: + return self._format_actor_fullname(author) + elif isinstance(author, dict): + if author.get("id"): + return self._format_author(irc, author["id"]) + elif isinstance(author, list): + return format("%L", [self._format_author(irc, item) for item in author]) + else: + return "" + 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 = _("") % 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", @@ -275,6 +287,15 @@ class Fediverse(callbacks.PluginRegexp): return self._format_status(irc, msg, status) except ap.ActivityPubProtocolError as e: return "" % 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, + status["content"], + ) else: assert False, "Unknown status type %s: %r" % ( status["type"], diff --git a/plugins/Fediverse/test.py b/plugins/Fediverse/test.py index 87b956cca..e28b5facc 100644 --- a/plugins/Fediverse/test.py +++ b/plugins/Fediverse/test.py @@ -60,6 +60,10 @@ from .test_data import ( BOOSTED_DATA, BOOSTED_ACTOR_URL, BOOSTED_ACTOR_DATA, + PEERTUBE_VIDEO_URL, + PEERTUBE_VIDEO_DATA, + PEERTUBE_ACTOR_URL, + PEERTUBE_ACTOR_DATA, ) @@ -430,6 +434,21 @@ class NetworklessFediverseTestCase(BaseFediverseTestCase): + "", ) + def testVideo(self): + expected_requests = [ + (PEERTUBE_VIDEO_URL, PEERTUBE_VIDEO_DATA), + (PEERTUBE_ACTOR_URL, PEERTUBE_ACTOR_DATA), + (ACTOR_URL, 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 (@chocobozzz@peertube.cpy.re) " + "and \x02someuser\x02 (@someuser@example.org): description of video" + ) + def testStatusUrlSnarferDisabled(self): with self.mockWebfingerSupport("not called"), self.mockRequests([]): self.assertSnarfNoResponse( diff --git a/plugins/Fediverse/test_data.py b/plugins/Fediverse/test_data.py index c035e63ee..68b6cfdc4 100644 --- a/plugins/Fediverse/test_data.py +++ b/plugins/Fediverse/test_data.py @@ -384,3 +384,180 @@ BOOSTED_ACTOR_VALUE = { "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 video", + "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 --git a/plugins/Fediverse/utils.py b/plugins/Fediverse/utils.py new file mode 100644 index 000000000..c55b1c42a --- /dev/null +++ b/plugins/Fediverse/utils.py @@ -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-?)P" + "(?:(?P\d+)Y)?" + "(?:(?P\d+)M)?" + "(?:(?P\d+)D)?" + "(?:T(?:(?P\d+)H)?(?:(?P\d+)M)?(?:(?P\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 From 34f8842273e006a2d46af0fb1b39d4dae3f94858 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Wed, 20 Jul 2022 18:09:41 +0200 Subject: [PATCH 15/71] Fediverse: Add support for descriptions with line breaks --- plugins/Fediverse/plugin.py | 26 ++++++++++++++++++-------- plugins/Fediverse/test.py | 3 ++- plugins/Fediverse/test_data.py | 2 +- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/plugins/Fediverse/plugin.py b/plugins/Fediverse/plugin.py index 134569f31..a82cfb15e 100644 --- a/plugins/Fediverse/plugin.py +++ b/plugins/Fediverse/plugin.py @@ -50,6 +50,10 @@ _ = PluginInternationalization("Fediverse") _username_regexp = re.compile("@(?P[^@ ]+)@(?P[^@ ]+)") +def html_to_text(html): + return utils.web.htmlToText(html).split("\n", 1)[0].strip() + + class FediverseHttp(httpserver.SupyHTTPServerCallback): name = "minimal ActivityPub server" defaultResponse = _( @@ -236,7 +240,9 @@ class Fediverse(callbacks.PluginRegexp): if author.get("id"): return self._format_author(irc, author["id"]) elif isinstance(author, list): - return format("%L", [self._format_author(irc, item) for item in author]) + return format( + "%L", [self._format_author(irc, item) for item in author] + ) else: return "" @@ -245,7 +251,9 @@ class Fediverse(callbacks.PluginRegexp): return self._format_status(irc, msg, status["object"]) elif status["type"] == "Note": cw = status.get("summary") - author_fullname = self._format_author(irc, status.get("attributedTo")) + author_fullname = self._format_author( + irc, status.get("attributedTo") + ) if cw: if self.registryValue( "format.statuses.showContentWithCW", @@ -258,7 +266,7 @@ class Fediverse(callbacks.PluginRegexp): % ( author_fullname, cw, - utils.web.htmlToText(status["content"]), + html_to_text(status["content"]), ) ] else: @@ -270,7 +278,7 @@ class Fediverse(callbacks.PluginRegexp): _("%s: %s") % ( author_fullname, - utils.web.htmlToText(status["content"]), + html_to_text(status["content"]), ) ] @@ -288,13 +296,15 @@ class Fediverse(callbacks.PluginRegexp): except ap.ActivityPubProtocolError as e: return "" % e.args[0] elif status["type"] == "Video": - author_fullname = self._format_author(irc, status.get("attributedTo")) + 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, - status["content"], + html_to_text(status["content"]), ) else: assert False, "Unknown status type %s: %r" % ( @@ -313,14 +323,14 @@ class Fediverse(callbacks.PluginRegexp): _("%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 --git a/plugins/Fediverse/test.py b/plugins/Fediverse/test.py index e28b5facc..5c0b2f88b 100644 --- a/plugins/Fediverse/test.py +++ b/plugins/Fediverse/test.py @@ -446,7 +446,8 @@ class NetworklessFediverseTestCase(BaseFediverseTestCase): "status https://example.org/w/gABde9e210FGHre", "\x02name of video\x02 (1 hour, 26 minutes, and 0 seconds) " "by \x02chocobozzz\x02 (@chocobozzz@peertube.cpy.re) " - "and \x02someuser\x02 (@someuser@example.org): description of video" + "and \x02someuser\x02 (@someuser@example.org): " + "description of the video with a second line", ) def testStatusUrlSnarferDisabled(self): diff --git a/plugins/Fediverse/test_data.py b/plugins/Fediverse/test_data.py index 68b6cfdc4..9de1a48ef 100644 --- a/plugins/Fediverse/test_data.py +++ b/plugins/Fediverse/test_data.py @@ -535,7 +535,7 @@ PEERTUBE_VIDEO_VALUE = { "originallyPublishedAt": None, "updated": "2022-07-13T07:03:12.373Z", "mediaType": "text/markdown", - "content": "description of video", + "content": "description of the video\r\nwith a second line", "support": None, "subtitleLanguage": [], "icon": [ From 461c091b94c6258024918525a6c9c7ab40d9b217 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Wed, 20 Jul 2022 18:15:51 +0200 Subject: [PATCH 16/71] Fediverse: Hide channel actor on PeerTube --- plugins/Fediverse/plugin.py | 6 +++++- plugins/Fediverse/test.py | 4 +--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/plugins/Fediverse/plugin.py b/plugins/Fediverse/plugin.py index a82cfb15e..13d30d14b 100644 --- a/plugins/Fediverse/plugin.py +++ b/plugins/Fediverse/plugin.py @@ -237,11 +237,15 @@ class Fediverse(callbacks.PluginRegexp): else: return self._format_actor_fullname(author) elif isinstance(author, dict): + if author.get("type") == "Group": + # Typically, there is an actor named "Default 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", [self._format_author(irc, item) for item in author] + "%L", filter(bool, [self._format_author(irc, item) for item in author]) ) else: return "" diff --git a/plugins/Fediverse/test.py b/plugins/Fediverse/test.py index 5c0b2f88b..d06c0ead7 100644 --- a/plugins/Fediverse/test.py +++ b/plugins/Fediverse/test.py @@ -438,15 +438,13 @@ class NetworklessFediverseTestCase(BaseFediverseTestCase): expected_requests = [ (PEERTUBE_VIDEO_URL, PEERTUBE_VIDEO_DATA), (PEERTUBE_ACTOR_URL, PEERTUBE_ACTOR_DATA), - (ACTOR_URL, 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 (@chocobozzz@peertube.cpy.re) " - "and \x02someuser\x02 (@someuser@example.org): " + "by \x02chocobozzz\x02 (@chocobozzz@peertube.cpy.re): " "description of the video with a second line", ) From 7b9a94460353aba46ea63df243bf40a3cb57691b Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Wed, 20 Jul 2022 18:25:27 +0200 Subject: [PATCH 17/71] Remove dependency on 'mock' It is a backport of unittest.mock for Python versions before 3.3, which we do not support anymore. --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f1c707452..953108e55 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,5 @@ python-dateutil python-gnupg feedparser PySocks -mock cryptography pyxmpp2-scram From 796f717d09183c7e3ae10c3e82664909a17c9f9f Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Wed, 20 Jul 2022 18:31:24 +0200 Subject: [PATCH 18/71] requirements.txt: Classify and provide a rationale for each dependency --- requirements.txt | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/requirements.txt b/requirements.txt index 953108e55..7fbd151c4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,17 @@ +# mandatory: + setuptools -chardet -pytz;python_version<'3.9' -python-dateutil -python-gnupg -feedparser -PySocks -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 From f549ec12c6570ea8a3d69745cdd58b41c02ac5a7 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Fri, 29 Jul 2022 09:45:09 +0200 Subject: [PATCH 19/71] Add debug logging of SASL mechanisms --- src/irclib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/irclib.py b/src/irclib.py index 70715ec98..dcd4ba2c0 100644 --- a/src/irclib.py +++ b/src/irclib.py @@ -1862,6 +1862,7 @@ class Irc(IrcCommandDispatcher, log.Firewalled): 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', From ee60431396a5269d94dd1fcc50be1509c06d0e5f Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Fri, 29 Jul 2022 10:03:39 +0200 Subject: [PATCH 20/71] Add debug logging when skipping SASL mechanisms It is useful to figure out what you forgot to configure --- src/irclib.py | 49 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/src/irclib.py b/src/irclib.py index dcd4ba2c0..c95458485 100644 --- a/src/irclib.py +++ b/src/irclib.py @@ -1731,20 +1731,41 @@ class Irc(IrcCommandDispatcher, log.Firewalled): 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') From ef081746b119db28ce6f1a63bd4600c4fd6032ca Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Fri, 29 Jul 2022 10:29:48 +0200 Subject: [PATCH 21/71] commands: Silence noisy logging of command evaluation --- src/commands.py | 51 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/src/commands.py b/src/commands.py index f58d6d059..f93e7ce47 100644 --- a/src/commands.py +++ b/src/commands.py @@ -50,6 +50,13 @@ from .utils import minisix 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 @@ def getChannel(irc, msg, args, state): 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 @@ def getChannels(irc, msg, args, state): 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 @@ class context(object): 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 @@ class additional(context): 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 @@ class optional(additional): 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 @@ class any(context): 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 @@ class getopts(context): 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 @@ class getopts(context): 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 @@ class getopts(context): 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 @@ class Spec(object): 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 @@ def _wrap(f, specList=[], name=None, checkDoc=True, **kw): 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) From 2afa3c49a19c6c35e7ba04585cee33d9c7acbb45 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 30 Jul 2022 21:23:10 +0200 Subject: [PATCH 22/71] Fediverse: run black --- plugins/Fediverse/plugin.py | 5 +- plugins/Fediverse/test_data.py | 276 +++++++++++++-------------------- 2 files changed, 114 insertions(+), 167 deletions(-) diff --git a/plugins/Fediverse/plugin.py b/plugins/Fediverse/plugin.py index 13d30d14b..ab8607193 100644 --- a/plugins/Fediverse/plugin.py +++ b/plugins/Fediverse/plugin.py @@ -245,7 +245,10 @@ class Fediverse(callbacks.PluginRegexp): 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]) + "%L", + filter( + bool, [self._format_author(irc, item) for item in author] + ), ) else: return "" diff --git a/plugins/Fediverse/test_data.py b/plugins/Fediverse/test_data.py index 9de1a48ef..0242a65ed 100644 --- a/plugins/Fediverse/test_data.py +++ b/plugins/Fediverse/test_data.py @@ -386,178 +386,122 @@ BOOSTED_ACTOR_VALUE = { 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 + "@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 the 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, + "@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 the 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" From 28c52c2818148568c833fd51c2f85d6c747f63b5 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 30 Jul 2022 21:25:47 +0200 Subject: [PATCH 23/71] Poll: Add @poll list command --- plugins/Poll/plugin.py | 23 +++++++++++++++++++++- plugins/Poll/test.py | 44 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/plugins/Poll/plugin.py b/plugins/Poll/plugin.py index f038af757..e1b83747f 100644 --- a/plugins/Poll/plugin.py +++ b/plugins/Poll/plugin.py @@ -218,11 +218,32 @@ class Poll_(callbacks.Plugin): 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")), k) for (k, v) in counts.most_common() ] irc.replies(results) + @wrap(["channel"]) + def list(self, irc, msg, args, channel): + """[] + + Lists open polls in the .""" + 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 --git a/plugins/Poll/test.py b/plugins/Poll/test.py index a5dd12ad5..73c05f3fa 100644 --- a/plugins/Poll/test.py +++ b/plugins/Poll/test.py @@ -49,6 +49,17 @@ class PollTestCase(ChannelPluginTestCase): "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"', @@ -122,3 +133,36 @@ class PollTestCase(ChannelPluginTestCase): 'poll add "Is this a test?" "Yes totally" "Yes and no" "Maybe"', "Error: Duplicate answer identifier(s): Yes", ) + + 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.") From cd0f9f262867abe7594a2b53111c9a654d196fbc Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sun, 31 Jul 2022 09:04:27 +0200 Subject: [PATCH 24/71] Polls: Make nick matching case-insensitive This prevents the same nick from voting twice by changing the capitalization --- plugins/Poll/plugin.py | 5 ++++- plugins/Poll/test.py | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/plugins/Poll/plugin.py b/plugins/Poll/plugin.py index e1b83747f..680a884d2 100644 --- a/plugins/Poll/plugin.py +++ b/plugins/Poll/plugin.py @@ -149,7 +149,10 @@ class Poll_(callbacks.Plugin): ) 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) diff --git a/plugins/Poll/test.py b/plugins/Poll/test.py index 73c05f3fa..b891fd862 100644 --- a/plugins/Poll/test.py +++ b/plugins/Poll/test.py @@ -73,6 +73,11 @@ class PollTestCase(ChannelPluginTestCase): "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", From 8f837a676dab9ea39da52b408d50682828ad0441 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Tue, 2 Aug 2022 12:59:27 +0200 Subject: [PATCH 25/71] Time: Add support for omitting space in @seconds --- plugins/Time/plugin.py | 16 +++++++++++----- plugins/Time/test.py | 6 ++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/plugins/Time/plugin.py b/plugins/Time/plugin.py index a0d8fcdd2..94b2fa62a 100644 --- a/plugins/Time/plugin.py +++ b/plugins/Time/plugin.py @@ -72,10 +72,13 @@ try: except ImportError: tzlocal = None + +_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): """[y] [w] [d] [h] [m] [s] Returns the number of seconds in the number of , , @@ -84,11 +87,13 @@ class Time(callbacks.Plugin): 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 +113,7 @@ class Time(callbacks.Plugin): elif kind == 's': seconds += i irc.reply(str(seconds)) + seconds = wrap(seconds, ['text']) @internationalizeDocstring def at(self, irc, msg, args, s=None): diff --git a/plugins/Time/test.py b/plugins/Time/test.py index 96233edb7..57b2a486a 100644 --- a/plugins/Time/test.py +++ b/plugins/Time/test.py @@ -77,16 +77,22 @@ class TimeTestCase(PluginTestCase): self.assertResponse('seconds 10s', '10') self.assertResponse('seconds 1m', '60') self.assertResponse('seconds 1m 1s', '61') + self.assertResponse('seconds 1m1s', '61') self.assertResponse('seconds 1h', '3600') self.assertResponse('seconds 1h 1s', '3601') + self.assertResponse('seconds 1h1s', '3601') self.assertResponse('seconds 1d', '86400') self.assertResponse('seconds 1d 1s', '86401') + self.assertResponse('seconds 1d1s', '86401') self.assertResponse('seconds 2s', '2') self.assertResponse('seconds 2m', '120') self.assertResponse('seconds 2d 2h 2m 2s', '180122') + self.assertResponse('seconds 2d2h2m2s', '180122') self.assertResponse('seconds 1s', '1') self.assertResponse('seconds 1y 1s', '31536001') + self.assertResponse('seconds 1y1s', '31536001') self.assertResponse('seconds 1w 1s', '604801') + self.assertResponse('seconds 1w1s', '604801') def testNoErrors(self): self.assertNotError('ctime') From b8dce0d7db3c84ade83111c4f8ff99302a0b3833 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Tue, 2 Aug 2022 13:16:21 +0200 Subject: [PATCH 26/71] Time: Skip new assertions on Python 3.6 so testSeconds passes --- plugins/Time/plugin.py | 3 +++ plugins/Time/test.py | 15 ++++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/plugins/Time/plugin.py b/plugins/Time/plugin.py index 94b2fa62a..66ffd34cd 100644 --- a/plugins/Time/plugin.py +++ b/plugins/Time/plugin.py @@ -73,8 +73,11 @@ 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 diff --git a/plugins/Time/test.py b/plugins/Time/test.py index 57b2a486a..a0127f34c 100644 --- a/plugins/Time/test.py +++ b/plugins/Time/test.py @@ -77,21 +77,26 @@ class TimeTestCase(PluginTestCase): self.assertResponse('seconds 10s', '10') self.assertResponse('seconds 1m', '60') self.assertResponse('seconds 1m 1s', '61') - self.assertResponse('seconds 1m1s', '61') self.assertResponse('seconds 1h', '3600') self.assertResponse('seconds 1h 1s', '3601') - self.assertResponse('seconds 1h1s', '3601') self.assertResponse('seconds 1d', '86400') self.assertResponse('seconds 1d 1s', '86401') - self.assertResponse('seconds 1d1s', '86401') self.assertResponse('seconds 2s', '2') self.assertResponse('seconds 2m', '120') self.assertResponse('seconds 2d 2h 2m 2s', '180122') - self.assertResponse('seconds 2d2h2m2s', '180122') self.assertResponse('seconds 1s', '1') self.assertResponse('seconds 1y 1s', '31536001') - self.assertResponse('seconds 1y1s', '31536001') self.assertResponse('seconds 1w 1s', '604801') + + @skipIf(sys.version_info < (3, 7, 0), + "Python 3.6 does not support emptypattern 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): From 07806244501f9e4a03f674a48d3ae71564c62b65 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 6 Aug 2022 15:08:51 +0200 Subject: [PATCH 27/71] Time: Fix typo --- plugins/Time/test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Time/test.py b/plugins/Time/test.py index a0127f34c..09bd6a7de 100644 --- a/plugins/Time/test.py +++ b/plugins/Time/test.py @@ -89,7 +89,7 @@ class TimeTestCase(PluginTestCase): self.assertResponse('seconds 1w 1s', '604801') @skipIf(sys.version_info < (3, 7, 0), - "Python 3.6 does not support emptypattern matches, see: " + "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') From 4db32e24a527a8b101a3c3b1eca04b015585c343 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 6 Aug 2022 15:09:10 +0200 Subject: [PATCH 28/71] Ctcp: Fix 'RuntimeError: dictionary changed size during iteration' --- plugins/Ctcp/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Ctcp/plugin.py b/plugins/Ctcp/plugin.py index 5cc92ae0e..f18535cf1 100644 --- a/plugins/Ctcp/plugin.py +++ b/plugins/Ctcp/plugin.py @@ -64,7 +64,7 @@ class Ctcp(callbacks.PluginRegexp): 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): From fccb4f705bb5ead64059a1f5994ef5e593095062 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sun, 7 Aug 2022 18:50:14 +0200 Subject: [PATCH 29/71] RSS: Log feed URL when feedparser.parse raises exceptions --- plugins/RSS/plugin.py | 13 +++++++++++-- plugins/RSS/test.py | 18 +++++++++++++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/plugins/RSS/plugin.py b/plugins/RSS/plugin.py index ff8b15e4b..8c63c0350 100644 --- a/plugins/RSS/plugin.py +++ b/plugins/RSS/plugin.py @@ -356,8 +356,17 @@ class RSS(callbacks.Plugin): 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 --git a/plugins/RSS/test.py b/plugins/RSS/test.py index 0ec8a9ad2..6de976a65 100644 --- a/plugins/RSS/test.py +++ b/plugins/RSS/test.py @@ -31,6 +31,7 @@ import functools from unittest.mock import patch +import socket import sys import feedparser @@ -362,7 +363,22 @@ class RSSTestCase(ChannelPluginTestCase): 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 From 86b389618f61b330da67aa698651698bb62eda30 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Tue, 16 Aug 2022 00:23:33 +0200 Subject: [PATCH 30/71] MessageParser: Ignore chathistory batches To be consistent with commands and PluginRegexp (snarfers) --- plugins/MessageParser/plugin.py | 14 ++++++++++++++ plugins/MessageParser/test.py | 23 ++++++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/plugins/MessageParser/plugin.py b/plugins/MessageParser/plugin.py index bbb8ae0a5..981f3a70b 100644 --- a/plugins/MessageParser/plugin.py +++ b/plugins/MessageParser/plugin.py @@ -163,6 +163,20 @@ class MessageParser(callbacks.Plugin, plugins.ChannelDBHandler): 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 --git a/plugins/MessageParser/test.py b/plugins/MessageParser/test.py index b460868fd..f65c7d0bd 100644 --- a/plugins/MessageParser/test.py +++ b/plugins/MessageParser/test.py @@ -116,10 +116,31 @@ class MessageParserTestCase(ChannelPluginTestCase): 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"') From 200acdfa93b18a807199869d7426b5d394ed0a29 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Wed, 7 Sep 2022 12:31:19 +0200 Subject: [PATCH 31/71] registry: Normalize values before checking they are valid Otherwise, normalization is useless, and gives a surprising error message, such as: ``` config plugins.rss.sortfeeditems oldestfirst Error: Valid values include 'asInFeed', 'oldestFirst', 'newestFirst', 'outdatedFirst', and 'updatedFirst', not 'oldestFirst'. ``` --- plugins/Config/test.py | 21 ++++++++++++++++++++- src/registry.py | 2 +- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/plugins/Config/test.py b/plugins/Config/test.py index 288b20cfc..7dd073862 100644 --- a/plugins/Config/test.py +++ b/plugins/Config/test.py @@ -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 @@ import random 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 @@ class ConfigTestCase(ChannelPluginTestCase): 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 --git a/src/registry.py b/src/registry.py index 430e2bd45..252c2fcf0 100644 --- a/src/registry.py +++ b/src/registry.py @@ -710,7 +710,7 @@ class OnlySomeStrings(String): 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) From bc3a4418885f1711c2533ce61408cf97fc2f8d9f Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sun, 18 Sep 2022 19:25:48 +0200 Subject: [PATCH 32/71] Poll: Make answers case-insensitive --- plugins/Poll/plugin.py | 6 ++++-- plugins/Poll/test.py | 19 +++++++++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/plugins/Poll/plugin.py b/plugins/Poll/plugin.py index 680a884d2..c21be4359 100644 --- a/plugins/Poll/plugin.py +++ b/plugins/Poll/plugin.py @@ -131,7 +131,7 @@ class Poll_(callbacks.Plugin): 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 @@ -194,6 +194,8 @@ class Poll_(callbacks.Plugin): 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( @@ -221,7 +223,7 @@ class Poll_(callbacks.Plugin): 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() ] diff --git a/plugins/Poll/test.py b/plugins/Poll/test.py index b891fd862..988157d70 100644 --- a/plugins/Poll/test.py +++ b/plugins/Poll/test.py @@ -131,12 +131,27 @@ class PollTestCase(ChannelPluginTestCase): 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): From acdae12bbd5351b1a6c4323f45e3ef189d0205bf Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sun, 18 Sep 2022 19:32:35 +0200 Subject: [PATCH 33/71] Bump CI version from 3.11.0-alpha.2 to 3.11.0-rc.2 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 767777e83..a0aaacf78 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ jobs: runs-on: ${{ matrix.runs-on }} strategy: matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11.0-alpha.2", "pypy-3.6", "pypy-3.7"] + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11.0-rc.2", "pypy-3.6", "pypy-3.7"] with-opt-deps: [false, true] runs-on: [ubuntu-latest] exclude: From 169824a9d220cc46fca9b1f03201ba541a479fe9 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sun, 18 Sep 2022 19:47:01 +0200 Subject: [PATCH 34/71] Math: make `@icalc` fail early when result is too large This avoids inconsistent errors between CPython 3.10.7 and older versions; and the result would not be readable anyway. Closes GH-1517. --- plugins/Math/plugin.py | 15 +++++++++++++-- plugins/Math/test.py | 10 ++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/plugins/Math/plugin.py b/plugins/Math/plugin.py index 81c2ff3eb..21250aacc 100644 --- a/plugins/Math/plugin.py +++ b/plugins/Math/plugin.py @@ -166,8 +166,8 @@ class Math(callbacks.Plugin): """ 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 @@ class Math(callbacks.Plugin): 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 --git a/plugins/Math/test.py b/plugins/Math/test.py index d33aff396..22578c9ed 100644 --- a/plugins/Math/test.py +++ b/plugins/Math/test.py @@ -112,7 +112,10 @@ class MathTestCase(PluginTestCase): 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 @@ class MathTestCase(PluginTestCase): 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') From 35bf5998564eda288ca4ee86e0fa45b28c64d7ba Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Tue, 20 Sep 2022 07:51:46 +0200 Subject: [PATCH 35/71] utils/web: Add
to the list of block elements It should always be replaced with a space. --- src/utils/web.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/utils/web.py b/src/utils/web.py index bde24e1e1..4dfaf8bd9 100644 --- a/src/utils/web.py +++ b/src/utils/web.py @@ -226,6 +226,8 @@ def getEncoding(s): # 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() From b1cfb87e71b8252dfd086274c54e347c4b49e662 Mon Sep 17 00:00:00 2001 From: James Lu Date: Wed, 5 Oct 2022 18:40:03 -0700 Subject: [PATCH 36/71] String: allow multi-character inputs in "ord" and "unicodename" This allows them to support emoji ZWJ sequences, which render like one character but are actually multiple. --- plugins/String/plugin.py | 33 ++++++++++++++++++--------------- plugins/String/test.py | 14 +++++++++++--- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/plugins/String/plugin.py b/plugins/String/plugin.py index 495d33387..20cae111d 100644 --- a/plugins/String/plugin.py +++ b/plugins/String/plugin.py @@ -51,13 +51,13 @@ import multiprocessing class String(callbacks.Plugin): """Provides useful commands for manipulating characters and strings.""" - def ord(self, irc, msg, args, letter): - """ + def ord(self, irc, msg, args, s): + """ - Returns the unicode codepoint of . + Returns the unicode codepoint of characters in . """ - irc.reply(str(ord(letter))) - ord = wrap(ord, ['letter']) + irc.replies([str(ord(char)) for char in s]) + ord = wrap(ord, ['text']) def chr(self, irc, msg, args, i): """ @@ -70,17 +70,20 @@ class String(callbacks.Plugin): irc.error(_('That number doesn\'t map to a unicode character.')) chr = wrap(chr, ['int']) - def unicodename(self, irc, msg, args, character): - """ + def unicodename(self, irc, msg, args, s): + """ - Returns the name of the given unicode .""" - if len(character) != 1: - irc.errorInvalid('character', character) - try: - irc.reply(unicodedata.name(character)) - except ValueError: - irc.error(_('No name found for this character.')) - unicodename = wrap(unicodename, ['something']) + Returns the name of characters in . + This will error if any character is not a valid Unicode character.""" + replies = [] + for idx, char in enumerate(s): + try: + replies.append(unicodedata.name(char)) + except ValueError: + irc.error(_('No name found for character %r at position %d.') % + (char, idx), Raise=True) + irc.replies(replies) + unicodename = wrap(unicodename, ['text']) def unicodesearch(self, irc, msg, args, name): """ diff --git a/plugins/String/test.py b/plugins/String/test.py index 4f21d4e70..766a2c4a0 100644 --- a/plugins/String/test.py +++ b/plugins/String/test.py @@ -103,13 +103,21 @@ class StringTestCase(PluginTestCase): for c in map(chr, range(256)): i = ord(c) self.assertResponse('ord %s' % utils.str.dqrepr(c), str(i)) - + self.assertResponse('ord é', '233') + self.assertResponse('ord 🆒', '127378') + self.assertResponse('ord 🇦🇶', '127462 and 127478') def testUnicode(self): self.assertResponse('unicodename ☃', 'SNOWMAN') self.assertResponse('unicodesearch SNOWMAN', '☃') - #self.assertResponse('unicodename ?', - # 'No name found for this character.') + self.assertResponse('unicodename ?', 'QUESTION MARK') + + # multi-char strings and ZWJ sequences + self.assertResponse('unicodename :O', 'COLON and LATIN CAPITAL LETTER O') + self.assertResponse('unicodename 🤷‍♂️', 'SHRUG, ZERO WIDTH JOINER, MALE SIGN, and VARIATION SELECTOR-16') + + self.assertError('unicodename "\\uFFFF"') + self.assertError('unicodename "!@#\\uFFFF$"') self.assertResponse('unicodesearch FOO', 'Error: No character found with this name.') From a6aa5530dd5f7a611af4715edba12212bd2ac3ce Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Tue, 27 Sep 2022 23:16:21 +0200 Subject: [PATCH 37/71] Ensure files written with AtomicFile are read in UTF8 With some locale configurations (not that uncommon on CentOS), open() may default to non-UTF8 encodings (eg. ANSI_X3.4-1968). This is usually not an issue, because we use open() both for writing and reading. However, AtomicFile implicitly enforces UTF8; which needs to be mirrored when reading. --- plugins/Karma/plugin.py | 2 +- src/dbi.py | 14 +++++++------- src/registry.py | 2 +- src/unpreserve.py | 2 +- src/utils/file.py | 4 +++- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/plugins/Karma/plugin.py b/plugins/Karma/plugin.py index 2625ffe22..bd896bf30 100644 --- a/plugins/Karma/plugin.py +++ b/plugins/Karma/plugin.py @@ -209,7 +209,7 @@ class SqliteKarmaDB(object): def load(self, channel, filename): filename = conf.supybot.directories.data.dirize(filename) - fd = open(filename) + fd = open(filename, encoding='utf8') reader = csv.reader(fd) db = self._getDb(channel) cursor = db.cursor() diff --git a/src/dbi.py b/src/dbi.py index 3578def04..d5bdfc466 100644 --- a/src/dbi.py +++ b/src/dbi.py @@ -150,7 +150,7 @@ class FlatfileMapping(MappingInterface): def __init__(self, filename, maxSize=10**6): self.filename = filename try: - fd = open(self.filename) + fd = open(self.filename, encoding='utf8') strId = fd.readline().rstrip() self.maxSize = len(strId) try: @@ -175,7 +175,7 @@ class FlatfileMapping(MappingInterface): def _incrementCurrentId(self, fd=None): fdWasNone = fd is None if fdWasNone: - fd = open(self.filename, 'a') + fd = open(self.filename, 'a', encoding='utf8') fd.seek(0) self.currentId += 1 fd.write(self._canonicalId(self.currentId)) @@ -193,7 +193,7 @@ class FlatfileMapping(MappingInterface): def add(self, s): line = self._joinLine(self.currentId, s) - fd = open(self.filename, 'r+') + fd = open(self.filename, 'r+', encoding='utf8') try: fd.seek(0, 2) # End. fd.write(line) @@ -205,7 +205,7 @@ class FlatfileMapping(MappingInterface): def get(self, id): strId = self._canonicalId(id) try: - fd = open(self.filename) + fd = open(self.filename, encoding='utf8') fd.readline() # First line, nextId. for line in fd: (lineId, s) = self._splitLine(line) @@ -221,7 +221,7 @@ class FlatfileMapping(MappingInterface): def set(self, id, s): strLine = self._joinLine(id, s) try: - fd = open(self.filename, 'r+') + fd = open(self.filename, 'r+', encoding='utf8') self.remove(id, fd) fd.seek(0, 2) # End. fd.write(strLine) @@ -233,7 +233,7 @@ class FlatfileMapping(MappingInterface): strId = self._canonicalId(id) try: if fdWasNone: - fd = open(self.filename, 'r+') + fd = open(self.filename, 'r+', encoding='utf8') fd.seek(0) fd.readline() # First line, nextId pos = fd.tell() @@ -262,7 +262,7 @@ class FlatfileMapping(MappingInterface): fd.close() def vacuum(self): - infd = open(self.filename) + infd = open(self.filename, encoding='utf8') outfd = utils.file.AtomicFile(self.filename,makeBackupIfSmaller=False) outfd.write(infd.readline()) # First line, nextId. for line in infd: diff --git a/src/registry.py b/src/registry.py index 252c2fcf0..3ded34814 100644 --- a/src/registry.py +++ b/src/registry.py @@ -84,7 +84,7 @@ def open_registry(filename, clear=False): global _lastModified if clear: _cache.clear() - _fd = open(filename) + _fd = open(filename, encoding='utf8') fd = utils.file.nonCommentNonEmptyLines(_fd) acc = '' slashEnd = re.compile(r'\\*$') diff --git a/src/unpreserve.py b/src/unpreserve.py index 5cf79b5a0..3b7ed91bc 100644 --- a/src/unpreserve.py +++ b/src/unpreserve.py @@ -70,7 +70,7 @@ class Reader(object): return s.lower() def readFile(self, filename): - self.read(open(filename)) + self.read(open(filename, encoding='utf8')) def read(self, fd): lineno = 0 diff --git a/src/utils/file.py b/src/utils/file.py index b0b17eb41..2fbe6140b 100644 --- a/src/utils/file.py +++ b/src/utils/file.py @@ -130,7 +130,9 @@ def chunks(fd, size): class AtomicFile(object): """Used for files that need to be atomically written -- i.e., if there's a - failure, the original file remains, unmodified. mode must be 'w' or 'wb'""" + failure, the original file remains, unmodified. mode must be 'w' or 'wb'. + If ``encoding`` is None (or not provided), files are open in `utf8` regardless + of the system locale.""" class default(object): # Holder for values. # Callables? tmpDir = None From 8c17505221f7663b39ce26ef2f4c58d2ff2bdd71 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Thu, 13 Oct 2022 21:46:50 +0200 Subject: [PATCH 38/71] User: Sort output of @capabilities It's more readable than a random order. --- plugins/User/plugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/User/plugin.py b/plugins/User/plugin.py index f3a4479dc..e3bce2f62 100644 --- a/plugins/User/plugin.py +++ b/plugins/User/plugin.py @@ -448,7 +448,8 @@ class User(callbacks.Plugin): irc.errorNotRegistered() else: if u == user or u._checkCapability('admin'): - irc.reply('[%s]' % '; '.join(user.capabilities), private=True) + irc.reply('[%s]' % '; '.join(sorted(user.capabilities)), + private=True) else: irc.error(conf.supybot.replies.incorrectAuthentication(), Raise=True) From dc94f8dc689ea5614c5a4a87c40b8fd923e70185 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Fri, 14 Oct 2022 23:15:11 +0200 Subject: [PATCH 39/71] registry: Default to sorting sets of values Not sorting them causes the config file to change when the bot writes it, because order is nondeterministic. This is usually fine, but can be annoying when configs are deployed with Ansible. Closes GH-1516 --- src/registry.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/registry.py b/src/registry.py index 3ded34814..2bc3323da 100644 --- a/src/registry.py +++ b/src/registry.py @@ -875,6 +875,7 @@ class SpaceSeparatedListOfStrings(SpaceSeparatedListOf): class SpaceSeparatedSetOfStrings(SpaceSeparatedListOfStrings): __slots__ = () List = set + sorted = True class CommaSeparatedListOfStrings(SeparatedListOf): __slots__ = () @@ -887,6 +888,7 @@ class CommaSeparatedSetOfStrings(SeparatedListOf): __slots__ = () List = set Value = String + sorted = True def splitter(self, s): return re.split(r'\s*,\s*', s) joiner = ', '.join From d0a484c11c8155b0b5361623b1867bade9d2e962 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Thu, 20 Oct 2022 18:35:58 +0200 Subject: [PATCH 40/71] Sort remaining nondeterministc sets of values Closes GH-1516 --- plugins/Protector/config.py | 1 + plugins/RSS/config.py | 2 +- plugins/Relay/config.py | 2 ++ plugins/Services/config.py | 1 + src/ircdb.py | 2 +- 5 files changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/Protector/config.py b/plugins/Protector/config.py index 7f0dbb179..2a90b0887 100644 --- a/plugins/Protector/config.py +++ b/plugins/Protector/config.py @@ -50,6 +50,7 @@ conf.registerChannelValue(Protector, 'enable', class ImmuneNicks(conf.ValidNicks): List = ircutils.IrcSet + sorted = True conf.registerChannelValue(Protector, 'immune', ImmuneNicks([], _("""Determines what nicks the bot will consider to diff --git a/plugins/RSS/config.py b/plugins/RSS/config.py index 16d0404bc..7702ebd9d 100644 --- a/plugins/RSS/config.py +++ b/plugins/RSS/config.py @@ -43,7 +43,7 @@ def configure(advanced): conf.registerPlugin('RSS', True) -class FeedNames(registry.SpaceSeparatedListOfStrings): +class FeedNames(registry.SpaceSeparatedSetOfStrings): List = callbacks.CanonicalNameSet class FeedItemSortOrder(registry.OnlySomeStrings): diff --git a/plugins/Relay/config.py b/plugins/Relay/config.py index 3c3f70fa1..8f2d3bf8b 100644 --- a/plugins/Relay/config.py +++ b/plugins/Relay/config.py @@ -50,10 +50,12 @@ def configure(advanced): class Ignores(registry.SpaceSeparatedListOf): List = ircutils.IrcSet Value = conf.ValidHostmask + sorted = True class Networks(registry.SpaceSeparatedListOf): List = ircutils.IrcSet Value = registry.String + sorted = True Relay = conf.registerPlugin('Relay') conf.registerChannelValue(Relay, 'color', diff --git a/plugins/Services/config.py b/plugins/Services/config.py index 37e007e62..3c324812d 100644 --- a/plugins/Services/config.py +++ b/plugins/Services/config.py @@ -64,6 +64,7 @@ class ValidNickOrEmptyString(registry.String): class ValidNickSet(conf.ValidNicks): List = ircutils.IrcSet + sorted = True Services = conf.registerPlugin('Services') conf.registerNetworkValue(Services, 'nicks', diff --git a/src/ircdb.py b/src/ircdb.py index f48b633ea..e3124fd07 100644 --- a/src/ircdb.py +++ b/src/ircdb.py @@ -1314,7 +1314,7 @@ def checkCapabilities(hostmask, capabilities, requireAll=False): # supybot.capabilities ### -class SpaceSeparatedListOfCapabilities(registry.SpaceSeparatedListOfStrings): +class SpaceSeparatedListOfCapabilities(registry.SpaceSeparatedSetOfStrings): __slots__ = () List = CapabilitySet From 47253e032ebbf2ecda9a3e10657f1f8382f208c2 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Mon, 24 Oct 2022 23:21:11 +0200 Subject: [PATCH 41/71] Add test for structures.TimeoutQueue.__iter__ --- test/test_utils.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/test_utils.py b/test/test_utils.py index e8ca9581d..20c09bc74 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -1152,6 +1152,29 @@ class TestTimeoutQueue(SupyTestCase): timeFastForward(1.1) self.assertFalse(1 in q) + def testIter(self): + q = TimeoutQueue(1) + q.enqueue(1) + it1 = iter(q) + timeFastForward(0.5) + q.enqueue(2) + it2 = iter(q) + self.assertEqual(next(it1), 1) + self.assertEqual(next(it2), 1) + self.assertEqual(next(it2), 2) + with self.assertRaises(StopIteration): + next(it2) + + timeFastForward(0.6) + self.assertEqual(next(it1), 2) + with self.assertRaises(StopIteration): + next(it1) + + it3 = iter(q) + self.assertEqual(next(it3), 2) + with self.assertRaises(StopIteration): + next(it3) + def testReset(self): q = TimeoutQueue(10) q.enqueue(1) From 2c5dc405fc6ae42ee131585cc284fbb147599862 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Mon, 24 Oct 2022 23:40:11 +0200 Subject: [PATCH 42/71] test: Revert generic 'The Limnoria Contributors' in copyright notices I forgot to include these in 63eb6672eac794a9c39dec87db3aa45147e06974 (This is fine because noone but me touched these files since the initial change in db7ef3f02517f9f2a3c56829a22b9fad3c36e374). --- test/__init__.py | 2 +- test/test.py | 2 +- test/test_callbacks.py | 2 +- test/test_commands.py | 2 +- test/test_drivers.py | 2 +- test/test_dynamicScope.py | 2 +- test/test_firewall.py | 2 +- test/test_format.py | 2 +- test/test_i18n.py | 2 +- test/test_ircdb.py | 2 +- test/test_irclib.py | 2 +- test/test_ircmsgs.py | 2 +- test/test_ircutils.py | 2 +- test/test_misc.py | 2 +- test/test_plugin.py | 2 +- test/test_plugin_create.py | 2 +- test/test_plugins.py | 2 +- test/test_registry.py | 2 +- test/test_schedule.py | 2 +- test/test_standardSubstitute.py | 2 +- test/test_utils.py | 2 +- test/test_yn.py | 2 +- 22 files changed, 22 insertions(+), 22 deletions(-) diff --git a/test/__init__.py b/test/__init__.py index d933c53ce..528910d9d 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -1,6 +1,6 @@ ### # Copyright (c) 2005, Jeremiah Fincher -# Copyright (c) 2010-2021, The Limnoria Contributors +# Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/test/test.py b/test/test.py index f51b711f7..52945bfde 100644 --- a/test/test.py +++ b/test/test.py @@ -1,6 +1,6 @@ ### # Copyright (c) 2005, Jeremiah Fincher -# Copyright (c) 2010-2021, The Limnoria Contributors +# Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/test/test_callbacks.py b/test/test_callbacks.py index 613d546a6..e477012f7 100644 --- a/test/test_callbacks.py +++ b/test/test_callbacks.py @@ -1,7 +1,7 @@ # -*- coding: utf8 -*- ### # Copyright (c) 2002-2005, Jeremiah Fincher -# Copyright (c) 2010-2021, The Limnoria Contributors +# Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/test/test_commands.py b/test/test_commands.py index 2071cc83d..cb66483b1 100644 --- a/test/test_commands.py +++ b/test/test_commands.py @@ -1,7 +1,7 @@ ### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2015, James McCoy -# Copyright (c) 2010-2021, The Limnoria Contributors +# Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/test/test_drivers.py b/test/test_drivers.py index 4942f16c8..ea333af4c 100644 --- a/test/test_drivers.py +++ b/test/test_drivers.py @@ -1,5 +1,5 @@ ## -# Copyright (c) 2019-2021, The Limnoria Contributors +# Copyright (c) 2019-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/test/test_dynamicScope.py b/test/test_dynamicScope.py index fc986107f..fe9980e09 100644 --- a/test/test_dynamicScope.py +++ b/test/test_dynamicScope.py @@ -1,6 +1,6 @@ ### # Copyright (c) 2005, Jeremiah Fincher -# Copyright (c) 2010-2021, The Limnoria Contributors +# Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/test/test_firewall.py b/test/test_firewall.py index 006319ab0..75ee22f9f 100644 --- a/test/test_firewall.py +++ b/test/test_firewall.py @@ -1,6 +1,6 @@ ### # Copyright (c) 2008, Jeremiah Fincher -# Copyright (c) 2010-2021, The Limnoria Contributors +# Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/test/test_format.py b/test/test_format.py index 9320dfad9..36906899d 100644 --- a/test/test_format.py +++ b/test/test_format.py @@ -1,6 +1,6 @@ ### # Copyright (c) 2005, Jeremiah Fincher -# Copyright (c) 2010-2021, The Limnoria Contributors +# Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/test/test_i18n.py b/test/test_i18n.py index 165c2d59f..374d67451 100644 --- a/test/test_i18n.py +++ b/test/test_i18n.py @@ -1,6 +1,6 @@ # -*- coding: utf8 -*- ### -# Copyright (c) 2012-2021, The Limnoria Contributors +# Copyright (c) 2012-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/test/test_ircdb.py b/test/test_ircdb.py index 7b1c06641..b95477523 100644 --- a/test/test_ircdb.py +++ b/test/test_ircdb.py @@ -1,6 +1,6 @@ ### # Copyright (c) 2002-2005, Jeremiah Fincher -# Copyright (c) 2010-2021, The Limnoria Contributors +# Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/test/test_irclib.py b/test/test_irclib.py index be5a360ec..22ef55290 100644 --- a/test/test_irclib.py +++ b/test/test_irclib.py @@ -1,6 +1,6 @@ ## # Copyright (c) 2002-2005, Jeremiah Fincher -# Copyright (c) 2010-2021, The Limnoria Contributors +# Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/test/test_ircmsgs.py b/test/test_ircmsgs.py index 86035e40a..d81536dd0 100644 --- a/test/test_ircmsgs.py +++ b/test/test_ircmsgs.py @@ -1,6 +1,6 @@ ### # Copyright (c) 2002-2005, Jeremiah Fincher -# Copyright (c) 2010-2021, The Limnoria Contributors +# Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/test/test_ircutils.py b/test/test_ircutils.py index cf0d98e4c..65fa2013e 100644 --- a/test/test_ircutils.py +++ b/test/test_ircutils.py @@ -1,6 +1,6 @@ ### # Copyright (c) 2002-2005, Jeremiah Fincher -# Copyright (c) 2010-2021, The Limnoria Contributors +# Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/test/test_misc.py b/test/test_misc.py index a663f4970..d66d74c2c 100644 --- a/test/test_misc.py +++ b/test/test_misc.py @@ -1,6 +1,6 @@ ### # Copyright (c) 2019, James Lu -# Copyright (c) 2019-2021, The Limnoria Contributors +# Copyright (c) 2019-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/test/test_plugin.py b/test/test_plugin.py index 461f03bb6..37608148a 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -1,6 +1,6 @@ ### # Copyright (c) 2005, Jeremiah Fincher -# Copyright (c) 2010-2021, The Limnoria Contributors +# Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/test/test_plugin_create.py b/test/test_plugin_create.py index 06c6d4135..4dbaf3e33 100644 --- a/test/test_plugin_create.py +++ b/test/test_plugin_create.py @@ -1,6 +1,6 @@ ### # Copyright (c) 2018, James Lu -# Copyright (c) 2018-2021, The Limnoria Contributors +# Copyright (c) 2018-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/test/test_plugins.py b/test/test_plugins.py index b818b2a22..4a43c6d4a 100644 --- a/test/test_plugins.py +++ b/test/test_plugins.py @@ -1,7 +1,7 @@ ### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2008, James McCoy -# Copyright (c) 2010-2021, The Limnoria Contributors +# Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/test/test_registry.py b/test/test_registry.py index 56efa2b38..48dfff547 100644 --- a/test/test_registry.py +++ b/test/test_registry.py @@ -1,6 +1,6 @@ ### # Copyright (c) 2004, Jeremiah Fincher -# Copyright (c) 2010-2021, The Limnoria Contributors +# Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/test/test_schedule.py b/test/test_schedule.py index 6b86399ee..628aea832 100644 --- a/test/test_schedule.py +++ b/test/test_schedule.py @@ -1,6 +1,6 @@ ### # Copyright (c) 2002-2005, Jeremiah Fincher -# Copyright (c) 2010-2021, The Limnoria Contributors +# Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/test/test_standardSubstitute.py b/test/test_standardSubstitute.py index 6a7bcd476..c64b495c8 100644 --- a/test/test_standardSubstitute.py +++ b/test/test_standardSubstitute.py @@ -1,6 +1,6 @@ ### # Copyright (c) 2002-2005, Jeremiah Fincher -# Copyright (c) 2010-2021, The Limnoria Contributors +# Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/test/test_utils.py b/test/test_utils.py index 20c09bc74..d3e068f0a 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -1,7 +1,7 @@ ### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2009,2011, James McCoy -# Copyright (c) 2010-2021, The Limnoria Contributors +# Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/test/test_yn.py b/test/test_yn.py index 37b7bfa85..c147710a2 100644 --- a/test/test_yn.py +++ b/test/test_yn.py @@ -1,6 +1,6 @@ ### # Copyright (c) 2014, Artur Krysiak -# Copyright (c) 2010-2021, The Limnoria Contributors +# Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without From 009b900100c9f98ed6c8e5588df88a5032bbc6da Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Mon, 24 Oct 2022 23:43:50 +0200 Subject: [PATCH 43/71] Make TimeoutQueue.iter() actually expire items It is functionally fine not to, but causes objects to never be freed if iter() is the only method called on the queue (ie. no enqueue/dequeue, len(), ...) --- src/utils/structures.py | 7 +++++-- test/test_utils.py | 27 ++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/utils/structures.py b/src/utils/structures.py index 4d3e7a623..251edb546 100644 --- a/src/utils/structures.py +++ b/src/utils/structures.py @@ -1,6 +1,6 @@ ### # Copyright (c) 2002-2009, Jeremiah Fincher -# 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 @@ -349,7 +349,10 @@ class TimeoutQueue(object): return self.queue.dequeue()[1] def __iter__(self): - # We could _clearOldElements here, but what happens if someone stores + self._clearOldElements() + + # You may think re-checking _getTimeout() after we just called + # _clearOldElements is redundant, but what happens if someone stores # the resulting generator and elements that should've timed out are # yielded? Hmm? What happens then, smarty-pants? for (t, elt) in self.queue: diff --git a/test/test_utils.py b/test/test_utils.py index d3e068f0a..5e8be3c8b 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -1,7 +1,7 @@ ### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2009,2011, 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 @@ -1182,6 +1182,31 @@ class TestTimeoutQueue(SupyTestCase): q.reset() self.assertFalse(1 in q) + def testClean(self): + def iter_and_next(q): + next(iter(q)) + + def contains(q): + 42 in q + + for f in (len, repr, list, iter_and_next, contains): + print(f) + with self.subTest(f=f.__name__): + q = TimeoutQueue(1) + q.enqueue(1) + timeFastForward(0.5) + q.enqueue(2) + + self.assertEqual([x for (_, x) in q.queue], [1, 2]) + f(q) + self.assertEqual([x for (_, x) in q.queue], [1, 2]) + + timeFastForward(0.6) + + self.assertEqual([x for (_, x) in q.queue], [1, 2]) # not cleaned yet + f(q) + self.assertEqual([x for (_, x) in q.queue], [2]) # now it is + class TestCacheDict(SupyTestCase): def testMaxNeverExceeded(self): max = 10 From 2cfc821203036ae7780c32cdc5705db9008503a5 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Fri, 28 Oct 2022 14:18:52 +0200 Subject: [PATCH 44/71] Web: Allow configuring higher peekSize on Youtube --- plugins/Web/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Web/plugin.py b/plugins/Web/plugin.py index 0b2912720..a0c91d7df 100644 --- a/plugins/Web/plugin.py +++ b/plugins/Web/plugin.py @@ -154,7 +154,7 @@ class Web(callbacks.PluginRegexp): if parsed_url.netloc == 'youtube.com' \ or parsed_url.netloc.endswith(('.youtube.com')): # there is a lot of Javascript before the - size = 409600 + size = max(409600, size) if parsed_url.netloc in ('reddit.com', 'www.reddit.com', 'new.reddit.com'): # Since 2022-03, New Reddit has 'Reddit - Dive into anything' as # <title> on every page. From b0525bcf421c3bbc8e4bf3e9346e888a74bd1615 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz <progval+git@progval.net> Date: Fri, 28 Oct 2022 14:19:09 +0200 Subject: [PATCH 45/71] Double default peekSize We bumped it to 8kB in 2015, but it is starting to be an issue again. --- src/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conf.py b/src/conf.py index 5d9523a56..16c554451 100644 --- a/src/conf.py +++ b/src/conf.py @@ -1318,7 +1318,7 @@ registerGlobalValue(supybot.protocols.irc.queuing.rateLimit, 'join', ### registerGroup(supybot.protocols, 'http') registerGlobalValue(supybot.protocols.http, 'peekSize', - registry.PositiveInteger(8192, _("""Determines how many bytes the bot will + registry.PositiveInteger(16384, _("""Determines how many bytes the bot will 'peek' at when looking through a URL for a doctype or title or something similar. It'll give up after it reads this many bytes, even if it hasn't found what it was looking for."""))) From e9a29e9159a3b8ec39a4b1c8ebb68e12f99e0b8d Mon Sep 17 00:00:00 2001 From: Valentin Lorentz <progval+git@progval.net> Date: Fri, 28 Oct 2022 14:30:17 +0200 Subject: [PATCH 46/71] irclib: Fix crashes on ecdsa/scram signature failures --- src/irclib.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/irclib.py b/src/irclib.py index c95458485..3c282186e 100644 --- a/src/irclib.py +++ b/src/irclib.py @@ -1927,7 +1927,7 @@ class Irc(IrcCommandDispatcher, log.Firewalled): mechanism = self.sasl_current_mechanism if mechanism == 'ecdsa-nist256p-challenge': - self._doAuthenticateEcdsa(string) + self._doAuthenticateEcdsa(msg, string) elif mechanism == 'external': self.sendSaslString(b'') elif mechanism.startswith('scram-'): @@ -1936,7 +1936,7 @@ class Irc(IrcCommandDispatcher, log.Firewalled): if step == 'uninitialized': log.debug('%s: starting SCRAM.', self.network) - self._doAuthenticateScramFirst(mechanism) + self._doAuthenticateScramFirst(msg, mechanism) elif step == 'first-sent': log.debug('%s: received SCRAM challenge.', self.network) @@ -1944,13 +1944,13 @@ class Irc(IrcCommandDispatcher, log.Firewalled): elif step == 'final-sent': log.debug('%s: finishing SCRAM.', self.network) - self._doAuthenticateScramFinish(string) + self._doAuthenticateScramFinish(msg, string) else: assert False except scram.ScramException: self.sendMsg(ircmsgs.IrcMsg(command='AUTHENTICATE', args=('*',))) - self.tryNextSaslMechanism() + self.tryNextSaslMechanism(msg) elif mechanism == 'plain': authstring = b'\0'.join([ self.sasl_username.encode('utf-8'), @@ -1959,7 +1959,7 @@ class Irc(IrcCommandDispatcher, log.Firewalled): ]) self.sendSaslString(authstring) - def _doAuthenticateEcdsa(self, string): + def _doAuthenticateEcdsa(self, msg, string): if string == b'': self.sendSaslString(self.sasl_username.encode('utf-8')) return @@ -1974,9 +1974,9 @@ class Irc(IrcCommandDispatcher, log.Firewalled): except (OSError, ValueError): self.sendMsg(ircmsgs.IrcMsg(command='AUTHENTICATE', args=('*',))) - self.tryNextSaslMechanism() + self.tryNextSaslMechanism(msg) - def _doAuthenticateScramFirst(self, mechanism): + def _doAuthenticateScramFirst(self, msg, mechanism): """Handle sending the client-first message of SCRAM auth.""" hash_name = mechanism[len('scram-'):] if hash_name.endswith('-plus'): @@ -1985,7 +1985,7 @@ class Irc(IrcCommandDispatcher, log.Firewalled): if hash_name not in scram.HASH_FACTORIES: log.debug('%s: SCRAM hash %r not supported, aborting.', self.network, hash_name) - self.tryNextSaslMechanism() + self.tryNextSaslMechanism(msg) return authenticator = scram.SCRAMClientAuthenticator(hash_name, channel_binding=False) @@ -2003,14 +2003,14 @@ class Irc(IrcCommandDispatcher, log.Firewalled): self.sendSaslString(client_final) self.sasl_scram_state['step'] = 'final-sent' - def _doAuthenticateScramFinish(self, data): + def _doAuthenticateScramFinish(self, msg, data): try: res = self.sasl_scram_state['authenticator'] \ .finish(data) except scram.BadSuccessException as e: log.warning('%s: SASL authentication failed with SCRAM error: %e', self.network, e) - self.tryNextSaslMechanism() + self.tryNextSaslMechanism(msg) else: self.sendSaslString(b'') self.sasl_scram_state['step'] = 'authenticated' From 77805ff36e6674ce6ebcce5dd54b45f452256f32 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz <progval+git@progval.net> Date: Fri, 28 Oct 2022 15:00:58 +0200 Subject: [PATCH 47/71] irclib: Abort authentication when server fails SCRAM challenge Will be tested by irctest: https://github.com/progval/irctest/pull/179 --- src/irclib.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/irclib.py b/src/irclib.py index 3c282186e..0e84d6685 100644 --- a/src/irclib.py +++ b/src/irclib.py @@ -2010,6 +2010,8 @@ class Irc(IrcCommandDispatcher, log.Firewalled): except scram.BadSuccessException as e: log.warning('%s: SASL authentication failed with SCRAM error: %e', self.network, e) + self.sendMsg(ircmsgs.IrcMsg(command='AUTHENTICATE', + args=('*',))) self.tryNextSaslMechanism(msg) else: self.sendSaslString(b'') From f4ac7f88fed02985440f97c02913dc6fa61fe193 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz <progval+git@progval.net> Date: Fri, 28 Oct 2022 22:41:43 +0200 Subject: [PATCH 48/71] RSS: Don't crash on invalid variable name It's confusing not to have feedback on IRC when a variable name is typoed. --- plugins/RSS/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/RSS/plugin.py b/plugins/RSS/plugin.py index 8c63c0350..d143ce6fe 100644 --- a/plugins/RSS/plugin.py +++ b/plugins/RSS/plugin.py @@ -493,7 +493,7 @@ class RSS(callbacks.Plugin): template = self.registryValue(key_name, channel, network) date = entry.get('published_parsed') date = utils.str.timestamp(date) - s = string.Template(template).substitute( + s = string.Template(template).safe_substitute( entry, feed_name=feed.name, date=date) From 4da1291876c8a1d3750f2633dcc71013bddcf01c Mon Sep 17 00:00:00 2001 From: Valentin Lorentz <progval+git@progval.net> Date: Sun, 30 Oct 2022 20:43:43 +0100 Subject: [PATCH 49/71] URL: Lazily deserialize records from the end in @last Before this commit, the plugin first fetched a list of all (deserialized) records in a list, then reversed the list, and iterated on the reverse list. This proved to be slow, with most of the time being spent in `dbi.DB._newRecord` (which essentially deserializes one list of CSV). After this commit, the list is reversed first, then the plugin iterates on its generator, which calls `_newRecord` on records as they are requested. This means that when there are many URLs in the database, `@last` does not need to waste time deserializing most records, when the result is near the end (and if the result is the first record, then it does exactly as much work as before). --- plugins/URL/plugin.py | 18 ++++++++++-------- src/dbi.py | 26 ++++++++++++++++++++++---- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/plugins/URL/plugin.py b/plugins/URL/plugin.py index 2a609a6c5..24921d523 100644 --- a/plugins/URL/plugin.py +++ b/plugins/URL/plugin.py @@ -1,7 +1,7 @@ ### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2010, 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 @@ -29,6 +29,8 @@ # POSSIBILITY OF SUCH DAMAGE. ### +import itertools + import supybot.dbi as dbi import supybot.conf as conf import supybot.utils as utils @@ -57,9 +59,7 @@ class DbiUrlDB(plugins.DbiChannelDB): near=msg.args[1], at=msg.receivedAt) super(self.__class__, self).add(record) def urls(self, p): - L = list(self.select(p)) - L.reverse() - return L + return self.select(p, reverse=True) URLDB = plugins.DB('URL', {'flat': DbiUrlDB}) @@ -142,16 +142,18 @@ class URL(callbacks.Plugin): if not predicate(record): return False return True - urls = [record.url for record in self.db.urls(channel, predicate)] - if not urls: + urls = (record.url for record in self.db.urls(channel, predicate)) + (urls, urls_copy) = itertools.tee(urls) + first_url = next(urls_copy, None) + if first_url is None: irc.reply(_('No URLs matched that criteria.')) else: if nolimit: - urls = [format('%u', url) for url in urls] + urls = (format('%u', url) for url in urls) s = ', '.join(urls) else: # We should optimize this with another URLDB method eventually. - s = urls[0] + s = first_url irc.reply(s) last = wrap(last, ['channeldb', getopts({'from': 'something', 'with': 'something', diff --git a/src/dbi.py b/src/dbi.py index d5bdfc466..33fa90fbd 100644 --- a/src/dbi.py +++ b/src/dbi.py @@ -1,6 +1,6 @@ ### # Copyright (c) 2002-2005, Jeremiah Fincher -# 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 @@ -358,12 +358,30 @@ class DB(object): self.map.remove(id) def __iter__(self): - for (id, s) in self.map: + yield from self._iter() + + def _iter(self, *, reverse=False): + if reverse: + if hasattr(self.map, "__reversed__"): + # neither FlatfileMapping nor CdbMapping support __reversed__ + # and DirMapping does not support iteration at all, but + # there is no harm in allowing this short-path in case + # plugins use their own mapping. + it = reversed(self.map) + else: + # This does load the whole database in memory instead of + # iterating lazily, but plugins requesting a reverse list + # would probably need do it themselves otherwise, so it does + # not make matters worse to do it here. + it = reversed(list(self.map)) + else: + it = self.map + for (id, s) in it: # We don't need to yield the id because it's in the record. yield self._newRecord(id, s) - def select(self, p): - for record in self: + def select(self, p, reverse=False): + for record in self._iter(reverse=reverse): if p(record): yield record From 73a23e220f8887f6e20b817386a836dd808e9317 Mon Sep 17 00:00:00 2001 From: James Lu <james@overdrivenetworks.com> Date: Sun, 6 Nov 2022 18:38:22 -0800 Subject: [PATCH 50/71] IrcState: fix typo in attribute docs capabilities_acq -> capabilities_ack --- src/irclib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/irclib.py b/src/irclib.py index 0e84d6685..273681d5b 100644 --- a/src/irclib.py +++ b/src/irclib.py @@ -624,7 +624,7 @@ class IrcState(IrcCommandDispatcher, log.Firewalled): :type: Set[str] - .. attribute:: capabilities_acq + .. attribute:: capabilities_ack Set of all capabilities requested from and acknowledged by the server. See <https://ircv3.net/specs/core/capability-negotiation> From d7d97d3b93e9ab14cb48349e221b4e62a4e7fa33 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz <progval+git@progval.net> Date: Thu, 10 Nov 2022 01:48:29 +0100 Subject: [PATCH 51/71] Google: Remove broken commands --- plugins/Google/plugin.py | 99 ---------------------------------------- plugins/Google/test.py | 22 --------- 2 files changed, 121 deletions(-) diff --git a/plugins/Google/plugin.py b/plugins/Google/plugin.py index e89a90124..b7c6006f4 100644 --- a/plugins/Google/plugin.py +++ b/plugins/Google/plugin.py @@ -231,52 +231,6 @@ class Google(callbacks.PluginRegexp): 'filter':''}), 'text']) - @internationalizeDocstring - def cache(self, irc, msg, args, url): - """<url> - - Returns a link to the cached version of <url> if it is available. - """ - data = self.search(url, msg.channel, irc.network, {'smallsearch': True}) - if data: - m = data[0] - if m['cacheUrl']: - url = m['cacheUrl'].encode('utf-8') - irc.reply(url) - return - irc.error(_('Google seems to have no cache for that site.')) - cache = wrap(cache, ['url']) - - _fight_re = re.compile(r'id="resultStats"[^>]*>(?P<stats>[^<]*)') - @internationalizeDocstring - def fight(self, irc, msg, args): - """<search string> <search string> [<search string> ...] - - Returns the results of each search, in order, from greatest number - of results to least. - """ - channel = msg.channel - network = irc.network - results = [] - for arg in args: - text = self.search(arg, channel, network, {'smallsearch': True}) - i = text.find('id="resultStats"') - stats = utils.web.htmlToText(self._fight_re.search(text).group('stats')) - if stats == '': - results.append((0, args)) - continue - count = ''.join(filter('0123456789'.__contains__, stats)) - results.append((int(count), arg)) - results.sort() - results.reverse() - if self.registryValue('bold', channel, network): - bold = ircutils.bold - else: - bold = repr - s = ', '.join([format('%s: %i', bold(s), i) for (i, s) in results]) - irc.reply(s) - - def _translate(self, sourceLang, targetLang, text): headers = dict(utils.web.defaultHeaders) headers['User-agent'] = ('Mozilla/5.0 (X11; U; Linux i686) ' @@ -339,59 +293,6 @@ class Google(callbacks.PluginRegexp): (self.registryValue('baseUrl', channel, network), s) return url - _calcRe1 = re.compile(r'<span class="cwcot".*?>(.*?)</span>', re.I) - _calcRe2 = re.compile(r'<div class="vk_ans.*?>(.*?)</div>', re.I | re.S) - _calcRe3 = re.compile(r'<div class="side_div" id="rhs_div">.*?<input class="ucw_data".*?value="(.*?)"', re.I) - @internationalizeDocstring - def calc(self, irc, msg, args, expr): - """<expression> - - Uses Google's calculator to calculate the value of <expression>. - """ - url = self._googleUrl(expr, msg.channel, irc.network) - h = {"User-Agent":"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1667.0 Safari/537.36"} - html = utils.web.getUrl(url, headers=h).decode('utf8') - match = self._calcRe1.search(html) - if not match: - match = self._calcRe2.search(html) - if not match: - match = self._calcRe3.search(html) - if not match: - irc.reply("I could not find an output from Google Calc for: %s" % expr) - return - else: - s = match.group(1) - else: - s = match.group(1) - else: - s = match.group(1) - # do some cleanup of text - s = re.sub(r'<sup>(.*)</sup>⁄<sub>(.*)</sub>', r' \1/\2', s) - s = re.sub(r'<sup>(.*)</sup>', r'^\1', s) - s = utils.web.htmlToText(s) - irc.reply("%s = %s" % (expr, s)) - calc = wrap(calc, ['text']) - - _phoneRe = re.compile(r'Phonebook.*?<font size=-1>(.*?)<a href') - @internationalizeDocstring - def phonebook(self, irc, msg, args, phonenumber): - """<phone number> - - Looks <phone number> up on Google. - """ - url = self._googleUrl(phonenumber, msg.channel, irc.network) - html = utils.web.getUrl(url).decode('utf8') - m = self._phoneRe.search(html) - if m is not None: - s = m.group(1) - s = s.replace('<b>', '') - s = s.replace('</b>', '') - s = utils.web.htmlToText(s) - irc.reply(s) - else: - irc.reply(_('Google\'s phonebook didn\'t come up with anything.')) - phonebook = wrap(phonebook, ['text']) - Class = Google diff --git a/plugins/Google/test.py b/plugins/Google/test.py index aa2a302f5..57f6e27a5 100644 --- a/plugins/Google/test.py +++ b/plugins/Google/test.py @@ -34,21 +34,6 @@ from supybot.test import * class GoogleTestCase(ChannelPluginTestCase): plugins = ('Google', 'Config') if network: - def testCalcHandlesMultiplicationSymbol(self): - self.assertNotRegexp('google calc seconds in a century', r'215') - - def testCalc(self): - self.assertNotRegexp('google calc e^(i*pi)+1', r'didn\'t') - self.assertNotRegexp('google calc 1 usd in gbp', r'didn\'t') - - def testHtmlHandled(self): - self.assertNotRegexp('google calc ' - 'the speed of light ' - 'in microns / fortnight', '<sup>') - self.assertNotRegexp('google calc ' - 'the speed of light ' - 'in microns / fortnight', '×') - def testSearch(self): self.assertNotError('google foo') self.assertRegexp('google dupa', r'dupa') @@ -76,14 +61,7 @@ class GoogleTestCase(ChannelPluginTestCase): self.assertNotError('config plugins.Google.oneToOne True') self.assertNotRegexp('google dupa', ';') - def testFight(self): - self.assertRegexp('fight supybot moobot', r'.*supybot.*: \d+') - self.assertNotError('fight ... !') - def testTranslate(self): self.assertRegexp('translate en es hello world', 'Hola mundo') - def testCalcDoesNotHaveExtraSpaces(self): - self.assertNotRegexp('google calc 1000^2', r'\s+,\s+') - # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: From fd248dc521952b6d2c0b8b4bf1bf5f10f51a3649 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz <progval+git@progval.net> Date: Sun, 20 Nov 2022 19:07:44 +0100 Subject: [PATCH 52/71] Channel: Fix documentation, --exact cannot be combined --- plugins/Channel/plugin.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/plugins/Channel/plugin.py b/plugins/Channel/plugin.py index d6332f9d4..26ad62501 100644 --- a/plugins/Channel/plugin.py +++ b/plugins/Channel/plugin.py @@ -313,17 +313,15 @@ class Channel(callbacks.Plugin): @internationalizeDocstring def kban(self, irc, msg, args, channel, optlist, bannedNick, expiry, reason): - """[<channel>] [--{exact,nick,user,host}] <nick> [<seconds>] [<reason>] + """[<channel>] [--{exact,nick,user,host,account}] <nick> [<seconds>] [<reason>] If you have the #channel,op capability, this will kickban <nick> for as many seconds as you specify, or else (if you specify 0 seconds or don't specify a number of seconds) it will ban the person indefinitely. --exact bans only the exact hostmask; --nick bans just the nick; - --user bans just the user, and --host bans just the host. You can - combine these options as you choose. <reason> is a reason to give for - the kick. - <channel> is only necessary if the message isn't sent in the channel - itself. + --user bans just the user, and --host bans just the host + You can combine the --nick, --user, and --host options as you choose. + <channel> is only necessary if the message isn't sent in the channel itself. """ self._ban(irc, msg, args, channel, optlist, bannedNick, expiry, reason, True) @@ -343,9 +341,9 @@ class Channel(callbacks.Plugin): If you have the #channel,op capability, this will ban <nick> for as many seconds as you specify, otherwise (if you specify 0 seconds or don't specify a number of seconds) it will ban the person indefinitely. - --exact can be used to specify an exact hostmask. You can combine the - exact, nick, user, and host options as you choose. <channel> is only - necessary if the message isn't sent in the channel itself. + --exact can be used to specify an exact hostmask. + You can combine the --nick, --user, and --host options as you choose. + <channel> is only necessary if the message isn't sent in the channel itself. """ self._ban(irc, msg, args, channel, optlist, bannedNick, expiry, None, False) From 314fad36eb65e91f31a5dcf8b82a05758057ebf2 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz <progval+git@progval.net> Date: Sun, 20 Nov 2022 19:32:38 +0100 Subject: [PATCH 53/71] Modernize tests Thanks to https://pypi.org/project/teyit/ --- plugins/Alias/test.py | 2 +- plugins/Anonymous/test.py | 4 +- plugins/Games/test.py | 7 +- plugins/Limiter/test.py | 2 +- plugins/Misc/test.py | 10 +- plugins/MoobotFactoids/test.py | 7 +- plugins/RSS/test.py | 2 +- plugins/Reply/test.py | 2 +- plugins/Seen/test.py | 2 +- plugins/Services/test.py | 12 +-- plugins/Status/test.py | 4 +- plugins/Topic/test.py | 2 +- plugins/User/test.py | 4 +- test/test_callbacks.py | 48 +++++----- test/test_ircdb.py | 26 +++--- test/test_irclib.py | 158 +++++++++++++++---------------- test/test_ircmsgs.py | 4 +- test/test_ircutils.py | 58 ++++++------ test/test_standardSubstitute.py | 4 +- test/test_utils.py | 160 ++++++++++++++++---------------- 20 files changed, 262 insertions(+), 256 deletions(-) diff --git a/plugins/Alias/test.py b/plugins/Alias/test.py index 4a3ba13b4..8dd28e797 100644 --- a/plugins/Alias/test.py +++ b/plugins/Alias/test.py @@ -111,7 +111,7 @@ class AliasTestCase(ChannelPluginTestCase): self.assertResponse('foobar', 'sbbone') self.assertRaises(Alias.AliasError, cb.removeAlias, 'foobar') cb.removeAlias('foobar', evenIfLocked=True) - self.assertFalse('foobar' in cb.aliases) + self.assertNotIn('foobar', cb.aliases) self.assertError('foobar') self.assertRegexp('alias add abc\x07 ignore', 'Error.*Invalid') diff --git a/plugins/Anonymous/test.py b/plugins/Anonymous/test.py index 0f290afc5..f3a073427 100644 --- a/plugins/Anonymous/test.py +++ b/plugins/Anonymous/test.py @@ -39,7 +39,7 @@ class AnonymousTestCase(ChannelPluginTestCase): with conf.supybot.plugins.Anonymous.requireRegistration.context(False): m = self.assertNotError('anonymous say %s foo!' % self.channel) - self.assertTrue(m.args[1] == 'foo!') + self.assertEqual(m.args[1], 'foo!') def testTell(self): self.assertError('anonymous tell %s I love you!' % self.nick) @@ -48,7 +48,7 @@ class AnonymousTestCase(ChannelPluginTestCase): self.assertError('anonymous tell %s foo!' % self.channel) with conf.supybot.plugins.Anonymous.allowPrivateTarget.context(True): m = self.assertNotError('anonymous tell %s foo!' % self.nick) - self.assertTrue(m.args[1] == 'foo!') + self.assertEqual(m.args[1], 'foo!') def testAction(self): m = self.assertError('anonymous do %s loves you!' % self.channel) diff --git a/plugins/Games/test.py b/plugins/Games/test.py index 5e19999eb..e47776b66 100644 --- a/plugins/Games/test.py +++ b/plugins/Games/test.py @@ -43,8 +43,11 @@ class GamesTestCase(ChannelPluginTestCase): 'Got a msg without bang|click|spin: %r' % m) elif m.command == 'KICK': sawKick = True - self.assertTrue('bang' in m.args[2].lower(), - 'Got a KICK without bang in it.') + self.assertIn( + 'bang', + m.args[2].lower(), + 'Got a KICK without bang in it.' + ) else: self.fail('Got something other than a kick or a privmsg.') self.assertTrue(sawKick, 'Didn\'t get a kick in %s iterations!' % i) diff --git a/plugins/Limiter/test.py b/plugins/Limiter/test.py index c4860b119..34e5c12e3 100644 --- a/plugins/Limiter/test.py +++ b/plugins/Limiter/test.py @@ -44,7 +44,7 @@ class LimiterTestCase(ChannelPluginTestCase): self.assertEqual(m, ircmsgs.limit('#foo', 1+10)) self.irc.feedMsg(ircmsgs.join('#foo', prefix='bar!root@host')) m = self.irc.takeMsg() - self.assertFalse(m is not None) + self.assertIsNone(m) conf.supybot.plugins.Limiter.maximumExcess.setValue(7) self.irc.feedMsg(ircmsgs.part('#foo', prefix='bar!root@host')) m = self.irc.takeMsg() diff --git a/plugins/Misc/test.py b/plugins/Misc/test.py index 06759fa58..def723e69 100644 --- a/plugins/Misc/test.py +++ b/plugins/Misc/test.py @@ -130,7 +130,7 @@ class MiscTestCase(ChannelPluginTestCase): def testHelpIncludeFullCommandName(self): self.assertHelp('help channel capability add') m = self.getMsg('help channel capability add') - self.assertTrue('channel capability add' in m.args[1]) + self.assertIn('channel capability add', m.args[1]) def testHelpDoesAmbiguityWithDefaultPlugins(self): m = self.getMsg('help list') # Misc.list and User.list. @@ -189,12 +189,12 @@ class MiscTestCase(ChannelPluginTestCase): oldprefix, self.prefix = self.prefix, 'tester!foo@bar__no_testcap__baz' self.nick = 'tester' m = self.getMsg('tell aljsdkfh [plugin tell]') - self.assertTrue('let you do' in m.args[1]) + self.assertIn('let you do', m.args[1]) m = self.getMsg('tell #foo [plugin tell]') - self.assertTrue('No need for' in m.args[1]) + self.assertIn('No need for', m.args[1]) m = self.getMsg('tell me you love me') m = self.irc.takeMsg() - self.assertTrue(m.args[0] == self.nick) + self.assertEqual(m.args[0], self.nick) def testNoNestedTell(self): self.assertRegexp('echo [tell %s foo]' % self.nick, 'nested') @@ -271,7 +271,7 @@ class MiscTestCase(ChannelPluginTestCase): self.assertResponse('more', 'abc '*112 + ' \x02(2 more messages)\x02') m = self.irc.takeMsg() - self.assertIsNot(m, None) + self.assertIsNotNone(m) self.assertEqual( m.args[1], 'abc '*112 + ' \x02(1 more message)\x02') diff --git a/plugins/MoobotFactoids/test.py b/plugins/MoobotFactoids/test.py index 2ba03c59e..bb5f365da 100644 --- a/plugins/MoobotFactoids/test.py +++ b/plugins/MoobotFactoids/test.py @@ -54,8 +54,11 @@ class OptionListTestCase(SupyTestCase): while max and L: max -= 1 option = plugin.pickOptions(s) - self.assertTrue(option in original, - 'Option %s not in %s' % (option, original)) + self.assertIn( + option, + original, + 'Option %s not in %s' % (option, original) + ) if option in L: L.remove(option) self.assertFalse(L, 'Some options never seen: %s' % L) diff --git a/plugins/RSS/test.py b/plugins/RSS/test.py index 6de976a65..9427d63dd 100644 --- a/plugins/RSS/test.py +++ b/plugins/RSS/test.py @@ -414,7 +414,7 @@ class RSSTestCase(ChannelPluginTestCase): timeFastForward(1.1) self.assertNotError('rss %s' % url) m = self.assertNotError('rss %s 2' % url) - self.assertTrue(m.args[1].count(' | ') == 1) + self.assertEqual(m.args[1].count(' | '), 1) def testRssAdd(self): timeFastForward(1.1) diff --git a/plugins/Reply/test.py b/plugins/Reply/test.py index 5206ba140..4876c5211 100644 --- a/plugins/Reply/test.py +++ b/plugins/Reply/test.py @@ -63,6 +63,6 @@ class ReplyNonChannelTestCase(PluginTestCase): self.prefix = 'something!else@somewhere.else' self.nick = 'something' m = self.assertAction('action foo', 'foo') - self.assertFalse(m.args[0] == self.irc.nick) + self.assertNotEqual(m.args[0], self.irc.nick) # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: diff --git a/plugins/Seen/test.py b/plugins/Seen/test.py index c2de5473d..8a45b15b9 100644 --- a/plugins/Seen/test.py +++ b/plugins/Seen/test.py @@ -86,7 +86,7 @@ class ChannelDBTestCase(ChannelPluginTestCase): self.assertError('seen *') self.assertNotError('seen %s' % self.nick) m = self.assertNotError('seen %s' % self.nick.upper()) - self.assertTrue(self.nick.upper() in m.args[1]) + self.assertIn(self.nick.upper(), m.args[1]) self.assertRegexp('seen user %s' % self.nick, '^%s was last seen' % self.nick) self.assertNotError('config plugins.Seen.minimumNonWildcard 0') diff --git a/plugins/Services/test.py b/plugins/Services/test.py index f2c4b6e9a..2fb560e75 100644 --- a/plugins/Services/test.py +++ b/plugins/Services/test.py @@ -50,12 +50,12 @@ class ServicesTestCase(PluginTestCase): try: self.assertNotError('services password %s baz' % self.nick) m = self.assertNotError('services identify') - self.assertTrue(m.args[0] == 'NickServ') - self.assertTrue(m.args[1].lower() == 'identify baz') + self.assertEqual(m.args[0], 'NickServ') + self.assertEqual(m.args[1].lower(), 'identify baz') self.assertNotError('services password %s biff' % self.nick) m = self.assertNotError('services identify') - self.assertTrue(m.args[0] == 'NickServ') - self.assertTrue(m.args[1].lower() == 'identify biff') + self.assertEqual(m.args[0], 'NickServ') + self.assertEqual(m.args[1].lower(), 'identify biff') finally: self.assertNotError('services password %s ""' % self.nick) @@ -84,8 +84,8 @@ class ServicesTestCase(PluginTestCase): 'Global: bar; test: bar2') m = self.assertNotError('services identify') - self.assertTrue(m.args[0] == 'NickServ') - self.assertTrue(m.args[1].lower() == 'identify bar2') + self.assertEqual(m.args[0], 'NickServ') + self.assertEqual(m.args[1].lower(), 'identify bar2') finally: self.assertNotError('services password %s ""' % self.nick) diff --git a/plugins/Status/test.py b/plugins/Status/test.py index 2404f9354..44318d3ba 100644 --- a/plugins/Status/test.py +++ b/plugins/Status/test.py @@ -41,8 +41,8 @@ class StatusTestCase(PluginTestCase): def testCpu(self): m = self.assertNotError('status cpu') - self.assertFalse('kB kB' in m.args[1]) - self.assertFalse('None' in m.args[1], 'None in cpu output: %r.' % m) + self.assertNotIn('kB kB', m.args[1]) + self.assertNotIn('None', m.args[1], 'None in cpu output: %r.' % m) for s in ['linux', 'freebsd', 'openbsd', 'netbsd', 'darwin']: if sys.platform.startswith(s): self.assertTrue('B' in m.args[1] or 'KB' in m.args[1] or diff --git a/plugins/Topic/test.py b/plugins/Topic/test.py index 834962a1a..59c8e9e07 100644 --- a/plugins/Topic/test.py +++ b/plugins/Topic/test.py @@ -129,7 +129,7 @@ class TopicTestCase(ChannelPluginTestCase): conf.supybot.plugins.Topic.separator.setValue(' <==> ') _ = self.getMsg('topic add foo') m = self.getMsg('topic add bar') - self.assertTrue('<==>' in m.args[1]) + self.assertIn('<==>', m.args[1]) finally: conf.supybot.plugins.Topic.separator.setValue(original) diff --git a/plugins/User/test.py b/plugins/User/test.py index 00558349b..a687ab51b 100644 --- a/plugins/User/test.py +++ b/plugins/User/test.py @@ -104,7 +104,7 @@ class UserTestCase(PluginTestCase): self.assertResponse('hostmask', self.prefix) self.assertError('@hostmask asdf') m = self.irc.takeMsg() - self.assertFalse(m is not None, m) + self.assertIsNone(m, m) def testRegisterPasswordLength(self): self.assertRegexp('register foo aa', 'at least 3 characters long.') @@ -131,7 +131,7 @@ class UserTestCase(PluginTestCase): try: self.assertError('unregister foo') m = self.irc.takeMsg() - self.assertFalse(m is not None, m) + self.assertIsNone(m, m) self.assertTrue(ircdb.users.getUserId('foo')) finally: conf.supybot.databases.users.allowUnregistration.setValue(orig) diff --git a/test/test_callbacks.py b/test/test_callbacks.py index e477012f7..8420a0263 100644 --- a/test/test_callbacks.py +++ b/test/test_callbacks.py @@ -417,14 +417,14 @@ class PrivmsgTestCase(ChannelPluginTestCase): def testReplyWithNickPrefix(self): self.feedMsg('@len foo') m = self.irc.takeMsg() - self.assertTrue(m is not None, 'm: %r' % m) + self.assertIsNotNone(m, 'm: %r' % m) self.assertTrue(m.args[1].startswith(self.nick)) try: original = conf.supybot.reply.withNickPrefix() conf.supybot.reply.withNickPrefix.setValue(False) self.feedMsg('@len foobar') m = self.irc.takeMsg() - self.assertTrue(m is not None) + self.assertIsNotNone(m) self.assertFalse(m.args[1].startswith(self.nick)) finally: conf.supybot.reply.withNickPrefix.setValue(original) @@ -448,7 +448,7 @@ class PrivmsgTestCase(ChannelPluginTestCase): conf.supybot.reply.error.withNotice.setValue(True) m = self.getMsg("eval irc.error('foo')") self.assertTrue(m, 'No message returned.') - self.assertTrue(m.command == 'NOTICE') + self.assertEqual(m.command, 'NOTICE') finally: conf.supybot.reply.error.withNotice.setValue(original) @@ -1112,10 +1112,10 @@ class WithPrivateNoticeTestCase(ChannelPluginTestCase): self.irc.addCallback(self.WithPrivateNotice(self.irc)) # Check normal behavior. m = self.assertNotError('normal') - self.assertFalse(m.command == 'NOTICE') + self.assertNotEqual(m.command, 'NOTICE') self.assertTrue(ircutils.isChannel(m.args[0])) m = self.assertNotError('explicit') - self.assertFalse(m.command == 'NOTICE') + self.assertNotEqual(m.command, 'NOTICE') self.assertTrue(ircutils.isChannel(m.args[0])) # Check abnormal behavior. originalInPrivate = conf.supybot.reply.inPrivate() @@ -1124,10 +1124,10 @@ class WithPrivateNoticeTestCase(ChannelPluginTestCase): conf.supybot.reply.inPrivate.setValue(True) conf.supybot.reply.withNotice.setValue(True) m = self.assertNotError('normal') - self.assertTrue(m.command == 'NOTICE') + self.assertEqual(m.command, 'NOTICE') self.assertFalse(ircutils.isChannel(m.args[0])) m = self.assertNotError('explicit') - self.assertFalse(m.command == 'NOTICE') + self.assertNotEqual(m.command, 'NOTICE') self.assertTrue(ircutils.isChannel(m.args[0])) finally: conf.supybot.reply.inPrivate.setValue(originalInPrivate) @@ -1136,10 +1136,10 @@ class WithPrivateNoticeTestCase(ChannelPluginTestCase): try: conf.supybot.reply.withNoticeWhenPrivate.setValue(True) m = self.assertNotError('implicit') - self.assertTrue(m.command == 'NOTICE') + self.assertEqual(m.command, 'NOTICE') self.assertFalse(ircutils.isChannel(m.args[0])) m = self.assertNotError('normal') - self.assertFalse(m.command == 'NOTICE') + self.assertNotEqual(m.command, 'NOTICE') self.assertTrue(ircutils.isChannel(m.args[0])) finally: conf.supybot.reply.withNoticeWhenPrivate.setValue(orig) @@ -1149,10 +1149,10 @@ class WithPrivateNoticeTestCase(ChannelPluginTestCase): try: conf.supybot.reply.withNoticeWhenPrivate.setValue(True) m = self.assertNotError("eval irc.reply('y',to='x',private=True)") - self.assertTrue(m.command == 'NOTICE') + self.assertEqual(m.command, 'NOTICE') m = self.getMsg(' ') m = self.assertNotError("eval irc.reply('y',to='#x',private=True)") - self.assertFalse(m.command == 'NOTICE') + self.assertNotEqual(m.command, 'NOTICE') finally: conf.supybot.reply.withNoticeWhenPrivate.setValue(original) @@ -1164,28 +1164,28 @@ class ProxyTestCase(SupyTestCase): irc = irclib.Irc('test') proxy = callbacks.SimpleProxy(irc, msg) # First one way... - self.assertFalse(proxy != irc) - self.assertTrue(proxy == irc) + self.assertEqual(proxy, irc) + self.assertEqual(proxy, irc) self.assertEqual(hash(proxy), hash(irc)) # Then the other! - self.assertFalse(irc != proxy) - self.assertTrue(irc == proxy) + self.assertEqual(irc, proxy) + self.assertEqual(irc, proxy) self.assertEqual(hash(irc), hash(proxy)) # And now dictionaries... d = {} d[irc] = 'foo' - self.assertTrue(len(d) == 1) - self.assertTrue(d[irc] == 'foo') - self.assertTrue(d[proxy] == 'foo') + self.assertEqual(len(d), 1) + self.assertEqual(d[irc], 'foo') + self.assertEqual(d[proxy], 'foo') d[proxy] = 'bar' - self.assertTrue(len(d) == 1) - self.assertTrue(d[irc] == 'bar') - self.assertTrue(d[proxy] == 'bar') + self.assertEqual(len(d), 1) + self.assertEqual(d[irc], 'bar') + self.assertEqual(d[proxy], 'bar') d[irc] = 'foo' - self.assertTrue(len(d) == 1) - self.assertTrue(d[irc] == 'foo') - self.assertTrue(d[proxy] == 'foo') + self.assertEqual(len(d), 1) + self.assertEqual(d[irc], 'foo') + self.assertEqual(d[proxy], 'foo') diff --git a/test/test_ircdb.py b/test/test_ircdb.py index b95477523..f8ba766b8 100644 --- a/test/test_ircdb.py +++ b/test/test_ircdb.py @@ -122,17 +122,17 @@ class CapabilitySetTestCase(SupyTestCase): def testContains(self): s = ircdb.CapabilitySet() - self.assertFalse('foo' in s) - self.assertFalse('-foo' in s) + self.assertNotIn('foo', s) + self.assertNotIn('-foo', s) s.add('foo') - self.assertTrue('foo' in s) - self.assertTrue('-foo' in s) + self.assertIn('foo', s) + self.assertIn('-foo', s) s.remove('foo') - self.assertFalse('foo' in s) - self.assertFalse('-foo' in s) + self.assertNotIn('foo', s) + self.assertNotIn('-foo', s) s.add('-foo') - self.assertTrue('foo' in s) - self.assertTrue('-foo' in s) + self.assertIn('foo', s) + self.assertIn('-foo', s) def testCheck(self): s = ircdb.CapabilitySet() @@ -170,8 +170,8 @@ class UserCapabilitySetTestCase(SupyTestCase): def testOwnerIsAlwaysPresent(self): d = ircdb.UserCapabilitySet() - self.assertTrue('owner' in d) - self.assertTrue('-owner' in d) + self.assertIn('owner', d) + self.assertIn('-owner', d) self.assertFalse(d.check('owner')) d.add('owner') self.assertTrue(d.check('owner')) @@ -187,8 +187,8 @@ class UserCapabilitySetTestCase(SupyTestCase): def testOwner(self): s = ircdb.UserCapabilitySet() s.add('owner') - self.assertTrue('foo' in s) - self.assertTrue('-foo' in s) + self.assertIn('foo', s) + self.assertIn('-foo', s) self.assertTrue(s.check('owner')) self.assertFalse(s.check('-owner')) self.assertFalse(s.check('-foo')) @@ -265,7 +265,7 @@ class IrcUserTestCase(IrcdbTestCase): self.assertTrue(u.checkHostmask('foo!bar@baz')) u.addAuth('foo!bar@baz') self.assertTrue(u.checkHostmask('foo!bar@baz')) - self.assertTrue(len(u.auth) == 1) + self.assertEqual(len(u.auth), 1) u.addAuth('boo!far@fizz') self.assertTrue(u.checkHostmask('boo!far@fizz')) timeFastForward(2.1) diff --git a/test/test_irclib.py b/test/test_irclib.py index 22ef55290..e5d87aad0 100644 --- a/test/test_irclib.py +++ b/test/test_irclib.py @@ -52,21 +52,21 @@ class CapNegMixin: def startCapNegociation(self, caps='sasl'): m = self.irc.takeMsg() - self.assertTrue(m.command == 'CAP', 'Expected CAP, got %r.' % m) - self.assertTrue(m.args == ('LS', '302'), 'Expected CAP LS 302, got %r.' % m) + self.assertEqual(m.command, 'CAP', 'Expected CAP, got %r.' % m) + self.assertEqual(m.args, ('LS', '302'), 'Expected CAP LS 302, got %r.' % m) m = self.irc.takeMsg() - self.assertTrue(m.command == 'NICK', 'Expected NICK, got %r.' % m) + self.assertEqual(m.command, 'NICK', 'Expected NICK, got %r.' % m) m = self.irc.takeMsg() - self.assertTrue(m.command == 'USER', 'Expected USER, got %r.' % m) + self.assertEqual(m.command, 'USER', 'Expected USER, got %r.' % m) self.irc.feedMsg(ircmsgs.IrcMsg(command='CAP', args=('*', 'LS', caps))) if caps: m = self.irc.takeMsg() - self.assertTrue(m.command == 'CAP', 'Expected CAP, got %r.' % m) + self.assertEqual(m.command, 'CAP', 'Expected CAP, got %r.' % m) self.assertEqual(m.args[0], 'REQ', m) self.assertEqual(m.args[1], 'sasl') @@ -75,7 +75,7 @@ class CapNegMixin: def endCapNegociation(self): m = self.irc.takeMsg() - self.assertTrue(m.command == 'CAP', 'Expected CAP, got %r.' % m) + self.assertEqual(m.command, 'CAP', 'Expected CAP, got %r.' % m) self.assertEqual(m.args, ('END',), m) @@ -204,13 +204,13 @@ class IrcMsgQueueTestCase(SupyTestCase): q.enqueue(self.msg) q.enqueue(self.msg) q.enqueue(self.msg) - self.assertTrue(self.msg in q) + self.assertIn(self.msg, q) q.dequeue() - self.assertTrue(self.msg in q) + self.assertIn(self.msg, q) q.dequeue() - self.assertTrue(self.msg in q) + self.assertIn(self.msg, q) q.dequeue() - self.assertFalse(self.msg in q) + self.assertNotIn(self.msg, q) def testRepr(self): q = irclib.IrcMsgQueue() @@ -313,39 +313,39 @@ class ChannelStateTestCase(SupyTestCase): c1 = pickle.loads(pickle.dumps(c)) self.assertEqual(c, c1) c.removeUser('jemfinch') - self.assertFalse('jemfinch' in c.users) - self.assertTrue('jemfinch' in c1.users) + self.assertNotIn('jemfinch', c.users) + self.assertIn('jemfinch', c1.users) def testCopy(self): c = irclib.ChannelState() c.addUser('jemfinch') c1 = copy.deepcopy(c) c.removeUser('jemfinch') - self.assertFalse('jemfinch' in c.users) - self.assertTrue('jemfinch' in c1.users) + self.assertNotIn('jemfinch', c.users) + self.assertIn('jemfinch', c1.users) def testAddUser(self): c = irclib.ChannelState() c.addUser('foo') - self.assertTrue('foo' in c.users) - self.assertFalse('foo' in c.ops) - self.assertFalse('foo' in c.voices) - self.assertFalse('foo' in c.halfops) + self.assertIn('foo', c.users) + self.assertNotIn('foo', c.ops) + self.assertNotIn('foo', c.voices) + self.assertNotIn('foo', c.halfops) c.addUser('+bar') - self.assertTrue('bar' in c.users) - self.assertTrue('bar' in c.voices) - self.assertFalse('bar' in c.ops) - self.assertFalse('bar' in c.halfops) + self.assertIn('bar', c.users) + self.assertIn('bar', c.voices) + self.assertNotIn('bar', c.ops) + self.assertNotIn('bar', c.halfops) c.addUser('%baz') - self.assertTrue('baz' in c.users) - self.assertTrue('baz' in c.halfops) - self.assertFalse('baz' in c.voices) - self.assertFalse('baz' in c.ops) + self.assertIn('baz', c.users) + self.assertIn('baz', c.halfops) + self.assertNotIn('baz', c.voices) + self.assertNotIn('baz', c.ops) c.addUser('@quuz') - self.assertTrue('quuz' in c.users) - self.assertTrue('quuz' in c.ops) - self.assertFalse('quuz' in c.halfops) - self.assertFalse('quuz' in c.voices) + self.assertIn('quuz', c.users) + self.assertIn('quuz', c.ops) + self.assertNotIn('quuz', c.halfops) + self.assertNotIn('quuz', c.voices) class IrcStateTestCase(SupyTestCase): @@ -362,7 +362,7 @@ class IrcStateTestCase(SupyTestCase): st.channels['#foo'] = irclib.ChannelState() m = ircmsgs.kick('#foo', self.irc.nick, prefix=self.irc.prefix) st.addMsg(self.irc, m) - self.assertFalse('#foo' in st.channels) + self.assertNotIn('#foo', st.channels) def testAddMsgRemovesOpsProperly(self): st = irclib.IrcState() @@ -370,18 +370,18 @@ class IrcStateTestCase(SupyTestCase): st.channels['#foo'].ops.add('bar') m = ircmsgs.mode('#foo', ('-o', 'bar')) st.addMsg(self.irc, m) - self.assertFalse('bar' in st.channels['#foo'].ops) + self.assertNotIn('bar', st.channels['#foo'].ops) def testNickChangesChangeChannelUsers(self): st = irclib.IrcState() st.channels['#foo'] = irclib.ChannelState() st.channels['#foo'].addUser('@bar') - self.assertTrue('bar' in st.channels['#foo'].users) + self.assertIn('bar', st.channels['#foo'].users) self.assertTrue(st.channels['#foo'].isOp('bar')) st.addMsg(self.irc, ircmsgs.IrcMsg(':bar!asfd@asdf.com NICK baz')) - self.assertFalse('bar' in st.channels['#foo'].users) + self.assertNotIn('bar', st.channels['#foo'].users) self.assertFalse(st.channels['#foo'].isOp('bar')) - self.assertTrue('baz' in st.channels['#foo'].users) + self.assertIn('baz', st.channels['#foo'].users) self.assertTrue(st.channels['#foo'].isOp('baz')) def testHistory(self): @@ -478,19 +478,19 @@ class IrcStateTestCase(SupyTestCase): state = irclib.IrcState() stateCopy = state.copy() state.channels['#foo'] = None - self.assertFalse('#foo' in stateCopy.channels) + self.assertNotIn('#foo', stateCopy.channels) def testJoin(self): st = irclib.IrcState() st.addMsg(self.irc, ircmsgs.join('#foo', prefix=self.irc.prefix)) - self.assertTrue('#foo' in st.channels) - self.assertTrue(self.irc.nick in st.channels['#foo'].users) + self.assertIn('#foo', st.channels) + self.assertIn(self.irc.nick, st.channels['#foo'].users) st.addMsg(self.irc, ircmsgs.join('#foo', prefix='foo!bar@baz')) - self.assertTrue('foo' in st.channels['#foo'].users) + self.assertIn('foo', st.channels['#foo'].users) st2 = st.copy() st.addMsg(self.irc, ircmsgs.quit(prefix='foo!bar@baz')) - self.assertFalse('foo' in st.channels['#foo'].users) - self.assertTrue('foo' in st2.channels['#foo'].users) + self.assertNotIn('foo', st.channels['#foo'].users) + self.assertIn('foo', st2.channels['#foo'].users) def testEq(self): @@ -508,23 +508,23 @@ class IrcStateTestCase(SupyTestCase): def testHandlesModes(self): st = irclib.IrcState() st.addMsg(self.irc, ircmsgs.join('#foo', prefix=self.irc.prefix)) - self.assertFalse('bar' in st.channels['#foo'].ops) + self.assertNotIn('bar', st.channels['#foo'].ops) st.addMsg(self.irc, ircmsgs.op('#foo', 'bar')) - self.assertTrue('bar' in st.channels['#foo'].ops) + self.assertIn('bar', st.channels['#foo'].ops) st.addMsg(self.irc, ircmsgs.deop('#foo', 'bar')) - self.assertFalse('bar' in st.channels['#foo'].ops) + self.assertNotIn('bar', st.channels['#foo'].ops) - self.assertFalse('bar' in st.channels['#foo'].voices) + self.assertNotIn('bar', st.channels['#foo'].voices) st.addMsg(self.irc, ircmsgs.voice('#foo', 'bar')) - self.assertTrue('bar' in st.channels['#foo'].voices) + self.assertIn('bar', st.channels['#foo'].voices) st.addMsg(self.irc, ircmsgs.devoice('#foo', 'bar')) - self.assertFalse('bar' in st.channels['#foo'].voices) + self.assertNotIn('bar', st.channels['#foo'].voices) - self.assertFalse('bar' in st.channels['#foo'].halfops) + self.assertNotIn('bar', st.channels['#foo'].halfops) st.addMsg(self.irc, ircmsgs.halfop('#foo', 'bar')) - self.assertTrue('bar' in st.channels['#foo'].halfops) + self.assertIn('bar', st.channels['#foo'].halfops) st.addMsg(self.irc, ircmsgs.dehalfop('#foo', 'bar')) - self.assertFalse('bar' in st.channels['#foo'].halfops) + self.assertNotIn('bar', st.channels['#foo'].halfops) def testDoModeOnlyChannels(self): st = irclib.IrcState() @@ -572,14 +572,14 @@ class IrcCapsTestCase(SupyTestCase, CapNegMixin): self.irc = irclib.Irc('test') m = self.irc.takeMsg() - self.assertTrue(m.command == 'CAP', 'Expected CAP, got %r.' % m) - self.assertTrue(m.args == ('LS', '302'), 'Expected CAP LS 302, got %r.' % m) + self.assertEqual(m.command, 'CAP', 'Expected CAP, got %r.' % m) + self.assertEqual(m.args, ('LS', '302'), 'Expected CAP LS 302, got %r.' % m) m = self.irc.takeMsg() - self.assertTrue(m.command == 'NICK', 'Expected NICK, got %r.' % m) + self.assertEqual(m.command, 'NICK', 'Expected NICK, got %r.' % m) m = self.irc.takeMsg() - self.assertTrue(m.command == 'USER', 'Expected USER, got %r.' % m) + self.assertEqual(m.command, 'USER', 'Expected USER, got %r.' % m) self.irc.REQUEST_CAPABILITIES = set(['a'*400, 'b'*400]) caps = ' '.join(self.irc.REQUEST_CAPABILITIES) @@ -589,12 +589,12 @@ class IrcCapsTestCase(SupyTestCase, CapNegMixin): args=('*', 'LS', 'b'*400))) m = self.irc.takeMsg() - self.assertTrue(m.command == 'CAP', 'Expected CAP, got %r.' % m) + self.assertEqual(m.command, 'CAP', 'Expected CAP, got %r.' % m) self.assertEqual(m.args[0], 'REQ', m) self.assertEqual(m.args[1], 'a'*400) m = self.irc.takeMsg() - self.assertTrue(m.command == 'CAP', 'Expected CAP, got %r.' % m) + self.assertEqual(m.command, 'CAP', 'Expected CAP, got %r.' % m) self.assertEqual(m.args[0], 'REQ', m) self.assertEqual(m.args[1], 'b'*400) @@ -602,20 +602,20 @@ class IrcCapsTestCase(SupyTestCase, CapNegMixin): self.irc = irclib.Irc('test') m = self.irc.takeMsg() - self.assertTrue(m.command == 'CAP', 'Expected CAP, got %r.' % m) - self.assertTrue(m.args == ('LS', '302'), 'Expected CAP LS 302, got %r.' % m) + self.assertEqual(m.command, 'CAP', 'Expected CAP, got %r.' % m) + self.assertEqual(m.args, ('LS', '302'), 'Expected CAP LS 302, got %r.' % m) m = self.irc.takeMsg() - self.assertTrue(m.command == 'NICK', 'Expected NICK, got %r.' % m) + self.assertEqual(m.command, 'NICK', 'Expected NICK, got %r.' % m) m = self.irc.takeMsg() - self.assertTrue(m.command == 'USER', 'Expected USER, got %r.' % m) + self.assertEqual(m.command, 'USER', 'Expected USER, got %r.' % m) self.irc.feedMsg(ircmsgs.IrcMsg(command='CAP', args=('*', 'LS', 'account-notify echo-message'))) m = self.irc.takeMsg() - self.assertTrue(m.command == 'CAP', 'Expected CAP, got %r.' % m) + self.assertEqual(m.command, 'CAP', 'Expected CAP, got %r.' % m) self.assertEqual(m.args[0], 'REQ', m) self.assertEqual(m.args[1], 'account-notify') @@ -626,21 +626,21 @@ class IrcCapsTestCase(SupyTestCase, CapNegMixin): args=('*', 'ACK', 'account-notify'))) m = self.irc.takeMsg() - self.assertTrue(m.command == 'CAP', 'Expected CAP, got %r.' % m) + self.assertEqual(m.command, 'CAP', 'Expected CAP, got %r.' % m) self.assertEqual(m.args, ('END',), m) def testEchomessageLabeledresponseGrouped(self): self.irc = irclib.Irc('test') m = self.irc.takeMsg() - self.assertTrue(m.command == 'CAP', 'Expected CAP, got %r.' % m) - self.assertTrue(m.args == ('LS', '302'), 'Expected CAP LS 302, got %r.' % m) + self.assertEqual(m.command, 'CAP', 'Expected CAP, got %r.' % m) + self.assertEqual(m.args, ('LS', '302'), 'Expected CAP LS 302, got %r.' % m) m = self.irc.takeMsg() - self.assertTrue(m.command == 'NICK', 'Expected NICK, got %r.' % m) + self.assertEqual(m.command, 'NICK', 'Expected NICK, got %r.' % m) m = self.irc.takeMsg() - self.assertTrue(m.command == 'USER', 'Expected USER, got %r.' % m) + self.assertEqual(m.command, 'USER', 'Expected USER, got %r.' % m) self.irc.REQUEST_CAPABILITIES = set([ 'account-notify', 'a'*490, 'echo-message', 'labeled-response']) @@ -649,17 +649,17 @@ class IrcCapsTestCase(SupyTestCase, CapNegMixin): 'account-notify ' + 'a'*490 + ' echo-message labeled-response'))) m = self.irc.takeMsg() - self.assertTrue(m.command == 'CAP', 'Expected CAP, got %r.' % m) + self.assertEqual(m.command, 'CAP', 'Expected CAP, got %r.' % m) self.assertEqual(m.args[0], 'REQ', m) self.assertEqual(m.args[1], 'echo-message labeled-response') m = self.irc.takeMsg() - self.assertTrue(m.command == 'CAP', 'Expected CAP, got %r.' % m) + self.assertEqual(m.command, 'CAP', 'Expected CAP, got %r.' % m) self.assertEqual(m.args[0], 'REQ', m) self.assertEqual(m.args[1], 'a'*490) m = self.irc.takeMsg() - self.assertTrue(m.command == 'CAP', 'Expected CAP, got %r.' % m) + self.assertEqual(m.command, 'CAP', 'Expected CAP, got %r.' % m) self.assertEqual(m.args[0], 'REQ', m) self.assertEqual(m.args[1], 'account-notify') @@ -836,14 +836,14 @@ class IrcTestCase(SupyTestCase): #self.assertTrue(m.command == 'PASS', 'Expected PASS, got %r.' % m) m = self.irc.takeMsg() - self.assertTrue(m.command == 'CAP', 'Expected CAP, got %r.' % m) - self.assertTrue(m.args == ('LS', '302'), 'Expected CAP LS 302, got %r.' % m) + self.assertEqual(m.command, 'CAP', 'Expected CAP, got %r.' % m) + self.assertEqual(m.args, ('LS', '302'), 'Expected CAP LS 302, got %r.' % m) m = self.irc.takeMsg() - self.assertTrue(m.command == 'NICK', 'Expected NICK, got %r.' % m) + self.assertEqual(m.command, 'NICK', 'Expected NICK, got %r.' % m) m = self.irc.takeMsg() - self.assertTrue(m.command == 'USER', 'Expected USER, got %r.' % m) + self.assertEqual(m.command, 'USER', 'Expected USER, got %r.' % m) # TODO self.irc.feedMsg(ircmsgs.IrcMsg(command='CAP', @@ -852,7 +852,7 @@ class IrcTestCase(SupyTestCase): args=('*', 'LS', 'extended-join'))) m = self.irc.takeMsg() - self.assertTrue(m.command == 'CAP', 'Expected CAP, got %r.' % m) + self.assertEqual(m.command, 'CAP', 'Expected CAP, got %r.' % m) self.assertEqual(m.args[0], 'REQ', m) # NOTE: Capabilities are requested in alphabetic order, because # sets are unordered, and their "order" is nondeterministic. @@ -862,11 +862,11 @@ class IrcTestCase(SupyTestCase): args=('*', 'ACK', 'account-tag multi-prefix extended-join'))) m = self.irc.takeMsg() - self.assertTrue(m.command == 'CAP', 'Expected CAP, got %r.' % m) + self.assertEqual(m.command, 'CAP', 'Expected CAP, got %r.' % m) self.assertEqual(m.args, ('END',), m) m = self.irc.takeMsg() - self.assertTrue(m is None, m) + self.assertIsNone(m, m) def testPingResponse(self): self.irc.feedMsg(ircmsgs.ping('123')) @@ -890,9 +890,9 @@ class IrcTestCase(SupyTestCase): self.irc.queueMsg(ircmsgs.IrcMsg('NOTICE #foo bar')) self.irc.sendMsg(ircmsgs.IrcMsg('PRIVMSG #foo yeah!')) msg = self.irc.takeMsg() - self.assertTrue(msg.command == 'PRIVMSG') + self.assertEqual(msg.command, 'PRIVMSG') msg = self.irc.takeMsg() - self.assertTrue(msg.command == 'NOTICE') + self.assertEqual(msg.command, 'NOTICE') def testNoMsgLongerThan512(self): self.irc.queueMsg(ircmsgs.privmsg('whocares', 'x'*1000)) @@ -1542,7 +1542,7 @@ class SaslTestCase(SupyTestCase, CapNegMixin): conf.supybot.networks.test.sasl.password.setValue('') m = self.irc.takeMsg() - self.assertTrue(m.command == 'CAP', 'Expected CAP, got %r.' % m) + self.assertEqual(m.command, 'CAP', 'Expected CAP, got %r.' % m) self.assertEqual(m.args[0], 'REQ', m) self.assertEqual(m.args[1], 'sasl') self.irc.feedMsg(ircmsgs.IrcMsg(command='CAP', diff --git a/test/test_ircmsgs.py b/test/test_ircmsgs.py index d81536dd0..6e9d59c0d 100644 --- a/test/test_ircmsgs.py +++ b/test/test_ircmsgs.py @@ -70,7 +70,7 @@ class IrcMsgTestCase(SupyTestCase): def testNe(self): for msg in msgs: - self.assertFalse(msg != msg) + self.assertEqual(msg, msg) ## def testImmutability(self): ## s = 'something else' @@ -99,7 +99,7 @@ class IrcMsgTestCase(SupyTestCase): prefix='foo!bar@baz') m = ircmsgs.IrcMsg(prefix='foo!bar@baz', args=('foo', 'bar'), command='CMD') - self.assertIs(m.time, None) + self.assertIsNone(m.time) m.time = 24 self.assertEqual(ircmsgs.IrcMsg(msg=m).time, 24) diff --git a/test/test_ircutils.py b/test/test_ircutils.py index 65fa2013e..cb62358c8 100644 --- a/test/test_ircutils.py +++ b/test/test_ircutils.py @@ -236,53 +236,53 @@ class FunctionsTestCase(SupyTestCase): s = ('foo bar baz qux ' * 100)[0:-1] r = ircutils.wrap(s, 10) - self.assertTrue(max(map(pred, r)) <= 10) + self.assertLessEqual(max(map(pred, r)), 10) self.assertEqual(''.join(r), s) r = ircutils.wrap(s, 100) - self.assertTrue(max(map(pred, r)) <= 100) + self.assertLessEqual(max(map(pred, r)), 100) self.assertEqual(''.join(r), s) s = (''.join([chr(0x1f527), chr(0x1f527), chr(0x1f527), ' ']) * 100)\ [0:-1] r = ircutils.wrap(s, 20) - self.assertTrue(max(map(pred, r)) <= 20, (max(map(pred, r)), repr(r))) + self.assertLessEqual(max(map(pred, r)), 20, (max(map(pred, r)), repr(r))) self.assertEqual(''.join(r), s) r = ircutils.wrap(s, 100) - self.assertTrue(max(map(pred, r)) <= 100) + self.assertLessEqual(max(map(pred, r)), 100) self.assertEqual(''.join(r), s) s = ('foobarbazqux ' * 100)[0:-1] r = ircutils.wrap(s, 10) - self.assertTrue(max(map(pred, r)) <= 10) + self.assertLessEqual(max(map(pred, r)), 10) self.assertEqual(''.join(r), s) r = ircutils.wrap(s, 100) - self.assertTrue(max(map(pred, r)) <= 100) + self.assertLessEqual(max(map(pred, r)), 100) self.assertEqual(''.join(r), s) s = ('foobarbazqux' * 100)[0:-1] r = ircutils.wrap(s, 10) - self.assertTrue(max(map(pred, r)) <= 10) + self.assertLessEqual(max(map(pred, r)), 10) self.assertEqual(''.join(r), s) r = ircutils.wrap(s, 100) - self.assertTrue(max(map(pred, r)) <= 100) + self.assertLessEqual(max(map(pred, r)), 100) self.assertEqual(''.join(r), s) s = chr(233)*500 r = ircutils.wrap(s, 500) - self.assertTrue(max(map(pred, r)) <= 500) + self.assertLessEqual(max(map(pred, r)), 500) r = ircutils.wrap(s, 139) - self.assertTrue(max(map(pred, r)) <= 139) + self.assertLessEqual(max(map(pred, r)), 139) s = '\x02\x16 barbazqux' + ('foobarbazqux ' * 20)[0:-1] r = ircutils.wrap(s, 91) - self.assertTrue(max(map(pred, r)) <= 91) + self.assertLessEqual(max(map(pred, r)), 91) def testSafeArgument(self): s = 'I have been running for 9 seconds' @@ -453,9 +453,9 @@ class IrcDictTestCase(SupyTestCase): def testContains(self): d = ircutils.IrcDict() d['#FOO'] = None - self.assertTrue('#foo' in d) + self.assertIn('#foo', d) d['#fOOBAR[]'] = None - self.assertTrue('#foobar{}' in d) + self.assertIn('#foobar{}', d) def testGetSetItem(self): d = ircutils.IrcDict() @@ -467,8 +467,8 @@ class IrcDictTestCase(SupyTestCase): def testCopyable(self): d = ircutils.IrcDict() d['foo'] = 'bar' - self.assertTrue(d == copy.copy(d)) - self.assertTrue(d == copy.deepcopy(d)) + self.assertEqual(d, copy.copy(d)) + self.assertEqual(d, copy.deepcopy(d)) class IrcSetTestCase(SupyTestCase): @@ -476,30 +476,30 @@ class IrcSetTestCase(SupyTestCase): s = ircutils.IrcSet() s.add('foo') s.add('bar') - self.assertTrue('foo' in s) - self.assertTrue('FOO' in s) + self.assertIn('foo', s) + self.assertIn('FOO', s) s.discard('alfkj') s.remove('FOo') - self.assertFalse('foo' in s) - self.assertFalse('FOo' in s) + self.assertNotIn('foo', s) + self.assertNotIn('FOo', s) def testCopy(self): s = ircutils.IrcSet() s.add('foo') s.add('bar') s1 = copy.deepcopy(s) - self.assertTrue('foo' in s) - self.assertTrue('FOO' in s) + self.assertIn('foo', s) + self.assertIn('FOO', s) s.discard('alfkj') s.remove('FOo') - self.assertFalse('foo' in s) - self.assertFalse('FOo' in s) - self.assertTrue('foo' in s1) - self.assertTrue('FOO' in s1) + self.assertNotIn('foo', s) + self.assertNotIn('FOo', s) + self.assertIn('foo', s1) + self.assertIn('FOO', s1) s1.discard('alfkj') s1.remove('FOo') - self.assertFalse('foo' in s1) - self.assertFalse('FOo' in s1) + self.assertNotIn('foo', s1) + self.assertNotIn('FOo', s1) class IrcStringTestCase(SupyTestCase): @@ -514,8 +514,8 @@ class IrcStringTestCase(SupyTestCase): def testInequality(self): s1 = 'supybot' s2 = ircutils.IrcString('Supybot') - self.assertTrue(s1 == s2) - self.assertFalse(s1 != s2) + self.assertEqual(s1, s2) + self.assertEqual(s1, s2) class AuthenticateTestCase(SupyTestCase): PAIRS = [ diff --git a/test/test_standardSubstitute.py b/test/test_standardSubstitute.py index c64b495c8..3a032e1a8 100644 --- a/test/test_standardSubstitute.py +++ b/test/test_standardSubstitute.py @@ -74,9 +74,9 @@ class FunctionsTestCase(SupyTestCase): self.assertNotEqual(f(irc, msg, '$today'), '$today') self.assertNotEqual(f(irc, msg, '$now'), '$now') n = f(irc, msg, '$randnick') - self.assertTrue(n in irc.state.channels['#foo'].users) + self.assertIn(n, irc.state.channels['#foo'].users) n = f(irc, msg, '$randomnick') - self.assertTrue(n in irc.state.channels['#foo'].users) + self.assertIn(n, irc.state.channels['#foo'].users) n = f(irc, msg, '$randomnick '*100) L = n.split() self.assertFalse(all(L[0].__eq__, L), 'all $randomnicks were the same') diff --git a/test/test_utils.py b/test/test_utils.py index 5e8be3c8b..fdefea65e 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -74,7 +74,7 @@ class GenTest(SupyTestCase): def testInsensitivePreservingDict(self): ipd = utils.InsensitivePreservingDict d = ipd(dict(Foo=10)) - self.assertTrue(d['foo'] == 10) + self.assertEqual(d['foo'], 10) self.assertEqual(d.keys(), ['Foo']) self.assertEqual(d.get('foo'), 10) self.assertEqual(d.get('Foo'), 10) @@ -399,7 +399,7 @@ class StrTest(SupyTestCase): def testEllipsisify(self): f = utils.str.ellipsisify self.assertEqual(f('x'*30, 30), 'x'*30) - self.assertTrue(len(f('x'*35, 30)) <= 30) + self.assertLessEqual(len(f('x'*35, 30)), 30) self.assertTrue(f(' '.join(['xxxx']*10), 30)[:-3].endswith('xxxx')) @@ -553,7 +553,7 @@ class WebTest(SupyTestCase): if network: def testGetUrlWithSize(self): url = 'http://slashdot.org/' - self.assertTrue(len(utils.web.getUrl(url, 1024)) == 1024) + self.assertEqual(len(utils.web.getUrl(url, 1024)), 1024) class FormatTestCase(SupyTestCase): def testNormal(self): @@ -619,10 +619,10 @@ class RingBufferTestCase(SupyTestCase): def testContains(self): b = RingBuffer(3, range(3)) - self.assertTrue(0 in b) - self.assertTrue(1 in b) - self.assertTrue(2 in b) - self.assertFalse(3 in b) + self.assertIn(0, b) + self.assertIn(1, b) + self.assertIn(2, b) + self.assertNotIn(3, b) def testGetitem(self): L = range(10) @@ -711,18 +711,18 @@ class RingBufferTestCase(SupyTestCase): def testEq(self): b = RingBuffer(3, range(3)) - self.assertFalse(b == list(range(3))) + self.assertNotEqual(b, list(range(3))) b1 = RingBuffer(3) - self.assertFalse(b == b1) + self.assertNotEqual(b, b1) b1.append(0) - self.assertFalse(b == b1) + self.assertNotEqual(b, b1) b1.append(1) - self.assertFalse(b == b1) + self.assertNotEqual(b, b1) b1.append(2) - self.assertTrue(b == b1) + self.assertEqual(b, b1) b = RingBuffer(100, range(10)) b1 = RingBuffer(10, range(10)) - self.assertFalse(b == b1) + self.assertNotEqual(b, b1) def testIter(self): b = RingBuffer(3, range(3)) @@ -799,24 +799,24 @@ class QueueTest(SupyTestCase): def testEq(self): q1 = queue() q2 = queue() - self.assertTrue(q1 == q1, 'queue not equal to itself') - self.assertTrue(q2 == q2, 'queue not equal to itself') - self.assertTrue(q1 == q2, 'initialized queues not equal') + self.assertEqual(q1, q1, 'queue not equal to itself') + self.assertEqual(q2, q2, 'queue not equal to itself') + self.assertEqual(q1, q2, 'initialized queues not equal') q1.enqueue(1) - self.assertTrue(q1 == q1, 'queue not equal to itself') - self.assertTrue(q2 == q2, 'queue not equal to itself') + self.assertEqual(q1, q1, 'queue not equal to itself') + self.assertEqual(q2, q2, 'queue not equal to itself') q2.enqueue(1) - self.assertTrue(q1 == q1, 'queue not equal to itself') - self.assertTrue(q2 == q2, 'queue not equal to itself') - self.assertTrue(q1 == q2, 'queues not equal after identical enqueue') + self.assertEqual(q1, q1, 'queue not equal to itself') + self.assertEqual(q2, q2, 'queue not equal to itself') + self.assertEqual(q1, q2, 'queues not equal after identical enqueue') q1.dequeue() - self.assertTrue(q1 == q1, 'queue not equal to itself') - self.assertTrue(q2 == q2, 'queue not equal to itself') - self.assertFalse(q1 == q2, 'queues equal after one dequeue') + self.assertEqual(q1, q1, 'queue not equal to itself') + self.assertEqual(q2, q2, 'queue not equal to itself') + self.assertNotEqual(q1, q2, 'queues equal after one dequeue') q2.dequeue() - self.assertTrue(q1 == q2, 'queues not equal after both are dequeued') - self.assertTrue(q1 == q1, 'queue not equal to itself') - self.assertTrue(q2 == q2, 'queue not equal to itself') + self.assertEqual(q1, q2, 'queues not equal after both are dequeued') + self.assertEqual(q1, q1, 'queue not equal to itself') + self.assertEqual(q2, q2, 'queue not equal to itself') def testInit(self): self.assertEqual(len(queue()), 0, 'queue len not 0 after init') @@ -876,17 +876,17 @@ class QueueTest(SupyTestCase): def testContains(self): q = queue() - self.assertFalse(1 in q, 'empty queue cannot have elements') + self.assertNotIn(1, q, 'empty queue cannot have elements') q.enqueue(1) - self.assertTrue(1 in q, 'recent enqueued element not in q') + self.assertIn(1, q, 'recent enqueued element not in q') q.enqueue(2) - self.assertTrue(1 in q, 'original enqueued element not in q') - self.assertTrue(2 in q, 'second enqueued element not in q') + self.assertIn(1, q, 'original enqueued element not in q') + self.assertIn(2, q, 'second enqueued element not in q') q.dequeue() - self.assertFalse(1 in q, 'dequeued element in q') - self.assertTrue(2 in q, 'not dequeued element not in q') + self.assertNotIn(1, q, 'dequeued element in q') + self.assertIn(2, q, 'not dequeued element not in q') q.dequeue() - self.assertFalse(2 in q, 'dequeued element in q') + self.assertNotIn(2, q, 'dequeued element in q') def testIter(self): q1 = queue((1, 2, 3)) @@ -964,24 +964,24 @@ class SmallQueueTest(SupyTestCase): def testEq(self): q1 = queue() q2 = queue() - self.assertTrue(q1 == q1, 'queue not equal to itself') - self.assertTrue(q2 == q2, 'queue not equal to itself') - self.assertTrue(q1 == q2, 'initialized queues not equal') + self.assertEqual(q1, q1, 'queue not equal to itself') + self.assertEqual(q2, q2, 'queue not equal to itself') + self.assertEqual(q1, q2, 'initialized queues not equal') q1.enqueue(1) - self.assertTrue(q1 == q1, 'queue not equal to itself') - self.assertTrue(q2 == q2, 'queue not equal to itself') + self.assertEqual(q1, q1, 'queue not equal to itself') + self.assertEqual(q2, q2, 'queue not equal to itself') q2.enqueue(1) - self.assertTrue(q1 == q1, 'queue not equal to itself') - self.assertTrue(q2 == q2, 'queue not equal to itself') - self.assertTrue(q1 == q2, 'queues not equal after identical enqueue') + self.assertEqual(q1, q1, 'queue not equal to itself') + self.assertEqual(q2, q2, 'queue not equal to itself') + self.assertEqual(q1, q2, 'queues not equal after identical enqueue') q1.dequeue() - self.assertTrue(q1 == q1, 'queue not equal to itself') - self.assertTrue(q2 == q2, 'queue not equal to itself') - self.assertFalse(q1 == q2, 'queues equal after one dequeue') + self.assertEqual(q1, q1, 'queue not equal to itself') + self.assertEqual(q2, q2, 'queue not equal to itself') + self.assertNotEqual(q1, q2, 'queues equal after one dequeue') q2.dequeue() - self.assertTrue(q1 == q2, 'queues not equal after both are dequeued') - self.assertTrue(q1 == q1, 'queue not equal to itself') - self.assertTrue(q2 == q2, 'queue not equal to itself') + self.assertEqual(q1, q2, 'queues not equal after both are dequeued') + self.assertEqual(q1, q1, 'queue not equal to itself') + self.assertEqual(q2, q2, 'queue not equal to itself') def testInit(self): self.assertEqual(len(queue()), 0, 'queue len not 0 after init') @@ -1041,17 +1041,17 @@ class SmallQueueTest(SupyTestCase): def testContains(self): q = queue() - self.assertFalse(1 in q, 'empty queue cannot have elements') + self.assertNotIn(1, q, 'empty queue cannot have elements') q.enqueue(1) - self.assertTrue(1 in q, 'recent enqueued element not in q') + self.assertIn(1, q, 'recent enqueued element not in q') q.enqueue(2) - self.assertTrue(1 in q, 'original enqueued element not in q') - self.assertTrue(2 in q, 'second enqueued element not in q') + self.assertIn(1, q, 'original enqueued element not in q') + self.assertIn(2, q, 'second enqueued element not in q') q.dequeue() - self.assertFalse(1 in q, 'dequeued element in q') - self.assertTrue(2 in q, 'not dequeued element not in q') + self.assertNotIn(1, q, 'dequeued element in q') + self.assertIn(2, q, 'not dequeued element not in q') q.dequeue() - self.assertFalse(2 in q, 'dequeued element in q') + self.assertNotIn(2, q, 'dequeued element in q') def testIter(self): q1 = queue((1, 2, 3)) @@ -1092,28 +1092,28 @@ class MaxLengthQueueTestCase(SupyTestCase): class TwoWayDictionaryTestCase(SupyTestCase): def testInit(self): d = TwoWayDictionary(foo='bar') - self.assertTrue('foo' in d) - self.assertTrue('bar' in d) + self.assertIn('foo', d) + self.assertIn('bar', d) d = TwoWayDictionary({1: 2}) - self.assertTrue(1 in d) - self.assertTrue(2 in d) + self.assertIn(1, d) + self.assertIn(2, d) def testSetitem(self): d = TwoWayDictionary() d['foo'] = 'bar' - self.assertTrue('foo' in d) - self.assertTrue('bar' in d) + self.assertIn('foo', d) + self.assertIn('bar', d) def testDelitem(self): d = TwoWayDictionary(foo='bar') del d['foo'] - self.assertFalse('foo' in d) - self.assertFalse('bar' in d) + self.assertNotIn('foo', d) + self.assertNotIn('bar', d) d = TwoWayDictionary(foo='bar') del d['bar'] - self.assertFalse('bar' in d) - self.assertFalse('foo' in d) + self.assertNotIn('bar', d) + self.assertNotIn('foo', d) class TestTimeoutQueue(SupyTestCase): @@ -1146,11 +1146,11 @@ class TestTimeoutQueue(SupyTestCase): def testContains(self): q = TimeoutQueue(1) q.enqueue(1) - self.assertTrue(1 in q) - self.assertTrue(1 in q) # For some reason, the second one might fail. - self.assertFalse(2 in q) + self.assertIn(1, q) + self.assertIn(1, q) # For some reason, the second one might fail. + self.assertNotIn(2, q) timeFastForward(1.1) - self.assertFalse(1 in q) + self.assertNotIn(1, q) def testIter(self): q = TimeoutQueue(1) @@ -1178,9 +1178,9 @@ class TestTimeoutQueue(SupyTestCase): def testReset(self): q = TimeoutQueue(10) q.enqueue(1) - self.assertTrue(1 in q) + self.assertIn(1, q) q.reset() - self.assertFalse(1 in q) + self.assertNotIn(1, q) def testClean(self): def iter_and_next(q): @@ -1213,9 +1213,9 @@ class TestCacheDict(SupyTestCase): d = CacheDict(10) for i in range(max**2): d[i] = i - self.assertTrue(len(d) <= max) - self.assertTrue(i in d) - self.assertTrue(d[i] == i) + self.assertLessEqual(len(d), max) + self.assertIn(i, d) + self.assertEqual(d[i], i) class TestExpiringDict(SupyTestCase): def testInit(self): @@ -1313,14 +1313,14 @@ class TestTruncatableSet(SupyTestCase): def testBasics(self): s = TruncatableSet(['foo', 'bar', 'baz', 'qux']) self.assertEqual(s, set(['foo', 'bar', 'baz', 'qux'])) - self.assertTrue('foo' in s) - self.assertTrue('bar' in s) - self.assertFalse('quux' in s) + self.assertIn('foo', s) + self.assertIn('bar', s) + self.assertNotIn('quux', s) s.discard('baz') - self.assertTrue('foo' in s) - self.assertFalse('baz' in s) + self.assertIn('foo', s) + self.assertNotIn('baz', s) s.add('quux') - self.assertTrue('quux' in s) + self.assertIn('quux', s) def testTruncate(self): s = TruncatableSet(['foo', 'bar']) From 985ca23f710bfd7b3177d240ef9294c1fb6fe6fa Mon Sep 17 00:00:00 2001 From: Valentin Lorentz <progval+git@progval.net> Date: Sun, 20 Nov 2022 20:08:04 +0100 Subject: [PATCH 54/71] Add tests for nicksToHostmasks --- src/irclib.py | 2 +- test/messages.pot | 17 ++++++++ test/test_irclib.py | 94 +++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 108 insertions(+), 5 deletions(-) create mode 100644 test/messages.pot diff --git a/src/irclib.py b/src/irclib.py index 273681d5b..d6918c824 100644 --- a/src/irclib.py +++ b/src/irclib.py @@ -676,7 +676,7 @@ class IrcState(IrcCommandDispatcher, log.Firewalled): :type: ircutils.IrcDict[str, ChannelState] - .. attribute:: nickToHostmask + .. attribute:: nicksToHostmasks Stores the last hostmask of a seen nick. diff --git a/test/messages.pot b/test/messages.pot new file mode 100644 index 000000000..baa2f8587 --- /dev/null +++ b/test/messages.pot @@ -0,0 +1,17 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR ORGANIZATION +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"POT-Creation-Date: 2022-02-05 23:49+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: pygettext.py 1.5\n" + + diff --git a/test/test_irclib.py b/test/test_irclib.py index e5d87aad0..fdad88159 100644 --- a/test/test_irclib.py +++ b/test/test_irclib.py @@ -482,16 +482,102 @@ class IrcStateTestCase(SupyTestCase): def testJoin(self): st = irclib.IrcState() + st.addMsg(self.irc, ircmsgs.join('#foo', prefix=self.irc.prefix)) self.assertIn('#foo', st.channels) self.assertIn(self.irc.nick, st.channels['#foo'].users) + st.addMsg(self.irc, ircmsgs.join('#foo', prefix='foo!bar@baz')) self.assertIn('foo', st.channels['#foo'].users) - st2 = st.copy() - st.addMsg(self.irc, ircmsgs.quit(prefix='foo!bar@baz')) - self.assertNotIn('foo', st.channels['#foo'].users) - self.assertIn('foo', st2.channels['#foo'].users) + st2 = st.copy() + st2.addMsg(self.irc, ircmsgs.quit(prefix='foo!bar@baz')) + self.assertNotIn('foo', st2.channels['#foo'].users) + self.assertIn('foo', st.channels['#foo'].users) + + def testNickToHostmask(self): + st = irclib.IrcState() + + st.addMsg(self.irc, ircmsgs.join('#foo', prefix='foo!bar@baz')) + st.addMsg(self.irc, ircmsgs.join('#foo', prefix='bar!baz@qux')) + self.assertEqual(st.nickToHostmask('foo'), 'foo!bar@baz') + self.assertEqual(st.nickToHostmask('bar'), 'bar!baz@qux') + + # QUIT erases the entry + with self.subTest("QUIT"): + st2 = st.copy() + st2.addMsg(self.irc, ircmsgs.quit(prefix='foo!bar@baz')) + with self.assertRaises(KeyError): + st2.nickToHostmask('foo') + self.assertEqual(st2.nickToHostmask('bar'), 'bar!baz@qux') + self.assertEqual(st.nickToHostmask('foo'), 'foo!bar@baz') + self.assertEqual(st.nickToHostmask('bar'), 'bar!baz@qux') + + # NICK moves the entry + with self.subTest("NICK"): + st2 = st.copy() + st2.addMsg(self.irc, ircmsgs.IrcMsg(prefix='foo!bar@baz', + command='NICK', args=['foo2'])) + with self.assertRaises(KeyError): + st2.nickToHostmask('foo') + self.assertEqual(st2.nickToHostmask('foo2'), 'foo2!bar@baz') + self.assertEqual(st2.nickToHostmask('bar'), 'bar!baz@qux') + self.assertEqual(st.nickToHostmask('foo'), 'foo!bar@baz') + self.assertEqual(st.nickToHostmask('bar'), 'bar!baz@qux') + + # NICK moves the entry (and overwrites if needed) + with self.subTest("NICK with overwrite"): + st2 = st.copy() + st2.addMsg(self.irc, ircmsgs.IrcMsg(prefix='foo!bar@baz', + command='NICK', args=['bar'])) + with self.assertRaises(KeyError): + st2.nickToHostmask('foo') + self.assertEqual(st2.nickToHostmask('bar'), 'bar!bar@baz') + self.assertEqual(st.nickToHostmask('foo'), 'foo!bar@baz') + self.assertEqual(st.nickToHostmask('bar'), 'bar!baz@qux') + + with self.subTest("PRIVMSG"): + st2 = st.copy() + st2.addMsg(self.irc, ircmsgs.IrcMsg(prefix='foo!bar2@baz2', + command='PRIVMSG', + args=['#chan', 'foo'])) + self.assertEqual(st2.nickToHostmask('foo'), 'foo!bar2@baz2') + self.assertEqual(st2.nickToHostmask('bar'), 'bar!baz@qux') + self.assertEqual(st.nickToHostmask('foo'), 'foo!bar@baz') + self.assertEqual(st.nickToHostmask('bar'), 'bar!baz@qux') + + with self.subTest("PRIVMSG with no host is ignored"): + st2 = st.copy() + st2.addMsg(self.irc, ircmsgs.IrcMsg(prefix='foo', + command='PRIVMSG', + args=['#chan', 'foo'])) + self.assertEqual(st2.nickToHostmask('foo'), 'foo!bar@baz') + self.assertEqual(st2.nickToHostmask('bar'), 'bar!baz@qux') + self.assertEqual(st.nickToHostmask('foo'), 'foo!bar@baz') + self.assertEqual(st.nickToHostmask('bar'), 'bar!baz@qux') + + def testNickToHostmaskWho(self): + st = irclib.IrcState() + + st.addMsg(self.irc, ircmsgs.IrcMsg(command='352', # RPL_WHOREPLY + args=[self.irc.nick, '#chan', 'bar', 'baz', 'server.example', + 'foo', 'H', '0 real name'])) + self.assertEqual(st.nickToHostmask('foo'), 'foo!bar@baz') + + def testNickToHostmaskWhox(self): + st = irclib.IrcState() + + st.addMsg(self.irc, ircmsgs.IrcMsg(command='354', # RPL_WHOSPCRPL + args=[self.irc.nick, '1', 'bar', '127.0.0.1', 'baz', + 'foo', 'H', '0', 'real name'])) + self.assertEqual(st.nickToHostmask('foo'), 'foo!bar@baz') + + def testChghost(self): + st = irclib.IrcState() + + st.addMsg(self.irc, ircmsgs.IrcMsg(prefix='foo!bar@baz', + command='CHGHOST', args=['bar2', 'baz2'])) + self.assertEqual(st.nickToHostmask('foo'), 'foo!bar2@baz2') def testEq(self): state1 = irclib.IrcState() From e6c4da0fff624c6a0c612831b35855f757d44a39 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz <progval+git@progval.net> Date: Wed, 23 Nov 2022 18:20:44 +0100 Subject: [PATCH 55/71] Channel: Fix and re-enable disabled tests --- plugins/Channel/test.py | 81 +++++++++++++++++++++++------------------ 1 file changed, 46 insertions(+), 35 deletions(-) diff --git a/plugins/Channel/test.py b/plugins/Channel/test.py index ee0ec25ce..4295ef081 100644 --- a/plugins/Channel/test.py +++ b/plugins/Channel/test.py @@ -46,19 +46,19 @@ class ChannelTestCase(ChannelPluginTestCase): def testLobotomies(self): self.assertRegexp('lobotomy list', 'not.*any') -## def testCapabilities(self): -## self.prefix = 'foo!bar@baz' -## self.irc.feedMsg(ircmsgs.privmsg(self.irc.nick, 'register foo bar', -## prefix=self.prefix)) -## u = ircdb.users.getUser(0) -## u.addCapability('%s.op' % self.channel) -## ircdb.users.setUser(u) -## self.assertNotError(' ') -## self.assertResponse('user capabilities foo', '[]') -## self.assertNotError('channel addcapability foo op') -## self.assertRegexp('channel capabilities foo', 'op') -## self.assertNotError('channel removecapability foo op') -## self.assertResponse('user capabilities foo', '[]') + def testCapabilities(self): + self.prefix = 'foo!bar@baz' + self.irc.feedMsg(ircmsgs.privmsg(self.irc.nick, 'register foo bar', + prefix=self.prefix)) + u = ircdb.users.getUser(0) + u.addCapability('%s.op' % self.channel) + ircdb.users.setUser(u) + self.assertNotError(' ') + self.assertResponse('user capabilities foo', '[]') + self.assertNotError('channel addcapability foo op') + self.assertRegexp('channel capabilities foo', 'op') + self.assertNotError('channel removecapability foo op') + self.assertResponse('user capabilities foo', '[]') def testCapabilities(self): self.assertNotError('channel capability list') @@ -185,28 +185,39 @@ class ChannelTestCase(ChannelPluginTestCase): self.assertBan('iban $a:nyuszika7h', '$a:nyuszika7h') self.assertNotError('unban $a:nyuszika7h') -## def testKban(self): -## self.irc.prefix = 'something!else@somehwere.else' -## self.irc.nick = 'something' -## self.irc.feedMsg(ircmsgs.join(self.channel, -## prefix='foobar!user@host.domain.tld')) -## self.assertError('kban foobar') -## self.irc.feedMsg(ircmsgs.op(self.channel, self.irc.nick)) -## self.assertError('kban foobar -1') -## self.assertKban('kban foobar', '*!*@*.domain.tld') -## self.assertKban('kban --exact foobar', 'foobar!user@host.domain.tld') -## self.assertKban('kban --host foobar', '*!*@host.domain.tld') -## self.assertKban('kban --user foobar', '*!user@*') -## self.assertKban('kban --nick foobar', 'foobar!*@*') -## self.assertKban('kban --nick --user foobar', 'foobar!user@*') -## self.assertKban('kban --nick --host foobar', -## 'foobar!*@host.domain.tld') -## self.assertKban('kban --user --host foobar', '*!user@host.domain.tld') -## self.assertKban('kban --nick --user --host foobar', -## 'foobar!user@host.domain.tld') -## self.assertNotRegexp('kban adlkfajsdlfkjsd', 'KeyError') -## self.assertNotRegexp('kban foobar time', 'ValueError') -## self.assertError('kban %s' % self.irc.nick) + def testKban(self): + self.irc.prefix = 'something!else@somehwere.else' + self.irc.nick = 'something' + def join(): + self.irc.feedMsg(ircmsgs.join( + self.channel, prefix='foobar!user@host.domain.tld')) + join() + self.assertError('kban foobar') + self.irc.feedMsg(ircmsgs.op(self.channel, self.irc.nick)) + #self.assertError('kban foobar -1') + #self.assertKban('kban foobar', '*!*@*.domain.tld') + #join() + self.assertKban('kban --exact foobar', 'foobar!user@host.domain.tld') + join() + self.assertKban('kban --host foobar', '*!*@host.domain.tld') + join() + self.assertKban('kban --user foobar', '*!user@*') + join() + self.assertKban('kban --nick foobar', 'foobar!*@*') + join() + self.assertKban('kban --nick --user foobar', 'foobar!user@*') + join() + self.assertKban('kban --nick --host foobar', + 'foobar!*@host.domain.tld') + join() + self.assertKban('kban --user --host foobar', '*!user@host.domain.tld') + join() + self.assertKban('kban --nick --user --host foobar', + 'foobar!user@host.domain.tld') + join() + self.assertKban('kban foobar', '*!*@host.domain.tld') + + self.assertRegexp('kban adlkfajsdlfkjsd', 'adlkfajsdlfkjsd is not in') def testBan(self): with conf.supybot.protocols.irc.banmask.context(['exact']): From 1a7c14f4b3a09cdba7e4cd473a5b3a36a5612b9d Mon Sep 17 00:00:00 2001 From: Valentin Lorentz <progval+git@progval.net> Date: Sat, 26 Nov 2022 09:06:47 +0100 Subject: [PATCH 56/71] Web: Decode using the charset advertized in response headers And fall back to the sniffing when not present --- plugins/Web/plugin.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/plugins/Web/plugin.py b/plugins/Web/plugin.py index a0c91d7df..a3e258e4d 100644 --- a/plugins/Web/plugin.py +++ b/plugins/Web/plugin.py @@ -164,8 +164,17 @@ class Web(callbacks.PluginRegexp): timeout = self.registryValue('timeout') headers = conf.defaultHttpHeaders(irc.network, msg.channel) try: - (target, text) = utils.web.getUrlTargetAndContent(url, size=size, - timeout=timeout, headers=headers) + fd = utils.web.getUrlFd(url, timeout=timeout, headers=headers) + target = fd.geturl() + text = fd.read(size) + response_headers = fd.headers + fd.close() + except socket.timeout: + if raiseErrors: + irc.error(_('Connection to %s timed out') % url, Raise=True) + else: + selg.log.info('Web plugins TitleSnarfer: URL <%s> timed out', + url) except Exception as e: if raiseErrors: irc.error(_('That URL raised <' + str(e)) + '>', @@ -174,9 +183,19 @@ class Web(callbacks.PluginRegexp): self.log.info('Web plugin TitleSnarfer: URL <%s> raised <%s>', url, str(e)) return + + encoding = None + if 'Content-Type' in fd.headers: + mime_params = [p.split('=', 1) + for p in fd.headers['Content-Type'].split(';')[1:]] + mime_params = {k.strip(): v.strip() for (k, v) in mime_params} + if mime_params.get('charset'): + encoding = mime_params['charset'] + + encoding = encoding or utils.web.getEncoding(text) or 'utf8' + try: - text = text.decode(utils.web.getEncoding(text) or 'utf8', - 'replace') + text = text.decode(encoding, 'replace') except UnicodeDecodeError: if minisix.PY3: if raiseErrors: From d372d55c05e22a91e3a6f5ff27f67941b55ff0ea Mon Sep 17 00:00:00 2001 From: Valentin Lorentz <progval+git@progval.net> Date: Wed, 21 Dec 2022 21:55:35 +0100 Subject: [PATCH 57/71] ci: Make Ubuntu versions explicit Github just migrated us to Ubuntu 22.04, but it can't run Python 3.6 on it --- .github/workflows/test.yml | 44 ++++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a0aaacf78..c8429e592 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,15 +10,45 @@ jobs: runs-on: ${{ matrix.runs-on }} strategy: matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11.0-rc.2", "pypy-3.6", "pypy-3.7"] - with-opt-deps: [false, true] - runs-on: [ubuntu-latest] - exclude: - # Some of the dependencies don't work on old Python versions + include: + - python-version: "3.11.0-rc.2" + with-opt-deps: true + runs-on: ubuntu-22.04 + + - python-version: "3.10" + with-opt-deps: true + runs-on: ubuntu-22.04 + - python-version: "3.10" + with-opt-deps: false + runs-on: ubuntu-22.04 + + - python-version: "3.9" + with-opt-deps: true + runs-on: ubuntu-22.04 + + - python-version: "3.8" + with-opt-deps: true + runs-on: ubuntu-22.04 + + - python-version: "3.7" + with-opt-deps: true + runs-on: ubuntu-22.04 + - python-version: "3.7" + with-opt-deps: false + runs-on: ubuntu-22.04 + - python-version: "pypy-3.7" + with-opt-deps: true + runs-on: ubuntu-22.04 + - python-version: "pypy-3.7" + with-opt-deps: false + runs-on: ubuntu-22.04 + - python-version: "3.6" - with-opt-deps: true + with-opt-deps: false + runs-on: ubuntu-20.04 - python-version: "pypy-3.6" - with-opt-deps: true + with-opt-deps: false + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 From 21a2ace7a1410cf1c449a00b4d18d18c3946f18c Mon Sep 17 00:00:00 2001 From: James Lu <james@overdrivenetworks.com> Date: Sat, 9 Jul 2022 13:39:44 -0700 Subject: [PATCH 58/71] Services: allow adjusting GHOST command Anope 2.x has renamed this to /ns recover Closes GH-1510 --- plugins/Services/config.py | 4 ++++ plugins/Services/plugin.py | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/Services/config.py b/plugins/Services/config.py index 3c324812d..04fa08ffe 100644 --- a/plugins/Services/config.py +++ b/plugins/Services/config.py @@ -86,6 +86,10 @@ conf.registerNetworkValue(Services, 'noJoinsUntilIdentified', conf.registerNetworkValue(Services, 'ghostDelay', registry.NonNegativeInteger(60, _("""Determines how many seconds the bot will wait between successive GHOST attempts. Set this to 0 to disable GHOST."""))) +conf.registerNetworkValue(Services, 'ghostCommand', + registry.String("GHOST", _("""Determines the NickServ command to use for GHOST. If the network + you're using runs Anope, set this to "RECOVER". If the network you're using runs Atheme, + set this to "GHOST" or "REGAIN"."""))) conf.registerNetworkValue(Services, 'NickServ', ValidNickOrEmptyString('NickServ', _("""Determines what nick the 'NickServ' service has."""))) diff --git a/plugins/Services/plugin.py b/plugins/Services/plugin.py index 799ac5771..3087804b2 100644 --- a/plugins/Services/plugin.py +++ b/plugins/Services/plugin.py @@ -162,7 +162,8 @@ class Services(callbacks.Plugin): else: self.log.info('Sending ghost (current nick: %s; ghosting: %s)', irc.nick, nick) - ghost = 'GHOST %s %s' % (nick, password) + ghostCommand = self.registryValue('ghostCommand', network=irc.network) + ghost = '%s %s %s' % (ghostCommand, nick, password) # Ditto about the sendMsg (see _doIdentify). irc.sendMsg(ircmsgs.privmsg(nickserv, ghost)) state.sentGhost = time.time() @@ -297,7 +298,7 @@ class Services(callbacks.Plugin): elif irc.isChannel(msg.args[0]): # Atheme uses channel-wide notices for alerting channel access # changes if the FANTASY or VERBOSE setting is on; we can suppress - # these 'unexpected notice' warnings since they're not really + # these 'unexpected notice' warnings since they're not really # important. pass else: From 501770e544aca3e112ae86fc436366dd31f4121e Mon Sep 17 00:00:00 2001 From: Valentin Lorentz <progval+git@progval.net> Date: Wed, 28 Dec 2022 14:54:48 +0100 Subject: [PATCH 59/71] Fediverse: Add support for missing host-meta document --- plugins/Fediverse/activitypub.py | 11 ++++------- plugins/Fediverse/test.py | 13 +++++++++++++ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/plugins/Fediverse/activitypub.py b/plugins/Fediverse/activitypub.py index 0d062068a..511551c3b 100644 --- a/plugins/Fediverse/activitypub.py +++ b/plugins/Fediverse/activitypub.py @@ -61,10 +61,6 @@ class ProtocolError(ActivityPubError): pass -class HostmetaError(ProtocolError): - pass - - class ActivityPubProtocolError(ActivityPubError): pass @@ -118,7 +114,7 @@ def convert_exceptions(to_class, msg="", from_none=False): @sandbox def _get_webfinger_url(hostname): - with convert_exceptions(HostmetaError): + try: doc = ET.fromstring( web.getUrlContent("https://%s/.well-known/host-meta" % hostname) ) @@ -126,8 +122,9 @@ def _get_webfinger_url(hostname): for link in doc.iter(XRD_URI + "Link"): if link.attrib["rel"] == "lrdd": return link.attrib["template"] - - return "https://%s/.well-known/webfinger?resource={uri}" + except web.Error: + # Fall back to the default Webfinger URL + return "https://%s/.well-known/webfinger?resource={uri}" % hostname def has_webfinger_support(hostname): diff --git a/plugins/Fediverse/test.py b/plugins/Fediverse/test.py index d06c0ead7..a6046d346 100644 --- a/plugins/Fediverse/test.py +++ b/plugins/Fediverse/test.py @@ -255,6 +255,19 @@ class NetworklessFediverseTestCase(BaseFediverseTestCase): "\x02someuser\x02 (@someuser@example.org): My Biography", ) + def testProfileNoHostmeta(self): + expected_requests = [ + (HOSTMETA_URL, utils.web.Error("blah")), + (WEBFINGER_URL, WEBFINGER_DATA), + (ACTOR_URL, ACTOR_DATA), + ] + + with self.mockRequests(expected_requests): + self.assertResponse( + "profile @someuser@example.org", + "\x02someuser\x02 (@someuser@example.org): My Biography", + ) + def testProfileSnarfer(self): with self.mockWebfingerSupport("not called"), self.mockRequests([]): self.assertSnarfNoResponse("aaa @nonexistinguser@example.org bbb") From 64b1469a239eec9cdb6703f2b8cfb1e86e453806 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz <progval+git@progval.net> Date: Wed, 21 Dec 2022 22:01:32 +0100 Subject: [PATCH 60/71] ci: Bump Python versions --- .github/workflows/test.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c8429e592..5062a2c24 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,6 +4,9 @@ on: push: pull_request: +# Sources of supported versions: +# * https://github.com/actions/python-versions/blob/main/versions-manifest.json +# * https://downloads.python.org/pypy/versions.json jobs: build: @@ -11,7 +14,11 @@ jobs: strategy: matrix: include: - - python-version: "3.11.0-rc.2" + - python-version: "3.12.0-alpha.3" + with-opt-deps: true + runs-on: ubuntu-22.04 + + - python-version: "3.11" with-opt-deps: true runs-on: ubuntu-22.04 @@ -25,6 +32,9 @@ jobs: - python-version: "3.9" with-opt-deps: true runs-on: ubuntu-22.04 + - python-version: "pypy-3.9" + with-opt-deps: true + runs-on: ubuntu-22.04 - python-version: "3.8" with-opt-deps: true From ef960befa3409c4465b26b7315dcc564abf8bccd Mon Sep 17 00:00:00 2001 From: Valentin Lorentz <progval+git@progval.net> Date: Fri, 23 Dec 2022 22:58:51 +0100 Subject: [PATCH 61/71] Add test for registry reloading --- test/test_registry.py | 67 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/test/test_registry.py b/test/test_registry.py index 48dfff547..14616fa15 100644 --- a/test/test_registry.py +++ b/test/test_registry.py @@ -224,6 +224,73 @@ class ValuesTestCase(SupyTestCase): registry.open_registry(filename) self.assertEqual(conf.supybot.networks.test.password(), ' foo ') + def testReload(self): + import supybot.world as world + with conf.supybot.reply.whenAddressedBy.chars.context('@'): + with conf.supybot.reply.whenAddressedBy.chars\ + .get(':testreloadnet').get('#testreloadchan').context('#'): + with conf.supybot.reply.whenAddressedBy.chars \ + .get(':testreloadnet2').context(','): + + newircs = [] + for name in ("testreloadnet", "testreloadnet2", "testnet"): + class newirc: + network = name + newircs.append(newirc) + world.ircs.append(newirc) + try: + # separate function, to keep indent level to a sane + # level + self._testReload() + finally: + for newirc in newircs: + world.ircs.remove(newirc) + + def _testReload(self): + # sanity checks before the actual tests + self.assertEqual(conf.supybot.reply.whenAddressedBy.chars(), '@') + self.assertEqual(conf.supybot.reply.whenAddressedBy.chars + .getSpecific(network='testnet', channel='testchan')(), + '@') + self.assertEqual(conf.supybot.reply.whenAddressedBy.chars + .getSpecific(channel='#testchan')(), + '@') + self.assertEqual(conf.supybot.reply.whenAddressedBy.chars + .getSpecific(network='testreloadnet2')(), + ',') + self.assertEqual(conf.supybot.reply.whenAddressedBy.chars + .getSpecific(network='testreloadnet', + channel='#testreloadchan')(), + '#') + + filename = conf.supybot.directories.conf.dirize('reload.conf') + registry.close(conf.supybot, filename) + with open(filename, 'at') as fd: + fd.write('supybot.reply.whenAddressedBy.chars: !') + + registry.open_registry(filename) + + # new global value applies + self.assertEqual(conf.supybot.reply.whenAddressedBy.chars(), '!') + self.assertEqual(conf.supybot.reply.whenAddressedBy.chars + .getSpecific(network='testnet', channel='#testchan')(), + '!') + self.assertEqual(conf.supybot.reply.whenAddressedBy.chars + .getSpecific(network='testnet', channel='#testchan')(), + '!') + self.assertEqual(conf.supybot.reply.whenAddressedBy.chars + .getSpecific(channel='#testchan')(), + '!') + + # remain unchanged + self.assertEqual(conf.supybot.reply.whenAddressedBy.chars + .getSpecific(network='testreloadnet2')(), + ',') + self.assertEqual(conf.supybot.reply.whenAddressedBy.chars + .getSpecific(network='testreloadnet', + channel='#testreloadchan')(), + '#') + def testWith(self): v = registry.String('foo', 'help') self.assertEqual(v(), 'foo') From f5d39b0be2d1a0546fd649ca9f0c9ec8ee25e4b4 Mon Sep 17 00:00:00 2001 From: Val Lorentz <progval+git@progval.net> Date: Wed, 28 Dec 2022 23:15:30 +0100 Subject: [PATCH 62/71] Skip irctest on pypy-3.9 For some reason, it takes a whole hour to run --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5062a2c24..c3f661534 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -86,7 +86,7 @@ jobs: supybot-test test -v --plugins-dir=./plugins/ --no-network - name: Test with irctest - if: "${{ matrix.with-opt-deps && matrix.python-version != 'pypy-3.7' }}" + if: "${{ matrix.with-opt-deps && matrix.python-version != 'pypy-3.7' && matrix.python-version != 'pypy-3.9' }}" run: | git clone https://github.com/ProgVal/irctest.git cd irctest From 02a0204f80f788b225b18ac4efd40c0e34bea79d Mon Sep 17 00:00:00 2001 From: Valentin Lorentz <progval+git@progval.net> Date: Thu, 12 Jan 2023 22:23:29 +0100 Subject: [PATCH 63/71] Services: Improve doc of plugins.Services.nicks --- plugins/Services/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Services/config.py b/plugins/Services/config.py index 04fa08ffe..28abe6a07 100644 --- a/plugins/Services/config.py +++ b/plugins/Services/config.py @@ -68,7 +68,7 @@ class ValidNickSet(conf.ValidNicks): Services = conf.registerPlugin('Services') conf.registerNetworkValue(Services, 'nicks', - ValidNickSet([], _("""Determines what nicks the bot will use with + ValidNickSet([], _("""Space-separated list of nicks the bot will use with services."""))) class Networks(registry.SpaceSeparatedSetOfStrings): From b42596a0214cddfe8ff73daa8ac00cf64e52bb81 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz <progval+git@progval.net> Date: Thu, 12 Jan 2023 22:24:04 +0100 Subject: [PATCH 64/71] Regenerate READMEs --- plugins/BadWords/README.rst | 8 ++++++++ plugins/Channel/README.rst | 6 +++--- plugins/Config/README.rst | 2 +- plugins/Google/README.rst | 20 -------------------- plugins/MoobotFactoids/README.rst | 13 ++++++++++++- plugins/Poll/README.rst | 5 +++++ plugins/Services/README.rst | 10 +++++++++- plugins/String/README.rst | 8 ++++---- 8 files changed, 42 insertions(+), 30 deletions(-) diff --git a/plugins/BadWords/README.rst b/plugins/BadWords/README.rst index d52ab11b4..e6636d078 100644 --- a/plugins/BadWords/README.rst +++ b/plugins/BadWords/README.rst @@ -99,6 +99,14 @@ supybot.plugins.BadWords.requireWordBoundaries Determines whether the bot will require bad words to be independent words, or whether it will censor them within other words. For instance, if 'darn' is a bad word, then if this is true, 'darn' will be censored, but 'darnit' will not. You probably want this to be false. After changing this setting, the BadWords regexp needs to be regenerated by adding/removing a word to the list, or reloading the plugin. +.. _conf-supybot.plugins.BadWords.selfCensor: + + +supybot.plugins.BadWords.selfCensor + This config variable defaults to "True", is network-specific, and is channel-specific. + + Determines whether the bot will filter its own messages. + .. _conf-supybot.plugins.BadWords.simpleReplacement: diff --git a/plugins/Channel/README.rst b/plugins/Channel/README.rst index 1e16a12d1..ba49124b6 100644 --- a/plugins/Channel/README.rst +++ b/plugins/Channel/README.rst @@ -114,7 +114,7 @@ halfop [<channel>] [<nick> ...] .. _command-channel-iban: iban [<channel>] [--{exact,nick,user,host}] <nick> [<seconds>] - If you have the #channel,op capability, this will ban <nick> for as many seconds as you specify, otherwise (if you specify 0 seconds or don't specify a number of seconds) it will ban the person indefinitely. --exact can be used to specify an exact hostmask. You can combine the exact, nick, user, and host options as you choose. <channel> is only necessary if the message isn't sent in the channel itself. + If you have the #channel,op capability, this will ban <nick> for as many seconds as you specify, otherwise (if you specify 0 seconds or don't specify a number of seconds) it will ban the person indefinitely. --exact can be used to specify an exact hostmask. You can combine the --nick, --user, and --host options as you choose. <channel> is only necessary if the message isn't sent in the channel itself. .. _command-channel-ignore.add: @@ -138,8 +138,8 @@ invite [<channel>] <nick> .. _command-channel-kban: -kban [<channel>] [--{exact,nick,user,host}] <nick> [<seconds>] [<reason>] - If you have the #channel,op capability, this will kickban <nick> for as many seconds as you specify, or else (if you specify 0 seconds or don't specify a number of seconds) it will ban the person indefinitely. --exact bans only the exact hostmask; --nick bans just the nick; --user bans just the user, and --host bans just the host. You can combine these options as you choose. <reason> is a reason to give for the kick. <channel> is only necessary if the message isn't sent in the channel itself. +kban [<channel>] [--{exact,nick,user,host,account}] <nick> [<seconds>] [<reason>] + If you have the #channel,op capability, this will kickban <nick> for as many seconds as you specify, or else (if you specify 0 seconds or don't specify a number of seconds) it will ban the person indefinitely. --exact bans only the exact hostmask; --nick bans just the nick; --user bans just the user, and --host bans just the host You can combine the --nick, --user, and --host options as you choose. <channel> is only necessary if the message isn't sent in the channel itself. .. _command-channel-key: diff --git a/plugins/Config/README.rst b/plugins/Config/README.rst index 2c4a430a8..3cf295fc9 100644 --- a/plugins/Config/README.rst +++ b/plugins/Config/README.rst @@ -47,7 +47,7 @@ help <name> .. _command-config-list: list <group> - Returns the configuration variables available under the given configuration <group>. If a variable has values under it, it is preceded by an '@' sign. If a variable is a 'ChannelValue', that is, it can be separately configured for each channel using the 'channel' command in this plugin, it is preceded by an '#' sign. And if a variable is a 'NetworkValue', it is preceded by a ':' sign. + Returns the configuration variables available under the given configuration <group>. If a variable has values under it, it is preceded by an '@' sign. If a variable is channel-specific, that is, it can be separately configured for each channel using the 'channel' command in this plugin, it is preceded by an '#' sign. And if a variable is a network-specific, it is preceded by a ':' sign. .. _command-config-network: diff --git a/plugins/Google/README.rst b/plugins/Google/README.rst index 5a0233254..b416c9fef 100644 --- a/plugins/Google/README.rst +++ b/plugins/Google/README.rst @@ -43,21 +43,6 @@ Check: `Supported language codes`_ Commands -------- -.. _command-google-cache: - -cache <url> - Returns a link to the cached version of <url> if it is available. - -.. _command-google-calc: - -calc <expression> - Uses Google's calculator to calculate the value of <expression>. - -.. _command-google-fight: - -fight <search string> <search string> [<search string> ...] - Returns the results of each search, in order, from greatest number of results to least. - .. _command-google-google: google <search> [--{filter,language} <value>] @@ -68,11 +53,6 @@ google <search> [--{filter,language} <value>] lucky [--snippet] <search> Does a google search, but only returns the first result. If option --snippet is given, returns also the page text snippet. -.. _command-google-phonebook: - -phonebook <phone number> - Looks <phone number> up on Google. - .. _command-google-translate: translate <source language> [to] <target language> <text> diff --git a/plugins/MoobotFactoids/README.rst b/plugins/MoobotFactoids/README.rst index 257cabfb0..4ee29606e 100644 --- a/plugins/MoobotFactoids/README.rst +++ b/plugins/MoobotFactoids/README.rst @@ -20,13 +20,24 @@ To add factoid say ``@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.`` + .. _commands-MoobotFactoids: Commands diff --git a/plugins/Poll/README.rst b/plugins/Poll/README.rst index f7c50aa26..2e1434a49 100644 --- a/plugins/Poll/README.rst +++ b/plugins/Poll/README.rst @@ -61,6 +61,11 @@ add [<channel>] <question> <answer1> [<answer2> [<answer3> [...]]] close [<channel>] <poll_id> Closes the specified poll. +.. _command-poll-list: + +list [<channel>] + Lists open polls in the <channel>. + .. _command-poll-results: results [<channel>] <poll_id> diff --git a/plugins/Services/README.rst b/plugins/Services/README.rst index 6a963ad2a..6a2970777 100644 --- a/plugins/Services/README.rst +++ b/plugins/Services/README.rst @@ -153,6 +153,14 @@ supybot.plugins.Services.disabledNetworks Determines what networks this plugin will be disabled on. +.. _conf-supybot.plugins.Services.ghostCommand: + + +supybot.plugins.Services.ghostCommand + This config variable defaults to "GHOST", is network-specific, and is not channel-specific. + + Determines the NickServ command to use for GHOST. If the network you're using runs Anope, set this to "RECOVER". If the network you're using runs Atheme, set this to "GHOST" or "REGAIN". + .. _conf-supybot.plugins.Services.ghostDelay: @@ -167,7 +175,7 @@ supybot.plugins.Services.ghostDelay supybot.plugins.Services.nicks This config variable defaults to " ", is network-specific, and is not channel-specific. - Determines what nicks the bot will use with services. + Space-separated list of nicks the bot will use with services. .. _conf-supybot.plugins.Services.noJoinsUntilIdentified: diff --git a/plugins/String/README.rst b/plugins/String/README.rst index f0f1b0dc6..a0c79664e 100644 --- a/plugins/String/README.rst +++ b/plugins/String/README.rst @@ -50,8 +50,8 @@ md5 <text> .. _command-string-ord: -ord <letter> - Returns the unicode codepoint of <letter>. +ord <string> + Returns the unicode codepoint of characters in <string>. .. _command-string-re: @@ -70,8 +70,8 @@ soundex <string> [<length>] .. _command-string-unicodename: -unicodename <character> - Returns the name of the given unicode <character>. +unicodename <string> + Returns the name of characters in <string>. This will error if any character is not a valid Unicode character. .. _command-string-unicodesearch: From f409111872fe262a762981ad8e4936926b32157c Mon Sep 17 00:00:00 2001 From: Valentin Lorentz <progval+git@progval.net> Date: Thu, 19 Jan 2023 10:18:59 +0100 Subject: [PATCH 65/71] callbacks: Fix interference between Scheduler.repeat, Anonymous, and nested commands Specifically, the issue is with Anonymous using irc.noReply() in the first call, preventing nested commands' result from being used. Before this commit, the second and third responses in the test would be only "1" and "2" instead of "1 ['foo']" and "2 ['foo']". --- plugins/Scheduler/test.py | 29 ++++++++++++++++++++++++++--- src/callbacks.py | 9 +++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/plugins/Scheduler/test.py b/plugins/Scheduler/test.py index f6cb22fd8..a01069173 100644 --- a/plugins/Scheduler/test.py +++ b/plugins/Scheduler/test.py @@ -89,7 +89,6 @@ class SchedulerTestCase(ChannelPluginTestCase): self.assertResponse( 'scheduler list', 'There are currently no scheduled commands.') - def testRepeat(self): self.assertRegexp('scheduler repeat repeater 5 echo testRepeat', 'testRepeat') @@ -132,6 +131,32 @@ class SchedulerTestCase(ChannelPluginTestCase): timeFastForward(5) self.assertNoResponse(' ', timeout=1) + def testRepeatWorksWithNestedCommandsWithNoReply(self): + # the 'trylater' command uses ircmsgs.privmsg + irc.noReply(), + # similar to how the Anonymous plugin implements sending messages + # to channels/users without .reply() (as it is technically not a + # reply to the origin message) + count = 0 + class TryLater(callbacks.Plugin): + def trylater(self, irc, msg, args): + nonlocal count + msg = ircmsgs.privmsg(msg.nick, "%d %s" % (count, args)) + irc.queueMsg(msg) + irc.noReply() + count += 1 + + cb = TryLater(self.irc) + self.irc.addCallback(cb) + try: + self.assertResponse('scheduler repeat foo 5 "trylater [echo foo]"', + "0 ['foo']") + timeFastForward(5) + self.assertResponse(' ', "1 ['foo']") + timeFastForward(5) + self.assertResponse(' ', "2 ['foo']") + finally: + self.irc.removeCallback('TryLater') + def testRepeatDisallowsIntegerNames(self): self.assertError('scheduler repeat 1234 1234 "echo NoIntegerNames"') @@ -187,7 +212,5 @@ class SchedulerTestCase(ChannelPluginTestCase): self.assertResponse(' ', 'testRepeat', timeout=1) # T+106 - - # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: diff --git a/src/callbacks.py b/src/callbacks.py index dcccab3c5..85f4d8020 100644 --- a/src/callbacks.py +++ b/src/callbacks.py @@ -990,10 +990,19 @@ class NestedCommandsIrcProxy(ReplyIrcProxy): self.error(_('You\'ve attempted more nesting than is ' 'currently allowed on this bot.')) return + # The deepcopy here is necessary for Scheduler; it re-runs already # tokenized commands. There's a possibility a simple copy[:] would # work, but we're being careful. self.args = copy.deepcopy(args) + + # Another trick needed for Scheduler: + # A previous run of the command may have set 'ignored' to True, + # causing this run to not include response from nested commands; + # as NestedCommandsIrcProxy.reply() would confuse it with the + # subcommand setting 'ignored' to True itself. + msg.tag('ignored', False) + self.counter = 0 self._resetReplyAttributes() if not args: From efed7d8081af161f1ca0e3735f5f0ceddfcb871e Mon Sep 17 00:00:00 2001 From: Valentin Lorentz <progval+git@progval.net> Date: Thu, 19 Jan 2023 10:31:13 +0100 Subject: [PATCH 66/71] Move the 'ignore=False' trick from callbacks to Scheduler I fear putting it in callbacks would be overzealous and reset it within the processing of the same message, eg. when using conditional to set the 'ignore' tag before other nested commands run. --- plugins/Scheduler/plugin.py | 7 +++++++ src/callbacks.py | 7 ------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/Scheduler/plugin.py b/plugins/Scheduler/plugin.py index 104344153..91d4ca907 100644 --- a/plugins/Scheduler/plugin.py +++ b/plugins/Scheduler/plugin.py @@ -148,6 +148,13 @@ class Scheduler(callbacks.Plugin): channel=msg.channel, network=irc.network) if remove: del self.events[str(f.eventId)] + + # A previous run of the command may have set 'ignored' to True, + # causing this run to not include response from nested commands; + # as NestedCommandsIrcProxy.reply() would confuse it with the + # subcommand setting 'ignored' to True itself. + msg.tag('ignored', False) + self.Proxy(irc, msg, tokens) return f diff --git a/src/callbacks.py b/src/callbacks.py index 85f4d8020..97f6a1eca 100644 --- a/src/callbacks.py +++ b/src/callbacks.py @@ -996,13 +996,6 @@ class NestedCommandsIrcProxy(ReplyIrcProxy): # work, but we're being careful. self.args = copy.deepcopy(args) - # Another trick needed for Scheduler: - # A previous run of the command may have set 'ignored' to True, - # causing this run to not include response from nested commands; - # as NestedCommandsIrcProxy.reply() would confuse it with the - # subcommand setting 'ignored' to True itself. - msg.tag('ignored', False) - self.counter = 0 self._resetReplyAttributes() if not args: From 3f5a18e8d017a7961419b6e63022656b2b254957 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz <progval+git@progval.net> Date: Sat, 21 Jan 2023 18:51:17 +0100 Subject: [PATCH 67/71] Remove unused import fallback on the 'mock' library --- test/test_yn.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/test/test_yn.py b/test/test_yn.py index c147710a2..b40b348e7 100644 --- a/test/test_yn.py +++ b/test/test_yn.py @@ -30,17 +30,10 @@ import sys import unittest +from unittest import mock from supybot import questions from supybot.test import SupyTestCase -try: - from unittest import mock # Python 3.3+ -except ImportError: - try: - import mock # Everything else, an external 'mock' library - except ImportError: - mock = None - # so complicated construction because I want to # gain the string 'y' instead of the character 'y' # the reason of usage this construction is to prove From 922b00c8c31b888a0c567e51c335fa07ae08007a Mon Sep 17 00:00:00 2001 From: Valentin Lorentz <progval+git@progval.net> Date: Sat, 28 Jan 2023 13:00:14 +0100 Subject: [PATCH 68/71] Fediverse: Use default headers Some instances behind Cloudflare block requests without a User-Agent header. --- plugins/Fediverse/activitypub.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/Fediverse/activitypub.py b/plugins/Fediverse/activitypub.py index 511551c3b..a3a2e3770 100644 --- a/plugins/Fediverse/activitypub.py +++ b/plugins/Fediverse/activitypub.py @@ -229,7 +229,9 @@ def get_public_key_pem(): def signed_request(url, headers=None, data=None): method = "get" if data is None else "post" instance_actor_url = get_instance_actor_url() - headers = gen.InsensitivePreservingDict(headers or {}) + headers = gen.InsensitivePreservingDict( + {**web.defaultHeaders, **(headers or {})} + ) if "Date" not in headers: headers["Date"] = email.utils.formatdate(usegmt=True) From f518579c7747386e1e2c5d034212c714c2ec0a64 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz <progval+git@progval.net> Date: Tue, 21 Feb 2023 19:10:10 +0100 Subject: [PATCH 69/71] Request standard-replies capability Arbitrary standard-replies are already supported, this signals to servers that we do. --- src/irclib.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/irclib.py b/src/irclib.py index d6918c824..6929ebb70 100644 --- a/src/irclib.py +++ b/src/irclib.py @@ -1776,7 +1776,8 @@ class Irc(IrcCommandDispatcher, log.Firewalled): 'multi-prefix', 'metadata-notify', 'account-tag', 'userhost-in-names', 'invite-notify', 'server-time', 'chghost', 'batch', 'away-notify', 'message-tags', - 'msgid', 'setname', 'labeled-response', 'echo-message']) + 'msgid', 'setname', 'labeled-response', 'echo-message', + 'standard-replies']) """IRCv3 capabilities requested when they are available. echo-message is special-cased to be requested only with labeled-response. From add9306d30cc9dd01d59970dceb16d932b52c3fa Mon Sep 17 00:00:00 2001 From: Valentin Lorentz <progval+git@progval.net> Date: Fri, 24 Mar 2023 20:23:32 +0100 Subject: [PATCH 70/71] Socket: Clear buffers on reconnect --- src/drivers/Socket.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/drivers/Socket.py b/src/drivers/Socket.py index 098676606..3e73ac655 100644 --- a/src/drivers/Socket.py +++ b/src/drivers/Socket.py @@ -70,8 +70,6 @@ class SocketDriver(drivers.IrcDriver, drivers.ServersMixin): self._attempt = -1 self.servers = () self.eagains = 0 - self.inbuffer = b'' - self.outbuffer = '' self.zombie = False self.connected = False self.writeCheckTime = None @@ -248,6 +246,8 @@ class SocketDriver(drivers.IrcDriver, drivers.ServersMixin): def reconnect(self, wait=False, reset=True, server=None): self._attempt += 1 + self.inbuffer = b'' + self.outbuffer = '' self.nextReconnectTime = None if self.connected: self.onDisconnect() From 295798ac0ed85c4edae67c8fa9b21acff01ff6ad Mon Sep 17 00:00:00 2001 From: Valentin Lorentz <progval+git@progval.net> Date: Fri, 24 Mar 2023 20:48:57 +0100 Subject: [PATCH 71/71] Stop testing pypy3.7 with optional dependencies one of the dependencies stopped supporting it (probably 'cryptography' as it's the only one not in pure-Python) --- .github/workflows/test.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c3f661534..732a2cfaa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -46,9 +46,6 @@ jobs: - python-version: "3.7" with-opt-deps: false runs-on: ubuntu-22.04 - - python-version: "pypy-3.7" - with-opt-deps: true - runs-on: ubuntu-22.04 - python-version: "pypy-3.7" with-opt-deps: false runs-on: ubuntu-22.04