Fediverse: Add URL snarfer.

This commit is contained in:
Valentin Lorentz 2020-05-10 14:52:55 +02:00
parent 5908b86635
commit d1854cfc9b
4 changed files with 217 additions and 68 deletions

View File

@ -232,10 +232,10 @@ def actor_url(localuser, hostname):
def get_actor(localuser, hostname): def get_actor(localuser, hostname):
url = actor_url(localuser, hostname) url = actor_url(localuser, hostname)
return get_actor_from_url(url) return get_resource_from_url(url)
def get_actor_from_url(url): def get_resource_from_url(url):
content = signed_request(url, headers={"Accept": ACTIVITY_MIMETYPE}) content = signed_request(url, headers={"Accept": ACTIVITY_MIMETYPE})
assert content is not None assert content is not None

View File

@ -51,9 +51,10 @@ def configure(advanced):
Fediverse = conf.registerPlugin("Fediverse") Fediverse = conf.registerPlugin("Fediverse")
conf.registerGroup(Fediverse, "snarfers")
conf.registerChannelValue( conf.registerChannelValue(
Fediverse, Fediverse.snarfers,
"usernameSnarfer", "username",
registry.Boolean( registry.Boolean(
False, False,
_( _(
@ -62,6 +63,28 @@ conf.registerChannelValue(
), ),
), ),
) )
conf.registerChannelValue(
Fediverse.snarfers,
"profile",
registry.Boolean(
False,
_(
"""Determines whether the bot will output the profile of
URLs to Fediverse accounts it sees in channel messages."""
),
),
)
conf.registerChannelValue(
Fediverse.snarfers,
"status",
registry.Boolean(
False,
_(
"""Determines whether the bot will output the content of
statuses whose URLs it sees in channel messages."""
),
),
)
conf.registerGroup(Fediverse, "format") conf.registerGroup(Fediverse, "format")
conf.registerGroup(Fediverse.format, "statuses") conf.registerGroup(Fediverse.format, "statuses")

View File

@ -34,7 +34,7 @@ import importlib
import urllib.parse import urllib.parse
from supybot import utils, callbacks, httpserver from supybot import utils, callbacks, httpserver
from supybot.commands import wrap 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
@ -102,7 +102,7 @@ class FediverseHttp(httpserver.SupyHTTPServerCallback):
], ],
"id": actor_url, "id": actor_url,
"preferredUsername": hostname, "preferredUsername": hostname,
"type": "Person", "type": "Service",
"publicKey": { "publicKey": {
"id": actor_url + "#main-key", "id": actor_url + "#main-key",
"owner": actor_url, "owner": actor_url,
@ -117,7 +117,7 @@ class Fediverse(callbacks.PluginRegexp):
"""Fetches information from ActivityPub servers.""" """Fetches information from ActivityPub servers."""
threaded = True threaded = True
regexps = ["usernameSnarfer"] regexps = ["usernameSnarfer", "urlSnarfer_"]
def __init__(self, irc): def __init__(self, irc):
super().__init__(irc) super().__init__(irc)
@ -152,7 +152,7 @@ class Fediverse(callbacks.PluginRegexp):
match = utils.web.urlRe.match(username) match = utils.web.urlRe.match(username)
if match: if match:
# TODO: error handling # TODO: error handling
actor = ap.get_actor_from_url(match.group(0)) actor = ap.get_resource_from_url(match.group(0))
username = self._format_actor_username(actor) username = self._format_actor_username(actor)
else: else:
irc.errorInvalid("fediverse username", username) irc.errorInvalid("fediverse username", username)
@ -232,26 +232,64 @@ class Fediverse(callbacks.PluginRegexp):
) )
) )
def _format_profile(self, irc, msg, actor):
return _("\x02%s\x02 (%s): %s") % (
actor["name"],
self._format_actor_username(actor),
utils.web.htmlToText(actor["summary"]),
)
def usernameSnarfer(self, irc, msg, match): def usernameSnarfer(self, irc, msg, match):
if callbacks.addressed(irc, msg): if callbacks.addressed(irc, msg):
return return
if not self.registryValue(
"snarfers.username", msg.channel, irc.network
):
return
try: try:
actor = self._get_actor(irc, match.group(0)) actor = self._get_actor(irc, match.group(0))
except ap.ActivityPubError: except ap.ActivityPubError:
# Be silent on errors # Be silent on errors
return return
irc.reply( irc.reply(self._format_profile(irc, msg, actor))
_("\x02%s\x02 (%s): %s")
% (
actor["name"],
self._format_actor_username(actor),
utils.web.htmlToText(actor["summary"]),
)
)
usernameSnarfer.__doc__ = _username_regexp.pattern usernameSnarfer.__doc__ = _username_regexp.pattern
@urlSnarfer
def urlSnarfer_(self, irc, msg, match):
channel = msg.channel
network = irc.network
url = match.group(0)
if not channel:
return
if callbacks.addressed(irc, msg):
return
snarf_profile = self.registryValue(
"snarfers.profile", channel, network
)
snarf_status = self.registryValue("snarfers.status", channel, network)
if not snarf_profile and not snarf_status:
return
try:
resource = ap.get_resource_from_url(url)
except ap.ActivityPubError:
return
try:
if snarf_profile and resource["type"] in ("Person", "Service"):
irc.reply(self._format_profile(irc, msg, resource))
elif snarf_status and resource["type"] in (
"Create",
"Note",
"Announce",
):
irc.reply(self._format_status(irc, msg, resource))
except ap.ActivityPubError:
return
urlSnarfer_.__doc__ = utils.web._httpUrlRe
@wrap(["somethingWithoutSpaces"]) @wrap(["somethingWithoutSpaces"])
def featured(self, irc, msg, args, username): def featured(self, irc, msg, args, username):
"""<@user@instance> """<@user@instance>

