Handle deletions of post URIs

Fixes #306
This commit is contained in:
Andrew Godwin 2022-12-28 22:47:28 -07:00
parent 716b74404f
commit b03d9f0e12
4 changed files with 122 additions and 9 deletions

View file

@ -1,6 +1,7 @@
import re import re
from collections.abc import Iterable from collections.abc import Iterable
from typing import Optional from typing import Optional
from urllib.parse import urlparse
import httpx import httpx
import urlman import urlman
@ -660,6 +661,9 @@ class Post(StatorModel):
Raises DoesNotExist if it's not found and create is False, Raises DoesNotExist if it's not found and create is False,
or it's from a blocked domain. or it's from a blocked domain.
""" """
# Ensure the domain of the object's actor and ID match to prevent injection
if urlparse(data["id"]).hostname != urlparse(data["attributedTo"]).hostname:
raise ValueError("Object's ID domain is different to its author")
# Do we have one with the right ID? # Do we have one with the right ID?
created = False created = False
try: try:
@ -828,9 +832,14 @@ class Post(StatorModel):
Handles an incoming delete request Handles an incoming delete request
""" """
with transaction.atomic(): with transaction.atomic():
# Is this an embedded object or plain ID?
if isinstance(data["object"], str):
object_uri = data["object"]
else:
object_uri = data["object"]["id"]
# Find our post by ID if we have one # Find our post by ID if we have one
try: try:
post = cls.by_object_uri(data["object"]["id"]) post = cls.by_object_uri(object_uri)
except cls.DoesNotExist: except cls.DoesNotExist:
# It's already been deleted # It's already been deleted
return return

View file

@ -179,13 +179,11 @@ class StatorRunner:
async def run_single_cycle(self): async def run_single_cycle(self):
""" """
Testing entrypoint to advance things just one cycle Testing entrypoint to advance things just one cycle, and allow errors
to propagate out.
""" """
await asyncio.wait_for(self.fetch_and_process_tasks(), timeout=1) await asyncio.wait_for(self.fetch_and_process_tasks(), timeout=1)
for _ in range(100): for task in self.tasks:
if not self.tasks: await task
break
self.remove_completed_tasks()
await asyncio.sleep(0.05)
run_single_cycle_sync = async_to_sync(run_single_cycle) run_single_cycle_sync = async_to_sync(run_single_cycle)

View file

@ -2,6 +2,7 @@ import pytest
from pytest_httpx import HTTPXMock from pytest_httpx import HTTPXMock
from activities.models import Post, PostStates from activities.models import Post, PostStates
from users.models import Identity, InboxMessage
@pytest.mark.django_db @pytest.mark.django_db
@ -237,3 +238,97 @@ def test_content_map(remote_identity):
create=True, create=True,
) )
assert post3.content == "Hello World" assert post3.content == "Hello World"
@pytest.mark.django_db
@pytest.mark.parametrize("delete_type", ["note", "tombstone", "ref"])
def test_inbound_posts(
remote_identity: Identity,
stator,
delete_type: bool,
):
"""
Ensures that a remote post can arrive via inbox message, be edited, and be
deleted.
"""
# Create an inbound new post message
message = {
"id": "test",
"type": "Create",
"actor": remote_identity.actor_uri,
"object": {
"id": "https://remote.test/test-post",
"type": "Note",
"published": "2022-11-13T23:20:16Z",
"attributedTo": remote_identity.actor_uri,
"content": "post version one",
},
}
InboxMessage.objects.create(message=message)
# Run stator and ensure that made the post
stator.run_single_cycle_sync()
post = Post.objects.get(object_uri="https://remote.test/test-post")
assert post.content == "post version one"
assert post.published.day == 13
assert post.url == "https://remote.test/test-post"
# Create an inbound post edited message
message = {
"id": "test",
"type": "Update",
"actor": remote_identity.actor_uri,
"object": {
"id": "https://remote.test/test-post",
"type": "Note",
"published": "2022-11-13T23:20:16Z",
"updated": "2022-11-14T23:20:16Z",
"url": "https://remote.test/test-post/display",
"attributedTo": remote_identity.actor_uri,
"content": "post version two",
},
}
InboxMessage.objects.create(message=message)
# Run stator and ensure that edited the post
stator.run_single_cycle_sync()
post = Post.objects.get(object_uri="https://remote.test/test-post")
assert post.content == "post version two"
assert post.edited.day == 14
assert post.url == "https://remote.test/test-post/display"
# Create an inbound post deleted message
if delete_type == "ref":
message = {
"id": "test",
"type": "Delete",
"actor": remote_identity.actor_uri,
"object": "https://remote.test/test-post",
}
elif delete_type == "tombstone":
message = {
"id": "test",
"type": "Delete",
"actor": remote_identity.actor_uri,
"object": {
"id": "https://remote.test/test-post",
"type": "Tombstone",
},
}
else:
message = {
"id": "test",
"type": "Delete",
"actor": remote_identity.actor_uri,
"object": {
"id": "https://remote.test/test-post",
"type": "Note",
"published": "2022-11-13T23:20:16Z",
"attributedTo": remote_identity.actor_uri,
},
}
InboxMessage.objects.create(message=message)
# Run stator and ensure that deleted the post
stator.run_single_cycle_sync()
assert not Post.objects.filter(object_uri="https://remote.test/test-post").exists()

View file

@ -98,9 +98,20 @@ class InboxMessageStates(StateGraph):
f"Cannot handle activity of type undo.{unknown}" f"Cannot handle activity of type undo.{unknown}"
) )
case "delete": case "delete":
# If there is no object type, it's probably a profile # If there is no object type, we need to see if it's a profile or a post
if not isinstance(instance.message["object"], dict): if not isinstance(instance.message["object"], dict):
if await Identity.objects.filter(
actor_uri=instance.message["object"]
).aexists():
await sync_to_async(Identity.handle_delete_ap)(instance.message) await sync_to_async(Identity.handle_delete_ap)(instance.message)
elif await Post.objects.filter(
object_uri=instance.message["object"]
).aexists():
await sync_to_async(Post.handle_delete_ap)(instance.message)
else:
raise ValueError(
f"Cannot handle activity of type delete on URI {instance.message['object']}"
)
else: else:
match instance.message_object_type: match instance.message_object_type:
case "tombstone": case "tombstone":