Compare commits

...

23 Commits

Author SHA1 Message Date
Valentin Lorentz
d435442b39 Admin: Actually clean up test channel from configuration
943f39745dd23ffca9ec5a45eaf25b2efd4625e5 did not actually because:

1. the 'part' command is not available (it's in the Channel plugin)
   so it just didn't do anything
2. one of the tests was missing the cleanup
2024-04-26 09:04:45 +02:00
Valentin Lorentz
6758c00363 limnoria-test: Fix log config
Since 03a37771297d25b3455209898eaad5618f4f6345 we use .format() instead of % for substitution,
so these should not be escaped anymore.
2024-04-26 08:57:49 +02:00
Valentin Lorentz
943f39745d Admin: Fix leftover state change in testPart
it affects Channel's testPart
2024-04-18 19:47:22 +02:00
Valentin Lorentz
c8030be71a Web: Need to download even more Javascript from Youtube 2024-04-18 19:33:55 +02:00
Valentin Lorentz
03c638705f Channel: Fix error in @part when channel is configured but not joined
This typically happens when banned from the channel, and returning an error
gives bot admins the impression @part did not remove the channel from
the auto-join list
2024-04-12 19:17:13 +02:00
GMDSantana
03a3777129
Create temporary files in a temporary directory
But keep it if tests fail.

Closes #1061
2024-04-12 11:06:30 +02:00
Valentin Lorentz
ca8565b6d8 RSS: Don't log tracebacks for HTTP errors 2024-03-09 11:47:10 +01:00
James Lu
3e5291f6d2 ircdb.checkIgnored: return False for messages from servers
These do not pass the `ircutils.isUserHostmask` check despite being a valid msg.prefix. We should probably return gracefully here instead of forcing plugins to deal with such a case themselves.

Closes GH-1548
2024-02-06 16:49:56 +01:00
Valentin Lorentz
a2e55ca1f6 RSS: Update link to feedparser 2024-01-03 18:37:11 +01:00
Valentin Lorentz
d55a08c63e Regenerate plugin READMEs 2024-01-03 18:37:11 +01:00
Stathis Xantinidis
5ca0fcd87c Changed whois provider domain to whois.iana.org
The previous was giving timeouts
2023-12-15 22:18:10 +01:00
Valentin Lorentz
06c88581ec Services: Improve error on missing password or NickServ nick 2023-11-18 22:02:36 +01:00
Valentin Lorentz
fffdd82571 Fediverse: Catch URLErrors raised when checking webfinger support 2023-10-29 12:40:48 +01:00
Valentin Lorentz
689c633e92 Web: Fix crash on socket.timeout on snarfed URLs 2023-10-29 12:32:33 +01:00
Valentin Lorentz
3f9ab4b89c Web: Fix crash on trailing ';' in Content-Type 2023-10-28 09:47:55 +02:00
Valentin Lorentz
faa6474271 Geography: Add support for OSM node ids 2023-10-27 11:31:06 +02:00
James Lu
1fb0bbd1c0 Fix recursive loop in limnoria_reset_password
Closes GH-1565
2023-10-24 20:05:18 -07:00
Valentin Lorentz
18699b0cf2 Fix breakage of supybot.directories.data.web when it's a relative directory (the default) 2023-10-17 20:13:56 +02:00
Valentin Lorentz
15009caeff Remove requirement for supybot.directories.data.web to be a subdir of supybot.directories.data 2023-10-17 20:04:42 +02:00
Valentin Lorentz
2008088a07 RSS: Copy $summary to $description on Atom feeds
Otherwise $description would remain feedparser's default, which is
unescaped <content>; but $description is the only usable one on RSS
feeds.
2023-10-17 19:57:29 +02:00
Valentin Lorentz
04f0d70113 RSS: Add support for $content/$summary_detail/$title_detail 2023-10-17 19:00:54 +02:00
Valentin Lorentz
edb13f65df httpserver: Fix incorrect path joining 2023-10-17 19:00:54 +02:00
Valentin Lorentz
e7824213ae Debug: Remove useless shebang 2023-10-17 19:00:54 +02:00
23 changed files with 326 additions and 62 deletions

View File

@ -50,6 +50,7 @@ class AdminTestCase(PluginTestCase):
self.irc.feedMsg(ircmsgs.join('#Baz', prefix=self.prefix)) self.irc.feedMsg(ircmsgs.join('#Baz', prefix=self.prefix))
getAfterJoinMessages() getAfterJoinMessages()
self.assertRegexp('channels', '#bar, #Baz, and #foo') self.assertRegexp('channels', '#bar, #Baz, and #foo')
self.assertNotRegexp('config networks.test.channels', '.*#foo.*')
def testIgnoreAddRemove(self): def testIgnoreAddRemove(self):
self.assertNotError('admin ignore add foo!bar@baz') self.assertNotError('admin ignore add foo!bar@baz')
@ -87,13 +88,16 @@ class AdminTestCase(PluginTestCase):
ircdb.users.delUser(u.id) ircdb.users.delUser(u.id)
def testJoin(self): def testJoin(self):
m = self.getMsg('join #foo') try:
self.assertEqual(m.command, 'JOIN') m = self.getMsg('join #foo')
self.assertEqual(m.args[0], '#foo') self.assertEqual(m.command, 'JOIN')
m = self.getMsg('join #foo key') self.assertEqual(m.args[0], '#foo')
self.assertEqual(m.command, 'JOIN') m = self.getMsg('join #foo key')
self.assertEqual(m.args[0], '#foo') self.assertEqual(m.command, 'JOIN')
self.assertEqual(m.args[1], 'key') self.assertEqual(m.args[0], '#foo')
self.assertEqual(m.args[1], 'key')
finally:
conf.supybot.networks.test.channels.setValue('')
def testNick(self): def testNick(self):
try: try:
@ -107,10 +111,13 @@ class AdminTestCase(PluginTestCase):
self.assertError('admin capability add %s owner' % self.nick) self.assertError('admin capability add %s owner' % self.nick)
def testJoinOnOwnerInvite(self): def testJoinOnOwnerInvite(self):
self.irc.feedMsg(ircmsgs.invite(conf.supybot.nick(), '#foo', prefix=self.prefix)) try:
m = self.getMsg(' ') self.irc.feedMsg(ircmsgs.invite(conf.supybot.nick(), '#foo', prefix=self.prefix))
self.assertEqual(m.command, 'JOIN') m = self.getMsg(' ')
self.assertEqual(m.args[0], '#foo') self.assertEqual(m.command, 'JOIN')
self.assertEqual(m.args[0], '#foo')
finally:
conf.supybot.networks.test.channels.setValue('')
def testNoJoinOnUnprivilegedInvite(self): def testNoJoinOnUnprivilegedInvite(self):
try: try:
@ -121,6 +128,7 @@ class AdminTestCase(PluginTestCase):
'Error: "somecommand" is not a valid command.') 'Error: "somecommand" is not a valid command.')
finally: finally:
world.testing = True world.testing = True
self.assertNotRegexp('config networks.test.channels', '.*#foo.*')
def testAcmd(self): def testAcmd(self):
self.irc.feedMsg(ircmsgs.join('#foo', prefix=self.prefix)) self.irc.feedMsg(ircmsgs.join('#foo', prefix=self.prefix))

