libervia-backend/tests/unit/test_ap-gateway.py

1782 lines
74 KiB
Python

#!/usr/bin/env python3
# Libervia: an XMPP client
# Copyright (C) 2009-2022 Jérôme Poisson (goffi@goffi.org)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from copy import deepcopy
from functools import partial
import io
import json
from typing import Any, Dict, Optional, Union
from unittest.mock import AsyncMock, DEFAULT, MagicMock, patch
from urllib import parse
import pytest
from pytest_twisted import ensureDeferred as ed
from treq.response import _Response as TReqResponse
from twisted.internet import defer
from twisted.web.server import Request
from twisted.words.protocols.jabber import jid
from twisted.words.protocols.jabber.error import StanzaError
from twisted.words.xish import domish
from wokkel import pubsub, rsm
from libervia.backend.core import exceptions
from libervia.backend.core.constants import Const as C
from libervia.backend.memory.sqla_mapping import SubscriptionState
from libervia.backend.plugins import plugin_comp_ap_gateway
from libervia.backend.plugins.plugin_comp_ap_gateway import constants as ap_const
from libervia.backend.plugins.plugin_comp_ap_gateway import TYPE_ACTOR
from libervia.backend.plugins.plugin_comp_ap_gateway.http_server import HTTPServer
from libervia.backend.plugins.plugin_xep_0277 import NS_ATOM
from libervia.backend.plugins.plugin_xep_0424 import NS_MESSAGE_RETRACT
from libervia.backend.plugins.plugin_xep_0465 import NS_PPS
from libervia.backend.tools import xml_tools
from libervia.backend.tools.common import uri as xmpp_uri
from libervia.backend.tools.utils import xmpp_date
TEST_BASE_URL = "https://example.org"
TEST_USER = "test_user"
TEST_AP_ACCOUNT = f"{TEST_USER}@example.org"
TEST_AP_ACTOR_ID = f"{TEST_BASE_URL}/users/{TEST_USER}"
PUBLIC_URL = "test.example"
TEST_JID = jid.JID(f"some_user@{PUBLIC_URL}")
AP_REQUESTS = {
f"{TEST_BASE_URL}/.well-known/webfinger?"
f"resource=acct:{parse.quote(TEST_AP_ACCOUNT)}": {
"aliases": [
f"{TEST_BASE_URL}/@{TEST_USER}",
f"{TEST_BASE_URL}/users/{TEST_USER}",
],
"links": [
{
"href": f"{TEST_BASE_URL}/users/{TEST_USER}",
"rel": "self",
"type": "application/activity+json",
},
],
"subject": f"acct:{TEST_AP_ACCOUNT}",
},
f"{TEST_BASE_URL}/users/{TEST_USER}": {
"@context": [
"https://www.w3.org/ns/activitystreams",
],
"endpoints": {"sharedInbox": f"{TEST_BASE_URL}/inbox"},
"followers": f"{TEST_BASE_URL}/users/{TEST_USER}/followers",
"following": f"{TEST_BASE_URL}/users/{TEST_USER}/following",
"id": f"{TEST_BASE_URL}/users/{TEST_USER}",
"inbox": f"{TEST_BASE_URL}/users/{TEST_USER}/inbox",
"name": "test_user nickname",
"summary": "<p>test account</p>",
"outbox": f"{TEST_BASE_URL}/users/{TEST_USER}/outbox",
"preferredUsername": f"{TEST_USER}",
"type": "Person",
"url": f"{TEST_BASE_URL}/@{TEST_USER}",
},
f"{TEST_BASE_URL}/.well-known/webfinger?"
f"resource=acct:{parse.quote('ext_user@example.org')}": {
"aliases": [f"{TEST_BASE_URL}/@ext_user", f"{TEST_BASE_URL}/users/ext_user"],
"links": [
{
"href": f"{TEST_BASE_URL}/users/ext_user",
"rel": "self",
"type": "application/activity+json",
},
],
"subject": f"acct:ext_user@example.org",
},
f"{TEST_BASE_URL}/users/ext_user": {
"@context": [
"https://www.w3.org/ns/activitystreams",
],
"endpoints": {"sharedInbox": f"{TEST_BASE_URL}/inbox"},
"followers": f"{TEST_BASE_URL}/users/ext_user/followers",
"following": f"{TEST_BASE_URL}/users/ext_user/following",
"id": f"{TEST_BASE_URL}/users/ext_user",
"inbox": f"{TEST_BASE_URL}/users/ext_user/inbox",
"name": "",
"outbox": f"{TEST_BASE_URL}/users/ext_user/outbox",
"preferredUsername": f"ext_user",
"type": "Person",
"url": f"{TEST_BASE_URL}/@ext_user",
},
f"{TEST_BASE_URL}/users/{TEST_USER}/outbox": {
"@context": "https://www.w3.org/ns/activitystreams",
"first": f"{TEST_BASE_URL}/users/{TEST_USER}/outbox?page=true",
"id": f"{TEST_BASE_URL}/users/{TEST_USER}/outbox",
"last": f"{TEST_BASE_URL}/users/{TEST_USER}/outbox?page=true",
"totalItems": 4,
"type": "OrderedCollection",
},
f"{TEST_BASE_URL}/users/{TEST_USER}/outbox?page=true": {
"@context": [
"https://www.w3.org/ns/activitystreams",
],
"id": f"{TEST_BASE_URL}/users/{TEST_USER}/outbox?page=true",
"orderedItems": [
{
"actor": f"{TEST_BASE_URL}/users/{TEST_USER}",
"cc": [
f"{TEST_BASE_URL}/users/{TEST_USER}/followers",
],
"id": f"{TEST_BASE_URL}/users/{TEST_USER}/statuses/1/activity",
"object": {
"attributedTo": f"{TEST_BASE_URL}/users/{TEST_USER}",
"cc": [
f"{TEST_BASE_URL}/users/{TEST_USER}/followers",
],
"content": "<p>test message 1</p>",
"contentMap": {"en": "<p>test message 1</p>"},
"id": f"{TEST_BASE_URL}/users/{TEST_USER}/statuses/1",
"inReplyTo": None,
"published": "2021-12-16T17:28:03Z",
"sensitive": False,
"summary": None,
"tag": [],
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Note",
"url": f"{TEST_BASE_URL}/@{TEST_USER}/1",
},
"published": "2021-12-16T17:28:03Z",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Create",
},
{
"actor": f"{TEST_BASE_URL}/users/{TEST_USER}",
"cc": [
f"{TEST_BASE_URL}/users/{TEST_USER}/followers",
],
"id": f"{TEST_BASE_URL}/users/{TEST_USER}/statuses/2/activity",
"object": {
"attributedTo": f"{TEST_BASE_URL}/users/{TEST_USER}",
"cc": [
f"{TEST_BASE_URL}/users/{TEST_USER}/followers",
],
"content": "<p>test message 2</p>",
"contentMap": {"en": "<p>test message 2</p>"},
"id": f"{TEST_BASE_URL}/users/{TEST_USER}/statuses/2",
"inReplyTo": None,
"published": "2021-12-16T17:27:03Z",
"sensitive": False,
"summary": None,
"tag": [],
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Note",
"url": f"{TEST_BASE_URL}/@{TEST_USER}/2",
},
"published": "2021-12-16T17:27:03Z",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Create",
},
{
"actor": f"{TEST_BASE_URL}/users/{TEST_USER}",
"cc": [
f"{TEST_BASE_URL}/users/{TEST_USER}/followers",
],
"id": f"{TEST_BASE_URL}/users/{TEST_USER}/statuses/3/activity",
"object": {
"attributedTo": f"{TEST_BASE_URL}/users/{TEST_USER}",
"cc": [
f"{TEST_BASE_URL}/users/{TEST_USER}/followers",
],
"content": "<p>test message 3</p>",
"contentMap": {"en": "<p>test message 3</p>"},
"id": f"{TEST_BASE_URL}/users/{TEST_USER}/statuses/3",
"inReplyTo": None,
"published": "2021-12-16T17:26:03Z",
"sensitive": False,
"summary": None,
"tag": [],
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Note",
"url": f"{TEST_BASE_URL}/@{TEST_USER}/3",
},
"published": "2021-12-16T17:26:03Z",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Create",
},
{
"actor": f"{TEST_BASE_URL}/users/{TEST_USER}",
"cc": [
f"{TEST_BASE_URL}/users/{TEST_USER}/followers",
],
"id": f"{TEST_BASE_URL}/users/{TEST_USER}/statuses/4/activity",
"object": {
"attributedTo": f"{TEST_BASE_URL}/users/{TEST_USER}",
"cc": [
f"{TEST_BASE_URL}/users/{TEST_USER}/followers",
],
"content": "<p>test message 4</p>",
"contentMap": {"en": "<p>test message 4</p>"},
"id": f"{TEST_BASE_URL}/users/{TEST_USER}/statuses/4",
"inReplyTo": None,
"published": "2021-12-16T17:25:03Z",
"sensitive": False,
"summary": None,
"tag": [],
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Note",
"url": f"{TEST_BASE_URL}/@{TEST_USER}/4",
},
"published": "2021-12-16T17:25:03Z",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Create",
},
],
"partOf": f"{TEST_BASE_URL}/users/{TEST_USER}/outbox",
"prev": None,
"type": "OrderedCollectionPage",
},
f"{TEST_BASE_URL}/users/{TEST_USER}/following": {
"@context": "https://www.w3.org/ns/activitystreams",
"first": f"{TEST_BASE_URL}/users/{TEST_USER}/following?page=1",
"id": f"{TEST_BASE_URL}/users/{TEST_USER}/following",
"totalItems": 2,
"type": "OrderedCollection",
},
f"{TEST_BASE_URL}/users/{TEST_USER}/following?page=1": {
"@context": "https://www.w3.org/ns/activitystreams",
"id": f"{TEST_BASE_URL}/users/{TEST_USER}/following?page=1",
"orderedItems": [
f"{TEST_BASE_URL}/users/ext_user",
f"https://{PUBLIC_URL}/_ap/{TYPE_ACTOR}/local_user@{PUBLIC_URL}",
],
"partOf": "{TEST_BASE_URL}/users/{TEST_USER}/following",
"totalItems": 2,
"type": "OrderedCollectionPage",
},
f"{TEST_BASE_URL}/users/{TEST_USER}/followers": {
"@context": "https://www.w3.org/ns/activitystreams",
"first": f"{TEST_BASE_URL}/users/{TEST_USER}/followers?page=1",
"id": f"{TEST_BASE_URL}/users/{TEST_USER}/followers",
"totalItems": 2,
"type": "OrderedCollection",
},
f"{TEST_BASE_URL}/users/{TEST_USER}/followers?page=1": {
"@context": "https://www.w3.org/ns/activitystreams",
"id": f"{TEST_BASE_URL}/users/{TEST_USER}/followers?page=1",
"orderedItems": [
f"{TEST_BASE_URL}/users/ext_user",
f"https://{PUBLIC_URL}/_ap/{TYPE_ACTOR}/local_user@{PUBLIC_URL}",
],
"partOf": "{TEST_BASE_URL}/users/{TEST_USER}/followers",
"totalItems": 2,
"type": "OrderedCollectionPage",
},
}
XMPP_ITEM_TPL = """
<item id='{id}' publisher='{publisher_jid}'>
<entry xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>
<title type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<p>
XMPP item {id}
</p>
</div>
</title>
<title type='text'>
XMPP item {id}
</title>
<author>
<name>
test_user
</name>
<uri>
xmpp:{publisher_jid}
</uri>
</author>
<updated>
{updated}
</updated>
<published>
{published}
</published>
<id>
xmpp:{publisher_jid}?;node=urn%3Axmpp%3Amicroblog%3A0;item={id}
</id>
</entry>
</item>
"""
ITEM_BASE_TS = 1643385499
XMPP_ITEMS = [
xml_tools.parse(
"".join(
l.strip()
for l in XMPP_ITEM_TPL.format(
id=i,
publisher_jid="some_user@test.example",
updated=xmpp_date(ITEM_BASE_TS + i * 60),
published=xmpp_date(ITEM_BASE_TS + i * 60),
).split("\n")
),
namespace=pubsub.NS_PUBSUB,
)
for i in range(1, 5)
]
TEST_USER_DATA = AP_REQUESTS[f"{TEST_BASE_URL}/users/{TEST_USER}"]
OUTBOX_FIRST_PAGE = f"{TEST_BASE_URL}/users/{TEST_USER}/outbox?page=true"
TEST_AP_ITEMS = AP_REQUESTS[OUTBOX_FIRST_PAGE]["orderedItems"]
# request on an item ID must return the ID
for item in TEST_AP_ITEMS:
AP_REQUESTS[item["id"]] = item
async def mock_treq_get(url):
return deepcopy(AP_REQUESTS[url])
async def mock_ap_get(url, requestor_actor_id):
return deepcopy(AP_REQUESTS[url])
async def mock_treq_json(data):
return dict(data)
async def mock_get_items(client, service, node, *args, **kwargs):
"""Mock get_items
special kwargs can be used:
ret_items (List[Domish.Element]): items to be returned, by default XMPP_ITEMS are
returned
tested_node (str): node for which items must be returned. If specified and a
different node is requested, "item-not-found" StanzaError will be raised
"""
tested_node = kwargs.pop("tested_node", None)
if tested_node is not None and node != tested_node:
raise StanzaError("item-not-found")
ret_items = kwargs.pop("ret_items", XMPP_ITEMS)
rsm_resp = rsm.RSMResponse(
first=ret_items[0]["id"], last=ret_items[-1]["id"], index=0, count=len(ret_items)
)
return ret_items, {"rsm": rsm_resp.toDict(), "complete": True}
async def mock_get_pubsub_node(client, service, node, with_subscriptions=False, **kwargs):
"""Mock storage's get_pubsub_node
return an MagicMock with subscription attribute set to empty list
"""
fake_cached_node = MagicMock()
fake_cached_node.subscriptions = []
return fake_cached_node
def mock_client(jid):
client = MagicMock()
client.jid = jid
client.host = "test.example"
client._ap_storage.get = AsyncMock()
client._ap_storage.aset = AsyncMock()
return client
def get_virtual_client(jid):
return mock_client(jid)
class FakeTReqPostResponse:
code = 202
@pytest.fixture(scope="session")
def ap_gateway(host):
gateway = plugin_comp_ap_gateway.APGateway(host)
gateway.initialised = True
gateway.is_pubsub = AsyncMock()
gateway.is_pubsub.return_value = False
client = mock_client(jid.JID("ap.test.example"))
client.get_virtual_client = get_virtual_client
gateway.client = client
gateway.local_only = True
gateway.public_url = PUBLIC_URL
gateway.ap_path = "_ap"
gateway.auto_mentions = True
gateway.base_ap_url = parse.urljoin(
f"https://{gateway.public_url}", f"{gateway.ap_path}/"
)
gateway.server = HTTPServer(gateway)
gateway.public_key_pem = None
return gateway
class TestActivityPubGateway:
def get_title_xhtml(self, item_elt: domish.Element) -> domish.Element:
return next(
t
for t in item_elt.entry.elements(NS_ATOM, "title")
if t.getAttribute("type") == "xhtml"
)
@ed
async def test_jid_and_node_convert_to_ap_handle(self, ap_gateway):
"""JID and pubsub node are converted correctly to an AP actor handle"""
get_account = ap_gateway.get_ap_account_from_jid_and_node
# local jid
assert (
await get_account(jid_=jid.JID("simple@test.example"), node=None)
== "simple@test.example"
)
# non local jid
assert (
await get_account(jid_=jid.JID("simple@example.org"), node=None)
== "___simple.40example.2eorg@ap.test.example"
)
# local jid with non microblog node
assert (
await get_account(jid_=jid.JID("simple@test.example"), node="some_other_node")
== "some_other_node---simple@test.example"
)
# local pubsub node
with patch.object(ap_gateway, "is_pubsub") as is_pubsub:
is_pubsub.return_value = True
assert (
await get_account(jid_=jid.JID("pubsub.test.example"), node="some_node")
== "some_node@pubsub.test.example"
)
# non local pubsub node
with patch.object(ap_gateway, "is_pubsub") as is_pubsub:
is_pubsub.return_value = True
assert (
await get_account(jid_=jid.JID("pubsub.example.org"), node="some_node")
== "___some_node.40pubsub.2eexample.2eorg@ap.test.example"
)
@ed
async def test_ap_handle_convert_to_jid_and_node(self, ap_gateway, monkeypatch):
"""AP actor handle convert correctly to JID and pubsub node"""
get_jid_node = ap_gateway.get_jid_and_node
# for following assertion, host is not a pubsub service
with patch.object(ap_gateway, "is_pubsub") as is_pubsub:
is_pubsub.return_value = False
# simple local jid
assert await get_jid_node("toto@test.example") == (
jid.JID("toto@test.example"),
None,
)
# simple external jid
## with "local_only" set, it should raise an exception
with pytest.raises(exceptions.PermissionError):
await get_jid_node("toto@example.org")
## with "local_only" unset, it should work
with monkeypatch.context() as m:
m.setattr(ap_gateway, "local_only", False, raising=True)
assert await get_jid_node("toto@example.org") == (
jid.JID("toto@example.org"),
None,
)
# explicit node
assert await get_jid_node("tata---toto@test.example") == (
jid.JID("toto@test.example"),
"tata",
)
# for following assertion, host is a pubsub service
with patch.object(ap_gateway, "is_pubsub") as is_pubsub:
is_pubsub.return_value = True
# simple local node
assert await get_jid_node("toto@pubsub.test.example") == (
jid.JID("pubsub.test.example"),
"toto",
)
# encoded local node
assert await get_jid_node(
"___urn.3axmpp.3amicroblog.3a0@pubsub.test.example"
) == (jid.JID("pubsub.test.example"), "urn:xmpp:microblog:0")
@ed
async def test_ap_to_pubsub_conversion(self, ap_gateway, monkeypatch):
"""AP requests are converted to pubsub"""
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "get", mock_treq_get)
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "json_content", mock_treq_json)
monkeypatch.setattr(ap_gateway, "ap_get", mock_ap_get)
actor_data = await ap_gateway.get_ap_actor_data_from_account(
TEST_AP_ACTOR_ID, TEST_AP_ACCOUNT
)
outbox = await ap_gateway.ap_get_object(TEST_AP_ACTOR_ID, actor_data, "outbox")
items, rsm_resp = await ap_gateway.get_ap_items(TEST_AP_ACTOR_ID, outbox, 2)
assert rsm_resp.count == 4
assert rsm_resp.index == 0
assert rsm_resp.first == "https://example.org/users/test_user/statuses/4"
assert rsm_resp.last == "https://example.org/users/test_user/statuses/3"
title_xhtml = self.get_title_xhtml(items[0])
assert title_xhtml.toXml() == (
"<title xmlns='http://www.w3.org/2005/Atom' type='xhtml'>"
"<div xmlns='http://www.w3.org/1999/xhtml'><p>test message 4</p></div>"
"</title>"
)
author_uri = str(
[e for e in items[0].entry.author.elements() if e.name == "uri"][0]
)
assert author_uri == "xmpp:test_user\\40example.org@ap.test.example"
assert str(items[0].entry.published) == "2021-12-16T17:25:03Z"
title_xhtml = self.get_title_xhtml(items[1])
assert title_xhtml.toXml() == (
"<title xmlns='http://www.w3.org/2005/Atom' type='xhtml'>"
"<div xmlns='http://www.w3.org/1999/xhtml'><p>test message 3</p></div>"
"</title>"
)
author_uri = str(
[e for e in items[1].entry.author.elements() if e.name == "uri"][0]
)
assert author_uri == "xmpp:test_user\\40example.org@ap.test.example"
assert str(items[1].entry.published) == "2021-12-16T17:26:03Z"
items, rsm_resp = await ap_gateway.get_ap_items(
TEST_AP_ACTOR_ID,
outbox,
max_items=2,
after_id="https://example.org/users/test_user/statuses/3",
)
assert rsm_resp.count == 4
assert rsm_resp.index == 2
assert rsm_resp.first == "https://example.org/users/test_user/statuses/2"
assert rsm_resp.last == "https://example.org/users/test_user/statuses/1"
title_xhtml = self.get_title_xhtml(items[0])
assert title_xhtml.toXml() == (
"<title xmlns='http://www.w3.org/2005/Atom' type='xhtml'>"
"<div xmlns='http://www.w3.org/1999/xhtml'><p>test message 2</p></div>"
"</title>"
)
author_uri = str(
[e for e in items[0].entry.author.elements() if e.name == "uri"][0]
)
assert author_uri == "xmpp:test_user\\40example.org@ap.test.example"
assert str(items[0].entry.published) == "2021-12-16T17:27:03Z"
title_xhtml = self.get_title_xhtml(items[1])
assert title_xhtml.toXml() == (
"<title xmlns='http://www.w3.org/2005/Atom' type='xhtml'>"
"<div xmlns='http://www.w3.org/1999/xhtml'><p>test message 1</p></div>"
"</title>"
)
author_uri = str(
[e for e in items[1].entry.author.elements() if e.name == "uri"][0]
)
assert author_uri == "xmpp:test_user\\40example.org@ap.test.example"
assert str(items[1].entry.published) == "2021-12-16T17:28:03Z"
items, rsm_resp = await ap_gateway.get_ap_items(
TEST_AP_ACTOR_ID, outbox, max_items=1, start_index=2
)
assert rsm_resp.count == 4
assert rsm_resp.index == 2
assert rsm_resp.first == "https://example.org/users/test_user/statuses/2"
assert rsm_resp.last == "https://example.org/users/test_user/statuses/2"
assert len(items) == 1
title_xhtml = self.get_title_xhtml(items[0])
assert title_xhtml.toXml() == (
"<title xmlns='http://www.w3.org/2005/Atom' type='xhtml'>"
"<div xmlns='http://www.w3.org/1999/xhtml'><p>test message 2</p></div>"
"</title>"
)
assert str(items[0].entry.published) == "2021-12-16T17:27:03Z"
items, rsm_resp = await ap_gateway.get_ap_items(
TEST_AP_ACTOR_ID, outbox, max_items=3, chronological_pagination=False
)
assert rsm_resp.count == 4
assert rsm_resp.index == 1
assert rsm_resp.first == "https://example.org/users/test_user/statuses/3"
assert rsm_resp.last == "https://example.org/users/test_user/statuses/1"
assert len(items) == 3
title_xhtml = self.get_title_xhtml(items[0])
assert title_xhtml.toXml() == (
"<title xmlns='http://www.w3.org/2005/Atom' type='xhtml'>"
"<div xmlns='http://www.w3.org/1999/xhtml'><p>test message 3</p></div>"
"</title>"
)
title_xhtml = self.get_title_xhtml(items[2])
assert title_xhtml.toXml() == (
"<title xmlns='http://www.w3.org/2005/Atom' type='xhtml'>"
"<div xmlns='http://www.w3.org/1999/xhtml'><p>test message 1</p></div>"
"</title>"
)
def ap_request_params(
self,
ap_gateway,
type_: Optional[str] = None,
url: Optional[str] = None,
data: Optional[dict] = None,
query_data: Optional[dict] = None,
signing_actor: Optional[str] = None,
) -> Dict[str, Any]:
"""Generate parameters for HTTPAPGServer's AP*Request
@param type_: one of the AP query type (e.g. "outbox")
@param url: URL to query (mutually exclusif with type_)
@param data: object used as body of the request
@param query_data: query data as returned by parse.parse_qs
@return dict with kw params to use
"""
assert type_ and url is None or url and type_ is None
if type_ is not None:
path = f"_ap/{type_}/some_user@test.example"
else:
url_parsed = parse.urlparse(url)
path = url_parsed.path.lstrip("/")
type_ = path.split("/")[1]
if query_data is None:
query_data = parse.parse_qs(url_parsed.query)
if query_data:
uri = f"{path}?{parse.urlencode(query_data, doseq=True)}"
else:
uri = path
test_jid = jid.JID("some_user@test.example")
request = Request(MagicMock())
request.path = path.encode()
request.uri = uri.encode()
ap_url = parse.urljoin(f"https://{ap_gateway.public_url}", path)
kwargs = {
"request": request,
"data": data,
"account_jid": test_jid,
"node": None,
"ap_account": test_jid.full(),
"ap_url": ap_url,
"signing_actor": signing_actor,
}
if type_ not in (ap_const.TYPE_ACTOR, "outbox", "following", "followers"):
kwargs["requestor_actor_id"] = TEST_AP_ACTOR_ID
if type_ == "outbox" and query_data:
kwargs["query_data"] = query_data
# signing_actor is not used for page requests
del kwargs["signing_actor"]
return kwargs
@ed
async def test_pubsub_to_ap_conversion(self, ap_gateway, monkeypatch):
"""Pubsub nodes are converted to AP collections"""
monkeypatch.setattr(ap_gateway._p, "get_items", mock_get_items)
outbox = await ap_gateway.server.resource.ap_outbox_request(
**self.ap_request_params(ap_gateway, "outbox")
)
assert outbox["@context"] == ["https://www.w3.org/ns/activitystreams"]
assert outbox["id"] == "https://test.example/_ap/outbox/some_user@test.example"
assert outbox["totalItems"] == len(XMPP_ITEMS)
assert outbox["type"] == "OrderedCollection"
assert outbox["first"]
assert outbox["last"]
first_page = await ap_gateway.server.resource.ap_outbox_page_request(
**self.ap_request_params(ap_gateway, url=outbox["first"])
)
assert first_page["@context"] == ["https://www.w3.org/ns/activitystreams"]
assert (
first_page["id"]
== "https://test.example/_ap/outbox/some_user@test.example?page=first"
)
assert first_page["type"] == "OrderedCollectionPage"
assert first_page["partOf"] == outbox["id"]
assert len(first_page["orderedItems"]) == len(XMPP_ITEMS)
first_item = first_page["orderedItems"][0]
assert first_item["@context"] == ["https://www.w3.org/ns/activitystreams"]
assert (
first_item["id"] == "https://test.example/_ap/item/some_user@test.example/4"
)
assert (
first_item["actor"] == "https://test.example/_ap/actor/some_user@test.example"
)
assert first_item["type"] == "Create"
first_item_obj = first_item["object"]
assert first_item_obj["id"] == first_item["id"]
assert first_item_obj["type"] == "Note"
assert first_item_obj["published"] == "2022-01-28T16:02:19Z"
assert first_item_obj["attributedTo"] == first_item["actor"]
assert first_item_obj["content"] == "<div><p>XMPP item 4</p></div>"
assert first_item_obj["to"] == ["https://www.w3.org/ns/activitystreams#Public"]
@ed
async def test_following_to_pps(self, ap_gateway, monkeypatch):
"""AP following items are converted to Public Pubsub Subscription subscriptions"""
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "get", mock_treq_get)
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "json_content", mock_treq_json)
monkeypatch.setattr(ap_gateway, "ap_get", mock_ap_get)
items, __ = await ap_gateway.pubsub_service.items(
jid.JID("toto@example.org"),
ap_gateway.get_local_jid_from_account(TEST_AP_ACCOUNT),
ap_gateway._pps.subscriptions_node,
None,
None,
None,
)
assert len(items) == 2
for idx, entity in enumerate(
("local_user@test.example", "ext_user\\40example.org@ap.test.example")
):
subscription_elt = next(items[idx].elements(NS_PPS, "subscription"), None)
assert subscription_elt is not None
assert subscription_elt["node"] == ap_gateway._m.namespace
assert subscription_elt["service"] == entity
@ed
async def test_followers_to_pps(self, ap_gateway, monkeypatch):
"""AP followers items are converted to Public Pubsub Subscription subscribers"""
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "get", mock_treq_get)
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "json_content", mock_treq_json)
monkeypatch.setattr(ap_gateway, "ap_get", mock_ap_get)
items, __ = await ap_gateway.pubsub_service.items(
jid.JID("toto@example.org"),
ap_gateway.get_local_jid_from_account(TEST_AP_ACCOUNT),
ap_gateway._pps.get_public_subscribers_node(ap_gateway._m.namespace),
None,
None,
None,
)
assert len(items) == 2
for idx, entity in enumerate(
("local_user@test.example", "ext_user\\40example.org@ap.test.example")
):
subscriber_elt = next(items[idx].elements(NS_PPS, "subscriber"), None)
assert subscriber_elt is not None
assert subscriber_elt["jid"] == entity
@ed
async def test_pps_to_following(self, ap_gateway, monkeypatch):
"""Public Pubsub Subscription subscriptions are converted to AP following items"""
subscriptions = [
pubsub.Item(
id="subscription_1",
payload=ap_gateway._pps.build_subscription_elt(
ap_gateway._m.namespace, jid.JID("local_user@test.example")
),
),
pubsub.Item(
id="subscription_2",
payload=ap_gateway._pps.build_subscription_elt(
ap_gateway._m.namespace,
jid.JID("ext_user\\40example.org@ap.test.example"),
),
),
]
monkeypatch.setattr(
ap_gateway._p, "get_items", partial(mock_get_items, ret_items=subscriptions)
)
following = await ap_gateway.server.resource.ap_following_request(
**self.ap_request_params(ap_gateway, "following")
)
assert following["@context"] == ["https://www.w3.org/ns/activitystreams"]
assert (
following["id"] == "https://test.example/_ap/following/some_user@test.example"
)
assert following["totalItems"] == len(subscriptions)
assert following["type"] == "OrderedCollection"
assert following.get("first")
first_page = following["first"]
assert first_page["type"] == "OrderedCollectionPage"
assert len(first_page["orderedItems"]) == len(subscriptions)
items = first_page["orderedItems"]
assert items == ["local_user@test.example", "ext_user@example.org"]
@ed
async def test_pps_to_followers(self, ap_gateway, monkeypatch):
"""Public Pubsub Subscription subscribers are converted to AP followers"""
subscribers = [
pubsub.Item(
id="subscriber_1",
payload=ap_gateway._pps.build_subscriber_elt(
jid.JID("local_user@test.example")
),
),
pubsub.Item(
id="subscriber_2",
payload=ap_gateway._pps.build_subscriber_elt(
jid.JID("ext_user\\40example.org@ap.test.example")
),
),
]
monkeypatch.setattr(
ap_gateway._p, "get_items", partial(mock_get_items, ret_items=subscribers)
)
followers = await ap_gateway.server.resource.ap_followers_request(
**self.ap_request_params(ap_gateway, "followers")
)
assert followers["@context"] == ["https://www.w3.org/ns/activitystreams"]
assert (
followers["id"] == "https://test.example/_ap/followers/some_user@test.example"
)
assert followers["totalItems"] == len(subscribers)
assert followers["type"] == "OrderedCollection"
assert followers.get("first")
first_page = followers["first"]
assert first_page["type"] == "OrderedCollectionPage"
assert len(first_page["orderedItems"]) == len(subscribers)
items = first_page["orderedItems"]
assert items == ["local_user@test.example", "ext_user@example.org"]
@ed
async def test_xmpp_message_to_ap_direct_message(self, ap_gateway, monkeypatch):
"""XMPP message are sent as AP direct message"""
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "get", mock_treq_get)
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "json_content", mock_treq_json)
monkeypatch.setattr(ap_gateway, "ap_get", mock_ap_get)
mess_data = {
"from": TEST_JID,
"to": ap_gateway.get_local_jid_from_account(TEST_AP_ACCOUNT),
"type": "chat",
"message": {"": "This is a test message."},
"extra": {"origin-id": "123"},
}
with patch.object(ap_gateway, "ap_post") as ap_post:
await ap_gateway.on_message(ap_gateway.client, mess_data)
url, actor_id, doc = ap_post.call_args[0]
assert url == "https://example.org/users/test_user/inbox"
assert actor_id == "https://test.example/_ap/actor/some_user@test.example"
obj = doc["object"]
assert doc["@context"] == ["https://www.w3.org/ns/activitystreams"]
assert doc["actor"] == "https://test.example/_ap/actor/some_user@test.example"
assert obj["type"] == "Note"
assert obj["content"] == "This is a test message."
assert obj["attributedTo"] == (
"https://test.example/_ap/actor/some_user@test.example"
)
# we must have a direct message, thus the item must be only addressed to destinee
# ("to" attribute of the message), and the "Public" namespace must not be set
assert doc["to"] == ["https://example.org/users/test_user"]
assert obj["to"] == ["https://example.org/users/test_user"]
for field in ("bto", "cc", "bcc", "audience"):
assert field not in doc
assert field not in obj
@ed
async def test_ap_direct_message_to_xmpp_message(self, ap_gateway, monkeypatch):
"""AP direct message are sent as XMPP message (not Pubsub)"""
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "get", mock_treq_get)
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "json_content", mock_treq_json)
monkeypatch.setattr(ap_gateway, "ap_get", mock_ap_get)
# we have to patch DeferredList to not wait forever
monkeypatch.setattr(defer, "DeferredList", AsyncMock())
xmpp_actor_id = ap_gateway.build_apurl(ap_const.TYPE_ACTOR, TEST_JID.userhost())
direct_ap_message = {
"attributedTo": TEST_AP_ACTOR_ID,
"cc": [],
"content": "<p>test direct message</p>",
"contentMap": {"en": "<p>test direct message</p>"},
"id": f"{TEST_AP_ACTOR_ID}/statuses/123",
"published": "2022-05-20T08:14:39Z",
"to": [xmpp_actor_id],
"type": "Note",
}
client = ap_gateway.client.get_virtual_client(
ap_gateway.get_local_jid_from_account(TEST_AP_ACCOUNT)
)
with patch.object(client, "sendMessage") as sendMessage:
await ap_gateway.new_ap_item(
client, None, ap_gateway._m.namespace, direct_ap_message
)
# sendMessage must be called for <message> stanza, and the "message" argument must
# be set to the content of the original AP message
assert sendMessage.called
assert sendMessage.call_args.args[0] == TEST_JID
assert sendMessage.call_args.args[1] == {"": "test direct message"}
@ed
async def test_pubsub_retract_to_ap_delete(self, ap_gateway, monkeypatch):
"""Pubsub retract requests are converted to AP delete activity"""
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "get", mock_treq_get)
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "json_content", mock_treq_json)
monkeypatch.setattr(ap_gateway, "ap_get", mock_ap_get)
retract_id = "retract_123"
retract_elt = domish.Element((pubsub.NS_PUBSUB_EVENT, "retract"))
retract_elt["id"] = retract_id
items_event = pubsub.ItemsEvent(
sender=TEST_JID,
recipient=ap_gateway.get_local_jid_from_account(TEST_AP_ACCOUNT),
nodeIdentifier=ap_gateway._m.namespace,
items=[retract_elt],
headers={},
)
with patch.object(ap_gateway, "ap_post") as ap_post:
ap_post.return_value = FakeTReqPostResponse()
# we simulate the reception of a retract event
await ap_gateway._items_received(ap_gateway.client, items_event)
url, actor_id, doc = ap_post.call_args[0]
jid_account = await ap_gateway.get_ap_account_from_jid_and_node(TEST_JID, None)
jid_actor_id = ap_gateway.build_apurl(ap_const.TYPE_ACTOR, jid_account)
assert url == f"{TEST_BASE_URL}/inbox"
assert actor_id == jid_actor_id
assert doc["type"] == "Delete"
assert doc["actor"] == jid_actor_id
obj = doc["object"]
assert obj["type"] == ap_const.TYPE_TOMBSTONE
url_item_id = ap_gateway.build_apurl(ap_const.TYPE_ITEM, jid_account, retract_id)
assert obj["id"] == url_item_id
@ed
async def test_ap_delete_to_pubsub_retract(self, ap_gateway):
"""AP delete activity is converted to pubsub retract"""
client = ap_gateway.client.get_virtual_client(
ap_gateway.get_local_jid_from_account(TEST_AP_ACCOUNT)
)
ap_item = {
"@context": "https://www.w3.org/ns/activitystreams",
"actor": TEST_AP_ACTOR_ID,
"id": "https://test.example/retract_123",
"type": "Delete",
"object": {"id": f"{TEST_AP_ACTOR_ID}/item/123", "type": "Tombstone"},
"to": ["https://www.w3.org/ns/activitystreams#Public"],
}
with patch.multiple(
ap_gateway.host.memory.storage,
get=DEFAULT,
get_pubsub_node=DEFAULT,
delete_pubsub_items=DEFAULT,
) as mock_objs:
mock_objs["get"].return_value = None
cached_node = MagicMock()
mock_objs["get_pubsub_node"].return_value = cached_node
subscription = MagicMock()
subscription.state = SubscriptionState.SUBSCRIBED
subscription.subscriber = TEST_JID
cached_node.subscriptions = [subscription]
with patch.object(
ap_gateway.pubsub_service, "notifyRetract"
) as notifyRetract:
# we simulate a received Delete activity
await ap_gateway.new_ap_delete_item(
client=client,
destinee=None,
node=ap_gateway._m.namespace,
item=ap_item,
)
# item is deleted from database
delete_pubsub_items = mock_objs["delete_pubsub_items"]
assert delete_pubsub_items.call_count == 1
assert delete_pubsub_items.call_args.args[1] == [ap_item["id"]]
# retraction notification is sent to subscribers
assert notifyRetract.call_count == 1
assert notifyRetract.call_args.args[0] == client.jid
assert notifyRetract.call_args.args[1] == ap_gateway._m.namespace
notifications = notifyRetract.call_args.args[2]
assert len(notifications) == 1
subscriber, __, item_elts = notifications[0]
assert subscriber == TEST_JID
assert len(item_elts) == 1
item_elt = item_elts[0]
assert isinstance(item_elt, domish.Element)
assert item_elt.name == "item"
assert item_elt["id"] == ap_item["id"]
@ed
async def test_message_retract_to_ap_delete(self, ap_gateway, monkeypatch):
"""Message retract requests are converted to AP delete activity"""
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "get", mock_treq_get)
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "json_content", mock_treq_json)
monkeypatch.setattr(ap_gateway, "ap_get", mock_ap_get)
# origin ID is the ID of the message to retract
origin_id = "mess_retract_123"
# we call send_retract to get the message element of a retraction request
fake_client = MagicMock()
fake_client.jid = TEST_JID
dest_jid = ap_gateway.get_local_jid_from_account(TEST_AP_ACCOUNT)
fake_history = MagicMock()
fake_history.type = C.MESS_TYPE_CHAT
ap_gateway._r.send_retract(fake_client, dest_jid, origin_id, fake_history)
# message_retract_elt is the message which would be sent for a retraction
message_retract_elt = fake_client.send.call_args.args[0]
retract_elt = next(message_retract_elt.elements(NS_MESSAGE_RETRACT, "retract"))
with patch.object(ap_gateway, "ap_post") as ap_post:
ap_post.return_value = FakeTReqPostResponse()
fake_fastened_elts = MagicMock()
fake_fastened_elts.id = origin_id
# we simulate the reception of a retract event using the message element that
# we generated above
await ap_gateway._on_message_retract(
ap_gateway.client, message_retract_elt, retract_elt, fake_fastened_elts
)
url, actor_id, doc = ap_post.call_args[0]
# the AP delete activity must have been sent through ap_post
# we check its values
jid_account = await ap_gateway.get_ap_account_from_jid_and_node(TEST_JID, None)
jid_actor_id = ap_gateway.build_apurl(ap_const.TYPE_ACTOR, jid_account)
assert url == f"{TEST_BASE_URL}/users/{TEST_USER}/inbox"
assert actor_id == jid_actor_id
assert doc["type"] == "Delete"
assert doc["actor"] == jid_actor_id
obj = doc["object"]
assert obj["type"] == ap_const.TYPE_TOMBSTONE
url_item_id = ap_gateway.build_apurl(ap_const.TYPE_ITEM, jid_account, origin_id)
assert obj["id"] == url_item_id
@ed
async def test_ap_delete_to_message_retract(self, ap_gateway, monkeypatch):
"""AP delete activity is converted to message retract"""
# note: a message retract is used when suitable message is found in history,
# otherwise it should be in pubsub cache and it's a pubsub retract (tested above
# by ``test_ap_delete_to_pubsub_retract``)
# we don't want actual queries in database
retract_db_history = AsyncMock()
monkeypatch.setattr(ap_gateway._r, "retract_db_history", retract_db_history)
client = ap_gateway.client.get_virtual_client(
ap_gateway.get_local_jid_from_account(TEST_AP_ACCOUNT)
)
fake_send = MagicMock()
monkeypatch.setattr(client, "send", fake_send)
ap_item = {
"@context": "https://www.w3.org/ns/activitystreams",
"actor": TEST_AP_ACTOR_ID,
"id": "https://test.example/retract_123",
"type": "Delete",
"object": {"id": f"{TEST_AP_ACTOR_ID}/item/123", "type": "Tombstone"},
"to": ["https://www.w3.org/ns/activitystreams#Public"],
}
with patch.object(ap_gateway.host.memory.storage, "get") as storage_get:
fake_history = MagicMock()
fake_history.source_jid = client.jid
fake_history.dest = str(TEST_JID)
fake_history.dest_jid = TEST_JID
fake_history.origin_id = ap_item["id"]
storage_get.return_value = fake_history
# we simulate a received Delete activity
await ap_gateway.new_ap_delete_item(
client=client, destinee=None, node=ap_gateway._m.namespace, item=ap_item
)
# item is deleted from database
assert retract_db_history.call_count == 1
assert retract_db_history.call_args.args[0] == client
assert retract_db_history.call_args.args[1] == fake_history
# retraction notification is sent to destinee
assert fake_send.call_count == 1
sent_elt = fake_send.call_args.args[0]
assert sent_elt.name == "message"
assert sent_elt["from"] == client.jid.full()
assert sent_elt["to"] == TEST_JID.full()
retract_elt = next(sent_elt.elements(NS_MESSAGE_RETRACT, "retract"))
assert retract_elt["id"] == ap_item["id"]
assert retract_elt is not None
assert retract_elt.uri == NS_MESSAGE_RETRACT
@ed
async def test_ap_actor_metadata_to_vcard(self, ap_gateway, monkeypatch):
"""AP actor metadata are converted to XMPP/vCard4"""
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "get", mock_treq_get)
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "json_content", mock_treq_json)
monkeypatch.setattr(ap_gateway, "ap_get", mock_ap_get)
items, __ = await ap_gateway.pubsub_service.items(
jid.JID("toto@example.org"),
ap_gateway.get_local_jid_from_account(TEST_AP_ACCOUNT),
# VCard4 node
ap_gateway._v.node,
None,
None,
None,
)
assert len(items) == 1
vcard_elt = next(items[0].elements(ap_gateway._v.namespace, "vcard"))
vcard = ap_gateway._v.vcard_2_dict(vcard_elt)
assert "test_user nickname" in vcard["nicknames"]
assert vcard["description"] == "test account"
@ed
async def test_identity_data_to_ap_actor_metadata(self, ap_gateway):
"""XMPP identity is converted to AP actor metadata"""
# XXX: XMPP identity is normally an amalgam of metadata from several
# XEPs/locations (vCard4, vcard-tmp, etc)
with patch.object(ap_gateway._i, "get_identity") as get_identity:
get_identity.return_value = {
"nicknames": ["nick1", "nick2"],
"description": "test description",
}
actor_data = await ap_gateway.server.resource.ap_actor_request(
**self.ap_request_params(ap_gateway, ap_const.TYPE_ACTOR)
)
# only the first nickname should be used
assert actor_data["name"] == "nick1"
assert actor_data["summary"] == "test description"
@ed
async def test_direct_addressing_mention_to_reference(self, ap_gateway, monkeypatch):
"""AP mentions by direct addressing are converted to XEP-0372 references"""
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "get", mock_treq_get)
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "json_content", mock_treq_json)
monkeypatch.setattr(ap_gateway, "ap_get", mock_ap_get)
xmpp_actor_id = ap_gateway.build_apurl(ap_const.TYPE_ACTOR, TEST_JID.userhost())
direct_addr_mention = {
"attributedTo": TEST_AP_ACTOR_ID,
"cc": [],
"content": "<p>test mention by direct addressing</p>",
"id": f"{TEST_AP_ACTOR_ID}/statuses/direct_addr_123",
"published": "2022-05-20T08:14:39Z",
"to": [ap_const.NS_AP_PUBLIC, xmpp_actor_id],
"type": "Note",
}
client = ap_gateway.client.get_virtual_client(
ap_gateway.get_local_jid_from_account(TEST_AP_ACCOUNT)
)
monkeypatch.setattr(client, "sendMessage", MagicMock())
with patch.object(ap_gateway._refs, "send_reference") as send_reference:
await ap_gateway.new_ap_item(
client, None, ap_gateway._m.namespace, direct_addr_mention
)
assert send_reference.call_count == 1
assert send_reference.call_args.kwargs["to_jid"] == TEST_JID
local_actor_jid = ap_gateway.get_local_jid_from_account(TEST_AP_ACCOUNT)
expected_anchor = xmpp_uri.build_xmpp_uri(
"pubsub",
path=local_actor_jid.full(),
node=ap_gateway._m.namespace,
item=direct_addr_mention["id"],
)
assert send_reference.call_args.kwargs["anchor"] == expected_anchor
@ed
async def test_tag_mention_to_reference(self, ap_gateway, monkeypatch):
"""AP mentions in "tag" field are converted to XEP-0372 references"""
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "get", mock_treq_get)
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "json_content", mock_treq_json)
monkeypatch.setattr(ap_gateway, "ap_get", mock_ap_get)
xmpp_actor_id = ap_gateway.build_apurl(ap_const.TYPE_ACTOR, TEST_JID.userhost())
direct_addr_mention = {
"attributedTo": TEST_AP_ACTOR_ID,
"cc": [],
"content": "<p>test mention by tag</p>",
"id": f"{TEST_AP_ACTOR_ID}/statuses/tag_123",
"published": "2022-05-20T08:14:39Z",
"to": [ap_const.NS_AP_PUBLIC],
"tag": [{"type": "Mention", "href": xmpp_actor_id, "name": f"@{TEST_JID}'"}],
"type": "Note",
}
client = ap_gateway.client.get_virtual_client(
ap_gateway.get_local_jid_from_account(TEST_AP_ACCOUNT)
)
monkeypatch.setattr(client, "sendMessage", MagicMock())
with patch.object(ap_gateway._refs, "send_reference") as send_reference:
await ap_gateway.new_ap_item(
client, None, ap_gateway._m.namespace, direct_addr_mention
)
assert send_reference.call_count == 1
assert send_reference.call_args.kwargs["to_jid"] == TEST_JID
local_actor_jid = ap_gateway.get_local_jid_from_account(TEST_AP_ACCOUNT)
expected_anchor = xmpp_uri.build_xmpp_uri(
"pubsub",
path=local_actor_jid.full(),
node=ap_gateway._m.namespace,
item=direct_addr_mention["id"],
)
assert send_reference.call_args.kwargs["anchor"] == expected_anchor
@ed
async def test_auto_mentions(self, ap_gateway, monkeypatch):
"""Check that mentions in body are converted to AP mentions"""
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "get", mock_treq_get)
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "json_content", mock_treq_json)
monkeypatch.setattr(ap_gateway, "ap_get", mock_ap_get)
mb_data = {
"author_jid": TEST_JID.full(),
"content": f"mention of @{TEST_AP_ACCOUNT}",
"service": TEST_JID.full(),
"node": ap_gateway._m.namespace,
}
ap_item = await ap_gateway.mb_data_2_ap_item(
ap_gateway.client, mb_data, public=True
)
ap_object = ap_item["object"]
assert TEST_AP_ACTOR_ID in ap_object["to"]
expected_mention = {
"type": ap_const.TYPE_MENTION,
"href": TEST_AP_ACTOR_ID,
"name": f"@{TEST_AP_ACCOUNT}",
}
assert expected_mention in ap_object["tag"]
@ed
async def test_no_auto_mentions_when_not_public(self, ap_gateway, monkeypatch):
"""Check that no mention is send when the message is not public"""
# this is the same test as test_auto_mentions above, except that public is not set
# in mb_data_2_ap_item
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "get", mock_treq_get)
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "json_content", mock_treq_json)
monkeypatch.setattr(ap_gateway, "ap_get", mock_ap_get)
mb_data = {
"author_jid": TEST_JID.full(),
"content": f"mention of @{TEST_AP_ACCOUNT}",
"service": TEST_JID.full(),
"node": ap_gateway._m.namespace,
}
ap_item = await ap_gateway.mb_data_2_ap_item(
ap_gateway.client, mb_data, public=False
)
ap_object = ap_item["object"]
assert "to" not in ap_object
assert "tag" not in ap_object
@ed
async def test_xmpp_reference_to_ap_mention(self, ap_gateway, monkeypatch):
"""Check that XEP-0372 references are converted to AP mention"""
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "get", mock_treq_get)
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "json_content", mock_treq_json)
monkeypatch.setattr(ap_gateway, "ap_get", mock_ap_get)
local_actor_jid = ap_gateway.get_local_jid_from_account(TEST_AP_ACCOUNT)
item_elt = XMPP_ITEMS[0]
anchor = xmpp_uri.build_xmpp_uri(
"pubsub",
path=TEST_JID.full(),
node=ap_gateway._m.namespace,
item=item_elt["id"],
)
ref_data: Dict[str, Union[str, int, dict]] = {
"uri": xmpp_uri.build_xmpp_uri(None, path=local_actor_jid.full()),
"type_": "mention",
"anchor": anchor,
}
reference_elt = ap_gateway._refs.build_ref_element(**ref_data)
# we now update ref_data to look like what is received in the trigger
ref_data["parsed_uri"] = xmpp_uri.parse_xmpp_uri(ref_data["uri"])
ref_data["parsed_anchor"] = xmpp_uri.parse_xmpp_uri(ref_data["anchor"])
# "type" is a builtin function, thus "type_" is used in build_ref_element, but in
# ref_data is "type" without underscore
ref_data["type"] = ref_data["type_"]
del ref_data["type_"]
message_elt = domish.Element((None, "message"))
message_elt.addChild(reference_elt)
with patch.object(ap_gateway.host.memory.storage, "get_items") as get_items:
# get_items returns a sqla_mapping.PubsubItem, thus we need to fake it and set
# the item_elt we want to use in its "data" attribute
mock_pubsub_item = MagicMock
mock_pubsub_item.data = item_elt
get_items.return_value = ([mock_pubsub_item], {})
with patch.object(ap_gateway, "ap_post") as ap_post:
ap_post.return_value.code = 202
await ap_gateway._on_reference_received(
ap_gateway.client, message_elt, ref_data
)
# when reference is received, the referencing item must be sent to referenced
# actor, and they must be in "to" field and in "tag"
assert ap_post.call_count == 1
send_ap_item = ap_post.call_args.args[-1]
ap_object = send_ap_item["object"]
assert TEST_AP_ACTOR_ID in ap_object["to"]
expected_mention = {
"type": ap_const.TYPE_MENTION,
"href": TEST_AP_ACTOR_ID,
# we don't have a prefixing "@" here, because it's not needed in referencing
# item with XMPP
"name": f"{TEST_AP_ACCOUNT}",
}
assert expected_mention in ap_object["tag"]
@ed
async def test_xmpp_repeat_to_ap_announce(self, ap_gateway, monkeypatch):
"""XEP-0272 post repeat is converted to AP Announce activity"""
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "get", mock_treq_get)
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "json_content", mock_treq_json)
monkeypatch.setattr(ap_gateway, "ap_get", mock_ap_get)
# JID repeated AP actor (also the recipient of the message)
recipient_jid = ap_gateway.get_local_jid_from_account(TEST_AP_ACCOUNT)
# repeated item
ap_item = TEST_AP_ITEMS[0]
ap_item_url = xmpp_uri.build_xmpp_uri(
"pubsub",
path=recipient_jid.full(),
node=ap_gateway._m.namespace,
item=ap_item["id"],
)
item_elt = xml_tools.parse(
f"""
<item id="123" publisher="{TEST_JID}/res.123">
<entry xmlns="http://www.w3.org/2005/Atom">
<title type="text">test message 1</title>
<title type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml">
<p>test message 1</p>
</div>
</title>
<author>
<name>test_user</name>
<uri>xmpp:{recipient_jid}</uri>
</author>
<updated>2022-07-21T14:38:53Z</updated>
<published>2022-07-21T14:38:53Z</published>
<id>{ap_item["id"]}</id>
<link href="{ap_item_url}" rel="via"/>
</entry>
</item>
"""
)
item_elt.uri = pubsub.NS_PUBSUB_EVENT
with patch.object(ap_gateway, "ap_post") as ap_post:
ap_post.return_value.code = 202
await ap_gateway.convert_and_post_items(
ap_gateway.client,
TEST_AP_ACCOUNT,
TEST_JID,
ap_gateway._m.namespace,
[item_elt],
)
assert ap_post.called
url, actor_id, doc = ap_post.call_args.args
assert url == TEST_USER_DATA["endpoints"]["sharedInbox"]
assert actor_id == ap_gateway.build_apurl(
ap_const.TYPE_ACTOR, TEST_JID.userhost()
)
assert doc["type"] == "Announce"
assert ap_const.NS_AP_PUBLIC in doc["to"]
assert doc["object"] == ap_item["id"]
@ed
async def test_ap_announce_to_xmpp_repeat(self, ap_gateway, monkeypatch):
"""AP Announce activity is converted to XEP-0272 post repeat"""
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "get", mock_treq_get)
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "json_content", mock_treq_json)
monkeypatch.setattr(ap_gateway, "ap_get", mock_ap_get)
xmpp_actor_id = ap_gateway.build_apurl(ap_const.TYPE_ACTOR, TEST_JID.userhost())
# announced item
xmpp_item = XMPP_ITEMS[0]
xmpp_item_url = ap_gateway.build_apurl(
ap_const.TYPE_ITEM, TEST_JID.userhost(), xmpp_item["id"]
)
announce = {
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Announce",
"actor": TEST_AP_ACTOR_ID,
"cc": [xmpp_actor_id, TEST_USER_DATA["followers"]],
"id": "https://example.org/announce/123",
"object": xmpp_item_url,
"published": "2022-07-22T09:24:12Z",
"to": [ap_const.NS_AP_PUBLIC],
}
with patch.object(ap_gateway.host.memory.storage, "get_items") as get_items:
mock_pubsub_item = MagicMock
mock_pubsub_item.data = xmpp_item
get_items.return_value = ([mock_pubsub_item], {})
with patch.object(
ap_gateway.host.memory.storage, "cache_pubsub_items"
) as cache_pubsub_items:
await ap_gateway.server.resource.handle_announce_activity(
TEST_AP_ACTOR_ID,
Request(MagicMock()),
announce,
None,
None,
None,
"",
TEST_AP_ACTOR_ID,
)
assert cache_pubsub_items.called
# the microblog data put in cache correspond to the item sent to subscribers
__, __, __, [mb_data] = cache_pubsub_items.call_args.args
extra = mb_data["extra"]
assert "repeated" in extra
repeated = extra["repeated"]
assert (
repeated["by"]
== ap_gateway.get_local_jid_from_account(TEST_AP_ACCOUNT).full()
)
xmpp_item_xmpp_url = xmpp_uri.build_xmpp_uri(
"pubsub",
path=TEST_JID.full(),
node=ap_gateway._m.namespace,
item=xmpp_item["id"],
)
assert repeated["uri"] == xmpp_item_xmpp_url
@ed
async def test_xmpp_attachment_noticed_to_ap_like(self, ap_gateway, monkeypatch):
"""Pubsub-attachments ``noticed`` is converted to AP Like activity"""
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "get", mock_treq_get)
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "json_content", mock_treq_json)
monkeypatch.setattr(ap_gateway, "ap_get", mock_ap_get)
recipient_jid = ap_gateway.get_local_jid_from_account(TEST_AP_ACCOUNT)
# noticed item
ap_item = TEST_AP_ITEMS[0]
attachment_node = ap_gateway._pa.get_attachment_node_name(
recipient_jid, ap_gateway._m.namespace, ap_item["id"]
)
item_elt = xml_tools.parse(
f"""
<item id="{TEST_JID.userhost()}" published="{TEST_JID.userhostJID()}">
<attachments xmlns="urn:xmpp:pubsub-attachments:1">
<noticed timestamp="2022-07-22T12:29:45Z"/>
</attachments>
</item>
"""
)
item_elt.uri = pubsub.NS_PUBSUB_EVENT
items_event = pubsub.ItemsEvent(
TEST_JID, recipient_jid, attachment_node, [item_elt], {}
)
with patch.object(ap_gateway, "ap_post") as ap_post:
ap_post.return_value.code = 202
await ap_gateway._items_received(ap_gateway.client, items_event)
assert ap_post.called
url, actor_id, doc = ap_post.call_args.args
assert url == TEST_USER_DATA["endpoints"]["sharedInbox"]
assert actor_id == ap_gateway.build_apurl(
ap_const.TYPE_ACTOR, TEST_JID.userhost()
)
assert doc["type"] == "Like"
assert ap_const.NS_AP_PUBLIC in doc["cc"]
assert doc["object"] == ap_item["id"]
@ed
async def test_ap_like_to_xmpp_noticed_attachment(self, ap_gateway, monkeypatch):
"""AP Like activity is converted to ``noticed`` attachment"""
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "get", mock_treq_get)
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "json_content", mock_treq_json)
monkeypatch.setattr(ap_gateway, "ap_get", mock_ap_get)
xmpp_actor_id = ap_gateway.build_apurl(ap_const.TYPE_ACTOR, TEST_JID.userhost())
# liked item
xmpp_item = XMPP_ITEMS[0]
xmpp_item_url = ap_gateway.build_apurl(
ap_const.TYPE_ITEM, TEST_JID.userhost(), xmpp_item["id"]
)
like = {
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Like",
"actor": TEST_AP_ACTOR_ID,
"cc": [xmpp_actor_id, TEST_USER_DATA["followers"]],
"id": "https://example.org/like/123",
"object": xmpp_item_url,
"published": "2022-07-22T09:24:12Z",
"to": [ap_const.NS_AP_PUBLIC],
}
with patch.object(ap_gateway.host.memory.storage, "get_items") as get_items:
get_items.return_value = ([], {})
with patch.object(ap_gateway._p, "send_items") as send_items:
await ap_gateway.server.resource.ap_inbox_request(
**self.ap_request_params(
ap_gateway, "inbox", data=like, signing_actor=TEST_AP_ACTOR_ID
)
)
assert send_items.called
si_client, si_service, si_node, [si_item] = send_items.call_args.args
assert si_client.jid == ap_gateway.get_local_jid_from_account(TEST_AP_ACCOUNT)
assert si_service == TEST_JID
assert si_node == ap_gateway._pa.get_attachment_node_name(
TEST_JID, ap_gateway._m.namespace, xmpp_item["id"]
)
[parsed_item] = ap_gateway._pa.items_2_attachment_data(si_client, [si_item])
assert parsed_item["from"] == si_client.jid.full()
assert "noticed" in parsed_item
assert parsed_item["noticed"]["noticed"] == True
@ed
async def test_xmpp_pubsub_reactions_to_ap(self, ap_gateway, monkeypatch):
"""Pubsub-attachments ``reactions`` is converted to AP EmojiReact activity"""
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "get", mock_treq_get)
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "json_content", mock_treq_json)
monkeypatch.setattr(ap_gateway, "ap_get", mock_ap_get)
recipient_jid = ap_gateway.get_local_jid_from_account(TEST_AP_ACCOUNT)
# noticed item
ap_item = TEST_AP_ITEMS[0]
ap_item_url = xmpp_uri.build_xmpp_uri(
"pubsub",
path=recipient_jid.full(),
node=ap_gateway._m.namespace,
item=ap_item["id"],
)
attachment_node = ap_gateway._pa.get_attachment_node_name(
recipient_jid, ap_gateway._m.namespace, ap_item["id"]
)
reactions = ["🦁", "🥜", "🎻"]
item_elt = xml_tools.parse(
f"""
<item id="{TEST_JID.userhost()}" published="{TEST_JID.userhostJID()}">
<attachments xmlns="urn:xmpp:pubsub-attachments:1">
<reactions timestamp="2022-08-31T12:17:23Z">
<reaction>{reactions[0]}</reaction>
<reaction>{reactions[1]}</reaction>
<reaction>{reactions[2]}</reaction>
</reactions>
</attachments>
</item>
"""
)
item_elt.uri = pubsub.NS_PUBSUB_EVENT
items_event = pubsub.ItemsEvent(
TEST_JID, recipient_jid, attachment_node, [item_elt], {}
)
with patch.object(ap_gateway, "ap_post") as ap_post:
ap_post.return_value.code = 202
await ap_gateway._items_received(ap_gateway.client, items_event)
assert ap_post.call_count == 3
for idx, call_args in enumerate(ap_post.call_args_list):
url, actor_id, doc = call_args.args
assert url == TEST_USER_DATA["endpoints"]["sharedInbox"]
assert actor_id == ap_gateway.build_apurl(
ap_const.TYPE_ACTOR, TEST_JID.userhost()
)
assert doc["type"] == "EmojiReact"
assert ap_const.NS_AP_PUBLIC in doc["cc"]
assert doc["object"] == ap_item["id"]
# reactions can be sent in random order (due to the use of set), thus we check
# if each reaction appear once, and that nothing is left after all calls are
# checked.
assert doc["content"] in reactions
reactions.remove(doc["content"])
assert len(reactions) == 0
@ed
async def test_ap_reactions_to_xmpp(self, ap_gateway, monkeypatch):
"""AP EmojiReact activity is converted to ``reactions`` attachment"""
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "get", mock_treq_get)
monkeypatch.setattr(plugin_comp_ap_gateway.treq, "json_content", mock_treq_json)
monkeypatch.setattr(ap_gateway, "ap_get", mock_ap_get)
xmpp_actor_id = ap_gateway.build_apurl(ap_const.TYPE_ACTOR, TEST_JID.userhost())
# item on which reaction is attached
xmpp_item = XMPP_ITEMS[0]
xmpp_item_url = ap_gateway.build_apurl(
ap_const.TYPE_ITEM, TEST_JID.userhost(), xmpp_item["id"]
)
like = {
"@context": "https://www.w3.org/ns/activitystreams",
"type": "EmojiReact",
"actor": TEST_AP_ACTOR_ID,
"cc": [xmpp_actor_id, TEST_USER_DATA["followers"]],
"id": "https://example.org/like/123",
"object": xmpp_item_url,
"content": "🐅",
"published": "2022-07-22T09:24:12Z",
"to": [ap_const.NS_AP_PUBLIC],
}
with patch.object(ap_gateway.host.memory.storage, "get_items") as get_items:
get_items.return_value = ([], {})
with patch.object(ap_gateway._p, "send_items") as send_items:
await ap_gateway.server.resource.ap_inbox_request(
**self.ap_request_params(
ap_gateway, "inbox", data=like, signing_actor=TEST_AP_ACTOR_ID
)
)
assert send_items.called
si_client, si_service, si_node, [si_item] = send_items.call_args.args
assert si_client.jid == ap_gateway.get_local_jid_from_account(TEST_AP_ACCOUNT)
assert si_service == TEST_JID
assert si_node == ap_gateway._pa.get_attachment_node_name(
TEST_JID, ap_gateway._m.namespace, xmpp_item["id"]
)
[parsed_item] = ap_gateway._pa.items_2_attachment_data(si_client, [si_item])
assert parsed_item["from"] == si_client.jid.full()
assert "reactions" in parsed_item
assert parsed_item["reactions"]["reactions"] == ["🐅"]
@ed
async def test_xmpp_event_2_ap_event(self, ap_gateway):
"""XMPP events are converted to AP events"""
# we use internal event data
event_data = {
"id": "event_123",
"name": {"": "test event"},
"start": 1664222400.0,
"end": 1664226000.0,
"head-picture": {
"sources": [{"url": "https://example.org/head_picture.jpg"}]
},
"descriptions": [{"description": "meeting for test", "type": "text"}],
"categories": [{"term": "test", "wikidata_id": "Q1003030"}],
"locations": [{"description": "somewhere in the world"}],
"rsvp": [
{
"fields": [
{
"type": "list-single",
"name": "attending",
"label": "Attending",
"options": [
{"value": "maybe", "label": "maybe"},
{"value": "yes", "label": "yes"},
{"value": "no", "label": "no"},
],
}
],
"namespace": "urn:xmpp:events:rsvp:0",
}
],
"extra": {
"status": "confirmed",
},
}
ap_item = await ap_gateway.ap_events.event_data_2_ap_item(event_data, TEST_JID)
ap_object = ap_item["object"]
actor_id = (
"https://test.example/_ap/actor/"
"___urn.3Axmpp.3Aevents.3A0---some_user@test.example"
)
event_id = (
"https://test.example/_ap/item/"
"___urn.3Axmpp.3Aevents.3A0---some_user@test.example/event_123"
)
assert ap_object["name"] == "test event"
assert ap_object["actor"] == actor_id
assert ap_object["attributedTo"] == actor_id
assert ap_object["type"] == "Event"
assert ap_object["startTime"] == "2022-09-26T20:00:00Z"
assert ap_object["endTime"] == "2022-09-26T21:00:00Z"
assert ap_object["id"] == event_id
assert ap_object["url"] == event_id
assert ap_object["attachment"] == [
{
"name": "Banner",
"type": "Document",
"mediaType": "image/jpeg",
"url": "https://example.org/head_picture.jpg",
}
]
assert ap_object["content"] == "<p>meeting for test</p>"
assert ap_object["tag"] == [{"name": "#test", "type": "Hashtag"}]
assert ap_object["location"] == {"name": "somewhere in the world"}
assert ap_object["ical:status"] == "CONFIRMED"
assert ap_item["type"] == "Create"
@ed
async def test_ap_event_2_xmpp_event(self, ap_gateway):
"""AP events are converted to XMPP events"""
test_actor = "___urn.3Axmpp.3Aevents.3A0---some_user@test.example"
ap_object = {
"actor": f"https://test.example/_ap/actor/{test_actor}",
"attachment": [
{
"mediaType": "image/jpeg",
"name": "Banner",
"type": "Document",
"url": "https://example.org/head_picture.jpg",
}
],
"attributedTo": f"https://test.example/_ap/actor/{test_actor}",
"content": "<p>meeting for test</p>",
"endTime": "2022-09-26T21:00:00Z",
"ical:status": "CONFIRMED",
"id": f"https://test.example/_ap/item/{test_actor}/event_123",
"location": {"name": "somewhere in the world"},
"name": "test event",
"startTime": "2022-09-26T20:00:00Z",
"tag": [{"name": "#test", "type": "Hashtag"}],
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"type": "Event",
"url": f"https://test.example/_ap/item/{test_actor}/event_123",
}
event_data = await ap_gateway.ap_events.ap_item_2_event_data(
TEST_AP_ACTOR_ID, ap_object
)
assert event_data["id"] == ap_object["id"]
assert event_data["name"] == {"": "test event"}
assert event_data["start"] == 1664222400
assert event_data["end"] == 1664226000
assert event_data["head-picture"] == {
"sources": [{"url": "https://example.org/head_picture.jpg"}]
}
assert event_data["descriptions"] == [
{"description": "<p>meeting for test</p>", "type": "xhtml"}
]
assert event_data["categories"] == [{"term": "test"}]
assert event_data["locations"] == [{"description": "somewhere in the world"}]
assert event_data["rsvp"] == [
{
"fields": [
{
"label": "Attending",
"name": "attending",
"options": [
{"label": "yes", "value": "yes"},
{"label": "no", "value": "no"},
],
"required": True,
"type": "list-single",
}
]
}
]
assert event_data["extra"] == {"status": "confirmed"}