parent
716b74404f
commit
b03d9f0e12
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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":
|
||||||
|
|
Loading…
Reference in New Issue