View File

@ -166,6 +166,60 @@ OUTBOX_VALUE = {
} }
OUTBOX_DATA = json.dumps(OUTBOX_VALUE).encode() OUTBOX_DATA = json.dumps(OUTBOX_VALUE).encode()
STATUS_URL = "https://example.org/users/someuser/statuses/1234"
STATUS_VALUE = {
"id": "https://example.org/users/someuser/statuses/1234/activity",
"type": "Create",
"actor": "https://example.org/users/someuser",
"published": "2020-05-08T01:23:45Z",
"to": ["https://example.org/users/someuser/followers"],
"cc": [
"https://www.w3.org/ns/activitystreams#Public",
"https://example.com/users/FirstAuthor",
],
"object": {
"id": "https://example.org/users/someuser/statuses/1234",
"type": "Note",
"summary": None,
"inReplyTo": "https://example.com/users/FirstAuthor/statuses/42",
"published": "2020-05-08T01:23:45Z",
"url": "https://example.org/@FirstAuthor/42",
"attributedTo": "https://example.org/users/someuser",
"to": ["https://example.org/users/someuser/followers"],
"cc": [
"https://www.w3.org/ns/activitystreams#Public",
"https://example.com/users/FirstAuthor",
],
"sensitive": False,
"atomUri": "https://example.org/users/someuser/statuses/1234",
"inReplyToAtomUri": "https://example.com/users/FirstAuthor/statuses/42",
"conversation": "tag:example.com,2020-05-08:objectId=aaaa:objectType=Conversation",
"content": '<p><span class="h-card"><a href="https://example.com/@FirstAuthor" class="u-url mention">@<span>FirstAuthor</span></a></span> I am replying to you</p>',
"contentMap": {
"en": '<p><span class="h-card"><a href="https://example.com/@FirstAuthor" class="u-url mention">@<span>FirstAuthor</span></a></span> I am replying to you</p>'
},
"attachment": [],
"tag": [
{
"type": "Mention",
"href": "https://example.com/users/FirstAuthor",
"name": "@FirstAuthor@example.com",
}
],
"replies": {
"id": "https://example.org/users/someuser/statuses/1234/replies",
"type": "Collection",
"first": {
"type": "CollectionPage",
"next": "https://example.org/users/someuser/statuses/1234/replies?only_other_accounts=true&page=true",
"partOf": "https://example.org/users/someuser/statuses/1234/replies",
"items": [],
},
},
},
}
STATUS_DATA = json.dumps(STATUS_VALUE).encode()
OUTBOX_FIRSTPAGE_URL = "https://example.org/users/someuser/outbox?page=true" OUTBOX_FIRSTPAGE_URL = "https://example.org/users/someuser/outbox?page=true"
OUTBOX_FIRSTPAGE_VALUE = { OUTBOX_FIRSTPAGE_VALUE = {
"@context": [ "@context": [
@ -188,57 +242,7 @@ OUTBOX_FIRSTPAGE_VALUE = {
"prev": "https://example.org/users/someuser/outbox?min_id=104135036335976677&page=true", "prev": "https://example.org/users/someuser/outbox?min_id=104135036335976677&page=true",
"partOf": "https://example.org/users/someuser/outbox", "partOf": "https://example.org/users/someuser/outbox",
"orderedItems": [ "orderedItems": [
{ STATUS_VALUE,
"id": "https://example.org/users/someuser/statuses/1234/activity",
"type": "Create",
"actor": "https://example.org/users/someuser",
"published": "2020-05-08T01:23:45Z",
"to": ["https://example.org/users/someuser/followers"],
"cc": [
"https://www.w3.org/ns/activitystreams#Public",
"https://example.com/users/FirstAuthor",
],
"object": {
"id": "https://example.org/users/someuser/statuses/1234",
"type": "Note",
"summary": None,
"inReplyTo": "https://example.com/users/FirstAuthor/statuses/42",
"published": "2020-05-08T01:23:45Z",
"url": "https://example.org/@FirstAuthor/42",
"attributedTo": "https://example.org/users/someuser",
"to": ["https://example.org/users/someuser/followers"],
"cc": [
"https://www.w3.org/ns/activitystreams#Public",
"https://example.com/users/FirstAuthor",
],
"sensitive": False,
"atomUri": "https://example.org/users/someuser/statuses/1234",
"inReplyToAtomUri": "https://example.com/users/FirstAuthor/statuses/42",
"conversation": "tag:example.com,2020-05-08:objectId=aaaa:objectType=Conversation",
"content": '<p><span class="h-card"><a href="https://example.com/@FirstAuthor" class="u-url mention">@<span>FirstAuthor</span></a></span> I am replying to you</p>',
"contentMap": {
"en": '<p><span class="h-card"><a href="https://example.com/@FirstAuthor" class="u-url mention">@<span>FirstAuthor</span></a></span> I am replying to you</p>'
},
"attachment": [],
"tag": [
{
"type": "Mention",
"href": "https://example.com/users/FirstAuthor",
"name": "@FirstAuthor@example.com",
}
],
"replies": {
"id": "https://example.org/users/someuser/statuses/1234/replies",
"type": "Collection",
"first": {
"type": "CollectionPage",
"next": "https://example.org/users/someuser/statuses/1234/replies?only_other_accounts=true&page=true",
"partOf": "https://example.org/users/someuser/statuses/1234/replies",
"items": [],
},
},
},
},
{ {
"id": "https://example.org/users/someuser/statuses/1235/activity", "id": "https://example.org/users/someuser/statuses/1235/activity",
"type": "Create", "type": "Create",
@ -517,7 +521,12 @@ class FediverseTestCase(ChannelPluginTestCase):
) )
def testProfileSnarfer(self): def testProfileSnarfer(self):
with conf.supybot.plugins.Fediverse.usernameSnarfer.context(True): with self.mockRequests([]):
self.assertSnarfNoResponse(
"aaa @nonexistinguser@example.org bbb", timeout=1
)
with conf.supybot.plugins.Fediverse.snarfers.username.context(True):
expected_requests = [ expected_requests = [
(HOSTMETA_URL, HOSTMETA_DATA), (HOSTMETA_URL, HOSTMETA_DATA),
(WEBFINGER_URL, WEBFINGER_DATA), (WEBFINGER_URL, WEBFINGER_DATA),
@ -540,6 +549,28 @@ class FediverseTestCase(ChannelPluginTestCase):
"aaa @nonexistinguser@example.org bbb", timeout=1 "aaa @nonexistinguser@example.org bbb", timeout=1
) )
def testProfileUrlSnarfer(self):
with self.mockRequests([]):
self.assertSnarfNoResponse(
"aaa https://example.org/users/someuser bbb", timeout=1
)
with conf.supybot.plugins.Fediverse.snarfers.profile.context(True):
expected_requests = [(ACTOR_URL, utils.web.Error("blah"))]
with self.mockRequests(expected_requests):
self.assertSnarfNoResponse(
"aaa https://example.org/users/someuser bbb", timeout=1
)
expected_requests = [(ACTOR_URL, ACTOR_DATA)]
with self.mockRequests(expected_requests):
self.assertSnarfResponse(
"aaa https://example.org/users/someuser bbb",
"\x02someuser\x02 (@someuser@example.org): My Biography",
)
def testProfileUnknown(self): def testProfileUnknown(self):
expected_requests = [ expected_requests = [
(HOSTMETA_URL, HOSTMETA_DATA), (HOSTMETA_URL, HOSTMETA_DATA),
@ -596,5 +627,62 @@ class FediverseTestCase(ChannelPluginTestCase):
+ "Status Content", + "Status Content",
) )
def testStatusUrlSnarfer(self):
with self.mockRequests([]):
self.assertSnarfNoResponse(
"aaa https://example.org/users/someuser/statuses/1234 bbb",
timeout=1,
)
with conf.supybot.plugins.Fediverse.snarfers.status.context(True):
expected_requests = [
(STATUS_URL, STATUS_DATA),
(ACTOR_URL, utils.web.Error("blah")),
]
with self.mockRequests(expected_requests):
self.assertSnarfNoResponse(
"aaa https://example.org/users/someuser/statuses/1234 bbb",
timeout=1,
)
expected_requests = [
(STATUS_URL, STATUS_DATA),
(ACTOR_URL, ACTOR_DATA),
]
with self.mockRequests(expected_requests):
self.assertSnarfResponse(
"aaa https://example.org/users/someuser/statuses/1234 bbb",
"\x02someuser (@someuser@example.org)\x02: "
+ "@ FirstAuthor I am replying to you",
)
def testSnarferType(self):
# Sends a request, notices it's a status, gives up
with conf.supybot.plugins.Fediverse.snarfers.profile.context(True):
expected_requests = [
(STATUS_URL, STATUS_DATA),
]
with self.mockRequests(expected_requests):
self.assertSnarfNoResponse(
"aaa https://example.org/users/someuser/statuses/1234 bbb",
timeout=1,
)
# Sends a request, notices it's a profile, gives up
with conf.supybot.plugins.Fediverse.snarfers.profile.context(True):
expected_requests = [
(ACTOR_URL, ACTOR_DATA),
]
with self.mockRequests(expected_requests):
self.assertSnarfNoResponse(
"aaa https://example.org/users/someuser/ bbb",
timeout=1,
)
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: