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
from collections.abc import Iterable
from typing import Optional
from urllib.parse import urlparse
import httpx
import urlman
@ -660,6 +661,9 @@ class Post(StatorModel):
Raises DoesNotExist if it's not found and create is False,
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?
created = False
try:
@ -828,9 +832,14 @@ class Post(StatorModel):
Handles an incoming delete request
"""
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
try:
post = cls.by_object_uri(data["object"]["id"])
post = cls.by_object_uri(object_uri)
except cls.DoesNotExist:
# It's already been deleted
return

View file

@ -179,13 +179,11 @@ class StatorRunner:
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)
for _ in range(100):
if not self.tasks:
break
self.remove_completed_tasks()
await asyncio.sleep(0.05)
for task in self.tasks:
await task
run_single_cycle_sync = async_to_sync(run_single_cycle)

View file

@ -2,6 +2,7 @@ import pytest
from pytest_httpx import HTTPXMock
from activities.models import Post, PostStates
from users.models import Identity, InboxMessage
@pytest.mark.django_db
@ -237,3 +238,97 @@ def test_content_map(remote_identity):
create=True,
)
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}"
)
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 await Identity.objects.filter(
actor_uri=instance.message["object"]
).aexists():
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:
match instance.message_object_type:
case "tombstone":