Compare commits

...

13 Commits

Author SHA1 Message Date
Valentin Lorentz 8fe517f48a irclib: Fix previous commits so RPL_NAMREPLY actually works
Turns out irclib parses ISUPPORT PREFIX properly, so it's not just
a raw string; but I didn't test the change live...
2021-07-16 22:53:38 +02:00
Valentin Lorentz 45c7615f4a irclib: Properly populate nickToHostmasks on RPL_NAMREPLY
It used to set the nick instead of the hostmask as values...
2021-07-16 19:53:25 +02:00
Valentin Lorentz d308329461 irclib: Fix support of power prefix chars in RPL_NAMREPLY
nickFromHostmask now (legitimately) complains when it's getting @ or !
at the beginning of a hostmask; so we need to strip them before passing
it to nickFromHostmask.

Then re-add them before calling c.addUser, because it uses them to
sort users in the right sets (ops/halfops/voices).

Additionally, this commit replaces the hardcoded set of prefix chars
(`@%+&~!`) with the one advertised in ISUPPORT when possible.
2021-07-16 19:50:13 +02:00
Valentin Lorentz aa6bd7257d Deprecate Python 3.4 and 3.5. 2021-07-15 22:29:56 +02:00
Valentin Lorentz bdb80b196a Switch from Travis-CI to Github Actions
Travis is dead now.

Closes GH-1481.
2021-07-15 22:19:03 +02:00
Valentin Lorentz 0f1011081e Socket: Fix cascading crash when Socket.run() crashes.
When a driver's run() method crashes, supybot.drivers.run() marks it
as dead and sets its 'irc' attribute to None.

This would be fine for "normal" independent drivers (like Socket used
to be), because this driver would never be called again.

But now that we use select(), some other thread may hold a reference
to this driver in a select() call frame, and call the dead driver's
'_read()' method when there is data to be read from the socket.

There is already a safeguard in '_read()' in the case the socket could
be read from, but this safeguard was missing from _handleSocketError.
This caused the "live" driver's select() to crash, which propagagated
to its run(), which caused the driver to be marked as dead, etc.

Eventually, all drivers could die, and we end up with the dreadful
"Schedule is the only remaining driver, why do we continue to live?"
in an infinite loop.
2021-07-14 23:55:31 +02:00
Valentin Lorentz e19282a2d3 Actually parse weird hostmasks like RFC1459 recommends
Thanks to @Noisytoot for pointing out the RFC actually specific them
with this:

<user>       ::= <nonwhite> { <nonwhite> }
2021-07-14 23:43:11 +02:00
Valentin Lorentz 5baf87ddba ircutils: Improve robustness when faced with invalid hostmasks
eg. @ in nicks, which happened on pissnet earlier today.
2021-07-14 23:25:11 +02:00
Valentin Lorentz 0af4af16d3 RSS: Fix random test failure
Closes GH-1479
2021-07-04 10:46:51 +02:00
James Lu 64ae28c0b8 Remove references to my old nick 2021-07-03 16:42:13 -07:00
Valentin Lorentz b8aa5aa33e User: Make @register automatically add the account tag
No need for '@nickauth nick add' right after registering anymore.
2021-06-30 21:28:17 +02:00
Valentin Lorentz c23227cdc7 MessageParser: Show error when the action has a syntax error
Instead of being silent
2021-06-28 23:10:36 +02:00
Valentin Lorentz 6b72672a1e Poll: Fix typo in documentation 2021-06-28 23:10:36 +02:00
22 changed files with 202 additions and 81 deletions