View File

@ -991,9 +991,14 @@ class Channel(callbacks.Plugin):
network = conf.supybot.networks.get(irc.network) network = conf.supybot.networks.get(irc.network)
network.channels().remove(channel) network.channels().remove(channel)
except KeyError: except KeyError:
pass if channel not in irc.state.channels:
if channel not in irc.state.channels: # Not configured AND not in the channel
irc.error(_('I\'m not in %s.') % channel, Raise=True) irc.error(_('I\'m not in %s.') % channel, Raise=True)
else:
if channel not in irc.state.channels:
# Configured, but not in the channel
irc.reply(_('%s removed from configured join list.') % channel)
return
reason = (reason or self.registryValue("partMsg", channel, irc.network)) reason = (reason or self.registryValue("partMsg", channel, irc.network))
reason = ircutils.standardSubstitute(irc, msg, reason) reason = ircutils.standardSubstitute(irc, msg, reason)
irc.queueMsg(ircmsgs.part(channel, reason)) irc.queueMsg(ircmsgs.part(channel, reason))

View File

@ -1,5 +1,3 @@
#!/usr/bin/python
### ###
# Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2002-2005, Jeremiah Fincher
# Copyright (c) 2010-2021, Valentin Lorentz # Copyright (c) 2010-2021, Valentin Lorentz

