mirror of
https://github.com/Mikaela/Limnoria.git
synced 2024-11-16 15:39:21 +01:00
Merge branch 'testing' into entrypoints
This commit is contained in:
commit
6f663e4140
53
.github/workflows/test.yml
vendored
53
.github/workflows/test.yml
vendored
@ -4,21 +4,58 @@ on:
|
|||||||
push:
|
push:
|
||||||
pull_request:
|
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:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
||||||
runs-on: ${{ matrix.runs-on }}
|
runs-on: ${{ matrix.runs-on }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11.0-alpha.2", "pypy-3.6", "pypy-3.7"]
|
include:
|
||||||
with-opt-deps: [false, true]
|
- python-version: "3.12.0-alpha.3"
|
||||||
runs-on: [ubuntu-latest]
|
with-opt-deps: true
|
||||||
exclude:
|
runs-on: ubuntu-22.04
|
||||||
# Some of the dependencies don't work on old Python versions
|
|
||||||
|
- python-version: "3.11"
|
||||||
|
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: "pypy-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: false
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
- python-version: "3.6"
|
- python-version: "3.6"
|
||||||
with-opt-deps: true
|
with-opt-deps: false
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
- python-version: "pypy-3.6"
|
- python-version: "pypy-3.6"
|
||||||
with-opt-deps: true
|
with-opt-deps: false
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
@ -46,7 +83,7 @@ jobs:
|
|||||||
supybot-test test -v --plugins-dir=./plugins/ --no-network
|
supybot-test test -v --plugins-dir=./plugins/ --no-network
|
||||||
|
|
||||||
- name: Test with irctest
|
- 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: |
|
run: |
|
||||||
git clone https://github.com/ProgVal/irctest.git
|
git clone https://github.com/ProgVal/irctest.git
|
||||||
cd irctest
|
cd irctest
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -23,6 +23,9 @@ supybot.egg-info/
|
|||||||
test-conf/
|
test-conf/
|
||||||
test-data/
|
test-data/
|
||||||
test-logs/
|
test-logs/
|
||||||
|
doc-conf/
|
||||||
|
doc-data/
|
||||||
|
doc-logs/
|
||||||
src/version.py
|
src/version.py
|
||||||
INSTALL
|
INSTALL
|
||||||
README.txt
|
README.txt
|
||||||
|
@ -46,7 +46,7 @@ Now you can use Aka as you used Alias before.
|
|||||||
Trout
|
Trout
|
||||||
^^^^^
|
^^^^^
|
||||||
|
|
||||||
Add an aka, trout, which expects a word as an argument::
|
Add an aka, ``trout``, which expects a word as an argument::
|
||||||
|
|
||||||
<jamessan> @aka add trout "reply action slaps $1 with a large trout"
|
<jamessan> @aka add trout "reply action slaps $1 with a large trout"
|
||||||
<bot> jamessan: The operation succeeded.
|
<bot> 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
|
This ``trout`` aka requires the plugin ``Reply`` to be loaded since it
|
||||||
provides the ``action`` command.
|
provides the ``action`` command.
|
||||||
|
|
||||||
LastFM
|
Random percentage
|
||||||
^^^^^^
|
^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Add an aka, ``lastfm``, which expects a last.fm username and replies with
|
Add an aka, ``randpercent``, which returns a random percentage value::
|
||||||
their most recently played item::
|
|
||||||
|
|
||||||
@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``,
|
This requires the ``Filter`` and ``Games`` plugins to be loaded.
|
||||||
``Format`` and ``Web``.
|
|
||||||
|
|
||||||
``RSS`` provides ``rss``, ``Format`` provides ``concat`` and ``Web`` provides
|
Note that nested commands in an alias should be quoted, or they will only
|
||||||
``urlquote``.
|
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
|
||||||
Note that if the nested commands being aliased hadn't been quoted, then
|
``@randpercent`` always responds with the same value!)
|
||||||
those commands would have been run immediately, and ``@lastfm`` would always
|
|
||||||
reply with the same information, the result of those commands.
|
|
||||||
|
|
||||||
.. _commands-Aka:
|
.. _commands-Aka:
|
||||||
|
|
||||||
|
@ -532,7 +532,7 @@ class Aka(callbacks.Plugin):
|
|||||||
Trout
|
Trout
|
||||||
^^^^^
|
^^^^^
|
||||||
|
|
||||||
Add an aka, trout, which expects a word as an argument::
|
Add an aka, ``trout``, which expects a word as an argument::
|
||||||
|
|
||||||
<jamessan> @aka add trout "reply action slaps $1 with a large trout"
|
<jamessan> @aka add trout "reply action slaps $1 with a large trout"
|
||||||
<bot> jamessan: The operation succeeded.
|
<bot> jamessan: The operation succeeded.
|
||||||
@ -542,23 +542,19 @@ class Aka(callbacks.Plugin):
|
|||||||
This ``trout`` aka requires the plugin ``Reply`` to be loaded since it
|
This ``trout`` aka requires the plugin ``Reply`` to be loaded since it
|
||||||
provides the ``action`` command.
|
provides the ``action`` command.
|
||||||
|
|
||||||
LastFM
|
Random percentage
|
||||||
^^^^^^
|
^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Add an aka, ``lastfm``, which expects a last.fm username and replies with
|
Add an aka, ``randpercent``, which returns a random percentage value::
|
||||||
their most recently played item::
|
|
||||||
|
|
||||||
@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``,
|
This requires the ``Filter`` and ``Games`` plugins to be loaded.
|
||||||
``Format`` and ``Web``.
|
|
||||||
|
|
||||||
``RSS`` provides ``rss``, ``Format`` provides ``concat`` and ``Web`` provides
|
Note that nested commands in an alias should be quoted, or they will only
|
||||||
``urlquote``.
|
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
|
||||||
Note that if the nested commands being aliased hadn't been quoted, then
|
``@randpercent`` always responds with the same value!)
|
||||||
those commands would have been run immediately, and ``@lastfm`` would always
|
|
||||||
reply with the same information, the result of those commands.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, irc):
|
def __init__(self, irc):
|
||||||
|
@ -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
|
built-in Aka plugin instead (you can migrate your existing aliases using
|
||||||
the 'importaliasdatabase' command.
|
the 'importaliasdatabase' command.
|
||||||
|
|
||||||
To add an alias, `trout`, which expects a word as an argument::
|
To add an alias, ``trout``, which expects a word as an argument::
|
||||||
|
|
||||||
<jamessan> @alias add trout "action slaps $1 with a large trout"
|
<jamessan> @alias add trout "action slaps $1 with a large trout"
|
||||||
<bot> jamessan: The operation succeeded.
|
<bot> jamessan: The operation succeeded.
|
||||||
<jamessan> @trout me
|
<jamessan> @trout me
|
||||||
* bot slaps me with a large trout
|
* bot slaps me with a large trout
|
||||||
|
|
||||||
To add an alias, `lastfm`, which expects a last.fm user and replies with
|
Add an alias, ``randpercent``, which returns a random percentage value::
|
||||||
their recently played items::
|
|
||||||
|
|
||||||
@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
|
This requires the ``Filter`` and ``Games`` plugins to be loaded.
|
||||||
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-Alias:
|
.. _commands-Alias:
|
||||||
|
|
||||||
|
@ -243,21 +243,23 @@ class Alias(callbacks.Plugin):
|
|||||||
built-in Aka plugin instead (you can migrate your existing aliases using
|
built-in Aka plugin instead (you can migrate your existing aliases using
|
||||||
the 'importaliasdatabase' command.
|
the 'importaliasdatabase' command.
|
||||||
|
|
||||||
To add an alias, `trout`, which expects a word as an argument::
|
To add an alias, ``trout``, which expects a word as an argument::
|
||||||
|
|
||||||
<jamessan> @alias add trout "action slaps $1 with a large trout"
|
<jamessan> @alias add trout "action slaps $1 with a large trout"
|
||||||
<bot> jamessan: The operation succeeded.
|
<bot> jamessan: The operation succeeded.
|
||||||
<jamessan> @trout me
|
<jamessan> @trout me
|
||||||
* bot slaps me with a large trout
|
* bot slaps me with a large trout
|
||||||
|
|
||||||
To add an alias, `lastfm`, which expects a last.fm user and replies with
|
Add an alias, ``randpercent``, which returns a random percentage value::
|
||||||
their recently played items::
|
|
||||||
|
|
||||||
@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
|
This requires the ``Filter`` and ``Games`` plugins to be loaded.
|
||||||
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):
|
def __init__(self, irc):
|
||||||
self.__parent = super(Alias, self)
|
self.__parent = super(Alias, self)
|
||||||
|
@ -111,7 +111,7 @@ class AliasTestCase(ChannelPluginTestCase):
|
|||||||
self.assertResponse('foobar', 'sbbone')
|
self.assertResponse('foobar', 'sbbone')
|
||||||
self.assertRaises(Alias.AliasError, cb.removeAlias, 'foobar')
|
self.assertRaises(Alias.AliasError, cb.removeAlias, 'foobar')
|
||||||
cb.removeAlias('foobar', evenIfLocked=True)
|
cb.removeAlias('foobar', evenIfLocked=True)
|
||||||
self.assertFalse('foobar' in cb.aliases)
|
self.assertNotIn('foobar', cb.aliases)
|
||||||
self.assertError('foobar')
|
self.assertError('foobar')
|
||||||
|
|
||||||
self.assertRegexp('alias add abc\x07 ignore', 'Error.*Invalid')
|
self.assertRegexp('alias add abc\x07 ignore', 'Error.*Invalid')
|
||||||
|
@ -39,7 +39,7 @@ class AnonymousTestCase(ChannelPluginTestCase):
|
|||||||
|
|
||||||
with conf.supybot.plugins.Anonymous.requireRegistration.context(False):
|
with conf.supybot.plugins.Anonymous.requireRegistration.context(False):
|
||||||
m = self.assertNotError('anonymous say %s foo!' % self.channel)
|
m = self.assertNotError('anonymous say %s foo!' % self.channel)
|
||||||
self.assertTrue(m.args[1] == 'foo!')
|
self.assertEqual(m.args[1], 'foo!')
|
||||||
|
|
||||||
def testTell(self):
|
def testTell(self):
|
||||||
self.assertError('anonymous tell %s I love you!' % self.nick)
|
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)
|
self.assertError('anonymous tell %s foo!' % self.channel)
|
||||||
with conf.supybot.plugins.Anonymous.allowPrivateTarget.context(True):
|
with conf.supybot.plugins.Anonymous.allowPrivateTarget.context(True):
|
||||||
m = self.assertNotError('anonymous tell %s foo!' % self.nick)
|
m = self.assertNotError('anonymous tell %s foo!' % self.nick)
|
||||||
self.assertTrue(m.args[1] == 'foo!')
|
self.assertEqual(m.args[1], 'foo!')
|
||||||
|
|
||||||
def testAction(self):
|
def testAction(self):
|
||||||
m = self.assertError('anonymous do %s loves you!' % self.channel)
|
m = self.assertError('anonymous do %s loves you!' % self.channel)
|
||||||
|
@ -61,6 +61,7 @@ from . import config
|
|||||||
from . import plugin
|
from . import plugin
|
||||||
|
|
||||||
from importlib import reload
|
from importlib import reload
|
||||||
|
|
||||||
# In case we're being reloaded.
|
# In case we're being reloaded.
|
||||||
reload(config)
|
reload(config)
|
||||||
reload(plugin)
|
reload(plugin)
|
||||||
|
@ -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.
|
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:
|
.. _conf-supybot.plugins.BadWords.simpleReplacement:
|
||||||
|
|
||||||
|
|
||||||
|
@ -113,6 +113,9 @@ conf.registerGlobalValue(BadWords, 'stripFormatting',
|
|||||||
filtering. If it's True, however, it will interact poorly with other
|
filtering. If it's True, however, it will interact poorly with other
|
||||||
plugins that do coloring or bolding of text.""")))
|
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',
|
conf.registerChannelValue(BadWords, 'kick',
|
||||||
registry.Boolean(False, _("""Determines whether the bot will kick people with
|
registry.Boolean(False, _("""Determines whether the bot will kick people with
|
||||||
a warning when they use bad words.""")))
|
a warning when they use bad words.""")))
|
||||||
|
@ -81,18 +81,24 @@ msgstr ""
|
|||||||
|
|
||||||
#: config.py:117
|
#: config.py:117
|
||||||
msgid ""
|
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"
|
"Determines whether the bot will kick people with\n"
|
||||||
" a warning when they use bad words."
|
" a warning when they use bad words."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: config.py:120
|
#: config.py:123
|
||||||
msgid ""
|
msgid ""
|
||||||
"You have been kicked for using a word\n"
|
"You have been kicked for using a word\n"
|
||||||
" prohibited in the presence of this bot. Please use more appropriate\n"
|
" prohibited in the presence of this bot. Please use more appropriate\n"
|
||||||
" language in the future."
|
" language in the future."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: config.py:122
|
#: config.py:125
|
||||||
msgid ""
|
msgid ""
|
||||||
"Determines the kick message used by the\n"
|
"Determines the kick message used by the\n"
|
||||||
" bot when kicking users for saying bad words."
|
" bot when kicking users for saying bad words."
|
||||||
|
@ -104,9 +104,10 @@ class BadWords(callbacks.Privmsg):
|
|||||||
self.lastModified = time.time()
|
self.lastModified = time.time()
|
||||||
|
|
||||||
def outFilter(self, irc, msg):
|
def outFilter(self, irc, msg):
|
||||||
if self.filtering and msg.command == 'PRIVMSG' \
|
|
||||||
and (self.words() or self.phrases()):
|
|
||||||
channel = msg.channel
|
channel = msg.channel
|
||||||
|
if self.filtering and msg.command == 'PRIVMSG' \
|
||||||
|
and (self.words() or self.phrases()) \
|
||||||
|
and self.registryValue('selfCensor', channel, irc.network):
|
||||||
self.updateRegexp(channel, irc.network)
|
self.updateRegexp(channel, irc.network)
|
||||||
s = msg.args[1]
|
s = msg.args[1]
|
||||||
if self.registryValue('stripFormatting'):
|
if self.registryValue('stripFormatting'):
|
||||||
|
@ -114,7 +114,7 @@ halfop [<channel>] [<nick> ...]
|
|||||||
.. _command-channel-iban:
|
.. _command-channel-iban:
|
||||||
|
|
||||||
iban [<channel>] [--{exact,nick,user,host}] <nick> [<seconds>]
|
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:
|
.. _command-channel-ignore.add:
|
||||||
|
|
||||||
@ -138,8 +138,8 @@ invite [<channel>] <nick>
|
|||||||
|
|
||||||
.. _command-channel-kban:
|
.. _command-channel-kban:
|
||||||
|
|
||||||
kban [<channel>] [--{exact,nick,user,host}] <nick> [<seconds>] [<reason>]
|
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 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.
|
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:
|
.. _command-channel-key:
|
||||||
|
|
||||||
|
@ -313,17 +313,15 @@ class Channel(callbacks.Plugin):
|
|||||||
@internationalizeDocstring
|
@internationalizeDocstring
|
||||||
def kban(self, irc, msg, args,
|
def kban(self, irc, msg, args,
|
||||||
channel, optlist, bannedNick, expiry, reason):
|
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
|
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
|
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.
|
don't specify a number of seconds) it will ban the person indefinitely.
|
||||||
--exact bans only the exact hostmask; --nick bans just the nick;
|
--exact bans only the exact hostmask; --nick bans just the nick;
|
||||||
--user bans just the user, and --host bans just the host. You can
|
--user bans just the user, and --host bans just the host
|
||||||
combine these options as you choose. <reason> is a reason to give for
|
You can combine the --nick, --user, and --host options as you choose.
|
||||||
the kick.
|
<channel> is only necessary if the message isn't sent in the channel itself.
|
||||||
<channel> is only necessary if the message isn't sent in the channel
|
|
||||||
itself.
|
|
||||||
"""
|
"""
|
||||||
self._ban(irc, msg, args,
|
self._ban(irc, msg, args,
|
||||||
channel, optlist, bannedNick, expiry, reason, True)
|
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
|
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
|
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.
|
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 can be used to specify an exact hostmask.
|
||||||
exact, nick, user, and host options as you choose. <channel> is only
|
You can combine the --nick, --user, and --host options as you choose.
|
||||||
necessary if the message isn't sent in the channel itself.
|
<channel> is only necessary if the message isn't sent in the channel itself.
|
||||||
"""
|
"""
|
||||||
self._ban(irc, msg, args,
|
self._ban(irc, msg, args,
|
||||||
channel, optlist, bannedNick, expiry, None, False)
|
channel, optlist, bannedNick, expiry, None, False)
|
||||||
@ -381,8 +379,12 @@ class Channel(callbacks.Plugin):
|
|||||||
msg.prefix, bannedNick)
|
msg.prefix, bannedNick)
|
||||||
raise callbacks.ArgumentError
|
raise callbacks.ArgumentError
|
||||||
elif bannedNick == irc.nick:
|
elif bannedNick == irc.nick:
|
||||||
|
if kick:
|
||||||
self.log.warning('%q tried to make me kban myself.', msg.prefix)
|
self.log.warning('%q tried to make me kban myself.', msg.prefix)
|
||||||
irc.error(_('I cowardly refuse to kickban myself.'))
|
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
|
return
|
||||||
if not reason:
|
if not reason:
|
||||||
reason = msg.nick
|
reason = msg.nick
|
||||||
|
@ -46,19 +46,19 @@ class ChannelTestCase(ChannelPluginTestCase):
|
|||||||
def testLobotomies(self):
|
def testLobotomies(self):
|
||||||
self.assertRegexp('lobotomy list', 'not.*any')
|
self.assertRegexp('lobotomy list', 'not.*any')
|
||||||
|
|
||||||
## def testCapabilities(self):
|
def testCapabilities(self):
|
||||||
## self.prefix = 'foo!bar@baz'
|
self.prefix = 'foo!bar@baz'
|
||||||
## self.irc.feedMsg(ircmsgs.privmsg(self.irc.nick, 'register foo bar',
|
self.irc.feedMsg(ircmsgs.privmsg(self.irc.nick, 'register foo bar',
|
||||||
## prefix=self.prefix))
|
prefix=self.prefix))
|
||||||
## u = ircdb.users.getUser(0)
|
u = ircdb.users.getUser(0)
|
||||||
## u.addCapability('%s.op' % self.channel)
|
u.addCapability('%s.op' % self.channel)
|
||||||
## ircdb.users.setUser(u)
|
ircdb.users.setUser(u)
|
||||||
## self.assertNotError(' ')
|
self.assertNotError(' ')
|
||||||
## self.assertResponse('user capabilities foo', '[]')
|
self.assertResponse('user capabilities foo', '[]')
|
||||||
## self.assertNotError('channel addcapability foo op')
|
self.assertNotError('channel addcapability foo op')
|
||||||
## self.assertRegexp('channel capabilities foo', 'op')
|
self.assertRegexp('channel capabilities foo', 'op')
|
||||||
## self.assertNotError('channel removecapability foo op')
|
self.assertNotError('channel removecapability foo op')
|
||||||
## self.assertResponse('user capabilities foo', '[]')
|
self.assertResponse('user capabilities foo', '[]')
|
||||||
|
|
||||||
def testCapabilities(self):
|
def testCapabilities(self):
|
||||||
self.assertNotError('channel capability list')
|
self.assertNotError('channel capability list')
|
||||||
@ -185,28 +185,39 @@ class ChannelTestCase(ChannelPluginTestCase):
|
|||||||
self.assertBan('iban $a:nyuszika7h', '$a:nyuszika7h')
|
self.assertBan('iban $a:nyuszika7h', '$a:nyuszika7h')
|
||||||
self.assertNotError('unban $a:nyuszika7h')
|
self.assertNotError('unban $a:nyuszika7h')
|
||||||
|
|
||||||
## def testKban(self):
|
def testKban(self):
|
||||||
## self.irc.prefix = 'something!else@somehwere.else'
|
self.irc.prefix = 'something!else@somehwere.else'
|
||||||
## self.irc.nick = 'something'
|
self.irc.nick = 'something'
|
||||||
## self.irc.feedMsg(ircmsgs.join(self.channel,
|
def join():
|
||||||
## prefix='foobar!user@host.domain.tld'))
|
self.irc.feedMsg(ircmsgs.join(
|
||||||
## self.assertError('kban foobar')
|
self.channel, prefix='foobar!user@host.domain.tld'))
|
||||||
## self.irc.feedMsg(ircmsgs.op(self.channel, self.irc.nick))
|
join()
|
||||||
## self.assertError('kban foobar -1')
|
self.assertError('kban foobar')
|
||||||
## self.assertKban('kban foobar', '*!*@*.domain.tld')
|
self.irc.feedMsg(ircmsgs.op(self.channel, self.irc.nick))
|
||||||
## self.assertKban('kban --exact foobar', 'foobar!user@host.domain.tld')
|
#self.assertError('kban foobar -1')
|
||||||
## self.assertKban('kban --host foobar', '*!*@host.domain.tld')
|
#self.assertKban('kban foobar', '*!*@*.domain.tld')
|
||||||
## self.assertKban('kban --user foobar', '*!user@*')
|
#join()
|
||||||
## self.assertKban('kban --nick foobar', 'foobar!*@*')
|
self.assertKban('kban --exact foobar', 'foobar!user@host.domain.tld')
|
||||||
## self.assertKban('kban --nick --user foobar', 'foobar!user@*')
|
join()
|
||||||
## self.assertKban('kban --nick --host foobar',
|
self.assertKban('kban --host foobar', '*!*@host.domain.tld')
|
||||||
## 'foobar!*@host.domain.tld')
|
join()
|
||||||
## self.assertKban('kban --user --host foobar', '*!user@host.domain.tld')
|
self.assertKban('kban --user foobar', '*!user@*')
|
||||||
## self.assertKban('kban --nick --user --host foobar',
|
join()
|
||||||
## 'foobar!user@host.domain.tld')
|
self.assertKban('kban --nick foobar', 'foobar!*@*')
|
||||||
## self.assertNotRegexp('kban adlkfajsdlfkjsd', 'KeyError')
|
join()
|
||||||
## self.assertNotRegexp('kban foobar time', 'ValueError')
|
self.assertKban('kban --nick --user foobar', 'foobar!user@*')
|
||||||
## self.assertError('kban %s' % self.irc.nick)
|
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):
|
def testBan(self):
|
||||||
with conf.supybot.protocols.irc.banmask.context(['exact']):
|
with conf.supybot.protocols.irc.banmask.context(['exact']):
|
||||||
|
@ -47,7 +47,7 @@ help <name>
|
|||||||
.. _command-config-list:
|
.. _command-config-list:
|
||||||
|
|
||||||
list <group>
|
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:
|
.. _command-config-network:
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
###
|
###
|
||||||
# Copyright (c) 2002-2005, Jeremiah Fincher
|
# Copyright (c) 2002-2005, Jeremiah Fincher
|
||||||
# Copyright (c) 2009, James McCoy
|
# Copyright (c) 2009, James McCoy
|
||||||
# Copyright (c) 2010-2021, Valentin Lorentz
|
# Copyright (c) 2010-2022, Valentin Lorentz
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
@ -33,11 +33,20 @@ import random
|
|||||||
|
|
||||||
from supybot.test import *
|
from supybot.test import *
|
||||||
import supybot.conf as conf
|
import supybot.conf as conf
|
||||||
|
import supybot.registry as registry
|
||||||
|
|
||||||
_letters = 'abcdefghijklmnopqrstuvwxyz'
|
_letters = 'abcdefghijklmnopqrstuvwxyz'
|
||||||
def random_string():
|
def random_string():
|
||||||
return ''.join(random.choice(_letters) for _ in range(16))
|
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):
|
class ConfigTestCase(ChannelPluginTestCase):
|
||||||
# We add utilities so there's something in supybot.plugins.
|
# We add utilities so there's something in supybot.plugins.
|
||||||
plugins = ('Config', 'User', 'Utilities', 'Web')
|
plugins = ('Config', 'User', 'Utilities', 'Web')
|
||||||
@ -50,6 +59,16 @@ class ConfigTestCase(ChannelPluginTestCase):
|
|||||||
self.assertNotRegexp('config get supybot.reply', r'registry\.Group')
|
self.assertNotRegexp('config get supybot.reply', r'registry\.Group')
|
||||||
self.assertResponse('config supybot.protocols.irc.throttleTime', '0.0')
|
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):
|
def testList(self):
|
||||||
self.assertError('config list asldfkj')
|
self.assertError('config list asldfkj')
|
||||||
self.assertError('config list supybot.asdfkjsldf')
|
self.assertError('config list supybot.asdfkjsldf')
|
||||||
|
@ -64,7 +64,7 @@ class Ctcp(callbacks.PluginRegexp):
|
|||||||
def callCommand(self, command, irc, msg, *args, **kwargs):
|
def callCommand(self, command, irc, msg, *args, **kwargs):
|
||||||
if conf.supybot.abuse.flood.ctcp():
|
if conf.supybot.abuse.flood.ctcp():
|
||||||
now = time.time()
|
now = time.time()
|
||||||
for (ignore, expiration) in self.ignores.items():
|
for (ignore, expiration) in list(self.ignores.items()):
|
||||||
if expiration < now:
|
if expiration < now:
|
||||||
del self.ignores[ignore]
|
del self.ignores[ignore]
|
||||||
elif ircutils.hostmaskPatternEqual(ignore, msg.prefix):
|
elif ircutils.hostmaskPatternEqual(ignore, msg.prefix):
|
||||||
|
@ -53,6 +53,7 @@ from . import config
|
|||||||
from . import plugin
|
from . import plugin
|
||||||
|
|
||||||
from importlib import reload
|
from importlib import reload
|
||||||
|
|
||||||
# In case we're being reloaded.
|
# In case we're being reloaded.
|
||||||
reload(config)
|
reload(config)
|
||||||
reload(plugin)
|
reload(plugin)
|
||||||
|
@ -61,10 +61,6 @@ class ProtocolError(ActivityPubError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class HostmetaError(ProtocolError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ActivityPubProtocolError(ActivityPubError):
|
class ActivityPubProtocolError(ActivityPubError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -118,7 +114,7 @@ def convert_exceptions(to_class, msg="", from_none=False):
|
|||||||
|
|
||||||
@sandbox
|
@sandbox
|
||||||
def _get_webfinger_url(hostname):
|
def _get_webfinger_url(hostname):
|
||||||
with convert_exceptions(HostmetaError):
|
try:
|
||||||
doc = ET.fromstring(
|
doc = ET.fromstring(
|
||||||
web.getUrlContent("https://%s/.well-known/host-meta" % hostname)
|
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"):
|
for link in doc.iter(XRD_URI + "Link"):
|
||||||
if link.attrib["rel"] == "lrdd":
|
if link.attrib["rel"] == "lrdd":
|
||||||
return link.attrib["template"]
|
return link.attrib["template"]
|
||||||
|
except web.Error:
|
||||||
return "https://%s/.well-known/webfinger?resource={uri}"
|
# Fall back to the default Webfinger URL
|
||||||
|
return "https://%s/.well-known/webfinger?resource={uri}" % hostname
|
||||||
|
|
||||||
|
|
||||||
def has_webfinger_support(hostname):
|
def has_webfinger_support(hostname):
|
||||||
@ -232,7 +229,9 @@ def get_public_key_pem():
|
|||||||
def signed_request(url, headers=None, data=None):
|
def signed_request(url, headers=None, data=None):
|
||||||
method = "get" if data is None else "post"
|
method = "get" if data is None else "post"
|
||||||
instance_actor_url = get_instance_actor_url()
|
instance_actor_url = get_instance_actor_url()
|
||||||
headers = gen.InsensitivePreservingDict(headers or {})
|
headers = gen.InsensitivePreservingDict(
|
||||||
|
{**web.defaultHeaders, **(headers or {})}
|
||||||
|
)
|
||||||
|
|
||||||
if "Date" not in headers:
|
if "Date" not in headers:
|
||||||
headers["Date"] = email.utils.formatdate(usegmt=True)
|
headers["Date"] = email.utils.formatdate(usegmt=True)
|
||||||
|
@ -38,6 +38,7 @@ from supybot.commands import urlSnarfer, wrap
|
|||||||
from supybot.i18n import PluginInternationalization
|
from supybot.i18n import PluginInternationalization
|
||||||
|
|
||||||
from . import activitypub as ap
|
from . import activitypub as ap
|
||||||
|
from .utils import parse_xsd_duration
|
||||||
|
|
||||||
|
|
||||||
importlib.reload(ap)
|
importlib.reload(ap)
|
||||||
@ -49,6 +50,10 @@ _ = PluginInternationalization("Fediverse")
|
|||||||
_username_regexp = re.compile("@(?P<localuser>[^@ ]+)@(?P<hostname>[^@ ]+)")
|
_username_regexp = re.compile("@(?P<localuser>[^@ ]+)@(?P<hostname>[^@ ]+)")
|
||||||
|
|
||||||
|
|
||||||
|
def html_to_text(html):
|
||||||
|
return utils.web.htmlToText(html).split("\n", 1)[0].strip()
|
||||||
|
|
||||||
|
|
||||||
class FediverseHttp(httpserver.SupyHTTPServerCallback):
|
class FediverseHttp(httpserver.SupyHTTPServerCallback):
|
||||||
name = "minimal ActivityPub server"
|
name = "minimal ActivityPub server"
|
||||||
defaultResponse = _(
|
defaultResponse = _(
|
||||||
@ -222,18 +227,40 @@ class Fediverse(callbacks.PluginRegexp):
|
|||||||
name = actor.get("name", username)
|
name = actor.get("name", username)
|
||||||
return "\x02%s\x02 (@%s@%s)" % (name, username, hostname)
|
return "\x02%s\x02 (@%s@%s)" % (name, username, hostname)
|
||||||
|
|
||||||
|
def _format_author(self, irc, author):
|
||||||
|
if isinstance(author, str):
|
||||||
|
# it's an URL
|
||||||
|
try:
|
||||||
|
author = self._get_actor(irc, author)
|
||||||
|
except ap.ActivityPubError as e:
|
||||||
|
return _("<error: %s>") % str(e)
|
||||||
|
else:
|
||||||
|
return self._format_actor_fullname(author)
|
||||||
|
elif isinstance(author, dict):
|
||||||
|
if author.get("type") == "Group":
|
||||||
|
# Typically, there is an actor named "Default <username> channel"
|
||||||
|
# on PeerTube, which we do not want to show.
|
||||||
|
return None
|
||||||
|
if author.get("id"):
|
||||||
|
return self._format_author(irc, author["id"])
|
||||||
|
elif isinstance(author, list):
|
||||||
|
return format(
|
||||||
|
"%L",
|
||||||
|
filter(
|
||||||
|
bool, [self._format_author(irc, item) for item in author]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return "<unknown>"
|
||||||
|
|
||||||
def _format_status(self, irc, msg, status):
|
def _format_status(self, irc, msg, status):
|
||||||
if status["type"] == "Create":
|
if status["type"] == "Create":
|
||||||
return self._format_status(irc, msg, status["object"])
|
return self._format_status(irc, msg, status["object"])
|
||||||
elif status["type"] == "Note":
|
elif status["type"] == "Note":
|
||||||
author_url = status["attributedTo"]
|
|
||||||
try:
|
|
||||||
author = self._get_actor(irc, author_url)
|
|
||||||
except ap.ActivityPubError as e:
|
|
||||||
author_fullname = _("<error: %s>") % str(e)
|
|
||||||
else:
|
|
||||||
author_fullname = self._format_actor_fullname(author)
|
|
||||||
cw = status.get("summary")
|
cw = status.get("summary")
|
||||||
|
author_fullname = self._format_author(
|
||||||
|
irc, status.get("attributedTo")
|
||||||
|
)
|
||||||
if cw:
|
if cw:
|
||||||
if self.registryValue(
|
if self.registryValue(
|
||||||
"format.statuses.showContentWithCW",
|
"format.statuses.showContentWithCW",
|
||||||
@ -246,7 +273,7 @@ class Fediverse(callbacks.PluginRegexp):
|
|||||||
% (
|
% (
|
||||||
author_fullname,
|
author_fullname,
|
||||||
cw,
|
cw,
|
||||||
utils.web.htmlToText(status["content"]),
|
html_to_text(status["content"]),
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
@ -258,7 +285,7 @@ class Fediverse(callbacks.PluginRegexp):
|
|||||||
_("%s: %s")
|
_("%s: %s")
|
||||||
% (
|
% (
|
||||||
author_fullname,
|
author_fullname,
|
||||||
utils.web.htmlToText(status["content"]),
|
html_to_text(status["content"]),
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -275,6 +302,17 @@ class Fediverse(callbacks.PluginRegexp):
|
|||||||
return self._format_status(irc, msg, status)
|
return self._format_status(irc, msg, status)
|
||||||
except ap.ActivityPubProtocolError as e:
|
except ap.ActivityPubProtocolError as e:
|
||||||
return "<Could not fetch status: %s>" % e.args[0]
|
return "<Could not fetch status: %s>" % e.args[0]
|
||||||
|
elif status["type"] == "Video":
|
||||||
|
author_fullname = self._format_author(
|
||||||
|
irc, status.get("attributedTo")
|
||||||
|
)
|
||||||
|
return format(
|
||||||
|
_("\x02%s\x02 (%T) by %s: %s"),
|
||||||
|
status["name"],
|
||||||
|
abs(parse_xsd_duration(status["duration"]).total_seconds()),
|
||||||
|
author_fullname,
|
||||||
|
html_to_text(status["content"]),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
assert False, "Unknown status type %s: %r" % (
|
assert False, "Unknown status type %s: %r" % (
|
||||||
status["type"],
|
status["type"],
|
||||||
@ -292,14 +330,14 @@ class Fediverse(callbacks.PluginRegexp):
|
|||||||
_("%s: %s")
|
_("%s: %s")
|
||||||
% (
|
% (
|
||||||
self._format_actor_fullname(actor),
|
self._format_actor_fullname(actor),
|
||||||
utils.web.htmlToText(actor["summary"]),
|
html_to_text(actor["summary"]),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def _format_profile(self, irc, msg, actor):
|
def _format_profile(self, irc, msg, actor):
|
||||||
return _("%s: %s") % (
|
return _("%s: %s") % (
|
||||||
self._format_actor_fullname(actor),
|
self._format_actor_fullname(actor),
|
||||||
utils.web.htmlToText(actor["summary"]),
|
html_to_text(actor["summary"]),
|
||||||
)
|
)
|
||||||
|
|
||||||
def usernameSnarfer(self, irc, msg, match):
|
def usernameSnarfer(self, irc, msg, match):
|
||||||
|
@ -60,6 +60,10 @@ from .test_data import (
|
|||||||
BOOSTED_DATA,
|
BOOSTED_DATA,
|
||||||
BOOSTED_ACTOR_URL,
|
BOOSTED_ACTOR_URL,
|
||||||
BOOSTED_ACTOR_DATA,
|
BOOSTED_ACTOR_DATA,
|
||||||
|
PEERTUBE_VIDEO_URL,
|
||||||
|
PEERTUBE_VIDEO_DATA,
|
||||||
|
PEERTUBE_ACTOR_URL,
|
||||||
|
PEERTUBE_ACTOR_DATA,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -251,6 +255,19 @@ class NetworklessFediverseTestCase(BaseFediverseTestCase):
|
|||||||
"\x02someuser\x02 (@someuser@example.org): My Biography",
|
"\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):
|
def testProfileSnarfer(self):
|
||||||
with self.mockWebfingerSupport("not called"), self.mockRequests([]):
|
with self.mockWebfingerSupport("not called"), self.mockRequests([]):
|
||||||
self.assertSnarfNoResponse("aaa @nonexistinguser@example.org bbb")
|
self.assertSnarfNoResponse("aaa @nonexistinguser@example.org bbb")
|
||||||
@ -430,6 +447,20 @@ class NetworklessFediverseTestCase(BaseFediverseTestCase):
|
|||||||
+ "<https://example.net/system/media_attachments/image.png>",
|
+ "<https://example.net/system/media_attachments/image.png>",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def testVideo(self):
|
||||||
|
expected_requests = [
|
||||||
|
(PEERTUBE_VIDEO_URL, PEERTUBE_VIDEO_DATA),
|
||||||
|
(PEERTUBE_ACTOR_URL, PEERTUBE_ACTOR_DATA),
|
||||||
|
]
|
||||||
|
|
||||||
|
with self.mockRequests(expected_requests):
|
||||||
|
self.assertResponse(
|
||||||
|
"status https://example.org/w/gABde9e210FGHre",
|
||||||
|
"\x02name of video\x02 (1 hour, 26 minutes, and 0 seconds) "
|
||||||
|
"by \x02chocobozzz\x02 (@chocobozzz@peertube.cpy.re): "
|
||||||
|
"description of the video with a second line",
|
||||||
|
)
|
||||||
|
|
||||||
def testStatusUrlSnarferDisabled(self):
|
def testStatusUrlSnarferDisabled(self):
|
||||||
with self.mockWebfingerSupport("not called"), self.mockRequests([]):
|
with self.mockWebfingerSupport("not called"), self.mockRequests([]):
|
||||||
self.assertSnarfNoResponse(
|
self.assertSnarfNoResponse(
|
||||||
|
@ -384,3 +384,124 @@ BOOSTED_ACTOR_VALUE = {
|
|||||||
"endpoints": {"sharedInbox": "https://example.net/inbox"},
|
"endpoints": {"sharedInbox": "https://example.net/inbox"},
|
||||||
}
|
}
|
||||||
BOOSTED_ACTOR_DATA = json.dumps(BOOSTED_ACTOR_VALUE).encode()
|
BOOSTED_ACTOR_DATA = json.dumps(BOOSTED_ACTOR_VALUE).encode()
|
||||||
|
|
||||||
|
PEERTUBE_ACTOR_VALUE = {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
"https://w3id.org/security/v1",
|
||||||
|
{"RsaSignature2017": "https://w3id.org/security#RsaSignature2017"},
|
||||||
|
{
|
||||||
|
"pt": "https://joinpeertube.org/ns#",
|
||||||
|
"sc": "http://schema.org/",
|
||||||
|
"playlists": {"@id": "pt:playlists", "@type": "@id"},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"type": "Person",
|
||||||
|
"id": "https://peertube.cpy.re/accounts/chocobozzz",
|
||||||
|
"following": "https://peertube.cpy.re/accounts/chocobozzz/following",
|
||||||
|
"followers": "https://peertube.cpy.re/accounts/chocobozzz/followers",
|
||||||
|
"playlists": "https://peertube.cpy.re/accounts/chocobozzz/playlists",
|
||||||
|
"inbox": "https://peertube.cpy.re/accounts/chocobozzz/inbox",
|
||||||
|
"outbox": "https://peertube.cpy.re/accounts/chocobozzz/outbox",
|
||||||
|
"preferredUsername": "chocobozzz",
|
||||||
|
"url": "https://peertube.cpy.re/accounts/chocobozzz",
|
||||||
|
"name": "chocobozzz",
|
||||||
|
"published": "2017-11-28T08:48:24.271Z",
|
||||||
|
"summary": None,
|
||||||
|
}
|
||||||
|
PEERTUBE_ACTOR_DATA = json.dumps(PEERTUBE_ACTOR_VALUE).encode()
|
||||||
|
PEERTUBE_ACTOR_URL = "https://peertube.cpy.re/accounts/chocobozzz"
|
||||||
|
|
||||||
|
|
||||||
|
PEERTUBE_VIDEO_VALUE = {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
"https://w3id.org/security/v1",
|
||||||
|
{"RsaSignature2017": "https://w3id.org/security#RsaSignature2017"},
|
||||||
|
{
|
||||||
|
"pt": "https://joinpeertube.org/ns#",
|
||||||
|
"sc": "http://schema.org/",
|
||||||
|
"Hashtag": "as:Hashtag",
|
||||||
|
"uuid": "sc:identifier",
|
||||||
|
"category": "sc:category",
|
||||||
|
"licence": "sc:license",
|
||||||
|
"subtitleLanguage": "sc:subtitleLanguage",
|
||||||
|
"sensitive": "as:sensitive",
|
||||||
|
"language": "sc:inLanguage",
|
||||||
|
"icons": "as:icon",
|
||||||
|
"isLiveBroadcast": "sc:isLiveBroadcast",
|
||||||
|
"liveSaveReplay": {
|
||||||
|
"@type": "sc:Boolean",
|
||||||
|
"@id": "pt:liveSaveReplay",
|
||||||
|
},
|
||||||
|
"permanentLive": {
|
||||||
|
"@type": "sc:Boolean",
|
||||||
|
"@id": "pt:permanentLive",
|
||||||
|
},
|
||||||
|
"latencyMode": {"@type": "sc:Number", "@id": "pt:latencyMode"},
|
||||||
|
"Infohash": "pt:Infohash",
|
||||||
|
"originallyPublishedAt": "sc:datePublished",
|
||||||
|
"views": {"@type": "sc:Number", "@id": "pt:views"},
|
||||||
|
"state": {"@type": "sc:Number", "@id": "pt:state"},
|
||||||
|
"size": {"@type": "sc:Number", "@id": "pt:size"},
|
||||||
|
"fps": {"@type": "sc:Number", "@id": "pt:fps"},
|
||||||
|
"commentsEnabled": {
|
||||||
|
"@type": "sc:Boolean",
|
||||||
|
"@id": "pt:commentsEnabled",
|
||||||
|
},
|
||||||
|
"downloadEnabled": {
|
||||||
|
"@type": "sc:Boolean",
|
||||||
|
"@id": "pt:downloadEnabled",
|
||||||
|
},
|
||||||
|
"waitTranscoding": {
|
||||||
|
"@type": "sc:Boolean",
|
||||||
|
"@id": "pt:waitTranscoding",
|
||||||
|
},
|
||||||
|
"support": {"@type": "sc:Text", "@id": "pt:support"},
|
||||||
|
"likes": {"@id": "as:likes", "@type": "@id"},
|
||||||
|
"dislikes": {"@id": "as:dislikes", "@type": "@id"},
|
||||||
|
"shares": {"@id": "as:shares", "@type": "@id"},
|
||||||
|
"comments": {"@id": "as:comments", "@type": "@id"},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
"type": "Video",
|
||||||
|
"name": "name of video",
|
||||||
|
"duration": "PT5160S",
|
||||||
|
"tag": [{"type": "Hashtag", "name": "vostfr"}],
|
||||||
|
"category": {"identifier": "2", "name": "Films"},
|
||||||
|
"licence": {"identifier": "4", "name": "Attribution - Non Commercial"},
|
||||||
|
"language": {"identifier": "en", "name": "English"},
|
||||||
|
"views": 13718,
|
||||||
|
"sensitive": False,
|
||||||
|
"waitTranscoding": False,
|
||||||
|
"state": 1,
|
||||||
|
"commentsEnabled": True,
|
||||||
|
"downloadEnabled": True,
|
||||||
|
"published": "2017-10-23T07:54:38.155Z",
|
||||||
|
"originallyPublishedAt": None,
|
||||||
|
"updated": "2022-07-13T07:03:12.373Z",
|
||||||
|
"mediaType": "text/markdown",
|
||||||
|
"content": "description of <strong>the</strong> video\r\nwith a second line",
|
||||||
|
"support": None,
|
||||||
|
"subtitleLanguage": [],
|
||||||
|
"icon": [
|
||||||
|
# redacted
|
||||||
|
],
|
||||||
|
"url": [
|
||||||
|
# redacted
|
||||||
|
],
|
||||||
|
"attributedTo": [
|
||||||
|
{"type": "Person", "id": PEERTUBE_ACTOR_URL},
|
||||||
|
{
|
||||||
|
"type": "Group",
|
||||||
|
"id": ACTOR_URL,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"isLiveBroadcast": False,
|
||||||
|
"liveSaveReplay": None,
|
||||||
|
"permanentLive": None,
|
||||||
|
"latencyMode": None,
|
||||||
|
}
|
||||||
|
PEERTUBE_VIDEO_DATA = json.dumps(PEERTUBE_VIDEO_VALUE).encode()
|
||||||
|
PEERTUBE_VIDEO_URL = "https://example.org/w/gABde9e210FGHre"
|
||||||
|
63
plugins/Fediverse/utils.py
Normal file
63
plugins/Fediverse/utils.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
###
|
||||||
|
# Copyright (c) 2022, Valentin Lorentz
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
#
|
||||||
|
# * Redistributions of source code must retain the above copyright notice,
|
||||||
|
# this list of conditions, and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
# this list of conditions, and the following disclaimer in the
|
||||||
|
# documentation and/or other materials provided with the distribution.
|
||||||
|
# * Neither the name of the author of this software nor the name of
|
||||||
|
# contributors to this software may be used to endorse or promote products
|
||||||
|
# derived from this software without specific prior written consent.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||||
|
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
import re
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
# Credits for the regexp and function: https://stackoverflow.com/a/2765366/539465
|
||||||
|
_XSD_DURATION_RE = re.compile(
|
||||||
|
"(?P<sign>-?)P"
|
||||||
|
"(?:(?P<years>\d+)Y)?"
|
||||||
|
"(?:(?P<months>\d+)M)?"
|
||||||
|
"(?:(?P<days>\d+)D)?"
|
||||||
|
"(?:T(?:(?P<hours>\d+)H)?(?:(?P<minutes>\d+)M)?(?:(?P<seconds>\d+)S)?)?"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_xsd_duration(s):
|
||||||
|
"""Parses this format to a timedelta:
|
||||||
|
https://www.w3.org/TR/xmlschema11-2/#duration"""
|
||||||
|
# Fetch the match groups with default value of 0 (not None)
|
||||||
|
duration = _XSD_DURATION_RE.match(s).groupdict(0)
|
||||||
|
|
||||||
|
# Create the timedelta object from extracted groups
|
||||||
|
delta = datetime.timedelta(
|
||||||
|
days=int(duration["days"])
|
||||||
|
+ (int(duration["months"]) * 30)
|
||||||
|
+ (int(duration["years"]) * 365),
|
||||||
|
hours=int(duration["hours"]),
|
||||||
|
minutes=int(duration["minutes"]),
|
||||||
|
seconds=int(duration["seconds"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
if duration["sign"] == "-":
|
||||||
|
delta *= -1
|
||||||
|
|
||||||
|
return delta
|
@ -43,8 +43,11 @@ class GamesTestCase(ChannelPluginTestCase):
|
|||||||
'Got a msg without bang|click|spin: %r' % m)
|
'Got a msg without bang|click|spin: %r' % m)
|
||||||
elif m.command == 'KICK':
|
elif m.command == 'KICK':
|
||||||
sawKick = True
|
sawKick = True
|
||||||
self.assertTrue('bang' in m.args[2].lower(),
|
self.assertIn(
|
||||||
'Got a KICK without bang in it.')
|
'bang',
|
||||||
|
m.args[2].lower(),
|
||||||
|
'Got a KICK without bang in it.'
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.fail('Got something other than a kick or a privmsg.')
|
self.fail('Got something other than a kick or a privmsg.')
|
||||||
self.assertTrue(sawKick, 'Didn\'t get a kick in %s iterations!' % i)
|
self.assertTrue(sawKick, 'Didn\'t get a kick in %s iterations!' % i)
|
||||||
|
@ -150,7 +150,8 @@ class Geography(callbacks.Plugin):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
offset_seconds = int(
|
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)
|
offset = self._format_utc_offset(offset_seconds)
|
||||||
|
|
||||||
# Extract a human-friendly name, depending on the type of
|
# Extract a human-friendly name, depending on the type of
|
||||||
|
@ -83,7 +83,7 @@ class GeographyTimezoneTestCase(PluginTestCase):
|
|||||||
with patch.object(wikidata, "timezone_from_uri", return_value=tz):
|
with patch.object(wikidata, "timezone_from_uri", return_value=tz):
|
||||||
self.assertRegexp(
|
self.assertRegexp(
|
||||||
"timezone Newfoundland",
|
"timezone Newfoundland",
|
||||||
r"Canada/Newfoundland \(currently UTC-[23]:30\)"
|
r"Canada/Newfoundland \(currently UTC-[23]:30\)",
|
||||||
)
|
)
|
||||||
|
|
||||||
tz = pytz.timezone("Asia/Kolkata")
|
tz = pytz.timezone("Asia/Kolkata")
|
||||||
@ -111,7 +111,7 @@ class GeographyTimezoneTestCase(PluginTestCase):
|
|||||||
with patch.object(wikidata, "timezone_from_uri", return_value=tz):
|
with patch.object(wikidata, "timezone_from_uri", return_value=tz):
|
||||||
self.assertRegexp(
|
self.assertRegexp(
|
||||||
"timezone Newfoundland",
|
"timezone Newfoundland",
|
||||||
r"Canada/Newfoundland \(currently UTC-[23]:30\)"
|
r"Canada/Newfoundland \(currently UTC-[23]:30\)",
|
||||||
)
|
)
|
||||||
|
|
||||||
tz = zoneinfo.ZoneInfo("Asia/Kolkata")
|
tz = zoneinfo.ZoneInfo("Asia/Kolkata")
|
||||||
@ -144,9 +144,7 @@ class GeographyTimezoneTestCase(PluginTestCase):
|
|||||||
self.assertRegexp(
|
self.assertRegexp(
|
||||||
"timezone Delhi", r"Asia/Kolkata \(currently UTC\+5:30\)"
|
"timezone Delhi", r"Asia/Kolkata \(currently UTC\+5:30\)"
|
||||||
)
|
)
|
||||||
self.assertRegexp(
|
self.assertRegexp("timezone Newfoundland", r"UTC-[23]:30")
|
||||||
"timezone Newfoundland", r"UTC-[23]:30"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class GeographyLocaltimeTestCase(PluginTestCase):
|
class GeographyLocaltimeTestCase(PluginTestCase):
|
||||||
|
@ -137,7 +137,9 @@ def timezone_from_uri(location_uri):
|
|||||||
"http://www.wikidata.org/entity/Q17272692", # IANA timezones first
|
"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/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"]
|
results = data["results"]["bindings"]
|
||||||
for result in results:
|
for result in results:
|
||||||
if "tzid" in result:
|
if "tzid" in result:
|
||||||
|
@ -43,21 +43,6 @@ Check: `Supported language codes`_
|
|||||||
Commands
|
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:
|
.. _command-google-google:
|
||||||
|
|
||||||
google <search> [--{filter,language} <value>]
|
google <search> [--{filter,language} <value>]
|
||||||
@ -68,11 +53,6 @@ google <search> [--{filter,language} <value>]
|
|||||||
lucky [--snippet] <search>
|
lucky [--snippet] <search>
|
||||||
Does a google search, but only returns the first result. If option --snippet is given, returns also the page text snippet.
|
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:
|
.. _command-google-translate:
|
||||||
|
|
||||||
translate <source language> [to] <target language> <text>
|
translate <source language> [to] <target language> <text>
|
||||||
|
@ -231,52 +231,6 @@ class Google(callbacks.PluginRegexp):
|
|||||||
'filter':''}),
|
'filter':''}),
|
||||||
'text'])
|
'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):
|
def _translate(self, sourceLang, targetLang, text):
|
||||||
headers = dict(utils.web.defaultHeaders)
|
headers = dict(utils.web.defaultHeaders)
|
||||||
headers['User-agent'] = ('Mozilla/5.0 (X11; U; Linux i686) '
|
headers['User-agent'] = ('Mozilla/5.0 (X11; U; Linux i686) '
|
||||||
@ -339,59 +293,6 @@ class Google(callbacks.PluginRegexp):
|
|||||||
(self.registryValue('baseUrl', channel, network), s)
|
(self.registryValue('baseUrl', channel, network), s)
|
||||||
return url
|
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
|
Class = Google
|
||||||
|
|
||||||
|
@ -34,21 +34,6 @@ from supybot.test import *
|
|||||||
class GoogleTestCase(ChannelPluginTestCase):
|
class GoogleTestCase(ChannelPluginTestCase):
|
||||||
plugins = ('Google', 'Config')
|
plugins = ('Google', 'Config')
|
||||||
if network:
|
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):
|
def testSearch(self):
|
||||||
self.assertNotError('google foo')
|
self.assertNotError('google foo')
|
||||||
self.assertRegexp('google dupa', r'dupa')
|
self.assertRegexp('google dupa', r'dupa')
|
||||||
@ -76,14 +61,7 @@ class GoogleTestCase(ChannelPluginTestCase):
|
|||||||
self.assertNotError('config plugins.Google.oneToOne True')
|
self.assertNotError('config plugins.Google.oneToOne True')
|
||||||
self.assertNotRegexp('google dupa', ';')
|
self.assertNotRegexp('google dupa', ';')
|
||||||
|
|
||||||
def testFight(self):
|
|
||||||
self.assertRegexp('fight supybot moobot', r'.*supybot.*: \d+')
|
|
||||||
self.assertNotError('fight ... !')
|
|
||||||
|
|
||||||
def testTranslate(self):
|
def testTranslate(self):
|
||||||
self.assertRegexp('translate en es hello world', 'Hola mundo')
|
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:
|
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
|
||||||
|
@ -209,7 +209,7 @@ class SqliteKarmaDB(object):
|
|||||||
|
|
||||||
def load(self, channel, filename):
|
def load(self, channel, filename):
|
||||||
filename = conf.supybot.directories.data.dirize(filename)
|
filename = conf.supybot.directories.data.dirize(filename)
|
||||||
fd = open(filename)
|
fd = open(filename, encoding='utf8')
|
||||||
reader = csv.reader(fd)
|
reader = csv.reader(fd)
|
||||||
db = self._getDb(channel)
|
db = self._getDb(channel)
|
||||||
cursor = db.cursor()
|
cursor = db.cursor()
|
||||||
|
@ -44,7 +44,7 @@ class LimiterTestCase(ChannelPluginTestCase):
|
|||||||
self.assertEqual(m, ircmsgs.limit('#foo', 1+10))
|
self.assertEqual(m, ircmsgs.limit('#foo', 1+10))
|
||||||
self.irc.feedMsg(ircmsgs.join('#foo', prefix='bar!root@host'))
|
self.irc.feedMsg(ircmsgs.join('#foo', prefix='bar!root@host'))
|
||||||
m = self.irc.takeMsg()
|
m = self.irc.takeMsg()
|
||||||
self.assertFalse(m is not None)
|
self.assertIsNone(m)
|
||||||
conf.supybot.plugins.Limiter.maximumExcess.setValue(7)
|
conf.supybot.plugins.Limiter.maximumExcess.setValue(7)
|
||||||
self.irc.feedMsg(ircmsgs.part('#foo', prefix='bar!root@host'))
|
self.irc.feedMsg(ircmsgs.part('#foo', prefix='bar!root@host'))
|
||||||
m = self.irc.takeMsg()
|
m = self.irc.takeMsg()
|
||||||
|
@ -166,8 +166,8 @@ class Math(callbacks.Plugin):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
self.log.info('evaluating %q from %s', text, msg.prefix)
|
self.log.info('evaluating %q from %s', text, msg.prefix)
|
||||||
x = safe_eval(text, allow_ints=True)
|
result = safe_eval(text, allow_ints=True)
|
||||||
irc.reply(str(x))
|
float(result) # fail early if it is too large to be displayed
|
||||||
except OverflowError:
|
except OverflowError:
|
||||||
maxFloat = math.ldexp(0.9999999999999999, 1024)
|
maxFloat = math.ldexp(0.9999999999999999, 1024)
|
||||||
irc.error(_('The answer exceeded %s or so.') % maxFloat)
|
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])
|
irc.error(_('%s is not a defined function.') % str(e).split()[1])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
irc.error(utils.exnToString(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'])
|
icalc = wrap(icalc, [('checkCapability', 'trusted'), 'text'])
|
||||||
|
|
||||||
_rpnEnv = {
|
_rpnEnv = {
|
||||||
|
@ -112,7 +112,10 @@ class MathTestCase(PluginTestCase):
|
|||||||
self.assertNotError('calc (1600 * 1200) - 2*(1024*1280)')
|
self.assertNotError('calc (1600 * 1200) - 2*(1024*1280)')
|
||||||
self.assertNotError('calc 3-2*4')
|
self.assertNotError('calc 3-2*4')
|
||||||
self.assertNotError('calc (1600 * 1200)-2*(1024*1280)')
|
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):
|
def testCalcNoNameError(self):
|
||||||
self.assertRegexp('calc foobar(x)', 'foobar is not a defined function')
|
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 1^1', '0')
|
||||||
self.assertResponse('icalc 10**24', '1' + '0'*24)
|
self.assertResponse('icalc 10**24', '1' + '0'*24)
|
||||||
self.assertRegexp('icalc 49/6', '8.16')
|
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):
|
def testRpn(self):
|
||||||
self.assertResponse('rpn 5 2 +', '7')
|
self.assertResponse('rpn 5 2 +', '7')
|
||||||
|
@ -163,6 +163,20 @@ class MessageParser(callbacks.Plugin, plugins.ChannelDBHandler):
|
|||||||
channel = msg.channel
|
channel = msg.channel
|
||||||
if not channel:
|
if not channel:
|
||||||
return
|
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):
|
if self.registryValue('enable', channel, irc.network):
|
||||||
actions = []
|
actions = []
|
||||||
results = []
|
results = []
|
||||||
|
@ -116,10 +116,31 @@ class MessageParserTestCase(ChannelPluginTestCase):
|
|||||||
|
|
||||||
def testTrigger(self):
|
def testTrigger(self):
|
||||||
self.assertNotError('messageparser add "stuff" "echo i saw some stuff"')
|
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(' ')
|
m = self.getMsg(' ')
|
||||||
self.assertTrue(str(m).startswith('PRIVMSG #test :i saw some stuff'))
|
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):
|
def testMaxTriggers(self):
|
||||||
self.assertNotError('messageparser add "stuff" "echo i saw some stuff"')
|
self.assertNotError('messageparser add "stuff" "echo i saw some stuff"')
|
||||||
self.assertNotError('messageparser add "sbd" "echo i saw somebody"')
|
self.assertNotError('messageparser add "sbd" "echo i saw somebody"')
|
||||||
|
@ -130,7 +130,7 @@ class MiscTestCase(ChannelPluginTestCase):
|
|||||||
def testHelpIncludeFullCommandName(self):
|
def testHelpIncludeFullCommandName(self):
|
||||||
self.assertHelp('help channel capability add')
|
self.assertHelp('help channel capability add')
|
||||||
m = self.getMsg('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):
|
def testHelpDoesAmbiguityWithDefaultPlugins(self):
|
||||||
m = self.getMsg('help list') # Misc.list and User.list.
|
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'
|
oldprefix, self.prefix = self.prefix, 'tester!foo@bar__no_testcap__baz'
|
||||||
self.nick = 'tester'
|
self.nick = 'tester'
|
||||||
m = self.getMsg('tell aljsdkfh [plugin tell]')
|
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]')
|
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.getMsg('tell me you love me')
|
||||||
m = self.irc.takeMsg()
|
m = self.irc.takeMsg()
|
||||||
self.assertTrue(m.args[0] == self.nick)
|
self.assertEqual(m.args[0], self.nick)
|
||||||
|
|
||||||
def testNoNestedTell(self):
|
def testNoNestedTell(self):
|
||||||
self.assertRegexp('echo [tell %s foo]' % self.nick, 'nested')
|
self.assertRegexp('echo [tell %s foo]' % self.nick, 'nested')
|
||||||
@ -271,7 +271,7 @@ class MiscTestCase(ChannelPluginTestCase):
|
|||||||
self.assertResponse('more',
|
self.assertResponse('more',
|
||||||
'abc '*112 + ' \x02(2 more messages)\x02')
|
'abc '*112 + ' \x02(2 more messages)\x02')
|
||||||
m = self.irc.takeMsg()
|
m = self.irc.takeMsg()
|
||||||
self.assertIsNot(m, None)
|
self.assertIsNotNone(m)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
m.args[1],
|
m.args[1],
|
||||||
'abc '*112 + ' \x02(1 more message)\x02')
|
'abc '*112 + ' \x02(1 more message)\x02')
|
||||||
|
@ -20,13 +20,24 @@ To add factoid say
|
|||||||
``@something is something`` And when you call ``@something`` the bot says
|
``@something is something`` And when you call ``@something`` the bot says
|
||||||
``something is something``.
|
``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.``
|
``@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.
|
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 is <action> tests.`` and everytime when someone calls for
|
||||||
``test`` the bot answers ``* bot tests.``
|
``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-MoobotFactoids:
|
||||||
|
|
||||||
Commands
|
Commands
|
||||||
|
@ -293,12 +293,23 @@ class MoobotFactoids(callbacks.Plugin):
|
|||||||
``@something is something`` And when you call ``@something`` the bot says
|
``@something is something`` And when you call ``@something`` the bot says
|
||||||
``something is something``.
|
``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.``
|
``@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.
|
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 is <action> tests.`` and everytime when someone calls for
|
||||||
``test`` the bot answers ``* bot tests.``
|
``test`` the bot answers ``* bot tests.``
|
||||||
|
|
||||||
|
If you want the factoid to have random answers say (for example):
|
||||||
|
``@fruit is <reply> (orange|apple|banana)``. So when ``@fruit`` is called
|
||||||
|
the bot will reply with one of the listed fruits (random): ``orange``.
|
||||||
|
|
||||||
|
If you want to replace the value of the factoid, for example:
|
||||||
|
``@no Hi is <reply> Hey`` when you call ``@hi`` the bot says ``Hey``.
|
||||||
|
|
||||||
|
If you want to append to the current value of a factoid say:
|
||||||
|
``@Hi is also Hello``, so that when you call ``@hi`` the
|
||||||
|
bot says ``Hey, or Hello.``
|
||||||
"""
|
"""
|
||||||
callBefore = ['Dunno']
|
callBefore = ['Dunno']
|
||||||
def __init__(self, irc):
|
def __init__(self, irc):
|
||||||
|
@ -54,8 +54,11 @@ class OptionListTestCase(SupyTestCase):
|
|||||||
while max and L:
|
while max and L:
|
||||||
max -= 1
|
max -= 1
|
||||||
option = plugin.pickOptions(s)
|
option = plugin.pickOptions(s)
|
||||||
self.assertTrue(option in original,
|
self.assertIn(
|
||||||
'Option %s not in %s' % (option, original))
|
option,
|
||||||
|
original,
|
||||||
|
'Option %s not in %s' % (option, original)
|
||||||
|
)
|
||||||
if option in L:
|
if option in L:
|
||||||
L.remove(option)
|
L.remove(option)
|
||||||
self.assertFalse(L, 'Some options never seen: %s' % L)
|
self.assertFalse(L, 'Some options never seen: %s' % L)
|
||||||
|
@ -309,8 +309,9 @@ class Owner(callbacks.Plugin):
|
|||||||
# Either sent automatically by the server upon join,
|
# Either sent automatically by the server upon join,
|
||||||
# or triggered by a plugin (why?!)
|
# or triggered by a plugin (why?!)
|
||||||
# Either way, replying to commands from the history would
|
# Either way, replying to commands from the history would
|
||||||
# look weird, because it may have been sent a while ago,
|
# look weird, because they may have been sent a while ago,
|
||||||
# and we may have already answered to it.
|
# and we may have already answered to them.
|
||||||
|
# (this is the same behavior as in PluginRegexp.doPrivmsg)
|
||||||
return
|
return
|
||||||
|
|
||||||
self._doPrivmsgs(irc, msg)
|
self._doPrivmsgs(irc, msg)
|
||||||
|
@ -121,11 +121,11 @@ class GithubRepository(GitRepository):
|
|||||||
assert directory is not None, \
|
assert directory is not None, \
|
||||||
'No valid directory in supybot.directories.plugins.'
|
'No valid directory in supybot.directories.plugins.'
|
||||||
|
|
||||||
|
possibly_incompatible = False
|
||||||
try:
|
try:
|
||||||
assert archive.getmember(prefix + dirname).isdir(), \
|
assert archive.getmember(prefix + dirname).isdir(), \
|
||||||
'This is not a valid plugin (it is a file, not a directory).'
|
'This is not a valid plugin (it is a file, not a directory).'
|
||||||
|
|
||||||
run_2to3 = minisix.PY3
|
|
||||||
for file in archive.getmembers():
|
for file in archive.getmembers():
|
||||||
if file.name.startswith(prefix + dirname):
|
if file.name.startswith(prefix + dirname):
|
||||||
extractedFile = archive.extractfile(file)
|
extractedFile = archive.extractfile(file)
|
||||||
@ -140,41 +140,17 @@ class GithubRepository(GitRepository):
|
|||||||
os.mkdir(newFileName)
|
os.mkdir(newFileName)
|
||||||
else:
|
else:
|
||||||
with open(newFileName, 'ab') as fd:
|
with open(newFileName, 'ab') as fd:
|
||||||
reload_imported = False
|
|
||||||
for line in extractedFile.readlines():
|
for line in extractedFile.readlines():
|
||||||
if minisix.PY3:
|
if file.name.endswith('__init__.py') and \
|
||||||
if b'import reload' in line:
|
line.startswith((b'import config', b'import plugin')):
|
||||||
reload_imported = True
|
possibly_incompatible = True
|
||||||
elif not reload_imported and \
|
|
||||||
b'reload(' in line:
|
|
||||||
fd.write(b'from importlib import reload\n')
|
|
||||||
reload_imported = True
|
|
||||||
fd.write(line)
|
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:
|
finally:
|
||||||
archive.close()
|
archive.close()
|
||||||
del archive
|
del archive
|
||||||
if run_2to3:
|
if possibly_incompatible:
|
||||||
try:
|
return _('Plugin installed. However, it may be incompatible with '
|
||||||
import lib2to3
|
'Python 3 and require manual code changes to work correctly.')
|
||||||
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.')
|
return _('Plugin successfully installed.')
|
||||||
|
|
||||||
def getInfo(self, plugin):
|
def getInfo(self, plugin):
|
||||||
@ -201,65 +177,11 @@ repositories = utils.InsensitivePreservingDict({
|
|||||||
'progval',
|
'progval',
|
||||||
'Supybot-plugins'
|
'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': GithubRepository(
|
||||||
'SpiderDave',
|
'SpiderDave',
|
||||||
'spidey-supybot-plugins',
|
'spidey-supybot-plugins',
|
||||||
'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': GithubRepository(
|
||||||
'Hoaas',
|
'Hoaas',
|
||||||
'Supybot-plugins'
|
'Supybot-plugins'
|
||||||
@ -268,23 +190,11 @@ repositories = utils.InsensitivePreservingDict({
|
|||||||
'nyuszika7h',
|
'nyuszika7h',
|
||||||
'limnoria-plugins'
|
'limnoria-plugins'
|
||||||
),
|
),
|
||||||
'nyuszika7h-old': GithubRepository(
|
|
||||||
'nyuszika7h',
|
|
||||||
'Supybot-plugins'
|
|
||||||
),
|
|
||||||
'resistivecorpse': GithubRepository(
|
|
||||||
'resistivecorpse',
|
|
||||||
'supybot-plugins'
|
|
||||||
),
|
|
||||||
'frumious': GithubRepository(
|
'frumious': GithubRepository(
|
||||||
'frumiousbandersnatch',
|
'frumiousbandersnatch',
|
||||||
'sobrieti-plugins',
|
'sobrieti-plugins',
|
||||||
'plugins',
|
'plugins',
|
||||||
),
|
),
|
||||||
'jonimoose': GithubRepository(
|
|
||||||
'Jonimoose',
|
|
||||||
'Supybot-plugins',
|
|
||||||
),
|
|
||||||
'skgsergio': GithubRepository(
|
'skgsergio': GithubRepository(
|
||||||
'skgsergio',
|
'skgsergio',
|
||||||
'Limnoria-plugins',
|
'Limnoria-plugins',
|
||||||
|
@ -29,11 +29,9 @@
|
|||||||
###
|
###
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from supybot.test import *
|
from supybot.test import *
|
||||||
import supybot.utils.minisix as minisix
|
|
||||||
|
|
||||||
pluginsPath = '%s/test-plugins' % os.getcwd()
|
pluginsPath = '%s/test-plugins' % os.getcwd()
|
||||||
|
|
||||||
@ -62,7 +60,7 @@ class PluginDownloaderTestCase(PluginTestCase):
|
|||||||
|
|
||||||
def testRepolist(self):
|
def testRepolist(self):
|
||||||
self.assertRegexp('repolist', '(.*, )?progval(, .*)?')
|
self.assertRegexp('repolist', '(.*, )?progval(, .*)?')
|
||||||
self.assertRegexp('repolist', '(.*, )?quantumlemur(, .*)?')
|
self.assertRegexp('repolist', '(.*, )?jlu5(, .*)?')
|
||||||
self.assertRegexp('repolist progval', '(.*, )?AttackProtector(, .*)?')
|
self.assertRegexp('repolist progval', '(.*, )?AttackProtector(, .*)?')
|
||||||
|
|
||||||
def testInstallprogval(self):
|
def testInstallprogval(self):
|
||||||
@ -76,44 +74,19 @@ class PluginDownloaderTestCase(PluginTestCase):
|
|||||||
self.assertRegexp('plugindownloader install progval Darcs',
|
self.assertRegexp('plugindownloader install progval Darcs',
|
||||||
'Error:.*not available.*supybot.commands.allowShell')
|
'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):
|
def testInstallNonAsciiInit(self):
|
||||||
self.assertNotError('plugindownloader install Hoaas DuckDuckGo')
|
self.assertNotError('plugindownloader install Hoaas DuckDuckGo')
|
||||||
self._testPluginInstalled('DuckDuckGo')
|
self._testPluginInstalled('DuckDuckGo')
|
||||||
|
|
||||||
|
def testInstallLegacyWarning(self):
|
||||||
|
self.assertRegexp('plugindownloader install frumious Codepoints',
|
||||||
|
'may be incompatible')
|
||||||
|
|
||||||
def testInfo(self):
|
def testInfo(self):
|
||||||
self.assertResponse('plugindownloader info progval Twitter',
|
self.assertResponse('plugindownloader info progval Twitter',
|
||||||
'Advanced Twitter plugin for Supybot, with capabilities '
|
'Advanced Twitter plugin for Supybot, with capabilities '
|
||||||
'handling, and per-channel user account.')
|
'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:
|
if not network:
|
||||||
class PluginDownloaderTestCase(PluginTestCase):
|
class PluginDownloaderTestCase(PluginTestCase):
|
||||||
pass
|
pass
|
||||||
|
@ -61,6 +61,11 @@ add [<channel>] <question> <answer1> [<answer2> [<answer3> [...]]]
|
|||||||
close [<channel>] <poll_id>
|
close [<channel>] <poll_id>
|
||||||
Closes the specified poll.
|
Closes the specified poll.
|
||||||
|
|
||||||
|
.. _command-poll-list:
|
||||||
|
|
||||||
|
list [<channel>]
|
||||||
|
Lists open polls in the <channel>.
|
||||||
|
|
||||||
.. _command-poll-results:
|
.. _command-poll-results:
|
||||||
|
|
||||||
results [<channel>] <poll_id>
|
results [<channel>] <poll_id>
|
||||||
|
@ -53,6 +53,7 @@ from . import config
|
|||||||
from . import plugin
|
from . import plugin
|
||||||
|
|
||||||
from importlib import reload
|
from importlib import reload
|
||||||
|
|
||||||
# In case we're being reloaded.
|
# In case we're being reloaded.
|
||||||
reload(config)
|
reload(config)
|
||||||
reload(plugin)
|
reload(plugin)
|
||||||
|
@ -131,7 +131,7 @@ class Poll_(callbacks.Plugin):
|
|||||||
|
|
||||||
poll_id = max(self._polls[(irc.network, channel)], default=0) + 1
|
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(
|
answer_id_counts = collections.Counter(
|
||||||
id_ for (id_, _) in answers
|
id_ for (id_, _) in answers
|
||||||
@ -149,7 +149,10 @@ class Poll_(callbacks.Plugin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self._polls[(irc.network, channel)][poll_id] = Poll(
|
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)
|
irc.replySuccess(_("Poll # %d created.") % poll_id)
|
||||||
@ -191,6 +194,8 @@ class Poll_(callbacks.Plugin):
|
|||||||
if msg.nick in poll.votes:
|
if msg.nick in poll.votes:
|
||||||
irc.error(_("You already voted on this poll."), Raise=True)
|
irc.error(_("You already voted on this poll."), Raise=True)
|
||||||
|
|
||||||
|
answer_id = answer_id.casefold()
|
||||||
|
|
||||||
if answer_id not in poll.answers:
|
if answer_id not in poll.answers:
|
||||||
irc.error(
|
irc.error(
|
||||||
format(
|
format(
|
||||||
@ -218,11 +223,32 @@ class Poll_(callbacks.Plugin):
|
|||||||
counts.update({answer_id: 0 for answer_id in poll.answers})
|
counts.update({answer_id: 0 for answer_id in poll.answers})
|
||||||
|
|
||||||
results = [
|
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()
|
for (k, v) in counts.most_common()
|
||||||
]
|
]
|
||||||
|
|
||||||
irc.replies(results)
|
irc.replies(results)
|
||||||
|
|
||||||
|
@wrap(["channel"])
|
||||||
|
def list(self, irc, msg, args, channel):
|
||||||
|
"""[<channel>]
|
||||||
|
|
||||||
|
Lists open polls in the <channel>."""
|
||||||
|
results = [
|
||||||
|
format(
|
||||||
|
_("%i: %s (%n)"),
|
||||||
|
poll_id,
|
||||||
|
poll.question,
|
||||||
|
(len(poll.votes), _("vote")),
|
||||||
|
)
|
||||||
|
for (poll_id, poll) in self._polls[(irc.network, channel)].items()
|
||||||
|
if poll.open
|
||||||
|
]
|
||||||
|
|
||||||
|
if results:
|
||||||
|
irc.replies(results)
|
||||||
|
else:
|
||||||
|
irc.reply(_("There are no open polls."))
|
||||||
|
|
||||||
|
|
||||||
Class = Poll_
|
Class = Poll_
|
||||||
|
@ -49,6 +49,17 @@ class PollTestCase(ChannelPluginTestCase):
|
|||||||
"2 votes for No, 1 vote for Yes, and 0 votes for Maybe",
|
"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):
|
def testDoubleVoting(self):
|
||||||
self.assertResponse(
|
self.assertResponse(
|
||||||
'poll add "Is this a test?" "Yes" "No" "Maybe"',
|
'poll add "Is this a test?" "Yes" "No" "Maybe"',
|
||||||
@ -62,6 +73,11 @@ class PollTestCase(ChannelPluginTestCase):
|
|||||||
"voter1: Error: You already voted on this poll.",
|
"voter1: Error: You already voted on this poll.",
|
||||||
frm="voter1!foo@bar",
|
frm="voter1!foo@bar",
|
||||||
)
|
)
|
||||||
|
self.assertResponse(
|
||||||
|
"vote 1 Yes",
|
||||||
|
"VOTER1: Error: You already voted on this poll.",
|
||||||
|
frm="VOTER1!foo@bar",
|
||||||
|
)
|
||||||
|
|
||||||
self.assertRegexp(
|
self.assertRegexp(
|
||||||
"results 1",
|
"results 1",
|
||||||
@ -115,10 +131,58 @@ class PollTestCase(ChannelPluginTestCase):
|
|||||||
def testDuplicateId(self):
|
def testDuplicateId(self):
|
||||||
self.assertResponse(
|
self.assertResponse(
|
||||||
'poll add "Is this a test?" "Yes" "Yes" "Maybe"',
|
'poll add "Is this a test?" "Yes" "Yes" "Maybe"',
|
||||||
"Error: Duplicate answer identifier(s): Yes",
|
"Error: Duplicate answer identifier(s): yes",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertResponse(
|
self.assertResponse(
|
||||||
'poll add "Is this a test?" "Yes totally" "Yes and no" "Maybe"',
|
'poll add "Is this a test?" "Yes totally" "Yes and no" "Maybe"',
|
||||||
"Error: Duplicate answer identifier(s): Yes",
|
"Error: Duplicate answer identifier(s): yes",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def testCaseInsensitive(self):
|
||||||
|
self.assertResponse(
|
||||||
|
'poll add "Is this a test?" "Yeß" "No" "Maybe"',
|
||||||
|
"The operation succeeded. Poll # 1 created.",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertNotError("vote 1 Yeß", frm="voter1!foo@bar")
|
||||||
|
self.assertNotError("vote 1 yESS", frm="voter2!foo@bar")
|
||||||
|
self.assertNotError("vote 1 no", frm="voter3!foo@bar")
|
||||||
|
|
||||||
|
self.assertResponse(
|
||||||
|
"results 1",
|
||||||
|
"2 votes for Yeß, 1 vote for No, and 0 votes for Maybe",
|
||||||
|
)
|
||||||
|
|
||||||
|
def testList(self):
|
||||||
|
self.assertResponse("poll list", "There are no open polls.")
|
||||||
|
|
||||||
|
self.assertResponse(
|
||||||
|
'poll add "Is this a test?" "Yes" "No" "Maybe"',
|
||||||
|
"The operation succeeded. Poll # 1 created.",
|
||||||
|
)
|
||||||
|
self.assertResponse("poll list", "1: Is this a test? (0 votes)")
|
||||||
|
|
||||||
|
self.assertNotError("vote 1 Yes", frm="voter1!foo@bar")
|
||||||
|
self.assertResponse("poll list", "1: Is this a test? (1 vote)")
|
||||||
|
|
||||||
|
self.assertNotError("vote 1 No", frm="voter2!foo@bar")
|
||||||
|
self.assertResponse("poll list", "1: Is this a test? (2 votes)")
|
||||||
|
|
||||||
|
self.assertResponse(
|
||||||
|
'poll add "Is this another test?" "Yes" "No" "Maybe"',
|
||||||
|
"The operation succeeded. Poll # 2 created.",
|
||||||
|
)
|
||||||
|
self.assertResponse(
|
||||||
|
"poll list",
|
||||||
|
"1: Is this a test? (2 votes) and 2: Is this another test? (0 votes)",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertNotError("poll close 1")
|
||||||
|
self.assertResponse(
|
||||||
|
"poll list",
|
||||||
|
"2: Is this another test? (0 votes)",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertNotError("poll close 2")
|
||||||
|
self.assertResponse("poll list", "There are no open polls.")
|
||||||
|
@ -50,6 +50,7 @@ conf.registerChannelValue(Protector, 'enable',
|
|||||||
|
|
||||||
class ImmuneNicks(conf.ValidNicks):
|
class ImmuneNicks(conf.ValidNicks):
|
||||||
List = ircutils.IrcSet
|
List = ircutils.IrcSet
|
||||||
|
sorted = True
|
||||||
|
|
||||||
conf.registerChannelValue(Protector, 'immune',
|
conf.registerChannelValue(Protector, 'immune',
|
||||||
ImmuneNicks([], _("""Determines what nicks the bot will consider to
|
ImmuneNicks([], _("""Determines what nicks the bot will consider to
|
||||||
|
@ -43,7 +43,7 @@ def configure(advanced):
|
|||||||
conf.registerPlugin('RSS', True)
|
conf.registerPlugin('RSS', True)
|
||||||
|
|
||||||
|
|
||||||
class FeedNames(registry.SpaceSeparatedListOfStrings):
|
class FeedNames(registry.SpaceSeparatedSetOfStrings):
|
||||||
List = callbacks.CanonicalNameSet
|
List = callbacks.CanonicalNameSet
|
||||||
|
|
||||||
class FeedItemSortOrder(registry.OnlySomeStrings):
|
class FeedItemSortOrder(registry.OnlySomeStrings):
|
||||||
|
@ -356,8 +356,17 @@ class RSS(callbacks.Plugin):
|
|||||||
handlers.append(ProxyHandler(
|
handlers.append(ProxyHandler(
|
||||||
{'https': utils.force(utils.web.proxy())}))
|
{'https': utils.force(utils.web.proxy())}))
|
||||||
with feed.lock:
|
with feed.lock:
|
||||||
|
try:
|
||||||
d = feedparser.parse(feed.url, etag=feed.etag,
|
d = feedparser.parse(feed.url, etag=feed.etag,
|
||||||
modified=feed.modified, handlers=handlers)
|
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 'status' not in d or d.status != 304: # Not modified
|
||||||
if 'etag' in d:
|
if 'etag' in d:
|
||||||
feed.etag = d.etag
|
feed.etag = d.etag
|
||||||
@ -484,7 +493,7 @@ class RSS(callbacks.Plugin):
|
|||||||
template = self.registryValue(key_name, channel, network)
|
template = self.registryValue(key_name, channel, network)
|
||||||
date = entry.get('published_parsed')
|
date = entry.get('published_parsed')
|
||||||
date = utils.str.timestamp(date)
|
date = utils.str.timestamp(date)
|
||||||
s = string.Template(template).substitute(
|
s = string.Template(template).safe_substitute(
|
||||||
entry,
|
entry,
|
||||||
feed_name=feed.name,
|
feed_name=feed.name,
|
||||||
date=date)
|
date=date)
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
|
|
||||||
import functools
|
import functools
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
import socket
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import feedparser
|
import feedparser
|
||||||
@ -362,7 +363,22 @@ class RSSTestCase(ChannelPluginTestCase):
|
|||||||
timeFastForward(1.1)
|
timeFastForward(1.1)
|
||||||
mock._data = not_well_formed
|
mock._data = not_well_formed
|
||||||
self.assertRegexp('rss http://example.com/',
|
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:
|
if network:
|
||||||
timeout = 5 # Note this applies also to the above tests
|
timeout = 5 # Note this applies also to the above tests
|
||||||
@ -398,7 +414,7 @@ class RSSTestCase(ChannelPluginTestCase):
|
|||||||
timeFastForward(1.1)
|
timeFastForward(1.1)
|
||||||
self.assertNotError('rss %s' % url)
|
self.assertNotError('rss %s' % url)
|
||||||
m = self.assertNotError('rss %s 2' % 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):
|
def testRssAdd(self):
|
||||||
timeFastForward(1.1)
|
timeFastForward(1.1)
|
||||||
|
@ -50,10 +50,12 @@ def configure(advanced):
|
|||||||
class Ignores(registry.SpaceSeparatedListOf):
|
class Ignores(registry.SpaceSeparatedListOf):
|
||||||
List = ircutils.IrcSet
|
List = ircutils.IrcSet
|
||||||
Value = conf.ValidHostmask
|
Value = conf.ValidHostmask
|
||||||
|
sorted = True
|
||||||
|
|
||||||
class Networks(registry.SpaceSeparatedListOf):
|
class Networks(registry.SpaceSeparatedListOf):
|
||||||
List = ircutils.IrcSet
|
List = ircutils.IrcSet
|
||||||
Value = registry.String
|
Value = registry.String
|
||||||
|
sorted = True
|
||||||
|
|
||||||
Relay = conf.registerPlugin('Relay')
|
Relay = conf.registerPlugin('Relay')
|
||||||
conf.registerChannelValue(Relay, 'color',
|
conf.registerChannelValue(Relay, 'color',
|
||||||
|
@ -63,6 +63,6 @@ class ReplyNonChannelTestCase(PluginTestCase):
|
|||||||
self.prefix = 'something!else@somewhere.else'
|
self.prefix = 'something!else@somewhere.else'
|
||||||
self.nick = 'something'
|
self.nick = 'something'
|
||||||
m = self.assertAction('action foo', 'foo')
|
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:
|
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
|
||||||
|
@ -148,6 +148,13 @@ class Scheduler(callbacks.Plugin):
|
|||||||
channel=msg.channel, network=irc.network)
|
channel=msg.channel, network=irc.network)
|
||||||
if remove:
|
if remove:
|
||||||
del self.events[str(f.eventId)]
|
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)
|
self.Proxy(irc, msg, tokens)
|
||||||
return f
|
return f
|
||||||
|
|
||||||
|
@ -89,7 +89,6 @@ class SchedulerTestCase(ChannelPluginTestCase):
|
|||||||
self.assertResponse(
|
self.assertResponse(
|
||||||
'scheduler list', 'There are currently no scheduled commands.')
|
'scheduler list', 'There are currently no scheduled commands.')
|
||||||
|
|
||||||
|
|
||||||
def testRepeat(self):
|
def testRepeat(self):
|
||||||
self.assertRegexp('scheduler repeat repeater 5 echo testRepeat',
|
self.assertRegexp('scheduler repeat repeater 5 echo testRepeat',
|
||||||
'testRepeat')
|
'testRepeat')
|
||||||
@ -132,6 +131,32 @@ class SchedulerTestCase(ChannelPluginTestCase):
|
|||||||
timeFastForward(5)
|
timeFastForward(5)
|
||||||
self.assertNoResponse(' ', timeout=1)
|
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):
|
def testRepeatDisallowsIntegerNames(self):
|
||||||
self.assertError('scheduler repeat 1234 1234 "echo NoIntegerNames"')
|
self.assertError('scheduler repeat 1234 1234 "echo NoIntegerNames"')
|
||||||
|
|
||||||
@ -187,7 +212,5 @@ class SchedulerTestCase(ChannelPluginTestCase):
|
|||||||
self.assertResponse(' ', 'testRepeat', timeout=1) # T+106
|
self.assertResponse(' ', 'testRepeat', timeout=1) # T+106
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
|
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
|
||||||
|
|
||||||
|
@ -86,7 +86,7 @@ class ChannelDBTestCase(ChannelPluginTestCase):
|
|||||||
self.assertError('seen *')
|
self.assertError('seen *')
|
||||||
self.assertNotError('seen %s' % self.nick)
|
self.assertNotError('seen %s' % self.nick)
|
||||||
m = self.assertNotError('seen %s' % self.nick.upper())
|
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,
|
self.assertRegexp('seen user %s' % self.nick,
|
||||||
'^%s was last seen' % self.nick)
|
'^%s was last seen' % self.nick)
|
||||||
self.assertNotError('config plugins.Seen.minimumNonWildcard 0')
|
self.assertNotError('config plugins.Seen.minimumNonWildcard 0')
|
||||||
|
@ -153,6 +153,14 @@ supybot.plugins.Services.disabledNetworks
|
|||||||
|
|
||||||
Determines what networks this plugin will be disabled on.
|
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:
|
.. _conf-supybot.plugins.Services.ghostDelay:
|
||||||
|
|
||||||
|
|
||||||
@ -167,7 +175,7 @@ supybot.plugins.Services.ghostDelay
|
|||||||
supybot.plugins.Services.nicks
|
supybot.plugins.Services.nicks
|
||||||
This config variable defaults to " ", is network-specific, and is not channel-specific.
|
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:
|
.. _conf-supybot.plugins.Services.noJoinsUntilIdentified:
|
||||||
|
|
||||||
|
@ -64,10 +64,11 @@ class ValidNickOrEmptyString(registry.String):
|
|||||||
|
|
||||||
class ValidNickSet(conf.ValidNicks):
|
class ValidNickSet(conf.ValidNicks):
|
||||||
List = ircutils.IrcSet
|
List = ircutils.IrcSet
|
||||||
|
sorted = True
|
||||||
|
|
||||||
Services = conf.registerPlugin('Services')
|
Services = conf.registerPlugin('Services')
|
||||||
conf.registerNetworkValue(Services, 'nicks',
|
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.""")))
|
services.""")))
|
||||||
|
|
||||||
class Networks(registry.SpaceSeparatedSetOfStrings):
|
class Networks(registry.SpaceSeparatedSetOfStrings):
|
||||||
@ -85,6 +86,10 @@ conf.registerNetworkValue(Services, 'noJoinsUntilIdentified',
|
|||||||
conf.registerNetworkValue(Services, 'ghostDelay',
|
conf.registerNetworkValue(Services, 'ghostDelay',
|
||||||
registry.NonNegativeInteger(60, _("""Determines how many seconds the bot will
|
registry.NonNegativeInteger(60, _("""Determines how many seconds the bot will
|
||||||
wait between successive GHOST attempts. Set this to 0 to disable GHOST.""")))
|
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',
|
conf.registerNetworkValue(Services, 'NickServ',
|
||||||
ValidNickOrEmptyString('NickServ', _("""Determines what nick the 'NickServ' service
|
ValidNickOrEmptyString('NickServ', _("""Determines what nick the 'NickServ' service
|
||||||
has.""")))
|
has.""")))
|
||||||
|
@ -162,7 +162,8 @@ class Services(callbacks.Plugin):
|
|||||||
else:
|
else:
|
||||||
self.log.info('Sending ghost (current nick: %s; ghosting: %s)',
|
self.log.info('Sending ghost (current nick: %s; ghosting: %s)',
|
||||||
irc.nick, nick)
|
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).
|
# Ditto about the sendMsg (see _doIdentify).
|
||||||
irc.sendMsg(ircmsgs.privmsg(nickserv, ghost))
|
irc.sendMsg(ircmsgs.privmsg(nickserv, ghost))
|
||||||
state.sentGhost = time.time()
|
state.sentGhost = time.time()
|
||||||
|
@ -50,12 +50,12 @@ class ServicesTestCase(PluginTestCase):
|
|||||||
try:
|
try:
|
||||||
self.assertNotError('services password %s baz' % self.nick)
|
self.assertNotError('services password %s baz' % self.nick)
|
||||||
m = self.assertNotError('services identify')
|
m = self.assertNotError('services identify')
|
||||||
self.assertTrue(m.args[0] == 'NickServ')
|
self.assertEqual(m.args[0], 'NickServ')
|
||||||
self.assertTrue(m.args[1].lower() == 'identify baz')
|
self.assertEqual(m.args[1].lower(), 'identify baz')
|
||||||
self.assertNotError('services password %s biff' % self.nick)
|
self.assertNotError('services password %s biff' % self.nick)
|
||||||
m = self.assertNotError('services identify')
|
m = self.assertNotError('services identify')
|
||||||
self.assertTrue(m.args[0] == 'NickServ')
|
self.assertEqual(m.args[0], 'NickServ')
|
||||||
self.assertTrue(m.args[1].lower() == 'identify biff')
|
self.assertEqual(m.args[1].lower(), 'identify biff')
|
||||||
finally:
|
finally:
|
||||||
self.assertNotError('services password %s ""' % self.nick)
|
self.assertNotError('services password %s ""' % self.nick)
|
||||||
|
|
||||||
@ -84,8 +84,8 @@ class ServicesTestCase(PluginTestCase):
|
|||||||
'Global: bar; test: bar2')
|
'Global: bar; test: bar2')
|
||||||
|
|
||||||
m = self.assertNotError('services identify')
|
m = self.assertNotError('services identify')
|
||||||
self.assertTrue(m.args[0] == 'NickServ')
|
self.assertEqual(m.args[0], 'NickServ')
|
||||||
self.assertTrue(m.args[1].lower() == 'identify bar2')
|
self.assertEqual(m.args[1].lower(), 'identify bar2')
|
||||||
finally:
|
finally:
|
||||||
self.assertNotError('services password %s ""' % self.nick)
|
self.assertNotError('services password %s ""' % self.nick)
|
||||||
|
|
||||||
|
@ -41,8 +41,8 @@ class StatusTestCase(PluginTestCase):
|
|||||||
|
|
||||||
def testCpu(self):
|
def testCpu(self):
|
||||||
m = self.assertNotError('status cpu')
|
m = self.assertNotError('status cpu')
|
||||||
self.assertFalse('kB kB' in m.args[1])
|
self.assertNotIn('kB kB', m.args[1])
|
||||||
self.assertFalse('None' in m.args[1], 'None in cpu output: %r.' % m)
|
self.assertNotIn('None', m.args[1], 'None in cpu output: %r.' % m)
|
||||||
for s in ['linux', 'freebsd', 'openbsd', 'netbsd', 'darwin']:
|
for s in ['linux', 'freebsd', 'openbsd', 'netbsd', 'darwin']:
|
||||||
if sys.platform.startswith(s):
|
if sys.platform.startswith(s):
|
||||||
self.assertTrue('B' in m.args[1] or 'KB' in m.args[1] or
|
self.assertTrue('B' in m.args[1] or 'KB' in m.args[1] or
|
||||||
|
@ -50,8 +50,8 @@ md5 <text>
|
|||||||
|
|
||||||
.. _command-string-ord:
|
.. _command-string-ord:
|
||||||
|
|
||||||
ord <letter>
|
ord <string>
|
||||||
Returns the unicode codepoint of <letter>.
|
Returns the unicode codepoint of characters in <string>.
|
||||||
|
|
||||||
.. _command-string-re:
|
.. _command-string-re:
|
||||||
|
|
||||||
@ -70,8 +70,8 @@ soundex <string> [<length>]
|
|||||||
|
|
||||||
.. _command-string-unicodename:
|
.. _command-string-unicodename:
|
||||||
|
|
||||||
unicodename <character>
|
unicodename <string>
|
||||||
Returns the name of the given unicode <character>.
|
Returns the name of characters in <string>. This will error if any character is not a valid Unicode character.
|
||||||
|
|
||||||
.. _command-string-unicodesearch:
|
.. _command-string-unicodesearch:
|
||||||
|
|
||||||
|
@ -51,13 +51,13 @@ import multiprocessing
|
|||||||
|
|
||||||
class String(callbacks.Plugin):
|
class String(callbacks.Plugin):
|
||||||
"""Provides useful commands for manipulating characters and strings."""
|
"""Provides useful commands for manipulating characters and strings."""
|
||||||
def ord(self, irc, msg, args, letter):
|
def ord(self, irc, msg, args, s):
|
||||||
"""<letter>
|
"""<string>
|
||||||
|
|
||||||
Returns the unicode codepoint of <letter>.
|
Returns the unicode codepoint of characters in <string>.
|
||||||
"""
|
"""
|
||||||
irc.reply(str(ord(letter)))
|
irc.replies([str(ord(char)) for char in s])
|
||||||
ord = wrap(ord, ['letter'])
|
ord = wrap(ord, ['text'])
|
||||||
|
|
||||||
def chr(self, irc, msg, args, i):
|
def chr(self, irc, msg, args, i):
|
||||||
"""<number>
|
"""<number>
|
||||||
@ -70,17 +70,20 @@ class String(callbacks.Plugin):
|
|||||||
irc.error(_('That number doesn\'t map to a unicode character.'))
|
irc.error(_('That number doesn\'t map to a unicode character.'))
|
||||||
chr = wrap(chr, ['int'])
|
chr = wrap(chr, ['int'])
|
||||||
|
|
||||||
def unicodename(self, irc, msg, args, character):
|
def unicodename(self, irc, msg, args, s):
|
||||||
"""<character>
|
"""<string>
|
||||||
|
|
||||||
Returns the name of the given unicode <character>."""
|
Returns the name of characters in <string>.
|
||||||
if len(character) != 1:
|
This will error if any character is not a valid Unicode character."""
|
||||||
irc.errorInvalid('character', character)
|
replies = []
|
||||||
|
for idx, char in enumerate(s):
|
||||||
try:
|
try:
|
||||||
irc.reply(unicodedata.name(character))
|
replies.append(unicodedata.name(char))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
irc.error(_('No name found for this character.'))
|
irc.error(_('No name found for character %r at position %d.') %
|
||||||
unicodename = wrap(unicodename, ['something'])
|
(char, idx), Raise=True)
|
||||||
|
irc.replies(replies)
|
||||||
|
unicodename = wrap(unicodename, ['text'])
|
||||||
|
|
||||||
def unicodesearch(self, irc, msg, args, name):
|
def unicodesearch(self, irc, msg, args, name):
|
||||||
"""<name>
|
"""<name>
|
||||||
|
@ -103,13 +103,21 @@ class StringTestCase(PluginTestCase):
|
|||||||
for c in map(chr, range(256)):
|
for c in map(chr, range(256)):
|
||||||
i = ord(c)
|
i = ord(c)
|
||||||
self.assertResponse('ord %s' % utils.str.dqrepr(c), str(i))
|
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):
|
def testUnicode(self):
|
||||||
self.assertResponse('unicodename ☃', 'SNOWMAN')
|
self.assertResponse('unicodename ☃', 'SNOWMAN')
|
||||||
self.assertResponse('unicodesearch SNOWMAN', '☃')
|
self.assertResponse('unicodesearch SNOWMAN', '☃')
|
||||||
#self.assertResponse('unicodename ?',
|
self.assertResponse('unicodename ?', 'QUESTION MARK')
|
||||||
# 'No name found for this character.')
|
|
||||||
|
# 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',
|
self.assertResponse('unicodesearch FOO',
|
||||||
'Error: No character found with this name.')
|
'Error: No character found with this name.')
|
||||||
|
|
||||||
|
@ -72,10 +72,16 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
tzlocal = None
|
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):
|
class Time(callbacks.Plugin):
|
||||||
"""This plugin allows you to use different time-related functions."""
|
"""This plugin allows you to use different time-related functions."""
|
||||||
@internationalizeDocstring
|
@internationalizeDocstring
|
||||||
def seconds(self, irc, msg, args):
|
def seconds(self, irc, msg, args, text):
|
||||||
"""[<years>y] [<weeks>w] [<days>d] [<hours>h] [<minutes>m] [<seconds>s]
|
"""[<years>y] [<weeks>w] [<days>d] [<hours>h] [<minutes>m] [<seconds>s]
|
||||||
|
|
||||||
Returns the number of seconds in the number of <years>, <weeks>,
|
Returns the number of seconds in the number of <years>, <weeks>,
|
||||||
@ -84,11 +90,13 @@ class Time(callbacks.Plugin):
|
|||||||
Useful for scheduling events at a given number of seconds in the
|
Useful for scheduling events at a given number of seconds in the
|
||||||
future.
|
future.
|
||||||
"""
|
"""
|
||||||
if not args:
|
|
||||||
raise callbacks.ArgumentError
|
|
||||||
seconds = 0
|
seconds = 0
|
||||||
for arg in args:
|
if not text:
|
||||||
if not arg or arg[-1] not in 'ywdhms':
|
raise callbacks.ArgumentError
|
||||||
|
for arg in _SECONDS_SPLIT_RE.split(text):
|
||||||
|
if not arg:
|
||||||
|
continue
|
||||||
|
if arg[-1] not in 'ywdhms':
|
||||||
raise callbacks.ArgumentError
|
raise callbacks.ArgumentError
|
||||||
(s, kind) = arg[:-1], arg[-1]
|
(s, kind) = arg[:-1], arg[-1]
|
||||||
try:
|
try:
|
||||||
@ -108,6 +116,7 @@ class Time(callbacks.Plugin):
|
|||||||
elif kind == 's':
|
elif kind == 's':
|
||||||
seconds += i
|
seconds += i
|
||||||
irc.reply(str(seconds))
|
irc.reply(str(seconds))
|
||||||
|
seconds = wrap(seconds, ['text'])
|
||||||
|
|
||||||
@internationalizeDocstring
|
@internationalizeDocstring
|
||||||
def at(self, irc, msg, args, s=None):
|
def at(self, irc, msg, args, s=None):
|
||||||
|
@ -88,6 +88,17 @@ class TimeTestCase(PluginTestCase):
|
|||||||
self.assertResponse('seconds 1y 1s', '31536001')
|
self.assertResponse('seconds 1y 1s', '31536001')
|
||||||
self.assertResponse('seconds 1w 1s', '604801')
|
self.assertResponse('seconds 1w 1s', '604801')
|
||||||
|
|
||||||
|
@skipIf(sys.version_info < (3, 7, 0),
|
||||||
|
"Python 3.6 does not support empty pattern matches, see: "
|
||||||
|
"https://docs.python.org/3/library/re.html#re.split")
|
||||||
|
def testSecondsNoSpace(self):
|
||||||
|
self.assertResponse('seconds 1m1s', '61')
|
||||||
|
self.assertResponse('seconds 1h1s', '3601')
|
||||||
|
self.assertResponse('seconds 1d1s', '86401')
|
||||||
|
self.assertResponse('seconds 2d2h2m2s', '180122')
|
||||||
|
self.assertResponse('seconds 1y1s', '31536001')
|
||||||
|
self.assertResponse('seconds 1w1s', '604801')
|
||||||
|
|
||||||
def testNoErrors(self):
|
def testNoErrors(self):
|
||||||
self.assertNotError('ctime')
|
self.assertNotError('ctime')
|
||||||
self.assertNotError('time %Y')
|
self.assertNotError('time %Y')
|
||||||
|
@ -129,7 +129,7 @@ class TopicTestCase(ChannelPluginTestCase):
|
|||||||
conf.supybot.plugins.Topic.separator.setValue(' <==> ')
|
conf.supybot.plugins.Topic.separator.setValue(' <==> ')
|
||||||
_ = self.getMsg('topic add foo')
|
_ = self.getMsg('topic add foo')
|
||||||
m = self.getMsg('topic add bar')
|
m = self.getMsg('topic add bar')
|
||||||
self.assertTrue('<==>' in m.args[1])
|
self.assertIn('<==>', m.args[1])
|
||||||
finally:
|
finally:
|
||||||
conf.supybot.plugins.Topic.separator.setValue(original)
|
conf.supybot.plugins.Topic.separator.setValue(original)
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
###
|
###
|
||||||
# Copyright (c) 2002-2004, Jeremiah Fincher
|
# Copyright (c) 2002-2004, Jeremiah Fincher
|
||||||
# Copyright (c) 2010, James McCoy
|
# Copyright (c) 2010, James McCoy
|
||||||
# Copyright (c) 2010-2021, Valentin Lorentz
|
# Copyright (c) 2010-2022, Valentin Lorentz
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
@ -29,6 +29,8 @@
|
|||||||
# POSSIBILITY OF SUCH DAMAGE.
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
###
|
###
|
||||||
|
|
||||||
|
import itertools
|
||||||
|
|
||||||
import supybot.dbi as dbi
|
import supybot.dbi as dbi
|
||||||
import supybot.conf as conf
|
import supybot.conf as conf
|
||||||
import supybot.utils as utils
|
import supybot.utils as utils
|
||||||
@ -57,9 +59,7 @@ class DbiUrlDB(plugins.DbiChannelDB):
|
|||||||
near=msg.args[1], at=msg.receivedAt)
|
near=msg.args[1], at=msg.receivedAt)
|
||||||
super(self.__class__, self).add(record)
|
super(self.__class__, self).add(record)
|
||||||
def urls(self, p):
|
def urls(self, p):
|
||||||
L = list(self.select(p))
|
return self.select(p, reverse=True)
|
||||||
L.reverse()
|
|
||||||
return L
|
|
||||||
|
|
||||||
URLDB = plugins.DB('URL', {'flat': DbiUrlDB})
|
URLDB = plugins.DB('URL', {'flat': DbiUrlDB})
|
||||||
|
|
||||||
@ -142,16 +142,18 @@ class URL(callbacks.Plugin):
|
|||||||
if not predicate(record):
|
if not predicate(record):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
urls = [record.url for record in self.db.urls(channel, predicate)]
|
urls = (record.url for record in self.db.urls(channel, predicate))
|
||||||
if not urls:
|
(urls, urls_copy) = itertools.tee(urls)
|
||||||
|
first_url = next(urls_copy, None)
|
||||||
|
if first_url is None:
|
||||||
irc.reply(_('No URLs matched that criteria.'))
|
irc.reply(_('No URLs matched that criteria.'))
|
||||||
else:
|
else:
|
||||||
if nolimit:
|
if nolimit:
|
||||||
urls = [format('%u', url) for url in urls]
|
urls = (format('%u', url) for url in urls)
|
||||||
s = ', '.join(urls)
|
s = ', '.join(urls)
|
||||||
else:
|
else:
|
||||||
# We should optimize this with another URLDB method eventually.
|
# We should optimize this with another URLDB method eventually.
|
||||||
s = urls[0]
|
s = first_url
|
||||||
irc.reply(s)
|
irc.reply(s)
|
||||||
last = wrap(last, ['channeldb',
|
last = wrap(last, ['channeldb',
|
||||||
getopts({'from': 'something', 'with': 'something',
|
getopts({'from': 'something', 'with': 'something',
|
||||||
|
@ -448,7 +448,8 @@ class User(callbacks.Plugin):
|
|||||||
irc.errorNotRegistered()
|
irc.errorNotRegistered()
|
||||||
else:
|
else:
|
||||||
if u == user or u._checkCapability('admin'):
|
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:
|
else:
|
||||||
irc.error(conf.supybot.replies.incorrectAuthentication(),
|
irc.error(conf.supybot.replies.incorrectAuthentication(),
|
||||||
Raise=True)
|
Raise=True)
|
||||||
|
@ -104,7 +104,7 @@ class UserTestCase(PluginTestCase):
|
|||||||
self.assertResponse('hostmask', self.prefix)
|
self.assertResponse('hostmask', self.prefix)
|
||||||
self.assertError('@hostmask asdf')
|
self.assertError('@hostmask asdf')
|
||||||
m = self.irc.takeMsg()
|
m = self.irc.takeMsg()
|
||||||
self.assertFalse(m is not None, m)
|
self.assertIsNone(m, m)
|
||||||
|
|
||||||
def testRegisterPasswordLength(self):
|
def testRegisterPasswordLength(self):
|
||||||
self.assertRegexp('register foo aa', 'at least 3 characters long.')
|
self.assertRegexp('register foo aa', 'at least 3 characters long.')
|
||||||
@ -131,7 +131,7 @@ class UserTestCase(PluginTestCase):
|
|||||||
try:
|
try:
|
||||||
self.assertError('unregister foo')
|
self.assertError('unregister foo')
|
||||||
m = self.irc.takeMsg()
|
m = self.irc.takeMsg()
|
||||||
self.assertFalse(m is not None, m)
|
self.assertIsNone(m, m)
|
||||||
self.assertTrue(ircdb.users.getUserId('foo'))
|
self.assertTrue(ircdb.users.getUserId('foo'))
|
||||||
finally:
|
finally:
|
||||||
conf.supybot.databases.users.allowUnregistration.setValue(orig)
|
conf.supybot.databases.users.allowUnregistration.setValue(orig)
|
||||||
|
@ -154,7 +154,7 @@ class Web(callbacks.PluginRegexp):
|
|||||||
if parsed_url.netloc == 'youtube.com' \
|
if parsed_url.netloc == 'youtube.com' \
|
||||||
or parsed_url.netloc.endswith(('.youtube.com')):
|
or parsed_url.netloc.endswith(('.youtube.com')):
|
||||||
# there is a lot of Javascript before the <title>
|
# there is a lot of Javascript before the <title>
|
||||||
size = 409600
|
size = max(409600, size)
|
||||||
if parsed_url.netloc in ('reddit.com', 'www.reddit.com', 'new.reddit.com'):
|
if parsed_url.netloc in ('reddit.com', 'www.reddit.com', 'new.reddit.com'):
|
||||||
# Since 2022-03, New Reddit has 'Reddit - Dive into anything' as
|
# Since 2022-03, New Reddit has 'Reddit - Dive into anything' as
|
||||||
# <title> on every page.
|
# <title> on every page.
|
||||||
@ -164,8 +164,17 @@ class Web(callbacks.PluginRegexp):
|
|||||||
timeout = self.registryValue('timeout')
|
timeout = self.registryValue('timeout')
|
||||||
headers = conf.defaultHttpHeaders(irc.network, msg.channel)
|
headers = conf.defaultHttpHeaders(irc.network, msg.channel)
|
||||||
try:
|
try:
|
||||||
(target, text) = utils.web.getUrlTargetAndContent(url, size=size,
|
fd = utils.web.getUrlFd(url, timeout=timeout, headers=headers)
|
||||||
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:
|
except Exception as e:
|
||||||
if raiseErrors:
|
if raiseErrors:
|
||||||
irc.error(_('That URL raised <' + str(e)) + '>',
|
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>',
|
self.log.info('Web plugin TitleSnarfer: URL <%s> raised <%s>',
|
||||||
url, str(e))
|
url, str(e))
|
||||||
return
|
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:
|
try:
|
||||||
text = text.decode(utils.web.getEncoding(text) or 'utf8',
|
text = text.decode(encoding, 'replace')
|
||||||
'replace')
|
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
if minisix.PY3:
|
if minisix.PY3:
|
||||||
if raiseErrors:
|
if raiseErrors:
|
||||||
|
@ -1,10 +1,17 @@
|
|||||||
|
# mandatory:
|
||||||
|
|
||||||
setuptools
|
setuptools
|
||||||
chardet
|
|
||||||
pytz;python_version<'3.9'
|
# optional core dependencies:
|
||||||
python-dateutil
|
|
||||||
python-gnupg
|
chardet # to detect encoding of incoming IRC lines, if they are not in UTF-8
|
||||||
feedparser
|
python-gnupg # for authenticated based on GPG tokens
|
||||||
PySocks
|
PySocks # for SOCKS proxy (typically used to connect to IRC via Tor)
|
||||||
mock
|
pyxmpp2-scram # for the scram-sha-256 SASL mechanism
|
||||||
cryptography
|
|
||||||
pyxmpp2-scram
|
# 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
|
||||||
|
@ -990,10 +990,12 @@ class NestedCommandsIrcProxy(ReplyIrcProxy):
|
|||||||
self.error(_('You\'ve attempted more nesting than is '
|
self.error(_('You\'ve attempted more nesting than is '
|
||||||
'currently allowed on this bot.'))
|
'currently allowed on this bot.'))
|
||||||
return
|
return
|
||||||
|
|
||||||
# The deepcopy here is necessary for Scheduler; it re-runs already
|
# The deepcopy here is necessary for Scheduler; it re-runs already
|
||||||
# tokenized commands. There's a possibility a simple copy[:] would
|
# tokenized commands. There's a possibility a simple copy[:] would
|
||||||
# work, but we're being careful.
|
# work, but we're being careful.
|
||||||
self.args = copy.deepcopy(args)
|
self.args = copy.deepcopy(args)
|
||||||
|
|
||||||
self.counter = 0
|
self.counter = 0
|
||||||
self._resetReplyAttributes()
|
self._resetReplyAttributes()
|
||||||
if not args:
|
if not args:
|
||||||
@ -1804,6 +1806,19 @@ class PluginRegexp(Plugin):
|
|||||||
def doPrivmsg(self, irc, msg):
|
def doPrivmsg(self, irc, msg):
|
||||||
if msg.isError:
|
if msg.isError:
|
||||||
return
|
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)
|
proxy = self.Proxy(irc, msg)
|
||||||
if not msg.addressed:
|
if not msg.addressed:
|
||||||
for (r, name) in self.unaddressedRes:
|
for (r, name) in self.unaddressedRes:
|
||||||
|
@ -50,6 +50,13 @@ from .utils import minisix
|
|||||||
from .i18n import PluginInternationalization, internationalizeDocstring
|
from .i18n import PluginInternationalization, internationalizeDocstring
|
||||||
_ = PluginInternationalization()
|
_ = 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
|
# Non-arg wrappers -- these just change the behavior of a command without
|
||||||
# changing the arguments given to it.
|
# changing the arguments given to it.
|
||||||
@ -509,6 +516,7 @@ def getChannel(irc, msg, args, state):
|
|||||||
elif msg.channel:
|
elif msg.channel:
|
||||||
channel = msg.channel
|
channel = msg.channel
|
||||||
else:
|
else:
|
||||||
|
if LOG_CONVERTERS:
|
||||||
state.log.debug('Raising ArgumentError because there is no channel.')
|
state.log.debug('Raising ArgumentError because there is no channel.')
|
||||||
raise callbacks.ArgumentError
|
raise callbacks.ArgumentError
|
||||||
state.channel = channel
|
state.channel = channel
|
||||||
@ -520,6 +528,7 @@ def getChannels(irc, msg, args, state):
|
|||||||
elif msg.channel:
|
elif msg.channel:
|
||||||
channels = [msg.channel]
|
channels = [msg.channel]
|
||||||
else:
|
else:
|
||||||
|
if LOG_CONVERTERS:
|
||||||
state.log.debug('Raising ArgumentError because there is no channel.')
|
state.log.debug('Raising ArgumentError because there is no channel.')
|
||||||
raise callbacks.ArgumentError
|
raise callbacks.ArgumentError
|
||||||
state.args.append(channels)
|
state.args.append(channels)
|
||||||
@ -898,8 +907,10 @@ class context(object):
|
|||||||
self.converter = spec
|
self.converter = spec
|
||||||
|
|
||||||
def __call__(self, irc, msg, args, state):
|
def __call__(self, irc, msg, args, state):
|
||||||
|
if LOG_CONVERTERS:
|
||||||
log.debug('args before %r: %r', self, args)
|
log.debug('args before %r: %r', self, args)
|
||||||
self.converter(irc, msg, args, state, *self.args)
|
self.converter(irc, msg, args, state, *self.args)
|
||||||
|
if LOG_CONVERTERS:
|
||||||
log.debug('args after %r: %r', self, args)
|
log.debug('args after %r: %r', self, args)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
@ -929,6 +940,7 @@ class additional(context):
|
|||||||
try:
|
try:
|
||||||
self.__parent.__call__(irc, msg, args, state)
|
self.__parent.__call__(irc, msg, args, state)
|
||||||
except IndexError:
|
except IndexError:
|
||||||
|
if LOG_CONVERTERS:
|
||||||
log.debug('Got IndexError, returning default.')
|
log.debug('Got IndexError, returning default.')
|
||||||
setDefault(state, self.default)
|
setDefault(state, self.default)
|
||||||
|
|
||||||
@ -939,6 +951,7 @@ class optional(additional):
|
|||||||
try:
|
try:
|
||||||
super(optional, self).__call__(irc, msg, args, state)
|
super(optional, self).__call__(irc, msg, args, state)
|
||||||
except (callbacks.ArgumentError, callbacks.Error) as e:
|
except (callbacks.ArgumentError, callbacks.Error) as e:
|
||||||
|
if LOG_CONVERTERS:
|
||||||
log.debug('Got %s, returning default.', utils.exnToString(e))
|
log.debug('Got %s, returning default.', utils.exnToString(e))
|
||||||
state.errored = False
|
state.errored = False
|
||||||
setDefault(state, self.default)
|
setDefault(state, self.default)
|
||||||
@ -960,6 +973,7 @@ class any(context):
|
|||||||
if not self.continueOnError:
|
if not self.continueOnError:
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
|
if LOG_CONVERTERS:
|
||||||
log.debug('Got %s, returning default.', utils.exnToString(e))
|
log.debug('Got %s, returning default.', utils.exnToString(e))
|
||||||
pass
|
pass
|
||||||
state.args.append(st.args)
|
state.args.append(st.args)
|
||||||
@ -1041,10 +1055,12 @@ class getopts(context):
|
|||||||
self.getopts[name] = contextify(spec)
|
self.getopts[name] = contextify(spec)
|
||||||
self.getoptL.append(name + '=')
|
self.getoptL.append(name + '=')
|
||||||
self.getopts[name] = contextify(spec)
|
self.getopts[name] = contextify(spec)
|
||||||
|
if LOG_CONVERTERS:
|
||||||
log.debug('getopts: %r', self.getopts)
|
log.debug('getopts: %r', self.getopts)
|
||||||
log.debug('getoptL: %r', self.getoptL)
|
log.debug('getoptL: %r', self.getoptL)
|
||||||
|
|
||||||
def __call__(self, irc, msg, args, state):
|
def __call__(self, irc, msg, args, state):
|
||||||
|
if LOG_CONVERTERS:
|
||||||
log.debug('args before %r: %r', self, args)
|
log.debug('args before %r: %r', self, args)
|
||||||
(optlist, rest) = getopt.getopt(args, self.getoptLs, self.getoptL)
|
(optlist, rest) = getopt.getopt(args, self.getoptLs, self.getoptL)
|
||||||
getopts = []
|
getopts = []
|
||||||
@ -1053,6 +1069,7 @@ class getopts(context):
|
|||||||
opt = opt[2:] # Strip --
|
opt = opt[2:] # Strip --
|
||||||
else:
|
else:
|
||||||
opt = opt[1:]
|
opt = opt[1:]
|
||||||
|
if LOG_CONVERTERS:
|
||||||
log.debug('opt: %r, arg: %r', opt, arg)
|
log.debug('opt: %r, arg: %r', opt, arg)
|
||||||
context = self.getopts[opt]
|
context = self.getopts[opt]
|
||||||
if context is not None:
|
if context is not None:
|
||||||
@ -1064,6 +1081,7 @@ class getopts(context):
|
|||||||
getopts.append((opt, True))
|
getopts.append((opt, True))
|
||||||
state.args.append(getopts)
|
state.args.append(getopts)
|
||||||
args[:] = rest
|
args[:] = rest
|
||||||
|
if LOG_CONVERTERS:
|
||||||
log.debug('args after %r: %r', self, args)
|
log.debug('args after %r: %r', self, args)
|
||||||
|
|
||||||
###
|
###
|
||||||
@ -1123,6 +1141,7 @@ class Spec(object):
|
|||||||
except IndexError:
|
except IndexError:
|
||||||
raise callbacks.ArgumentError
|
raise callbacks.ArgumentError
|
||||||
if args and not state.allowExtra:
|
if args and not state.allowExtra:
|
||||||
|
if LOG_CONVERTERS:
|
||||||
log.debug('args and not self.allowExtra: %r', args)
|
log.debug('args and not self.allowExtra: %r', args)
|
||||||
raise callbacks.ArgumentError
|
raise callbacks.ArgumentError
|
||||||
return state
|
return state
|
||||||
@ -1134,8 +1153,10 @@ def _wrap(f, specList=[], name=None, checkDoc=True, **kw):
|
|||||||
spec = Spec(specList, **kw)
|
spec = Spec(specList, **kw)
|
||||||
def newf(self, irc, msg, args, **kwargs):
|
def newf(self, irc, msg, args, **kwargs):
|
||||||
state = spec(irc, msg, args, stateAttrs={'cb': self, 'log': self.log})
|
state = spec(irc, msg, args, stateAttrs={'cb': self, 'log': self.log})
|
||||||
|
if LOG_CONVERTERS:
|
||||||
self.log.debug('State before call: %s', state)
|
self.log.debug('State before call: %s', state)
|
||||||
if state.errored:
|
if state.errored:
|
||||||
|
if LOG_CONVERTERS:
|
||||||
self.log.debug('Refusing to call %s due to state.errored.', f)
|
self.log.debug('Refusing to call %s due to state.errored.', f)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
|
@ -1318,7 +1318,7 @@ registerGlobalValue(supybot.protocols.irc.queuing.rateLimit, 'join',
|
|||||||
###
|
###
|
||||||
registerGroup(supybot.protocols, 'http')
|
registerGroup(supybot.protocols, 'http')
|
||||||
registerGlobalValue(supybot.protocols.http, 'peekSize',
|
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
|
'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
|
similar. It'll give up after it reads this many bytes, even if it hasn't
|
||||||
found what it was looking for.""")))
|
found what it was looking for.""")))
|
||||||
|
40
src/dbi.py
40
src/dbi.py
@ -1,6 +1,6 @@
|
|||||||
###
|
###
|
||||||
# Copyright (c) 2002-2005, Jeremiah Fincher
|
# Copyright (c) 2002-2005, Jeremiah Fincher
|
||||||
# Copyright (c) 2010-2021, Valentin Lorentz
|
# Copyright (c) 2010-2022, Valentin Lorentz
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
@ -150,7 +150,7 @@ class FlatfileMapping(MappingInterface):
|
|||||||
def __init__(self, filename, maxSize=10**6):
|
def __init__(self, filename, maxSize=10**6):
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
try:
|
try:
|
||||||
fd = open(self.filename)
|
fd = open(self.filename, encoding='utf8')
|
||||||
strId = fd.readline().rstrip()
|
strId = fd.readline().rstrip()
|
||||||
self.maxSize = len(strId)
|
self.maxSize = len(strId)
|
||||||
try:
|
try:
|
||||||
@ -175,7 +175,7 @@ class FlatfileMapping(MappingInterface):
|
|||||||
def _incrementCurrentId(self, fd=None):
|
def _incrementCurrentId(self, fd=None):
|
||||||
fdWasNone = fd is None
|
fdWasNone = fd is None
|
||||||
if fdWasNone:
|
if fdWasNone:
|
||||||
fd = open(self.filename, 'a')
|
fd = open(self.filename, 'a', encoding='utf8')
|
||||||
fd.seek(0)
|
fd.seek(0)
|
||||||
self.currentId += 1
|
self.currentId += 1
|
||||||
fd.write(self._canonicalId(self.currentId))
|
fd.write(self._canonicalId(self.currentId))
|
||||||
@ -193,7 +193,7 @@ class FlatfileMapping(MappingInterface):
|
|||||||
|
|
||||||
def add(self, s):
|
def add(self, s):
|
||||||
line = self._joinLine(self.currentId, s)
|
line = self._joinLine(self.currentId, s)
|
||||||
fd = open(self.filename, 'r+')
|
fd = open(self.filename, 'r+', encoding='utf8')
|
||||||
try:
|
try:
|
||||||
fd.seek(0, 2) # End.
|
fd.seek(0, 2) # End.
|
||||||
fd.write(line)
|
fd.write(line)
|
||||||
@ -205,7 +205,7 @@ class FlatfileMapping(MappingInterface):
|
|||||||
def get(self, id):
|
def get(self, id):
|
||||||
strId = self._canonicalId(id)
|
strId = self._canonicalId(id)
|
||||||
try:
|
try:
|
||||||
fd = open(self.filename)
|
fd = open(self.filename, encoding='utf8')
|
||||||
fd.readline() # First line, nextId.
|
fd.readline() # First line, nextId.
|
||||||
for line in fd:
|
for line in fd:
|
||||||
(lineId, s) = self._splitLine(line)
|
(lineId, s) = self._splitLine(line)
|
||||||
@ -221,7 +221,7 @@ class FlatfileMapping(MappingInterface):
|
|||||||
def set(self, id, s):
|
def set(self, id, s):
|
||||||
strLine = self._joinLine(id, s)
|
strLine = self._joinLine(id, s)
|
||||||
try:
|
try:
|
||||||
fd = open(self.filename, 'r+')
|
fd = open(self.filename, 'r+', encoding='utf8')
|
||||||
self.remove(id, fd)
|
self.remove(id, fd)
|
||||||
fd.seek(0, 2) # End.
|
fd.seek(0, 2) # End.
|
||||||
fd.write(strLine)
|
fd.write(strLine)
|
||||||
@ -233,7 +233,7 @@ class FlatfileMapping(MappingInterface):
|
|||||||
strId = self._canonicalId(id)
|
strId = self._canonicalId(id)
|
||||||
try:
|
try:
|
||||||
if fdWasNone:
|
if fdWasNone:
|
||||||
fd = open(self.filename, 'r+')
|
fd = open(self.filename, 'r+', encoding='utf8')
|
||||||
fd.seek(0)
|
fd.seek(0)
|
||||||
fd.readline() # First line, nextId
|
fd.readline() # First line, nextId
|
||||||
pos = fd.tell()
|
pos = fd.tell()
|
||||||
@ -262,7 +262,7 @@ class FlatfileMapping(MappingInterface):
|
|||||||
fd.close()
|
fd.close()
|
||||||
|
|
||||||
def vacuum(self):
|
def vacuum(self):
|
||||||
infd = open(self.filename)
|
infd = open(self.filename, encoding='utf8')
|
||||||
outfd = utils.file.AtomicFile(self.filename,makeBackupIfSmaller=False)
|
outfd = utils.file.AtomicFile(self.filename,makeBackupIfSmaller=False)
|
||||||
outfd.write(infd.readline()) # First line, nextId.
|
outfd.write(infd.readline()) # First line, nextId.
|
||||||
for line in infd:
|
for line in infd:
|
||||||
@ -358,12 +358,30 @@ class DB(object):
|
|||||||
self.map.remove(id)
|
self.map.remove(id)
|
||||||
|
|
||||||
def __iter__(self):
|
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.
|
# We don't need to yield the id because it's in the record.
|
||||||
yield self._newRecord(id, s)
|
yield self._newRecord(id, s)
|
||||||
|
|
||||||
def select(self, p):
|
def select(self, p, reverse=False):
|
||||||
for record in self:
|
for record in self._iter(reverse=reverse):
|
||||||
if p(record):
|
if p(record):
|
||||||
yield record
|
yield record
|
||||||
|
|
||||||
|
@ -70,8 +70,6 @@ class SocketDriver(drivers.IrcDriver, drivers.ServersMixin):
|
|||||||
self._attempt = -1
|
self._attempt = -1
|
||||||
self.servers = ()
|
self.servers = ()
|
||||||
self.eagains = 0
|
self.eagains = 0
|
||||||
self.inbuffer = b''
|
|
||||||
self.outbuffer = ''
|
|
||||||
self.zombie = False
|
self.zombie = False
|
||||||
self.connected = False
|
self.connected = False
|
||||||
self.writeCheckTime = None
|
self.writeCheckTime = None
|
||||||
@ -248,6 +246,8 @@ class SocketDriver(drivers.IrcDriver, drivers.ServersMixin):
|
|||||||
|
|
||||||
def reconnect(self, wait=False, reset=True, server=None):
|
def reconnect(self, wait=False, reset=True, server=None):
|
||||||
self._attempt += 1
|
self._attempt += 1
|
||||||
|
self.inbuffer = b''
|
||||||
|
self.outbuffer = ''
|
||||||
self.nextReconnectTime = None
|
self.nextReconnectTime = None
|
||||||
if self.connected:
|
if self.connected:
|
||||||
self.onDisconnect()
|
self.onDisconnect()
|
||||||
|
@ -1314,7 +1314,7 @@ def checkCapabilities(hostmask, capabilities, requireAll=False):
|
|||||||
# supybot.capabilities
|
# supybot.capabilities
|
||||||
###
|
###
|
||||||
|
|
||||||
class SpaceSeparatedListOfCapabilities(registry.SpaceSeparatedListOfStrings):
|
class SpaceSeparatedListOfCapabilities(registry.SpaceSeparatedSetOfStrings):
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
List = CapabilitySet
|
List = CapabilitySet
|
||||||
|
|
||||||
|
@ -624,7 +624,7 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
|
|||||||
|
|
||||||
:type: Set[str]
|
:type: Set[str]
|
||||||
|
|
||||||
.. attribute:: capabilities_acq
|
.. attribute:: capabilities_ack
|
||||||
|
|
||||||
Set of all capabilities requested from and acknowledged by the
|
Set of all capabilities requested from and acknowledged by the
|
||||||
server. See <https://ircv3.net/specs/core/capability-negotiation>
|
server. See <https://ircv3.net/specs/core/capability-negotiation>
|
||||||
@ -676,7 +676,7 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
|
|||||||
|
|
||||||
:type: ircutils.IrcDict[str, ChannelState]
|
:type: ircutils.IrcDict[str, ChannelState]
|
||||||
|
|
||||||
.. attribute:: nickToHostmask
|
.. attribute:: nicksToHostmasks
|
||||||
|
|
||||||
Stores the last hostmask of a seen nick.
|
Stores the last hostmask of a seen nick.
|
||||||
|
|
||||||
@ -1731,19 +1731,40 @@ class Irc(IrcCommandDispatcher, log.Firewalled):
|
|||||||
self.sasl_current_mechanism = None
|
self.sasl_current_mechanism = None
|
||||||
|
|
||||||
for mechanism in network_config.sasl.mechanisms():
|
for mechanism in network_config.sasl.mechanisms():
|
||||||
if mechanism == 'ecdsa-nist256p-challenge' and \
|
if mechanism == 'ecdsa-nist256p-challenge':
|
||||||
crypto and self.sasl_username and \
|
if not crypto:
|
||||||
self.sasl_ecdsa_key:
|
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)
|
self.sasl_next_mechanisms.append(mechanism)
|
||||||
elif mechanism == 'external' and (
|
elif mechanism == 'external':
|
||||||
network_config.certfile() or
|
if not network_config.certfile() and \
|
||||||
conf.supybot.protocols.irc.certfile()):
|
not conf.supybot.protocols.irc.certfile():
|
||||||
|
log.debug('Skipping SASL %s, missing cert file',
|
||||||
|
mechanism)
|
||||||
|
else:
|
||||||
self.sasl_next_mechanisms.append(mechanism)
|
self.sasl_next_mechanisms.append(mechanism)
|
||||||
elif mechanism.startswith('scram-') and scram and \
|
elif mechanism.startswith('scram-'):
|
||||||
self.sasl_username and self.sasl_password:
|
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)
|
self.sasl_next_mechanisms.append(mechanism)
|
||||||
elif mechanism == 'plain' and \
|
elif mechanism == 'plain':
|
||||||
self.sasl_username and self.sasl_password:
|
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)
|
self.sasl_next_mechanisms.append(mechanism)
|
||||||
|
|
||||||
if self.sasl_next_mechanisms:
|
if self.sasl_next_mechanisms:
|
||||||
@ -1755,7 +1776,8 @@ class Irc(IrcCommandDispatcher, log.Firewalled):
|
|||||||
'multi-prefix', 'metadata-notify', 'account-tag',
|
'multi-prefix', 'metadata-notify', 'account-tag',
|
||||||
'userhost-in-names', 'invite-notify', 'server-time',
|
'userhost-in-names', 'invite-notify', 'server-time',
|
||||||
'chghost', 'batch', 'away-notify', 'message-tags',
|
'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.
|
"""IRCv3 capabilities requested when they are available.
|
||||||
|
|
||||||
echo-message is special-cased to be requested only with labeled-response.
|
echo-message is special-cased to be requested only with labeled-response.
|
||||||
@ -1862,6 +1884,7 @@ class Irc(IrcCommandDispatcher, log.Firewalled):
|
|||||||
IrcStateFsm.States.INIT_SASL,
|
IrcStateFsm.States.INIT_SASL,
|
||||||
IrcStateFsm.States.CONNECTED_SASL,
|
IrcStateFsm.States.CONNECTED_SASL,
|
||||||
])
|
])
|
||||||
|
log.debug('Next SASL mechanisms: %s', self.sasl_next_mechanisms)
|
||||||
if self.sasl_next_mechanisms:
|
if self.sasl_next_mechanisms:
|
||||||
self.sasl_current_mechanism = self.sasl_next_mechanisms.pop(0)
|
self.sasl_current_mechanism = self.sasl_next_mechanisms.pop(0)
|
||||||
self.sendMsg(ircmsgs.IrcMsg(command='AUTHENTICATE',
|
self.sendMsg(ircmsgs.IrcMsg(command='AUTHENTICATE',
|
||||||
@ -1905,7 +1928,7 @@ class Irc(IrcCommandDispatcher, log.Firewalled):
|
|||||||
|
|
||||||
mechanism = self.sasl_current_mechanism
|
mechanism = self.sasl_current_mechanism
|
||||||
if mechanism == 'ecdsa-nist256p-challenge':
|
if mechanism == 'ecdsa-nist256p-challenge':
|
||||||
self._doAuthenticateEcdsa(string)
|
self._doAuthenticateEcdsa(msg, string)
|
||||||
elif mechanism == 'external':
|
elif mechanism == 'external':
|
||||||
self.sendSaslString(b'')
|
self.sendSaslString(b'')
|
||||||
elif mechanism.startswith('scram-'):
|
elif mechanism.startswith('scram-'):
|
||||||
@ -1914,7 +1937,7 @@ class Irc(IrcCommandDispatcher, log.Firewalled):
|
|||||||
if step == 'uninitialized':
|
if step == 'uninitialized':
|
||||||
log.debug('%s: starting SCRAM.',
|
log.debug('%s: starting SCRAM.',
|
||||||
self.network)
|
self.network)
|
||||||
self._doAuthenticateScramFirst(mechanism)
|
self._doAuthenticateScramFirst(msg, mechanism)
|
||||||
elif step == 'first-sent':
|
elif step == 'first-sent':
|
||||||
log.debug('%s: received SCRAM challenge.',
|
log.debug('%s: received SCRAM challenge.',
|
||||||
self.network)
|
self.network)
|
||||||
@ -1922,13 +1945,13 @@ class Irc(IrcCommandDispatcher, log.Firewalled):
|
|||||||
elif step == 'final-sent':
|
elif step == 'final-sent':
|
||||||
log.debug('%s: finishing SCRAM.',
|
log.debug('%s: finishing SCRAM.',
|
||||||
self.network)
|
self.network)
|
||||||
self._doAuthenticateScramFinish(string)
|
self._doAuthenticateScramFinish(msg, string)
|
||||||
else:
|
else:
|
||||||
assert False
|
assert False
|
||||||
except scram.ScramException:
|
except scram.ScramException:
|
||||||
self.sendMsg(ircmsgs.IrcMsg(command='AUTHENTICATE',
|
self.sendMsg(ircmsgs.IrcMsg(command='AUTHENTICATE',
|
||||||
args=('*',)))
|
args=('*',)))
|
||||||
self.tryNextSaslMechanism()
|
self.tryNextSaslMechanism(msg)
|
||||||
elif mechanism == 'plain':
|
elif mechanism == 'plain':
|
||||||
authstring = b'\0'.join([
|
authstring = b'\0'.join([
|
||||||
self.sasl_username.encode('utf-8'),
|
self.sasl_username.encode('utf-8'),
|
||||||
@ -1937,7 +1960,7 @@ class Irc(IrcCommandDispatcher, log.Firewalled):
|
|||||||
])
|
])
|
||||||
self.sendSaslString(authstring)
|
self.sendSaslString(authstring)
|
||||||
|
|
||||||
def _doAuthenticateEcdsa(self, string):
|
def _doAuthenticateEcdsa(self, msg, string):
|
||||||
if string == b'':
|
if string == b'':
|
||||||
self.sendSaslString(self.sasl_username.encode('utf-8'))
|
self.sendSaslString(self.sasl_username.encode('utf-8'))
|
||||||
return
|
return
|
||||||
@ -1952,9 +1975,9 @@ class Irc(IrcCommandDispatcher, log.Firewalled):
|
|||||||
except (OSError, ValueError):
|
except (OSError, ValueError):
|
||||||
self.sendMsg(ircmsgs.IrcMsg(command='AUTHENTICATE',
|
self.sendMsg(ircmsgs.IrcMsg(command='AUTHENTICATE',
|
||||||
args=('*',)))
|
args=('*',)))
|
||||||
self.tryNextSaslMechanism()
|
self.tryNextSaslMechanism(msg)
|
||||||
|
|
||||||
def _doAuthenticateScramFirst(self, mechanism):
|
def _doAuthenticateScramFirst(self, msg, mechanism):
|
||||||
"""Handle sending the client-first message of SCRAM auth."""
|
"""Handle sending the client-first message of SCRAM auth."""
|
||||||
hash_name = mechanism[len('scram-'):]
|
hash_name = mechanism[len('scram-'):]
|
||||||
if hash_name.endswith('-plus'):
|
if hash_name.endswith('-plus'):
|
||||||
@ -1963,7 +1986,7 @@ class Irc(IrcCommandDispatcher, log.Firewalled):
|
|||||||
if hash_name not in scram.HASH_FACTORIES:
|
if hash_name not in scram.HASH_FACTORIES:
|
||||||
log.debug('%s: SCRAM hash %r not supported, aborting.',
|
log.debug('%s: SCRAM hash %r not supported, aborting.',
|
||||||
self.network, hash_name)
|
self.network, hash_name)
|
||||||
self.tryNextSaslMechanism()
|
self.tryNextSaslMechanism(msg)
|
||||||
return
|
return
|
||||||
authenticator = scram.SCRAMClientAuthenticator(hash_name,
|
authenticator = scram.SCRAMClientAuthenticator(hash_name,
|
||||||
channel_binding=False)
|
channel_binding=False)
|
||||||
@ -1981,14 +2004,16 @@ class Irc(IrcCommandDispatcher, log.Firewalled):
|
|||||||
self.sendSaslString(client_final)
|
self.sendSaslString(client_final)
|
||||||
self.sasl_scram_state['step'] = 'final-sent'
|
self.sasl_scram_state['step'] = 'final-sent'
|
||||||
|
|
||||||
def _doAuthenticateScramFinish(self, data):
|
def _doAuthenticateScramFinish(self, msg, data):
|
||||||
try:
|
try:
|
||||||
res = self.sasl_scram_state['authenticator'] \
|
res = self.sasl_scram_state['authenticator'] \
|
||||||
.finish(data)
|
.finish(data)
|
||||||
except scram.BadSuccessException as e:
|
except scram.BadSuccessException as e:
|
||||||
log.warning('%s: SASL authentication failed with SCRAM error: %e',
|
log.warning('%s: SASL authentication failed with SCRAM error: %e',
|
||||||
self.network, e)
|
self.network, e)
|
||||||
self.tryNextSaslMechanism()
|
self.sendMsg(ircmsgs.IrcMsg(command='AUTHENTICATE',
|
||||||
|
args=('*',)))
|
||||||
|
self.tryNextSaslMechanism(msg)
|
||||||
else:
|
else:
|
||||||
self.sendSaslString(b'')
|
self.sendSaslString(b'')
|
||||||
self.sasl_scram_state['step'] = 'authenticated'
|
self.sasl_scram_state['step'] = 'authenticated'
|
||||||
|
@ -84,7 +84,7 @@ def open_registry(filename, clear=False):
|
|||||||
global _lastModified
|
global _lastModified
|
||||||
if clear:
|
if clear:
|
||||||
_cache.clear()
|
_cache.clear()
|
||||||
_fd = open(filename)
|
_fd = open(filename, encoding='utf8')
|
||||||
fd = utils.file.nonCommentNonEmptyLines(_fd)
|
fd = utils.file.nonCommentNonEmptyLines(_fd)
|
||||||
acc = ''
|
acc = ''
|
||||||
slashEnd = re.compile(r'\\*$')
|
slashEnd = re.compile(r'\\*$')
|
||||||
@ -710,7 +710,7 @@ class OnlySomeStrings(String):
|
|||||||
|
|
||||||
def setValue(self, s):
|
def setValue(self, s):
|
||||||
v = self.normalize(s)
|
v = self.normalize(s)
|
||||||
if s in self.validStrings:
|
if v in self.validStrings:
|
||||||
self.__parent.setValue(v)
|
self.__parent.setValue(v)
|
||||||
else:
|
else:
|
||||||
self.error(v)
|
self.error(v)
|
||||||
@ -875,6 +875,7 @@ class SpaceSeparatedListOfStrings(SpaceSeparatedListOf):
|
|||||||
class SpaceSeparatedSetOfStrings(SpaceSeparatedListOfStrings):
|
class SpaceSeparatedSetOfStrings(SpaceSeparatedListOfStrings):
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
List = set
|
List = set
|
||||||
|
sorted = True
|
||||||
|
|
||||||
class CommaSeparatedListOfStrings(SeparatedListOf):
|
class CommaSeparatedListOfStrings(SeparatedListOf):
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
@ -887,6 +888,7 @@ class CommaSeparatedSetOfStrings(SeparatedListOf):
|
|||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
List = set
|
List = set
|
||||||
Value = String
|
Value = String
|
||||||
|
sorted = True
|
||||||
def splitter(self, s):
|
def splitter(self, s):
|
||||||
return re.split(r'\s*,\s*', s)
|
return re.split(r'\s*,\s*', s)
|
||||||
joiner = ', '.join
|
joiner = ', '.join
|
||||||
|
@ -297,7 +297,7 @@ def main():
|
|||||||
'with the plugin\'s name and "$format" with the value '
|
'with the plugin\'s name and "$format" with the value '
|
||||||
'if --format.')
|
'if --format.')
|
||||||
parser.add_option('-f', '--format', dest='format', choices=['rst', 'stx'],
|
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.')
|
'use.')
|
||||||
parser.add_option('--plugins-dir',
|
parser.add_option('--plugins-dir',
|
||||||
action='append', dest='pluginsDirs', default=[],
|
action='append', dest='pluginsDirs', default=[],
|
||||||
|
@ -79,7 +79,14 @@ if setuptools:
|
|||||||
break
|
break
|
||||||
|
|
||||||
module_name = kwargs['name'].replace('-', '_')
|
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('package_dir', {module_name: '.'})
|
||||||
kwargs.setdefault('entry_points', {
|
kwargs.setdefault('entry_points', {
|
||||||
'limnoria.plugins': '%s = %s' % (capitalized_name, module_name)})
|
'limnoria.plugins': '%s = %s' % (capitalized_name, module_name)})
|
||||||
|
@ -70,7 +70,7 @@ class Reader(object):
|
|||||||
return s.lower()
|
return s.lower()
|
||||||
|
|
||||||
def readFile(self, filename):
|
def readFile(self, filename):
|
||||||
self.read(open(filename))
|
self.read(open(filename, encoding='utf8'))
|
||||||
|
|
||||||
def read(self, fd):
|
def read(self, fd):
|
||||||
lineno = 0
|
lineno = 0
|
||||||
|
@ -130,7 +130,9 @@ def chunks(fd, size):
|
|||||||
|
|
||||||
class AtomicFile(object):
|
class AtomicFile(object):
|
||||||
"""Used for files that need to be atomically written -- i.e., if there's a
|
"""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.
|
class default(object): # Holder for values.
|
||||||
# Callables?
|
# Callables?
|
||||||
tmpDir = None
|
tmpDir = None
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
###
|
###
|
||||||
# Copyright (c) 2002-2009, Jeremiah Fincher
|
# Copyright (c) 2002-2009, Jeremiah Fincher
|
||||||
# Copyright (c) 2010-2021, Valentin Lorentz
|
# Copyright (c) 2010-2022, Valentin Lorentz
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
@ -349,7 +349,10 @@ class TimeoutQueue(object):
|
|||||||
return self.queue.dequeue()[1]
|
return self.queue.dequeue()[1]
|
||||||
|
|
||||||
def __iter__(self):
|
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
|
# the resulting generator and elements that should've timed out are
|
||||||
# yielded? Hmm? What happens then, smarty-pants?
|
# yielded? Hmm? What happens then, smarty-pants?
|
||||||
for (t, elt) in self.queue:
|
for (t, elt) in self.queue:
|
||||||
|
@ -226,6 +226,8 @@ def getEncoding(s):
|
|||||||
# From beautifulsoup (version 4.10.0, bs4/builder/__init__.py, line 391)
|
# 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 = 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):
|
class HtmlToText(HTMLParser, object):
|
||||||
"""Taken from some eff-bot code on c.l.p."""
|
"""Taken from some eff-bot code on c.l.p."""
|
||||||
entitydefs = entitydefs.copy()
|
entitydefs = entitydefs.copy()
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
###
|
###
|
||||||
# Copyright (c) 2005, Jeremiah Fincher
|
# Copyright (c) 2005, Jeremiah Fincher
|
||||||
# Copyright (c) 2010-2021, The Limnoria Contributors
|
# Copyright (c) 2010-2021, Valentin Lorentz
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
17
test/messages.pot
Normal file
17
test/messages.pot
Normal file
@ -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"
|
||||||
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
|||||||
###
|
###
|
||||||
# Copyright (c) 2005, Jeremiah Fincher
|
# Copyright (c) 2005, Jeremiah Fincher
|
||||||
# Copyright (c) 2010-2021, The Limnoria Contributors
|
# Copyright (c) 2010-2021, Valentin Lorentz
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf8 -*-
|
# -*- coding: utf8 -*-
|
||||||
###
|
###
|
||||||
# Copyright (c) 2002-2005, Jeremiah Fincher
|
# Copyright (c) 2002-2005, Jeremiah Fincher
|
||||||
# Copyright (c) 2010-2021, The Limnoria Contributors
|
# Copyright (c) 2010-2021, Valentin Lorentz
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
@ -417,14 +417,14 @@ class PrivmsgTestCase(ChannelPluginTestCase):
|
|||||||
def testReplyWithNickPrefix(self):
|
def testReplyWithNickPrefix(self):
|
||||||
self.feedMsg('@len foo')
|
self.feedMsg('@len foo')
|
||||||
m = self.irc.takeMsg()
|
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))
|
self.assertTrue(m.args[1].startswith(self.nick))
|
||||||
try:
|
try:
|
||||||
original = conf.supybot.reply.withNickPrefix()
|
original = conf.supybot.reply.withNickPrefix()
|
||||||
conf.supybot.reply.withNickPrefix.setValue(False)
|
conf.supybot.reply.withNickPrefix.setValue(False)
|
||||||
self.feedMsg('@len foobar')
|
self.feedMsg('@len foobar')
|
||||||
m = self.irc.takeMsg()
|
m = self.irc.takeMsg()
|
||||||
self.assertTrue(m is not None)
|
self.assertIsNotNone(m)
|
||||||
self.assertFalse(m.args[1].startswith(self.nick))
|
self.assertFalse(m.args[1].startswith(self.nick))
|
||||||
finally:
|
finally:
|
||||||
conf.supybot.reply.withNickPrefix.setValue(original)
|
conf.supybot.reply.withNickPrefix.setValue(original)
|
||||||
@ -448,7 +448,7 @@ class PrivmsgTestCase(ChannelPluginTestCase):
|
|||||||
conf.supybot.reply.error.withNotice.setValue(True)
|
conf.supybot.reply.error.withNotice.setValue(True)
|
||||||
m = self.getMsg("eval irc.error('foo')")
|
m = self.getMsg("eval irc.error('foo')")
|
||||||
self.assertTrue(m, 'No message returned.')
|
self.assertTrue(m, 'No message returned.')
|
||||||
self.assertTrue(m.command == 'NOTICE')
|
self.assertEqual(m.command, 'NOTICE')
|
||||||
finally:
|
finally:
|
||||||
conf.supybot.reply.error.withNotice.setValue(original)
|
conf.supybot.reply.error.withNotice.setValue(original)
|
||||||
|
|
||||||
@ -975,16 +975,50 @@ class MultilinePrivmsgTestCase(ChannelPluginTestCase):
|
|||||||
'-' + batch_name,)))
|
'-' + batch_name,)))
|
||||||
|
|
||||||
|
|
||||||
class PluginRegexpTestCase(PluginTestCase):
|
class PluginRegexpTestCase(ChannelPluginTestCase):
|
||||||
plugins = ()
|
plugins = ()
|
||||||
class PCAR(callbacks.PluginRegexp):
|
class PCAR(callbacks.PluginRegexp):
|
||||||
|
regexps = ("test", "test2")
|
||||||
|
|
||||||
def test(self, irc, msg, args):
|
def test(self, irc, msg, args):
|
||||||
"<foo>"
|
"<foo>"
|
||||||
raise callbacks.ArgumentError
|
raise callbacks.ArgumentError
|
||||||
def testNoEscapingArgumentError(self):
|
|
||||||
|
def test2(self, irc, msg, args):
|
||||||
|
"<bar>"
|
||||||
|
irc.reply("hello")
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
self.irc.addCallback(self.PCAR(self.irc))
|
self.irc.addCallback(self.PCAR(self.irc))
|
||||||
|
|
||||||
|
def testNoEscapingArgumentError(self):
|
||||||
self.assertResponse('test', 'test <foo>')
|
self.assertResponse('test', 'test <foo>')
|
||||||
|
|
||||||
|
def testReply(self):
|
||||||
|
self.irc.feedMsg(ircmsgs.IrcMsg(
|
||||||
|
prefix=self.prefix,
|
||||||
|
command='PRIVMSG',
|
||||||
|
args=(self.channel, 'foo <bar> baz')))
|
||||||
|
self.assertResponse(' ', 'hello')
|
||||||
|
|
||||||
|
def testIgnoreChathistory(self):
|
||||||
|
self.irc.feedMsg(ircmsgs.IrcMsg(
|
||||||
|
command='BATCH',
|
||||||
|
args=('+123', 'chathistory', self.channel)))
|
||||||
|
|
||||||
|
self.irc.feedMsg(ircmsgs.IrcMsg(
|
||||||
|
server_tags={'batch': '123'},
|
||||||
|
prefix=self.prefix,
|
||||||
|
command='PRIVMSG',
|
||||||
|
args=(self.channel, 'foo <bar> baz')))
|
||||||
|
|
||||||
|
self.irc.feedMsg(ircmsgs.IrcMsg(
|
||||||
|
command='BATCH',
|
||||||
|
args=('-123',)))
|
||||||
|
|
||||||
|
self.assertNoResponse(' ')
|
||||||
|
|
||||||
class RichReplyMethodsTestCase(PluginTestCase):
|
class RichReplyMethodsTestCase(PluginTestCase):
|
||||||
plugins = ('Config',)
|
plugins = ('Config',)
|
||||||
class NoCapability(callbacks.Plugin):
|
class NoCapability(callbacks.Plugin):
|
||||||
@ -1078,10 +1112,10 @@ class WithPrivateNoticeTestCase(ChannelPluginTestCase):
|
|||||||
self.irc.addCallback(self.WithPrivateNotice(self.irc))
|
self.irc.addCallback(self.WithPrivateNotice(self.irc))
|
||||||
# Check normal behavior.
|
# Check normal behavior.
|
||||||
m = self.assertNotError('normal')
|
m = self.assertNotError('normal')
|
||||||
self.assertFalse(m.command == 'NOTICE')
|
self.assertNotEqual(m.command, 'NOTICE')
|
||||||
self.assertTrue(ircutils.isChannel(m.args[0]))
|
self.assertTrue(ircutils.isChannel(m.args[0]))
|
||||||
m = self.assertNotError('explicit')
|
m = self.assertNotError('explicit')
|
||||||
self.assertFalse(m.command == 'NOTICE')
|
self.assertNotEqual(m.command, 'NOTICE')
|
||||||
self.assertTrue(ircutils.isChannel(m.args[0]))
|
self.assertTrue(ircutils.isChannel(m.args[0]))
|
||||||
# Check abnormal behavior.
|
# Check abnormal behavior.
|
||||||
originalInPrivate = conf.supybot.reply.inPrivate()
|
originalInPrivate = conf.supybot.reply.inPrivate()
|
||||||
@ -1090,10 +1124,10 @@ class WithPrivateNoticeTestCase(ChannelPluginTestCase):
|
|||||||
conf.supybot.reply.inPrivate.setValue(True)
|
conf.supybot.reply.inPrivate.setValue(True)
|
||||||
conf.supybot.reply.withNotice.setValue(True)
|
conf.supybot.reply.withNotice.setValue(True)
|
||||||
m = self.assertNotError('normal')
|
m = self.assertNotError('normal')
|
||||||
self.assertTrue(m.command == 'NOTICE')
|
self.assertEqual(m.command, 'NOTICE')
|
||||||
self.assertFalse(ircutils.isChannel(m.args[0]))
|
self.assertFalse(ircutils.isChannel(m.args[0]))
|
||||||
m = self.assertNotError('explicit')
|
m = self.assertNotError('explicit')
|
||||||
self.assertFalse(m.command == 'NOTICE')
|
self.assertNotEqual(m.command, 'NOTICE')
|
||||||
self.assertTrue(ircutils.isChannel(m.args[0]))
|
self.assertTrue(ircutils.isChannel(m.args[0]))
|
||||||
finally:
|
finally:
|
||||||
conf.supybot.reply.inPrivate.setValue(originalInPrivate)
|
conf.supybot.reply.inPrivate.setValue(originalInPrivate)
|
||||||
@ -1102,10 +1136,10 @@ class WithPrivateNoticeTestCase(ChannelPluginTestCase):
|
|||||||
try:
|
try:
|
||||||
conf.supybot.reply.withNoticeWhenPrivate.setValue(True)
|
conf.supybot.reply.withNoticeWhenPrivate.setValue(True)
|
||||||
m = self.assertNotError('implicit')
|
m = self.assertNotError('implicit')
|
||||||
self.assertTrue(m.command == 'NOTICE')
|
self.assertEqual(m.command, 'NOTICE')
|
||||||
self.assertFalse(ircutils.isChannel(m.args[0]))
|
self.assertFalse(ircutils.isChannel(m.args[0]))
|
||||||
m = self.assertNotError('normal')
|
m = self.assertNotError('normal')
|
||||||
self.assertFalse(m.command == 'NOTICE')
|
self.assertNotEqual(m.command, 'NOTICE')
|
||||||
self.assertTrue(ircutils.isChannel(m.args[0]))
|
self.assertTrue(ircutils.isChannel(m.args[0]))
|
||||||
finally:
|
finally:
|
||||||
conf.supybot.reply.withNoticeWhenPrivate.setValue(orig)
|
conf.supybot.reply.withNoticeWhenPrivate.setValue(orig)
|
||||||
@ -1115,10 +1149,10 @@ class WithPrivateNoticeTestCase(ChannelPluginTestCase):
|
|||||||
try:
|
try:
|
||||||
conf.supybot.reply.withNoticeWhenPrivate.setValue(True)
|
conf.supybot.reply.withNoticeWhenPrivate.setValue(True)
|
||||||
m = self.assertNotError("eval irc.reply('y',to='x',private=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.getMsg(' ')
|
||||||
m = self.assertNotError("eval irc.reply('y',to='#x',private=True)")
|
m = self.assertNotError("eval irc.reply('y',to='#x',private=True)")
|
||||||
self.assertFalse(m.command == 'NOTICE')
|
self.assertNotEqual(m.command, 'NOTICE')
|
||||||
finally:
|
finally:
|
||||||
conf.supybot.reply.withNoticeWhenPrivate.setValue(original)
|
conf.supybot.reply.withNoticeWhenPrivate.setValue(original)
|
||||||
|
|
||||||
@ -1130,28 +1164,28 @@ class ProxyTestCase(SupyTestCase):
|
|||||||
irc = irclib.Irc('test')
|
irc = irclib.Irc('test')
|
||||||
proxy = callbacks.SimpleProxy(irc, msg)
|
proxy = callbacks.SimpleProxy(irc, msg)
|
||||||
# First one way...
|
# First one way...
|
||||||
self.assertFalse(proxy != irc)
|
self.assertEqual(proxy, irc)
|
||||||
self.assertTrue(proxy == irc)
|
self.assertEqual(proxy, irc)
|
||||||
self.assertEqual(hash(proxy), hash(irc))
|
self.assertEqual(hash(proxy), hash(irc))
|
||||||
# Then the other!
|
# Then the other!
|
||||||
self.assertFalse(irc != proxy)
|
self.assertEqual(irc, proxy)
|
||||||
self.assertTrue(irc == proxy)
|
self.assertEqual(irc, proxy)
|
||||||
self.assertEqual(hash(irc), hash(proxy))
|
self.assertEqual(hash(irc), hash(proxy))
|
||||||
|
|
||||||
# And now dictionaries...
|
# And now dictionaries...
|
||||||
d = {}
|
d = {}
|
||||||
d[irc] = 'foo'
|
d[irc] = 'foo'
|
||||||
self.assertTrue(len(d) == 1)
|
self.assertEqual(len(d), 1)
|
||||||
self.assertTrue(d[irc] == 'foo')
|
self.assertEqual(d[irc], 'foo')
|
||||||
self.assertTrue(d[proxy] == 'foo')
|
self.assertEqual(d[proxy], 'foo')
|
||||||
d[proxy] = 'bar'
|
d[proxy] = 'bar'
|
||||||
self.assertTrue(len(d) == 1)
|
self.assertEqual(len(d), 1)
|
||||||
self.assertTrue(d[irc] == 'bar')
|
self.assertEqual(d[irc], 'bar')
|
||||||
self.assertTrue(d[proxy] == 'bar')
|
self.assertEqual(d[proxy], 'bar')
|
||||||
d[irc] = 'foo'
|
d[irc] = 'foo'
|
||||||
self.assertTrue(len(d) == 1)
|
self.assertEqual(len(d), 1)
|
||||||
self.assertTrue(d[irc] == 'foo')
|
self.assertEqual(d[irc], 'foo')
|
||||||
self.assertTrue(d[proxy] == 'foo')
|
self.assertEqual(d[proxy], 'foo')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
###
|
###
|
||||||
# Copyright (c) 2002-2005, Jeremiah Fincher
|
# Copyright (c) 2002-2005, Jeremiah Fincher
|
||||||
# Copyright (c) 2015, James McCoy
|
# Copyright (c) 2015, James McCoy
|
||||||
# Copyright (c) 2010-2021, The Limnoria Contributors
|
# Copyright (c) 2010-2021, Valentin Lorentz
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
##
|
##
|
||||||
# Copyright (c) 2019-2021, The Limnoria Contributors
|
# Copyright (c) 2019-2021, Valentin Lorentz
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
###
|
###
|
||||||
# Copyright (c) 2005, Jeremiah Fincher
|
# Copyright (c) 2005, Jeremiah Fincher
|
||||||
# Copyright (c) 2010-2021, The Limnoria Contributors
|
# Copyright (c) 2010-2021, Valentin Lorentz
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
###
|
###
|
||||||
# Copyright (c) 2008, Jeremiah Fincher
|
# Copyright (c) 2008, Jeremiah Fincher
|
||||||
# Copyright (c) 2010-2021, The Limnoria Contributors
|
# Copyright (c) 2010-2021, Valentin Lorentz
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
###
|
###
|
||||||
# Copyright (c) 2005, Jeremiah Fincher
|
# Copyright (c) 2005, Jeremiah Fincher
|
||||||
# Copyright (c) 2010-2021, The Limnoria Contributors
|
# Copyright (c) 2010-2021, Valentin Lorentz
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf8 -*-
|
# -*- coding: utf8 -*-
|
||||||
###
|
###
|
||||||
# Copyright (c) 2012-2021, The Limnoria Contributors
|
# Copyright (c) 2012-2021, Valentin Lorentz
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
###
|
###
|
||||||
# Copyright (c) 2002-2005, Jeremiah Fincher
|
# Copyright (c) 2002-2005, Jeremiah Fincher
|
||||||
# Copyright (c) 2010-2021, The Limnoria Contributors
|
# Copyright (c) 2010-2021, Valentin Lorentz
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
@ -122,17 +122,17 @@ class CapabilitySetTestCase(SupyTestCase):
|
|||||||
|
|
||||||
def testContains(self):
|
def testContains(self):
|
||||||
s = ircdb.CapabilitySet()
|
s = ircdb.CapabilitySet()
|
||||||
self.assertFalse('foo' in s)
|
self.assertNotIn('foo', s)
|
||||||
self.assertFalse('-foo' in s)
|
self.assertNotIn('-foo', s)
|
||||||
s.add('foo')
|
s.add('foo')
|
||||||
self.assertTrue('foo' in s)
|
self.assertIn('foo', s)
|
||||||
self.assertTrue('-foo' in s)
|
self.assertIn('-foo', s)
|
||||||
s.remove('foo')
|
s.remove('foo')
|
||||||
self.assertFalse('foo' in s)
|
self.assertNotIn('foo', s)
|
||||||
self.assertFalse('-foo' in s)
|
self.assertNotIn('-foo', s)
|
||||||
s.add('-foo')
|
s.add('-foo')
|
||||||
self.assertTrue('foo' in s)
|
self.assertIn('foo', s)
|
||||||
self.assertTrue('-foo' in s)
|
self.assertIn('-foo', s)
|
||||||
|
|
||||||
def testCheck(self):
|
def testCheck(self):
|
||||||
s = ircdb.CapabilitySet()
|
s = ircdb.CapabilitySet()
|
||||||
@ -170,8 +170,8 @@ class UserCapabilitySetTestCase(SupyTestCase):
|
|||||||
|
|
||||||
def testOwnerIsAlwaysPresent(self):
|
def testOwnerIsAlwaysPresent(self):
|
||||||
d = ircdb.UserCapabilitySet()
|
d = ircdb.UserCapabilitySet()
|
||||||
self.assertTrue('owner' in d)
|
self.assertIn('owner', d)
|
||||||
self.assertTrue('-owner' in d)
|
self.assertIn('-owner', d)
|
||||||
self.assertFalse(d.check('owner'))
|
self.assertFalse(d.check('owner'))
|
||||||
d.add('owner')
|
d.add('owner')
|
||||||
self.assertTrue(d.check('owner'))
|
self.assertTrue(d.check('owner'))
|
||||||
@ -187,8 +187,8 @@ class UserCapabilitySetTestCase(SupyTestCase):
|
|||||||
def testOwner(self):
|
def testOwner(self):
|
||||||
s = ircdb.UserCapabilitySet()
|
s = ircdb.UserCapabilitySet()
|
||||||
s.add('owner')
|
s.add('owner')
|
||||||
self.assertTrue('foo' in s)
|
self.assertIn('foo', s)
|
||||||
self.assertTrue('-foo' in s)
|
self.assertIn('-foo', s)
|
||||||
self.assertTrue(s.check('owner'))
|
self.assertTrue(s.check('owner'))
|
||||||
self.assertFalse(s.check('-owner'))
|
self.assertFalse(s.check('-owner'))
|
||||||
self.assertFalse(s.check('-foo'))
|
self.assertFalse(s.check('-foo'))
|
||||||
@ -265,7 +265,7 @@ class IrcUserTestCase(IrcdbTestCase):
|
|||||||
self.assertTrue(u.checkHostmask('foo!bar@baz'))
|
self.assertTrue(u.checkHostmask('foo!bar@baz'))
|
||||||
u.addAuth('foo!bar@baz')
|
u.addAuth('foo!bar@baz')
|
||||||
self.assertTrue(u.checkHostmask('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')
|
u.addAuth('boo!far@fizz')
|
||||||
self.assertTrue(u.checkHostmask('boo!far@fizz'))
|
self.assertTrue(u.checkHostmask('boo!far@fizz'))
|
||||||
timeFastForward(2.1)
|
timeFastForward(2.1)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
##
|
##
|
||||||
# Copyright (c) 2002-2005, Jeremiah Fincher
|
# Copyright (c) 2002-2005, Jeremiah Fincher
|
||||||
# Copyright (c) 2010-2021, The Limnoria Contributors
|
# Copyright (c) 2010-2021, Valentin Lorentz
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
@ -52,21 +52,21 @@ class CapNegMixin:
|
|||||||
|
|
||||||
def startCapNegociation(self, caps='sasl'):
|
def startCapNegociation(self, caps='sasl'):
|
||||||
m = self.irc.takeMsg()
|
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.assertTrue(m.args == ('LS', '302'), 'Expected CAP LS 302, got %r.' % m)
|
self.assertEqual(m.args, ('LS', '302'), 'Expected CAP LS 302, got %r.' % m)
|
||||||
|
|
||||||
m = self.irc.takeMsg()
|
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()
|
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',
|
self.irc.feedMsg(ircmsgs.IrcMsg(command='CAP',
|
||||||
args=('*', 'LS', caps)))
|
args=('*', 'LS', caps)))
|
||||||
|
|
||||||
if caps:
|
if caps:
|
||||||
m = self.irc.takeMsg()
|
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[0], 'REQ', m)
|
||||||
self.assertEqual(m.args[1], 'sasl')
|
self.assertEqual(m.args[1], 'sasl')
|
||||||
|
|
||||||
@ -75,7 +75,7 @@ class CapNegMixin:
|
|||||||
|
|
||||||
def endCapNegociation(self):
|
def endCapNegociation(self):
|
||||||
m = self.irc.takeMsg()
|
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)
|
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)
|
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()
|
q.dequeue()
|
||||||
self.assertTrue(self.msg in q)
|
self.assertIn(self.msg, q)
|
||||||
q.dequeue()
|
q.dequeue()
|
||||||
self.assertTrue(self.msg in q)
|
self.assertIn(self.msg, q)
|
||||||
q.dequeue()
|
q.dequeue()
|
||||||
self.assertFalse(self.msg in q)
|
self.assertNotIn(self.msg, q)
|
||||||
|
|
||||||
def testRepr(self):
|
def testRepr(self):
|
||||||
q = irclib.IrcMsgQueue()
|
q = irclib.IrcMsgQueue()
|
||||||
@ -313,39 +313,39 @@ class ChannelStateTestCase(SupyTestCase):
|
|||||||
c1 = pickle.loads(pickle.dumps(c))
|
c1 = pickle.loads(pickle.dumps(c))
|
||||||
self.assertEqual(c, c1)
|
self.assertEqual(c, c1)
|
||||||
c.removeUser('jemfinch')
|
c.removeUser('jemfinch')
|
||||||
self.assertFalse('jemfinch' in c.users)
|
self.assertNotIn('jemfinch', c.users)
|
||||||
self.assertTrue('jemfinch' in c1.users)
|
self.assertIn('jemfinch', c1.users)
|
||||||
|
|
||||||
def testCopy(self):
|
def testCopy(self):
|
||||||
c = irclib.ChannelState()
|
c = irclib.ChannelState()
|
||||||
c.addUser('jemfinch')
|
c.addUser('jemfinch')
|
||||||
c1 = copy.deepcopy(c)
|
c1 = copy.deepcopy(c)
|
||||||
c.removeUser('jemfinch')
|
c.removeUser('jemfinch')
|
||||||
self.assertFalse('jemfinch' in c.users)
|
self.assertNotIn('jemfinch', c.users)
|
||||||
self.assertTrue('jemfinch' in c1.users)
|
self.assertIn('jemfinch', c1.users)
|
||||||
|
|
||||||
def testAddUser(self):
|
def testAddUser(self):
|
||||||
c = irclib.ChannelState()
|
c = irclib.ChannelState()
|
||||||
c.addUser('foo')
|
c.addUser('foo')
|
||||||
self.assertTrue('foo' in c.users)
|
self.assertIn('foo', c.users)
|
||||||
self.assertFalse('foo' in c.ops)
|
self.assertNotIn('foo', c.ops)
|
||||||
self.assertFalse('foo' in c.voices)
|
self.assertNotIn('foo', c.voices)
|
||||||
self.assertFalse('foo' in c.halfops)
|
self.assertNotIn('foo', c.halfops)
|
||||||
c.addUser('+bar')
|
c.addUser('+bar')
|
||||||
self.assertTrue('bar' in c.users)
|
self.assertIn('bar', c.users)
|
||||||
self.assertTrue('bar' in c.voices)
|
self.assertIn('bar', c.voices)
|
||||||
self.assertFalse('bar' in c.ops)
|
self.assertNotIn('bar', c.ops)
|
||||||
self.assertFalse('bar' in c.halfops)
|
self.assertNotIn('bar', c.halfops)
|
||||||
c.addUser('%baz')
|
c.addUser('%baz')
|
||||||
self.assertTrue('baz' in c.users)
|
self.assertIn('baz', c.users)
|
||||||
self.assertTrue('baz' in c.halfops)
|
self.assertIn('baz', c.halfops)
|
||||||
self.assertFalse('baz' in c.voices)
|
self.assertNotIn('baz', c.voices)
|
||||||
self.assertFalse('baz' in c.ops)
|
self.assertNotIn('baz', c.ops)
|
||||||
c.addUser('@quuz')
|
c.addUser('@quuz')
|
||||||
self.assertTrue('quuz' in c.users)
|
self.assertIn('quuz', c.users)
|
||||||
self.assertTrue('quuz' in c.ops)
|
self.assertIn('quuz', c.ops)
|
||||||
self.assertFalse('quuz' in c.halfops)
|
self.assertNotIn('quuz', c.halfops)
|
||||||
self.assertFalse('quuz' in c.voices)
|
self.assertNotIn('quuz', c.voices)
|
||||||
|
|
||||||
|
|
||||||
class IrcStateTestCase(SupyTestCase):
|
class IrcStateTestCase(SupyTestCase):
|
||||||
@ -362,7 +362,7 @@ class IrcStateTestCase(SupyTestCase):
|
|||||||
st.channels['#foo'] = irclib.ChannelState()
|
st.channels['#foo'] = irclib.ChannelState()
|
||||||
m = ircmsgs.kick('#foo', self.irc.nick, prefix=self.irc.prefix)
|
m = ircmsgs.kick('#foo', self.irc.nick, prefix=self.irc.prefix)
|
||||||
st.addMsg(self.irc, m)
|
st.addMsg(self.irc, m)
|
||||||
self.assertFalse('#foo' in st.channels)
|
self.assertNotIn('#foo', st.channels)
|
||||||
|
|
||||||
def testAddMsgRemovesOpsProperly(self):
|
def testAddMsgRemovesOpsProperly(self):
|
||||||
st = irclib.IrcState()
|
st = irclib.IrcState()
|
||||||
@ -370,18 +370,18 @@ class IrcStateTestCase(SupyTestCase):
|
|||||||
st.channels['#foo'].ops.add('bar')
|
st.channels['#foo'].ops.add('bar')
|
||||||
m = ircmsgs.mode('#foo', ('-o', 'bar'))
|
m = ircmsgs.mode('#foo', ('-o', 'bar'))
|
||||||
st.addMsg(self.irc, m)
|
st.addMsg(self.irc, m)
|
||||||
self.assertFalse('bar' in st.channels['#foo'].ops)
|
self.assertNotIn('bar', st.channels['#foo'].ops)
|
||||||
|
|
||||||
def testNickChangesChangeChannelUsers(self):
|
def testNickChangesChangeChannelUsers(self):
|
||||||
st = irclib.IrcState()
|
st = irclib.IrcState()
|
||||||
st.channels['#foo'] = irclib.ChannelState()
|
st.channels['#foo'] = irclib.ChannelState()
|
||||||
st.channels['#foo'].addUser('@bar')
|
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'))
|
self.assertTrue(st.channels['#foo'].isOp('bar'))
|
||||||
st.addMsg(self.irc, ircmsgs.IrcMsg(':bar!asfd@asdf.com NICK baz'))
|
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.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'))
|
self.assertTrue(st.channels['#foo'].isOp('baz'))
|
||||||
|
|
||||||
def testHistory(self):
|
def testHistory(self):
|
||||||
@ -478,20 +478,106 @@ class IrcStateTestCase(SupyTestCase):
|
|||||||
state = irclib.IrcState()
|
state = irclib.IrcState()
|
||||||
stateCopy = state.copy()
|
stateCopy = state.copy()
|
||||||
state.channels['#foo'] = None
|
state.channels['#foo'] = None
|
||||||
self.assertFalse('#foo' in stateCopy.channels)
|
self.assertNotIn('#foo', stateCopy.channels)
|
||||||
|
|
||||||
def testJoin(self):
|
def testJoin(self):
|
||||||
st = irclib.IrcState()
|
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)
|
|
||||||
st.addMsg(self.irc, ircmsgs.join('#foo', prefix='foo!bar@baz'))
|
|
||||||
self.assertTrue('foo' in 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)
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
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):
|
def testEq(self):
|
||||||
state1 = irclib.IrcState()
|
state1 = irclib.IrcState()
|
||||||
@ -508,23 +594,23 @@ class IrcStateTestCase(SupyTestCase):
|
|||||||
def testHandlesModes(self):
|
def testHandlesModes(self):
|
||||||
st = irclib.IrcState()
|
st = irclib.IrcState()
|
||||||
st.addMsg(self.irc, ircmsgs.join('#foo', prefix=self.irc.prefix))
|
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'))
|
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'))
|
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'))
|
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'))
|
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'))
|
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'))
|
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):
|
def testDoModeOnlyChannels(self):
|
||||||
st = irclib.IrcState()
|
st = irclib.IrcState()
|
||||||
@ -572,14 +658,14 @@ class IrcCapsTestCase(SupyTestCase, CapNegMixin):
|
|||||||
self.irc = irclib.Irc('test')
|
self.irc = irclib.Irc('test')
|
||||||
|
|
||||||
m = self.irc.takeMsg()
|
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.assertTrue(m.args == ('LS', '302'), 'Expected CAP LS 302, got %r.' % m)
|
self.assertEqual(m.args, ('LS', '302'), 'Expected CAP LS 302, got %r.' % m)
|
||||||
|
|
||||||
m = self.irc.takeMsg()
|
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()
|
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])
|
self.irc.REQUEST_CAPABILITIES = set(['a'*400, 'b'*400])
|
||||||
caps = ' '.join(self.irc.REQUEST_CAPABILITIES)
|
caps = ' '.join(self.irc.REQUEST_CAPABILITIES)
|
||||||
@ -589,12 +675,12 @@ class IrcCapsTestCase(SupyTestCase, CapNegMixin):
|
|||||||
args=('*', 'LS', 'b'*400)))
|
args=('*', 'LS', 'b'*400)))
|
||||||
|
|
||||||
m = self.irc.takeMsg()
|
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[0], 'REQ', m)
|
||||||
self.assertEqual(m.args[1], 'a'*400)
|
self.assertEqual(m.args[1], 'a'*400)
|
||||||
|
|
||||||
m = self.irc.takeMsg()
|
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[0], 'REQ', m)
|
||||||
self.assertEqual(m.args[1], 'b'*400)
|
self.assertEqual(m.args[1], 'b'*400)
|
||||||
|
|
||||||
@ -602,20 +688,20 @@ class IrcCapsTestCase(SupyTestCase, CapNegMixin):
|
|||||||
self.irc = irclib.Irc('test')
|
self.irc = irclib.Irc('test')
|
||||||
|
|
||||||
m = self.irc.takeMsg()
|
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.assertTrue(m.args == ('LS', '302'), 'Expected CAP LS 302, got %r.' % m)
|
self.assertEqual(m.args, ('LS', '302'), 'Expected CAP LS 302, got %r.' % m)
|
||||||
|
|
||||||
m = self.irc.takeMsg()
|
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()
|
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',
|
self.irc.feedMsg(ircmsgs.IrcMsg(command='CAP',
|
||||||
args=('*', 'LS', 'account-notify echo-message')))
|
args=('*', 'LS', 'account-notify echo-message')))
|
||||||
|
|
||||||
m = self.irc.takeMsg()
|
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[0], 'REQ', m)
|
||||||
self.assertEqual(m.args[1], 'account-notify')
|
self.assertEqual(m.args[1], 'account-notify')
|
||||||
|
|
||||||
@ -626,21 +712,21 @@ class IrcCapsTestCase(SupyTestCase, CapNegMixin):
|
|||||||
args=('*', 'ACK', 'account-notify')))
|
args=('*', 'ACK', 'account-notify')))
|
||||||
|
|
||||||
m = self.irc.takeMsg()
|
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)
|
self.assertEqual(m.args, ('END',), m)
|
||||||
|
|
||||||
def testEchomessageLabeledresponseGrouped(self):
|
def testEchomessageLabeledresponseGrouped(self):
|
||||||
self.irc = irclib.Irc('test')
|
self.irc = irclib.Irc('test')
|
||||||
|
|
||||||
m = self.irc.takeMsg()
|
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.assertTrue(m.args == ('LS', '302'), 'Expected CAP LS 302, got %r.' % m)
|
self.assertEqual(m.args, ('LS', '302'), 'Expected CAP LS 302, got %r.' % m)
|
||||||
|
|
||||||
m = self.irc.takeMsg()
|
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()
|
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([
|
self.irc.REQUEST_CAPABILITIES = set([
|
||||||
'account-notify', 'a'*490, 'echo-message', 'labeled-response'])
|
'account-notify', 'a'*490, 'echo-message', 'labeled-response'])
|
||||||
@ -649,17 +735,17 @@ class IrcCapsTestCase(SupyTestCase, CapNegMixin):
|
|||||||
'account-notify ' + 'a'*490 + ' echo-message labeled-response')))
|
'account-notify ' + 'a'*490 + ' echo-message labeled-response')))
|
||||||
|
|
||||||
m = self.irc.takeMsg()
|
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[0], 'REQ', m)
|
||||||
self.assertEqual(m.args[1], 'echo-message labeled-response')
|
self.assertEqual(m.args[1], 'echo-message labeled-response')
|
||||||
|
|
||||||
m = self.irc.takeMsg()
|
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[0], 'REQ', m)
|
||||||
self.assertEqual(m.args[1], 'a'*490)
|
self.assertEqual(m.args[1], 'a'*490)
|
||||||
|
|
||||||
m = self.irc.takeMsg()
|
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[0], 'REQ', m)
|
||||||
self.assertEqual(m.args[1], 'account-notify')
|
self.assertEqual(m.args[1], 'account-notify')
|
||||||
|
|
||||||
@ -836,14 +922,14 @@ class IrcTestCase(SupyTestCase):
|
|||||||
#self.assertTrue(m.command == 'PASS', 'Expected PASS, got %r.' % m)
|
#self.assertTrue(m.command == 'PASS', 'Expected PASS, got %r.' % m)
|
||||||
|
|
||||||
m = self.irc.takeMsg()
|
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.assertTrue(m.args == ('LS', '302'), 'Expected CAP LS 302, got %r.' % m)
|
self.assertEqual(m.args, ('LS', '302'), 'Expected CAP LS 302, got %r.' % m)
|
||||||
|
|
||||||
m = self.irc.takeMsg()
|
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()
|
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
|
# TODO
|
||||||
self.irc.feedMsg(ircmsgs.IrcMsg(command='CAP',
|
self.irc.feedMsg(ircmsgs.IrcMsg(command='CAP',
|
||||||
@ -852,7 +938,7 @@ class IrcTestCase(SupyTestCase):
|
|||||||
args=('*', 'LS', 'extended-join')))
|
args=('*', 'LS', 'extended-join')))
|
||||||
|
|
||||||
m = self.irc.takeMsg()
|
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[0], 'REQ', m)
|
||||||
# NOTE: Capabilities are requested in alphabetic order, because
|
# NOTE: Capabilities are requested in alphabetic order, because
|
||||||
# sets are unordered, and their "order" is nondeterministic.
|
# sets are unordered, and their "order" is nondeterministic.
|
||||||
@ -862,11 +948,11 @@ class IrcTestCase(SupyTestCase):
|
|||||||
args=('*', 'ACK', 'account-tag multi-prefix extended-join')))
|
args=('*', 'ACK', 'account-tag multi-prefix extended-join')))
|
||||||
|
|
||||||
m = self.irc.takeMsg()
|
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)
|
self.assertEqual(m.args, ('END',), m)
|
||||||
|
|
||||||
m = self.irc.takeMsg()
|
m = self.irc.takeMsg()
|
||||||
self.assertTrue(m is None, m)
|
self.assertIsNone(m, m)
|
||||||
|
|
||||||
def testPingResponse(self):
|
def testPingResponse(self):
|
||||||
self.irc.feedMsg(ircmsgs.ping('123'))
|
self.irc.feedMsg(ircmsgs.ping('123'))
|
||||||
@ -890,9 +976,9 @@ class IrcTestCase(SupyTestCase):
|
|||||||
self.irc.queueMsg(ircmsgs.IrcMsg('NOTICE #foo bar'))
|
self.irc.queueMsg(ircmsgs.IrcMsg('NOTICE #foo bar'))
|
||||||
self.irc.sendMsg(ircmsgs.IrcMsg('PRIVMSG #foo yeah!'))
|
self.irc.sendMsg(ircmsgs.IrcMsg('PRIVMSG #foo yeah!'))
|
||||||
msg = self.irc.takeMsg()
|
msg = self.irc.takeMsg()
|
||||||
self.assertTrue(msg.command == 'PRIVMSG')
|
self.assertEqual(msg.command, 'PRIVMSG')
|
||||||
msg = self.irc.takeMsg()
|
msg = self.irc.takeMsg()
|
||||||
self.assertTrue(msg.command == 'NOTICE')
|
self.assertEqual(msg.command, 'NOTICE')
|
||||||
|
|
||||||
def testNoMsgLongerThan512(self):
|
def testNoMsgLongerThan512(self):
|
||||||
self.irc.queueMsg(ircmsgs.privmsg('whocares', 'x'*1000))
|
self.irc.queueMsg(ircmsgs.privmsg('whocares', 'x'*1000))
|
||||||
@ -1542,7 +1628,7 @@ class SaslTestCase(SupyTestCase, CapNegMixin):
|
|||||||
conf.supybot.networks.test.sasl.password.setValue('')
|
conf.supybot.networks.test.sasl.password.setValue('')
|
||||||
|
|
||||||
m = self.irc.takeMsg()
|
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[0], 'REQ', m)
|
||||||
self.assertEqual(m.args[1], 'sasl')
|
self.assertEqual(m.args[1], 'sasl')
|
||||||
self.irc.feedMsg(ircmsgs.IrcMsg(command='CAP',
|
self.irc.feedMsg(ircmsgs.IrcMsg(command='CAP',
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user