62
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,62 @@
name: Test
on:
push:
pull_request:
jobs:
build:
runs-on: ${{ matrix.runs-on }}
strategy:
matrix:
python-version: ["3.5", "3.6", "3.7", "3.8", "3.9", "3.10.0-beta.4", "pypy-3.6", "pypy-3.7"]
with-opt-deps: [false, true]
runs-on: [ubuntu-latest]
exclude:
# Some of the dependencies don't work on old Python versions
- python-version: "3.4"
with-opt-deps: true
- python-version: "3.5"
with-opt-deps: true
- python-version: "3.6"
with-opt-deps: true
- python-version: "pypy-3.6"
with-opt-deps: true
include:
- python-version: "3.4"
with-opt-deps: false
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Upgrade pip
run: |
python3 -m pip install --upgrade pip
- name: Install optional dependencies
if: ${{ matrix.with-opt-deps }}
run: |
python3 -m pip install --upgrade pip
pip3 install -r requirements.txt
- name: Install
run: |
LIMNORIA_WARN_OLD_PYTHON=0 python3 setup.py install
- name: Test with unittest
run: |
supybot-test test -v --plugins-dir=./plugins/ --no-network
- name: Test with irctest
if: "${{ matrix.with-opt-deps && matrix.python-version != 'pypy-3.7' }}"
run: |
git clone https://github.com/ProgVal/irctest.git
cd irctest
pip3 install -r requirements.txt
make limnoria

View File

@ -1,54 +0,0 @@
language: python
sudo: true
install:
- if [ "$WITH_OPT_DEPS" = "true" ] ; then pip install -vr requirements.txt pytest; fi
- git clone https://github.com/ProgVal/irctest.git
- echo "y" | pip uninstall limnoria || true
# command to run tests, e.g. python setup.py test
script:
- echo $TRAVIS_PYTHON_VERSION
- python setup.py install
- supybot-test test -v --plugins-dir=./plugins/ --no-network
- if [ "$WITH_OPT_DEPS" = "true" ] -a [[ "$TRAVIS_PYTHON_VERSION" =~ ^3\.[4-9] ]] ; then cd irctest; pytest --controllers irctest.controllers.limnoria; fi
notifications:
email: false
matrix:
include:
- python: "3.4"
env: WITH_OPT_DEPS=false
dist: trusty
- python: "3.5"
env: WITH_OPT_DEPS=false
dist: trusty
- python: "3.6"
env: WITH_OPT_DEPS=false
dist: trusty
- python: "3.7"
env: WITH_OPT_DEPS=false
dist: xenial
- python: "3.7"
env: WITH_OPT_DEPS=true
dist: xenial
- python: "3.8"
env: WITH_OPT_DEPS=true
dist: xenial
- python: "3.9"
env: WITH_OPT_DEPS=true
dist: xenial
- python: "nightly"
env: WITH_OPT_DEPS=true
dist: xenial
- python: "pypy3"
env: WITH_OPT_DEPS=false
dist: trusty
- python: "nightly"
env: WITH_OPT_DEPS=true
dist: xenial
allow_failures:
- python: "pypy3"
env: WITH_OPT_DEPS=true
dist: xenial

View File

@ -13,7 +13,7 @@ Searches for results on DuckDuckGo.
Example::
<+GLolol> %ddg search eiffel tower
<+jlu5> %ddg search eiffel tower
<@Atlas> The Eiffel Tower is an iron lattice tower located on the Champ de Mars in Paris. It was named after the engineer Gustave Eiffel, whose company designed and built the tower. - <https://en.wikipedia.org/wiki/Eiffel_Tower>
.. _commands-DDG:

View File