View File

@ -177,9 +177,15 @@ class Fediverse(callbacks.PluginRegexp):
def _has_webfinger_support(self, hostname): def _has_webfinger_support(self, hostname):
if hostname not in self._webfinger_support_cache: if hostname not in self._webfinger_support_cache:
self._webfinger_support_cache[hostname] = ap.has_webfinger_support( try:
hostname self._webfinger_support_cache[hostname] = ap.has_webfinger_support(
) hostname
)
except Exception as e:
self.log.error(
"Checking Webfinger support for %s raised %s", hostname, e
)
return False
return self._webfinger_support_cache[hostname] return self._webfinger_support_cache[hostname]
def _get_actor(self, irc, username): def _get_actor(self, irc, username):

View File

@ -187,7 +187,7 @@ class GeographyLocaltimeTestCase(PluginTestCase):
class GeographyWikidataTestCase(SupyTestCase): class GeographyWikidataTestCase(SupyTestCase):
@skipIf(not network, "Network test") @skipIf(not network, "Network test")
def testOsmidToTimezone(self): def testRelationOsmidToTimezone(self):
self.assertEqual( self.assertEqual(
wikidata.uri_from_osmid(450381), wikidata.uri_from_osmid(450381),
"http://www.wikidata.org/entity/Q22690", "http://www.wikidata.org/entity/Q22690",
@ -196,6 +196,12 @@ class GeographyWikidataTestCase(SupyTestCase):
wikidata.uri_from_osmid(192468), wikidata.uri_from_osmid(192468),
"http://www.wikidata.org/entity/Q47045", "http://www.wikidata.org/entity/Q47045",
) )
@skipIf(not network, "Network test")
def testNodeOsmidToTimezone(self):
self.assertEqual(
wikidata.uri_from_osmid(436012592),
"http://www.wikidata.org/entity/Q933",
)
@skipIf(not network, "Network test") @skipIf(not network, "Network test")
def testDirect(self): def testDirect(self):

View File

