Unfollowing or blocking users purges timeline

Fixes #366
This commit is contained in:
Andrew Godwin 2023-01-16 11:53:40 -07:00
parent c3caf26f22
commit 54e7755080
5 changed files with 131 additions and 12 deletions

View file

@ -928,14 +928,11 @@ class Post(StatorModel):
try: try:
cls.by_object_uri(object_uri) cls.by_object_uri(object_uri)
except cls.DoesNotExist: except cls.DoesNotExist:
InboxMessage.objects.create( InboxMessage.create_internal(
message={ {
"type": "__internal__",
"object": {
"type": "FetchPost", "type": "FetchPost",
"object": object_uri, "object": object_uri,
"reason": reason, "reason": reason,
},
} }
) )
@ -995,7 +992,7 @@ class Post(StatorModel):
Handles an internal fetch-request inbox message Handles an internal fetch-request inbox message
""" """
try: try:
uri = data["object"]["object"] uri = data["object"]
if "://" in uri: if "://" in uri:
cls.by_object_uri(uri, fetch=True) cls.by_object_uri(uri, fetch=True)
except (cls.DoesNotExist, KeyError): except (cls.DoesNotExist, KeyError):

View file

@ -166,6 +166,30 @@ class TimelineEvent(models.Model):
subject_identity_id=interaction.identity_id, subject_identity_id=interaction.identity_id,
).delete() ).delete()
### Background tasks ###
@classmethod
def handle_clear_timeline(cls, message):
"""
Internal stator handler for clearing all events by a user off another
user's timeline.
"""
actor_id = message["actor"]
object_id = message["object"]
full_erase = message.get("fullErase", False)
if full_erase:
q = (
models.Q(subject_post__author_id=object_id)
| models.Q(subject_post_interaction__identity_id=object_id)
| models.Q(subject_identity_id=object_id)
)
else:
q = models.Q(
type=cls.Types.post, subject_post__author_id=object_id
) | models.Q(type=cls.Types.boost, subject_identity_id=object_id)
TimelineEvent.objects.filter(q, identity_id=actor_id).delete()
### Mastodon Client API ### ### Mastodon Client API ###
def to_mastodon_notification_json(self, interactions=None): def to_mastodon_notification_json(self, interactions=None):

View file

@ -5,6 +5,7 @@ from activities.models import Post, TimelineEvent
from activities.services import PostService from activities.services import PostService
from core.ld import format_ld_date from core.ld import format_ld_date
from users.models import Block, Follow, Identity, InboxMessage from users.models import Block, Follow, Identity, InboxMessage
from users.services import IdentityService
@pytest.mark.django_db @pytest.mark.django_db
@ -192,3 +193,67 @@ def test_old_new_post(
).first() ).first()
assert event assert event
assert "Hello " in event.subject_post.content assert "Hello " in event.subject_post.content
@pytest.mark.django_db
@pytest.mark.parametrize("full", [True, False])
def test_clear_timeline(
identity: Identity,
remote_identity: Identity,
stator,
full: bool,
):
"""
Ensures that timeline clearing works as expected.
"""
# Follow the remote user
service = IdentityService(remote_identity)
service.follow_from(identity)
# Create an inbound new post message mentioning us
message = {
"id": "test",
"type": "Create",
"actor": remote_identity.actor_uri,
"object": {
"id": "https://remote.test/test-post",
"type": "Note",
"published": format_ld_date(timezone.now()),
"attributedTo": remote_identity.actor_uri,
"content": f"Hello @{identity.handle}!",
"tag": {
"type": "Mention",
"href": identity.actor_uri,
"name": f"@{identity.handle}",
},
},
}
InboxMessage.objects.create(message=message)
# Run stator twice - to make fanouts and then process them
stator.run_single_cycle_sync()
stator.run_single_cycle_sync()
# Make sure it appeared on our timeline as a post and a mentioned
assert TimelineEvent.objects.filter(
type=TimelineEvent.Types.post, identity=identity
).exists()
assert TimelineEvent.objects.filter(
type=TimelineEvent.Types.mentioned, identity=identity
).exists()
# Now, submit either a user block (for full clear) or unfollow (for post clear)
if full:
service.block_from(identity)
else:
service.unfollow_from(identity)
# Run stator once to process the timeline clear message
stator.run_single_cycle_sync()
# Verify that the right things vanished
assert not TimelineEvent.objects.filter(
type=TimelineEvent.Types.post, identity=identity
).exists()
assert TimelineEvent.objects.filter(
type=TimelineEvent.Types.mentioned, identity=identity
).exists() == (not full)

View file

@ -14,7 +14,7 @@ class InboxMessageStates(StateGraph):
@classmethod @classmethod
async def handle_received(cls, instance: "InboxMessage"): async def handle_received(cls, instance: "InboxMessage"):
from activities.models import Post, PostInteraction from activities.models import Post, PostInteraction, TimelineEvent
from users.models import Block, Follow, Identity, Report from users.models import Block, Follow, Identity, Report
match instance.message_type: match instance.message_type:
@ -148,7 +148,11 @@ class InboxMessageStates(StateGraph):
match instance.message_object_type: match instance.message_object_type:
case "fetchpost": case "fetchpost":
await sync_to_async(Post.handle_fetch_internal)( await sync_to_async(Post.handle_fetch_internal)(
instance.message instance.message["object"]
)
case "cleartimeline":
await sync_to_async(TimelineEvent.handle_clear_timeline)(
instance.message["object"]
) )
case unknown: case unknown:
raise ValueError( raise ValueError(
@ -171,6 +175,18 @@ class InboxMessage(StatorModel):
state = StateField(InboxMessageStates) state = StateField(InboxMessageStates)
@classmethod
def create_internal(cls, payload):
"""
Creates an internal action message
"""
cls.objects.create(
message={
"type": "__internal__",
"object": payload,
}
)
@property @property
def message_type(self): def message_type(self):
return self.message["type"].lower() return self.message["type"].lower()

View file

@ -11,6 +11,7 @@ from users.models import (
Follow, Follow,
FollowStates, FollowStates,
Identity, Identity,
InboxMessage,
User, User,
) )
@ -85,13 +86,29 @@ class IdentityService:
existing_follow = Follow.maybe_get(from_identity, self.identity) existing_follow = Follow.maybe_get(from_identity, self.identity)
if existing_follow: if existing_follow:
existing_follow.transition_perform(FollowStates.undone) existing_follow.transition_perform(FollowStates.undone)
InboxMessage.create_internal(
{
"type": "ClearTimeline",
"actor": from_identity.pk,
"object": self.identity.pk,
}
)
def block_from(self, from_identity: Identity) -> Block: def block_from(self, from_identity: Identity) -> Block:
""" """
Blocks a user. Blocks a user.
""" """
self.unfollow_from(from_identity) self.unfollow_from(from_identity)
return Block.create_local_block(from_identity, self.identity) block = Block.create_local_block(from_identity, self.identity)
InboxMessage.create_internal(
{
"type": "ClearTimeline",
"actor": from_identity.pk,
"object": self.identity.pk,
"fullErase": True,
}
)
return block
def unblock_from(self, from_identity: Identity): def unblock_from(self, from_identity: Identity):
""" """