@ -55,7 +55,7 @@ class DDG(callbacks.Plugin):
Example::
<+GLolol> %ddg search eiffel tower
<+jlu5> %ddg search eiffel tower
<@Atlas> The Eiffel Tower is an iron lattice tower located on the Champ de Mars in Paris. It was named after the engineer Gustave Eiffel, whose company designed and built the tower. - <https://en.wikipedia.org/wiki/Eiffel_Tower>
"""

View File

@ -126,7 +126,7 @@ class Google(callbacks.PluginRegexp):
'plugin for your searches, like '
'<https://github.com/Hoaas/Supybot-plugins/tree/master/DuckDuckGo>, '
'<https://github.com/joulez/GoogleCSE>, or '
'<https://github.com/GLolol/SupyPlugins/tree/master/DDG>.')
'<https://github.com/jlu5/SupyPlugins/tree/master/DDG>.')
ref = self.registryValue('referer')
if not ref:
ref = 'http://%s/%s' % (dynamic.irc.server,

View File

@ -129,8 +129,13 @@ class MessageParser(callbacks.Plugin, plugins.ChannelDBHandler):
def _runCommandFunction(self, irc, msg, command):
"""Run a command from message, as if command was sent over IRC."""
tokens = callbacks.tokenize(command,
channel=msg.channel, network=irc.network)
try:
tokens = callbacks.tokenize(command,
channel=msg.channel, network=irc.network)
except SyntaxError as e:
# Emulate what callbacks.py does
self.log.debug('Error return: %s', utils.exnToString(e))
irc.error(str(e))
try:
self.Proxy(irc.irc, msg, tokens)
except Exception as e:

View File

@ -85,6 +85,11 @@ class MessageParserTestCase(ChannelPluginTestCase):
self.assertResponse(' ', '$1')
self.assertNotError('messageparser remove "this( .+)? a(.*)"')
def testSyntaxError(self):
self.assertNotError(r'messageparser add "test" "echo foo \" bar"')
self.feedMsg('test')
self.assertResponse(' ', 'Error: No closing quotation')
def testShow(self):
self.assertNotError('messageparser add "stuff" "echo i saw some stuff"')
self.assertRegexp('messageparser show "nostuff"', 'there is no such regexp trigger')

View File

@ -37,7 +37,7 @@ Examples
< Mikaela> @load PluginDownloader
< Limnoria> Ok.
< Mikaela> @plugindownloader repolist
< Limnoria> Antibody, GLolol, Hoaas, Iota, ProgVal, SpiderDave, boombot, code4lib, code4lib-edsu, code4lib-snapshot, doorbot, frumious, jonimoose, mailed-notifier, mtughan-weather, nanotube-bitcoin, nyuszika7h, nyuszika7h-old, pingdom, quantumlemur, resistivecorpse, scrum, skgsergio, stepnem
< Limnoria> Antibody, jlu5, Hoaas, Iota, ProgVal, SpiderDave, boombot, code4lib, code4lib-edsu, code4lib-snapshot, doorbot, frumious, jonimoose, mailed-notifier, mtughan-weather, nanotube-bitcoin, nyuszika7h, nyuszika7h-old, pingdom, quantumlemur, resistivecorpse, scrum, skgsergio, stepnem
< Mikaela> @plugindownloader repolist ProgVal
< Limnoria> AttackProtector, AutoTrans, Biography, Brainfuck, ChannelStatus, Cleverbot, Coffee, Coinpan, Debian, ERepublik, Eureka, Fortune, GUI, GitHub, Glob2Chan, GoodFrench, I18nPlaceholder, IMDb, IgnoreNonVoice, Iwant, Kickme, LimnoriaChan, LinkRelay, ListEmpty, Listener, Markovgen, MegaHAL, MilleBornes, NoLatin1, NoisyKarma, OEIS, PPP, PingTime, Pinglist, RateLimit, Rbls, Redmine, Scheme, Seeks, (1 more message)
< Mikaela> more

View File

@ -348,7 +348,7 @@ class PluginDownloader(callbacks.Plugin):
< Mikaela> @load PluginDownloader
< Limnoria> Ok.
< Mikaela> @plugindownloader repolist
< Limnoria> Antibody, GLolol, Hoaas, Iota, ProgVal, SpiderDave, boombot, code4lib, code4lib-edsu, code4lib-snapshot, doorbot, frumious, jonimoose, mailed-notifier, mtughan-weather, nanotube-bitcoin, nyuszika7h, nyuszika7h-old, pingdom, quantumlemur, resistivecorpse, scrum, skgsergio, stepnem
< Limnoria> Antibody, jlu5, Hoaas, Iota, ProgVal, SpiderDave, boombot, code4lib, code4lib-edsu, code4lib-snapshot, doorbot, frumious, jonimoose, mailed-notifier, mtughan-weather, nanotube-bitcoin, nyuszika7h, nyuszika7h-old, pingdom, quantumlemur, resistivecorpse, scrum, skgsergio, stepnem
< Mikaela> @plugindownloader repolist ProgVal
< Limnoria> AttackProtector, AutoTrans, Biography, Brainfuck, ChannelStatus, Cleverbot, Coffee, Coinpan, Debian, ERepublik, Eureka, Fortune, GUI, GitHub, Glob2Chan, GoodFrench, I18nPlaceholder, IMDb, IgnoreNonVoice, Iwant, Kickme, LimnoriaChan, LinkRelay, ListEmpty, Listener, Markovgen, MegaHAL, MilleBornes, NoLatin1, NoisyKarma, OEIS, PPP, PingTime, Pinglist, RateLimit, Rbls, Redmine, Scheme, Seeks, (1 more message)
< Mikaela> more

View File

@ -25,7 +25,7 @@ Creates a poll that can be voted on in this way::
And results::
<admin> @poll results
<bot> 2 votes for No, 1 vote for Yes, and 0 votes for Maybe",
<bot> 2 votes for No, 1 vote for Yes, and 0 votes for Maybe
Longer answers are possible, and voters only need to use the first
word of each answer to vote. For example, this creates a poll that

View File

@ -64,7 +64,7 @@ class Poll_(callbacks.Plugin):
And results::
<admin> @poll results
<bot> 2 votes for No, 1 vote for Yes, and 0 votes for Maybe",
<bot> 2 votes for No, 1 vote for Yes, and 0 votes for Maybe
Longer answers are possible, and voters only need to use the first
word of each answer to vote. For example, this creates a poll that

View File

@ -102,6 +102,7 @@ class RSSTestCase(ChannelPluginTestCase):
@mock_urllib
def testRemoveAliasedFeed(self, mock):
mock._data = xkcd_new
try:
self.assertNotError('rss announce add http://xkcd.com/rss.xml')
self.assertNotError('rss add xkcd http://xkcd.com/rss.xml')

View File

@ -16,9 +16,9 @@ After enabling SedRegex, typing a regex in the form
::
20:24 <~GL> helli world
20:24 <~GL> s/i/o/
20:24 <@Lily> GL meant to say: hello world
20:24 <jlu5> helli world
20:24 <jlu5> s/i/o/
20:24 <Limnoria> jlu5 meant to say: hello world
You can also do ``othernick: s/text/replacement/`` to only replace
messages from a certain user. Supybot ignores are respected by the plugin,

View File

@ -69,9 +69,9 @@ class SedRegex(callbacks.PluginRegexp):
::
20:24 <~GL> helli world
20:24 <~GL> s/i/o/
20:24 <@Lily> GL meant to say: hello world
20:24 <jlu5> helli world
20:24 <jlu5> s/i/o/
20:24 <Limnoria> jlu5 meant to say: hello world
You can also do ``othernick: s/text/replacement/`` to only replace
messages from a certain user. Supybot ignores are respected by the plugin,

View File

@ -146,6 +146,9 @@ class User(callbacks.Plugin):
user.setPassword(password)
if addHostmask:
user.addHostmask(msg.prefix)
account = msg.server_tags.get('account')
if account:
user.addNick(irc.network, account)
ircdb.users.setUser(user)
irc.replySuccess()
register = wrap(register, ['private', 'something', 'something'])

View File

@ -82,10 +82,19 @@ if version:
fd.close()
if sys.version_info < (3, 4, 0):
sys.stderr.write("Limnoria requires Python 3.4 or newer.")
sys.stderr.write("Limnoria requires Python 3.6 or newer.")
sys.stderr.write(os.linesep)
sys.exit(-1)
if sys.version_info < (3, 6, 0) \
and os.environ.get('LIMNORIA_WARN_OLD_PYTHON') != '0':
sys.stderr.write('====================================================\n')
sys.stderr.write('Limnoria support for Python versions older than 3.6\n')
sys.stderr.write('is deprecated and may be removed in the near future.\n')
sys.stderr.write('You should upgrade ASAP.\n')
sys.stderr.write('Install will continue in 60s.\n')
sys.stderr.write('====================================================\n')
time.sleep(60)
import textwrap

View File

@ -114,6 +114,13 @@ class SocketDriver(drivers.IrcDriver, drivers.ServersMixin):
except:
pass
self.connected = False
if self.irc is None:
# This driver is dead already, but we're still running because
# of select() running in an other driver's thread that started
# before this one died and stil holding a reference to this
# instance.
# Just return, and we should never be called again.
return
self.scheduleReconnect()
else:
log.debug('Got EAGAIN, current count: %s.', self.eagains)
@ -208,6 +215,8 @@ class SocketDriver(drivers.IrcDriver, drivers.ServersMixin):
msg = drivers.parseMsg(line)
if msg is not None and self.irc is not None:
# self.irc may be None if this driver is already dead,
# see comment in _handleSocketError
self.irc.feedMsg(msg)
except socket.timeout:
pass

View File

@ -389,15 +389,15 @@ class ChannelState(utils.python.Object):
"""Returns whether the given nick is an halfop, or an op."""
return nick in self.halfops or nick in self.ops
def addUser(self, user):
def addUser(self, user, prefix_chars='@%+&~!'):
"Adds a given user to the ChannelState. Power prefixes are handled."
nick = user.lstrip('@%+&~!')
nick = user.lstrip(prefix_chars)
if not nick:
return
# & is used to denote protected users in UnrealIRCd
# ~ is used to denote channel owner in UnrealIRCd
# ! is used to denote protected users in UltimateIRCd
while user and user[0] in '@%+&~!':
while user and user[0] in prefix_chars:
(marker, user) = (user[0], user[1:])
assert user, 'Looks like my caller is passing chars, not nicks.'
if marker in '@&~!':
@ -963,13 +963,27 @@ class IrcState(IrcCommandDispatcher, log.Firewalled):
if channel not in self.channels:
self.channels[channel] = ChannelState()
c = self.channels[channel]
# Set of prefixes servers may append before a NAMES reply when
# the user is op/halfop/voice/...
# https://datatracker.ietf.org/doc/html/draft-hardy-irc-isupport-00#section-4.15
prefix = self.supported.get('PREFIX')
if prefix is None:
prefix_chars = '@%+&~!' # see the comments in addUser
else:
prefix_chars = ''.join(prefix.values())
for item in items.split():
if ircutils.isUserHostmask(item):
name = ircutils.nickFromHostmask(item)
self.nicksToHostmasks[name] = name
stripped_item = item.lstrip(prefix_chars)
item_prefix = item[0:-len(stripped_item)]
if ircutils.isUserHostmask(stripped_item):
nick = ircutils.nickFromHostmask(stripped_item)
self.nicksToHostmasks[nick] = stripped_item
name = item_prefix + nick
else:
name = item
c.addUser(name)
c.addUser(name, prefix_chars=prefix_chars)
if type == '@':
c.modes['s'] = None

View File

@ -60,7 +60,11 @@ def debug(s, *args):
"""Prints a debug string. Most likely replaced by our logging debug."""
print('***', s % args)
userHostmaskRe = re.compile(r'^\S+!\S+@\S+$')
def warning(s, *args):
"""Prints a debug string. Most likely replaced by our logging debug."""
print('###', s % args)
userHostmaskRe = re.compile(r'^(?P<nick>\S+?)!(?P<user>\S+)@(?P<host>\S+?)$')
def isUserHostmask(s):
"""Returns whether or not the string s is a valid User hostmask."""
return userHostmaskRe.match(s) is not None
@ -91,9 +95,17 @@ def hostFromHostmask(hostmask):
def splitHostmask(hostmask):
"""hostmask => (nick, user, host)
Returns the nick, user, host of a user hostmask."""
assert isUserHostmask(hostmask)
nick, rest = hostmask.rsplit('!', 1)
user, host = rest.rsplit('@', 1)
m = userHostmaskRe.match(hostmask)
assert m is not None, hostmask
nick = m.group("nick")
user = m.group("user")
host = m.group("host")
if set("!@") & set(nick+user+host):
# There should never be either of these characters in the part.
# As far as I know it never happens in practice aside from networks
# broken by design.
warning("Invalid hostmask format: %s", hostmask)
# TODO: error if strictRfc is True
return (minisix.intern(nick), minisix.intern(user), minisix.intern(host))
def joinHostmask(nick, ident, host):

View File

@ -329,6 +329,7 @@ atexit.register(logging.shutdown)
# ircutils will work without this, but it's useful.
ircutils.debug = debug
ircutils.warning = warning
def getPluginLogger(name):
if not conf.supybot.log.plugins.individualLogfiles():

View File

@ -529,6 +529,42 @@ class IrcStateTestCase(SupyTestCase):
st = irclib.IrcState()
self.assert_(st.addMsg(self.irc, ircmsgs.IrcMsg('MODE foo +i')) or 1)
def testNamreply(self):
"""RPL_NAMREPLY / reply to NAMES"""
# Just nicks (à la RFC 1459 + some common prefix chars)
st = irclib.IrcState()
st.addMsg(self.irc, ircmsgs.IrcMsg(command='353',
args=('*', '=', '#chan', 'nick1 @nick2 ~@nick3')))
chan_st = st.channels['#chan']
self.assertEqual(chan_st.users, ircutils.IrcSet(['nick1', 'nick2', 'nick3']))
self.assertEqual(chan_st.ops, ircutils.IrcSet(['nick2', 'nick3']))
self.assertEqual(st.nicksToHostmasks, ircutils.IrcDict({}))
# with userhost-in-names
st = irclib.IrcState()
st.addMsg(self.irc, ircmsgs.IrcMsg(command='353',
args=('*', '=', '#chan', 'nick1!u1@h1 @nick2!u2@h2 ~@nick3!u3@h3')))
chan_st = st.channels['#chan']
self.assertEqual(chan_st.users, ircutils.IrcSet(['nick1', 'nick2', 'nick3']))
self.assertEqual(chan_st.ops, ircutils.IrcSet(['nick2', 'nick3']))
self.assertEqual(st.nicksToHostmasks['nick1'], 'nick1!u1@h1')
self.assertEqual(st.nicksToHostmasks['nick2'], 'nick2!u2@h2')
self.assertEqual(st.nicksToHostmasks['nick3'], 'nick3!u3@h3')
# Prefixed with chars not in ISUPPORT PREFIX
st = irclib.IrcState()
st.addMsg(self.irc, ircmsgs.IrcMsg(command='005',
args=('*', 'PREFIX=(ov)@+', 'are supported')))
st.addMsg(self.irc, ircmsgs.IrcMsg(command='353',
args=('*', '=', '#chan', 'nick1!u1@h1 @nick2!u2@h2 ~@nick3!u3@h3')))
chan_st = st.channels['#chan']
self.assertEqual(chan_st.users, ircutils.IrcSet(['nick1', 'nick2', '~@nick3']))
self.assertEqual(chan_st.ops, ircutils.IrcSet(['nick2']))
self.assertEqual(st.nicksToHostmasks['nick1'], 'nick1!u1@h1')
self.assertEqual(st.nicksToHostmasks['nick2'], 'nick2!u2@h2')
self.assertEqual(st.nicksToHostmasks['~@nick3'], '~@nick3!u3@h3')
class IrcCapsTestCase(SupyTestCase, CapNegMixin):
def testReqLineLength(self):

View File

@ -113,6 +113,24 @@ class FunctionsTestCase(SupyTestCase):
self.assertFalse(ircutils.isUserHostmask('@'))
self.assertFalse(ircutils.isUserHostmask('!bar@baz'))
def testSplitHostmask(self):
# This is the only valid case:
self.assertEqual(ircutils.splitHostmask('foo!bar@baz'),
('foo', 'bar', 'baz'))
# This ones are technically allowed by RFC1459, but never happens in
# practice:
self.assertEqual(ircutils.splitHostmask('foo!bar!qux@quux'),
('foo', 'bar!qux', 'quux'))
self.assertEqual(ircutils.splitHostmask('foo!bar@baz@quux'),
('foo', 'bar@baz', 'quux'))
self.assertEqual(ircutils.splitHostmask('foo!bar@baz!qux@quux'),
('foo', 'bar@baz!qux', 'quux'))
# And this one in garbage, let's just make sure we don't crash:
self.assertEqual(ircutils.splitHostmask('foo!bar@baz!qux'),
('foo', 'bar', 'baz!qux'))
def testIsChannel(self):
self.assertTrue(ircutils.isChannel('#'))
self.assertTrue(ircutils.isChannel('&'))