@ -115,7 +115,14 @@ LIMIT 1
OSMID_QUERY = string.Template( OSMID_QUERY = string.Template(
""" """
SELECT ?item WHERE { SELECT ?item WHERE {
?item wdt:P402 "$osmid". {
?item wdt:P402 "$osmid". # OSM relation ID
}
UNION
{
?item wdt:P11693 "$osmid". # OSM node ID
}
} }
LIMIT 1 LIMIT 1
""" """

View File

@ -158,7 +158,7 @@ class Internet(callbacks.Plugin):
if not status: if not status:
status = 'unknown' status = 'unknown'
try: try:
t = telnetlib.Telnet('whois.pir.org', 43) t = telnetlib.Telnet('whois.iana.org', 43)
except socket.error as e: except socket.error as e:
irc.error(str(e)) irc.error(str(e))
return return

View File

@ -21,6 +21,11 @@ and checking latency to the server.
Commands Commands
-------- --------
.. _command-network-authenticate:
authenticate takes no arguments
Manually initiate SASL authentication.
.. _command-network-capabilities: .. _command-network-capabilities:
capabilities [<network>] capabilities [<network>]

View File

@ -8,9 +8,8 @@ Purpose
Provides basic functionality for handling RSS/RDF feeds, and allows announcing Provides basic functionality for handling RSS/RDF feeds, and allows announcing
them periodically to channels. them periodically to channels.
In order to use this plugin you must have the following modules In order to use this plugin you must have `python3-feedparser
installed: <https://pypi.org/project/feedparser/>`_ installed.
* feedparser: http://feedparser.org/
Usage Usage
----- -----
@ -140,7 +139,7 @@ supybot.plugins.RSS.feeds
supybot.plugins.RSS.format supybot.plugins.RSS.format
This config variable defaults to "$date: $title <$link>", is network-specific, and is channel-specific. This config variable defaults to "$date: $title <$link>", is network-specific, and is channel-specific.
The format the bot will use for displaying headlines of a RSS feed that is triggered manually. In addition to fields defined by feedparser ($published (the entry date), $title, $link, $description, $id, etc.), the following variables can be used: $feed_name, $date (parsed date, as defined in supybot.reply.format.time) The format the bot will use for displaying headlines of a RSS feed that is triggered manually. In addition to fields defined by feedparser ($published (the entry date), $title, $link, $description, $id, etc.), the following variables can be used: $feed_name (the configured name) $feed_title/$feed_subtitle/$feed_author/$feed_language/$feed_link, $date (parsed date, as defined in supybot.reply.format.time)
.. _conf-supybot.plugins.RSS.headlineSeparator: .. _conf-supybot.plugins.RSS.headlineSeparator:

View File

@ -31,9 +31,8 @@
""" """
Provides basic functionality for handling RSS/RDF feeds, and allows announcing Provides basic functionality for handling RSS/RDF feeds, and allows announcing
them periodically to channels. them periodically to channels.
In order to use this plugin you must have the following modules In order to use this plugin you must have `python3-feedparser
installed: <https://pypi.org/project/feedparser/>`_ installed.
* feedparser: http://feedparser.org/
""" """
import supybot import supybot

View File

@ -364,6 +364,11 @@ class RSS(callbacks.Plugin):
feed.url, e) feed.url, e)
feed.last_exception = e feed.last_exception = e
return return
except http.client.HTTPException as e:
self.log.warning("HTTP error while fetching <%s>: %s",
feed.url, e)
feed.last_exception = e
return
except Exception as e: except Exception as e:
self.log.error("Failed to fetch <%s>: %s", feed.url, e) self.log.error("Failed to fetch <%s>: %s", feed.url, e)
raise # reraise so @log.firewall prints the traceback raise # reraise so @log.firewall prints the traceback
@ -497,6 +502,48 @@ class RSS(callbacks.Plugin):
isinstance(v, str)} isinstance(v, str)}
kwargs["feed_name"] = feed.name kwargs["feed_name"] = feed.name
kwargs.update(entry) kwargs.update(entry)
for (key, value) in list(kwargs.items()):
# First look for plain text
if isinstance(value, list):
for item in value:
if isinstance(item, dict) and 'value' in item and \
item.get('type') == 'text/plain':
value = item['value']
break
# Then look for HTML text or URL
if isinstance(value, list):
for item in value:
if isinstance(item, dict) and item.get('type') in \
('text/html', 'application/xhtml+xml'):
if 'value' in item:
value = utils.web.htmlToText(item['value'])
elif 'href' in item:
value = item['href']
# Then fall back to any URL
if isinstance(value, list):
for item in value:
if isinstance(item, dict) and 'href' in item:
value = item['href']
break
# Finally, as a last resort, use the value as-is
if isinstance(value, list):
for item in value:
if isinstance(item, dict) and 'value' in item:
value = item['value']
kwargs[key] = value
for key in ('summary', 'title'):
detail = kwargs.get('%s_detail' % key)
if isinstance(detail, dict) and detail.get('type') in \
('text/html', 'application/xhtml+xml'):
kwargs[key] = utils.web.htmlToText(detail['value'])
if 'description' not in kwargs and kwargs[key]:
kwargs['description'] = kwargs[key]
if 'description' not in kwargs and kwargs.get('content'):
kwargs['description'] = kwargs['content']
s = string.Template(template).safe_substitute(entry, **kwargs, date=date) s = string.Template(template).safe_substitute(entry, **kwargs, date=date)
return self._normalize_entry(s) return self._normalize_entry(s)

View File

@ -59,7 +59,6 @@ not_well_formed = """<?xml version="1.0" encoding="utf-8"?>
</rss> </rss>
""" """
class MockResponse: class MockResponse:
headers = {} headers = {}
url = '' url = ''
@ -359,6 +358,130 @@ class RSSTestCase(ChannelPluginTestCase):
self.assertRegexp('rss http://xkcd.com/rss.xml', self.assertRegexp('rss http://xkcd.com/rss.xml',
'On the other hand, the refractor\'s') 'On the other hand, the refractor\'s')
@mock_urllib
def testAtomContentHtmlOnly(self, mock):
timeFastForward(1.1)
mock._data = """
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xml:lang="en-US">
<title>Recent Commits to anope:2.0</title>
<updated>2023-10-04T16:14:39Z</updated>
<entry>
<title>title with &lt;pre&gt;HTML&lt;/pre&gt;</title>
<updated>2023-10-04T16:14:39Z</updated>
<content type="html">
content with &lt;pre&gt;HTML&lt;/pre&gt;
</content>
</entry>
</feed>"""
with conf.supybot.plugins.RSS.format.context('$content'):
self.assertRegexp('rss https://example.org',
'content with HTML')
with conf.supybot.plugins.RSS.format.context('$description'):
self.assertRegexp('rss https://example.org',
'content with HTML')
@mock_urllib
def testAtomContentXhtmlOnly(self, mock):
timeFastForward(1.1)
mock._data = """
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xml:lang="en-US">
<title>Recent Commits to anope:2.0</title>
<updated>2023-10-04T16:14:39Z</updated>
<entry>
<title>title with &lt;pre&gt;HTML&lt;/pre&gt;</title>
<updated>2023-10-04T16:14:39Z</updated>
<content type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml">
content with <pre>XHTML</pre>
</div>
</content>
</entry>
</feed>"""
with conf.supybot.plugins.RSS.format.context('$content'):
self.assertRegexp('rss https://example.org',
'content with XHTML')
with conf.supybot.plugins.RSS.format.context('$description'):
self.assertRegexp('rss https://example.org',
'content with XHTML')
@mock_urllib
def testAtomContentHtmlAndPlaintext(self, mock):
timeFastForward(1.1)
mock._data = """
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xml:lang="en-US">
<title>Recent Commits to anope:2.0</title>
<updated>2023-10-04T16:14:39Z</updated>
<entry>
<title>title with &lt;pre&gt;HTML&lt;/pre&gt;</title>
<updated>2023-10-04T16:14:39Z</updated>
<!-- Atom spec says multiple contents is invalid, feedparser says it's not.
I like having the option, so let's make sure we support it. -->
<content type="html">
content with &lt;pre&gt;HTML&lt;/pre&gt;
</content>
<content type="text">
content with plaintext
</content>
</entry>
</feed>"""
with conf.supybot.plugins.RSS.format.context('$content'):
self.assertRegexp('rss https://example.org',
'content with plaintext')
with conf.supybot.plugins.RSS.format.context('$description'):
self.assertRegexp('rss https://example.org',
'content with plaintext')
@mock_urllib
def testAtomContentPlaintextAndHtml(self, mock):
timeFastForward(1.1)
mock._data = """
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xml:lang="en-US">
<title>Recent Commits to anope:2.0</title>
<updated>2023-10-04T16:14:39Z</updated>
<entry>
<title>title with &lt;pre&gt;HTML&lt;/pre&gt;</title>
<updated>2023-10-04T16:14:39Z</updated>
<!-- Atom spec says multiple contents is invalid, feedparser says it's not.
I like having the option, so let's make sure we support it. -->
<content type="text">
content with plaintext
</content>
<content type="html">
content with &lt;pre&gt;HTML&lt;/pre&gt;
</content>
</entry>
</feed>"""
with conf.supybot.plugins.RSS.format.context('$content'):
self.assertRegexp('rss https://example.org',
'content with plaintext')
with conf.supybot.plugins.RSS.format.context('$description'):
self.assertRegexp('rss https://example.org',
'content with plaintext')
@mock_urllib
def testRssDescriptionHtml(self, mock):
timeFastForward(1.1)
mock._data = """
<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:foaf="http://xmlns.com/foaf/0.1/" xmlns:og="http://ogp.me/ns#" xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#" xmlns:schema="http://schema.org/" xmlns:sioc="http://rdfs.org/sioc/ns#" xmlns:sioct="http://rdfs.org/sioc/types#" xmlns:skos="http://www.w3.org/2004/02/skos/core#" xmlns:xsd="http://www.w3.org/2001/XMLSchema#" version="2.0">
<channel>
<title>feed title</title>
<description/>
<language>en</language>
<item>
<title>title with &lt;pre&gt;HTML&lt;/pre&gt;</title>
<description>description with &lt;pre&gt;HTML&lt;/pre&gt;</description>
</item>
</channel>
</feed>"""
with conf.supybot.plugins.RSS.format.context('$description'):
self.assertRegexp('rss https://example.org',
'description with HTML')
@mock_urllib @mock_urllib
def testFeedAttribute(self, mock): def testFeedAttribute(self, mock):
timeFastForward(1.1) timeFastForward(1.1)

View File

@ -67,6 +67,22 @@ supybot.plugins.SedRegex.enable
Should Perl/sed-style regex replacing work in this channel? Should Perl/sed-style regex replacing work in this channel?
.. _conf-supybot.plugins.SedRegex.format:
supybot.plugins.SedRegex.format
This config variable defaults to "$nick meant to say: $replacement", is network-specific, and is channel-specific.
Sets the format string for a message edited by the original author. Required fields: $nick (nick of the author), $replacement (edited message)
.. _conf-supybot.plugins.SedRegex.format.other:
supybot.plugins.SedRegex.format.other
This config variable defaults to "$otherNick thinks $nick meant to say: $replacement", is network-specific, and is channel-specific.
Sets the format string for a message edited by another author. Required fields: $nick (nick of the original author), $otherNick (nick of the editor), $replacement (edited message)
.. _conf-supybot.plugins.SedRegex.ignoreRegex: .. _conf-supybot.plugins.SedRegex.ignoreRegex:

View File

@ -124,9 +124,11 @@ class Services(callbacks.Plugin):
return return
nickserv = self.registryValue('NickServ', network=irc.network) nickserv = self.registryValue('NickServ', network=irc.network)
password = self._getNickServPassword(nick, irc.network) password = self._getNickServPassword(nick, irc.network)
if not nickserv or not password: if not nickserv:
s = 'Tried to identify without a NickServ or password set.' self.log.warning('Tried to identify without a NickServ set.')
self.log.warning(s) return
if not password:
self.log.warning('Tried to identify without a password set.')
return return
assert ircutils.strEqual(irc.nick, nick), \ assert ircutils.strEqual(irc.nick, nick), \
'Identifying with not normal nick.' 'Identifying with not normal nick.'
@ -150,16 +152,15 @@ class Services(callbacks.Plugin):
ghostDelay = self.registryValue('ghostDelay', network=irc.network) ghostDelay = self.registryValue('ghostDelay', network=irc.network)
if not ghostDelay: if not ghostDelay:
return return
if not nickserv or not password: if not nickserv:
s = 'Tried to ghost without a NickServ or password set.' self.log.warning('Tried to ghost without a NickServ set.')
self.log.warning(s) return
if not password:
self.log.warning('Tried to ghost without a password set.')
return return
if state.sentGhost and time.time() < (state.sentGhost + ghostDelay): if state.sentGhost and time.time() < (state.sentGhost + ghostDelay):
self.log.warning('Refusing to send GHOST more than once every ' self.log.warning('Refusing to send GHOST more than once every '
'%s seconds.' % ghostDelay) '%s seconds.' % ghostDelay)
elif not password:
self.log.warning('Not ghosting: no password set.')
return
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)

View File

@ -144,7 +144,7 @@ supybot.plugins.Unix.ping
supybot.plugins.Unix.ping.command supybot.plugins.Unix.ping.command
This config variable defaults to "/bin/ping", is not network-specific, and is not channel-specific. This config variable defaults to "/usr/bin/ping", is not network-specific, and is not channel-specific.
Determines what command will be called for the ping command. Determines what command will be called for the ping command.
@ -166,7 +166,7 @@ supybot.plugins.Unix.ping6
supybot.plugins.Unix.ping6.command supybot.plugins.Unix.ping6.command
This config variable defaults to "/bin/ping6", is not network-specific, and is not channel-specific. This config variable defaults to "/usr/bin/ping6", is not network-specific, and is not channel-specific.
Determines what command will be called for the ping6 command. Determines what command will be called for the ping6 command.
@ -210,7 +210,7 @@ supybot.plugins.Unix.sysuname
supybot.plugins.Unix.sysuname.command supybot.plugins.Unix.sysuname.command
This config variable defaults to "/bin/uname", is not network-specific, and is not channel-specific. This config variable defaults to "/usr/bin/uname", is not network-specific, and is not channel-specific.
Determines what command will be called for the uname command. Determines what command will be called for the uname command.

View File

@ -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 = max(409600, size) size = max(819200, 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.
@ -173,8 +173,9 @@ class Web(callbacks.PluginRegexp):
if raiseErrors: if raiseErrors:
irc.error(_('Connection to %s timed out') % url, Raise=True) irc.error(_('Connection to %s timed out') % url, Raise=True)
else: else:
selg.log.info('Web plugins TitleSnarfer: URL <%s> timed out', self.log.info('Web plugins TitleSnarfer: URL <%s> timed out',
url) url)
return
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)) + '>',
@ -186,9 +187,14 @@ class Web(callbacks.PluginRegexp):
encoding = None encoding = None
if 'Content-Type' in fd.headers: if 'Content-Type' in fd.headers:
mime_params = [p.split('=', 1) # using p.partition('=') instead of 'p.split('=', 1)' because,
# unlike RFC 7231, RFC 9110 allows an empty parameter list
# after ';':
# * https://www.rfc-editor.org/rfc/rfc9110.html#name-media-type
# * https://www.rfc-editor.org/rfc/rfc9110.html#parameter
mime_params = [p.partition('=')
for p in fd.headers['Content-Type'].split(';')[1:]] for p in fd.headers['Content-Type'].split(';')[1:]]
mime_params = {k.strip(): v.strip() for (k, v) in mime_params} mime_params = {k.strip(): v.strip() for (k, sep, v) in mime_params}
if mime_params.get('charset'): if mime_params.get('charset'):
encoding = mime_params['charset'] encoding = mime_params['charset']

View File

@ -85,6 +85,12 @@ class WebTestCase(ChannelPluginTestCase):
'title https://www.reddit.com/r/irc/', 'title https://www.reddit.com/r/irc/',
'Internet Relay Chat') 'Internet Relay Chat')
def testTitleMarcinfo(self):
# Checks that we don't crash on 'Content-Type: text/html;'
self.assertResponse(
'title https://marc.info/?l=openbsd-tech&m=169841790407370&w=2',
"'Removing syscall(2) from libc and kernel' - MARC")
def testTitleSnarfer(self): def testTitleSnarfer(self):
try: try:
conf.supybot.plugins.Web.titleSnarfer.setValue(True) conf.supybot.plugins.Web.titleSnarfer.setValue(True)

View File

@ -941,7 +941,7 @@ class Directory(registry.String):
if os.path.isabs(filename): if os.path.isabs(filename):
filename = os.path.abspath(filename) filename = os.path.abspath(filename)
selfAbs = os.path.abspath(myself) selfAbs = os.path.abspath(myself)
commonPrefix = os.path.commonprefix([selfAbs, filename]) commonPrefix = os.path.commonpath([selfAbs, filename])
filename = filename[len(commonPrefix):] filename = filename[len(commonPrefix):]
elif not os.path.isabs(myself): elif not os.path.isabs(myself):
if filename.startswith(myself): if filename.startswith(myself):
@ -954,7 +954,7 @@ class DataFilename(registry.String):
def __call__(self): def __call__(self):
v = super(DataFilename, self).__call__() v = super(DataFilename, self).__call__()
dataDir = supybot.directories.data() dataDir = supybot.directories.data()
if not v.startswith(dataDir): if not v.startswith("/") and not v.startswith(dataDir):
v = os.path.basename(v) v = os.path.basename(v)
v = os.path.join(dataDir, v) v = os.path.join(dataDir, v)
self.setValue(v) self.setValue(v)

View File

@ -337,7 +337,7 @@ class Static(SupyHTTPServerCallback):
super(Static, self).__init__() super(Static, self).__init__()
self._mimetype = mimetype self._mimetype = mimetype
def doGetOrHead(self, handler, path, write_content): def doGetOrHead(self, handler, path, write_content):
response = get_template(path) response = get_template(path[1:]) # strip leading /
if minisix.PY3: if minisix.PY3:
response = response.encode() response = response.encode()
handler.send_response(200) handler.send_response(200)

View File

@ -468,7 +468,12 @@ class IrcChannel(object):
return True return True
if world.testing: if world.testing:
return False return False
assert ircutils.isUserHostmask(hostmask), 'got %s' % hostmask if not ircutils.isUserHostmask(hostmask):
# Treat messages from a server (e.g. snomasks) as not ignored, as
# the ignores system doesn't understand them
if '.' not in hostmask:
raise ValueError("Expected full prefix, got %r" % hostmask)
return False
if self.checkBan(hostmask): if self.checkBan(hostmask):
return True return True
if self.ignores.match(hostmask): if self.ignores.match(hostmask):

View File

@ -104,7 +104,7 @@ def _main():
def main(): def main():
try: try:
main() _main()
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass

View File

@ -36,6 +36,7 @@ import sys
import time import time
import shutil import shutil
import fnmatch import fnmatch
from tempfile import TemporaryDirectory
started = time.time() started = time.time()
import supybot import supybot
@ -43,21 +44,24 @@ import logging
import traceback import traceback
# We need to do this before we import conf. # We need to do this before we import conf.
if not os.path.exists('test-conf'): main_temp_dir = TemporaryDirectory()
os.mkdir('test-conf')
registryFilename = os.path.join('test-conf', 'test.conf') os.makedirs(os.path.join(main_temp_dir.name, 'conf'))
fd = open(registryFilename, 'w') os.makedirs(os.path.join(main_temp_dir.name, 'data'))
fd.write(""" os.makedirs(os.path.join(main_temp_dir.name, 'logs'))
registryFilename = os.path.join(main_temp_dir.name, 'conf', 'test.conf')
with open(registryFilename, 'w') as fd:
fd.write("""
supybot.directories.backup: /dev/null supybot.directories.backup: /dev/null
supybot.directories.conf: %(base_dir)s/test-conf supybot.directories.conf: {temp_conf}
supybot.directories.data: %(base_dir)s/test-data supybot.directories.data: {temp_data}
supybot.directories.log: %(base_dir)s/test-logs supybot.directories.log: {temp_logs}
supybot.reply.whenNotCommand: True supybot.reply.whenNotCommand: True
supybot.log.stdout: False supybot.log.stdout: False
supybot.log.stdout.level: ERROR supybot.log.stdout.level: ERROR
supybot.log.level: DEBUG supybot.log.level: DEBUG
supybot.log.format: %%(levelname)s %%(message)s supybot.log.format: %(levelname)s %(message)s
supybot.log.plugins.individualLogfiles: False supybot.log.plugins.individualLogfiles: False
supybot.protocols.irc.throttleTime: 0 supybot.protocols.irc.throttleTime: 0
supybot.reply.whenAddressedBy.chars: @ supybot.reply.whenAddressedBy.chars: @
@ -67,8 +71,11 @@ supybot.networks.testnet2.server: should.not.need.this
supybot.networks.testnet3.server: should.not.need.this supybot.networks.testnet3.server: should.not.need.this
supybot.nick: test supybot.nick: test
supybot.databases.users.allowUnregistration: True supybot.databases.users.allowUnregistration: True
""" % {'base_dir': os.getcwd()}) """.format(
fd.close() temp_conf=os.path.join(main_temp_dir.name, 'conf'),
temp_data=os.path.join(main_temp_dir.name, 'data'),
temp_logs=os.path.join(main_temp_dir.name, 'logs')
))
import supybot.registry as registry import supybot.registry as registry
registry.open_registry(registryFilename) registry.open_registry(registryFilename)
@ -251,6 +258,9 @@ def main():
if result.wasSuccessful(): if result.wasSuccessful():
sys.exit(0) sys.exit(0)
else: else:
# Deactivate autocleaning for the temporary directiories to allow inspection.
main_temp_dir._finalizer.detach()
print(f"Temporary directory path: {main_temp_dir.name}")
sys.exit(1) sys.exit(1)

View File

@ -350,6 +350,23 @@ class IrcChannelTestCase(IrcdbTestCase):
c.removeBan(banmask) c.removeBan(banmask)
self.assertFalse(c.checkIgnored(prefix)) self.assertFalse(c.checkIgnored(prefix))
# Only full n!u@h is accepted here
self.assertRaises(ValueError, c.checkIgnored, 'foo')
def testIgnoredServerNames(self):
c = ircdb.IrcChannel()
# Server names are not handled by the ignores system, so this is false
self.assertFalse(c.checkIgnored('irc.example.com'))
# But we should treat full prefixes that match nick!user@host normally,
# even if they include "." like a server name
prefix = 'irc.example.com!bar@baz'
banmask = ircutils.banmask(prefix)
self.assertFalse(c.checkIgnored(prefix))
c.addIgnore(banmask)
self.assertTrue(c.checkIgnored(prefix))
c.removeIgnore(banmask)
self.assertFalse(c.checkIgnored(prefix))
class IrcNetworkTestCase(IrcdbTestCase): class IrcNetworkTestCase(IrcdbTestCase):
def testDefaults(self): def testDefaults(self):
n = ircdb.IrcNetwork() n = ircdb.IrcNetwork()