From e3a02a8138b5a65287ab86ed6418abd575bbff54 Mon Sep 17 00:00:00 2001 From: Thomas Sileo Date: Wed, 17 Aug 2022 21:18:07 +0200 Subject: [PATCH] Start support for deleting remote actors --- ..._1758-5d3e3f2b9b4e_add_actor_is_deleted.py | 32 ++++ app/actor.py | 5 +- app/boxes.py | 138 +++++++++++++++--- app/models.py | 1 + 4 files changed, 158 insertions(+), 18 deletions(-) create mode 100644 alembic/versions/2022_08_17_1758-5d3e3f2b9b4e_add_actor_is_deleted.py diff --git a/alembic/versions/2022_08_17_1758-5d3e3f2b9b4e_add_actor_is_deleted.py b/alembic/versions/2022_08_17_1758-5d3e3f2b9b4e_add_actor_is_deleted.py new file mode 100644 index 0000000..e8e9e29 --- /dev/null +++ b/alembic/versions/2022_08_17_1758-5d3e3f2b9b4e_add_actor_is_deleted.py @@ -0,0 +1,32 @@ +"""Add Actor.is_deleted + +Revision ID: 5d3e3f2b9b4e +Revises: 6286262bb466 +Create Date: 2022-08-17 17:58:24.813194+00:00 + +""" +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = '5d3e3f2b9b4e' +down_revision = '6286262bb466' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('actor', schema=None) as batch_op: + batch_op.add_column(sa.Column('is_deleted', sa.Boolean(), server_default='0', nullable=False)) + + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('actor', schema=None) as batch_op: + batch_op.drop_column('is_deleted') + + # ### end Alembic commands ### diff --git a/app/actor.py b/app/actor.py index e7d10f1..4906edc 100644 --- a/app/actor.py +++ b/app/actor.py @@ -176,7 +176,10 @@ async def fetch_actor( existing_actor = ( await db_session.scalars( - select(models.Actor).where(models.Actor.ap_id == actor_id) + select(models.Actor).where( + models.Actor.ap_id == actor_id, + models.Actor.is_deleted.is_(False), + ) ) ).one_or_none() if existing_actor: diff --git a/app/boxes.py b/app/boxes.py index 1f57f33..7bd69a6 100644 --- a/app/boxes.py +++ b/app/boxes.py @@ -839,7 +839,7 @@ async def _handle_delete_activity( db_session: AsyncSession, from_actor: models.Actor, delete_activity: models.InboxObject, - ap_object_to_delete: models.InboxObject | None, + ap_object_to_delete: models.InboxObject | models.Actor | None, forwarded_by_actor: models.Actor | None, ) -> None: if ap_object_to_delete is None: @@ -847,24 +847,77 @@ async def _handle_delete_activity( "Received Delete for an unknown object " f"{delete_activity.activity_object_ap_id}" ) - # TODO(tsileo): support deleting actor return - if from_actor.ap_id != ap_object_to_delete.actor.ap_id: - logger.warning( - "Actor mismatch between the activity and the object: " - f"{from_actor.ap_id}/{ap_object_to_delete.actor.ap_id}" + if isinstance(ap_object_to_delete, models.InboxObject): + if from_actor.ap_id != ap_object_to_delete.actor.ap_id: + logger.warning( + "Actor mismatch between the activity and the object: " + f"{from_actor.ap_id}/{ap_object_to_delete.actor.ap_id}" + ) + return + + logger.info( + f"Deleting {ap_object_to_delete.ap_type}/{ap_object_to_delete.ap_id}" ) - return + await _revert_side_effect_for_deleted_object( + db_session, + delete_activity, + ap_object_to_delete, + forwarded_by_actor, + ) + ap_object_to_delete.is_deleted = True + elif isinstance(ap_object_to_delete, models.Actor): + if from_actor.ap_id != ap_object_to_delete.ap_id: + logger.warning( + "Actor mismatch between the activity and the object: " + f"{from_actor.ap_id}/{ap_object_to_delete.ap_id}" + ) + return - logger.info(f"Deleting {ap_object_to_delete.ap_type}/{ap_object_to_delete.ap_id}") - await _revert_side_effect_for_deleted_object( - db_session, - delete_activity, - ap_object_to_delete, - forwarded_by_actor, - ) - ap_object_to_delete.is_deleted = True + logger.info(f"Deleting actor {ap_object_to_delete.ap_id}") + follower = ( + await db_session.scalars( + select(models.Follower).where( + models.Follower.ap_actor_id == ap_object_to_delete.ap_id, + ) + ) + ).one_or_none() + if follower: + logger.info("Removing actor from follower") + await db_session.delete(follower) + + following = ( + await db_session.scalars( + select(models.Following).where( + models.Following.ap_actor_id == ap_object_to_delete.ap_id, + ) + ) + ).one_or_none() + if following: + logger.info("Removing actor from following") + await db_session.delete(following) + + # Mark the actor as deleted + ap_object_to_delete.is_deleted = True + + inbox_objects = ( + await db_session.scalars( + select(models.InboxObject).where( + models.InboxObject.actor_id == ap_object_to_delete.id, + models.InboxObject.is_deleted.is_(False), + ) + ) + ).all() + logger.info(f"Deleting {len(inbox_objects)} objects") + for inbox_object in inbox_objects: + await _revert_side_effect_for_deleted_object( + db_session, + delete_activity, + inbox_object, + forwarded_by_actor=None, + ) + inbox_object.is_deleted = True await db_session.flush() @@ -905,6 +958,38 @@ async def _revert_side_effect_for_deleted_object( .values(replies_count=models.InboxObject.replies_count - 1) ) + if deleted_ap_object.ap_type == "Like" and deleted_ap_object.activity_object_ap_id: + related_object = await get_outbox_object_by_ap_id( + db_session, + deleted_ap_object.activity_object_ap_id, + ) + if related_object: + if related_object.is_from_outbox: + await db_session.execute( + update(models.OutboxObject) + .where( + models.OutboxObject.id == related_object.id, + ) + .values(likes_count=models.OutboxObject.likes_count - 1) + ) + elif ( + deleted_ap_object.ap_type == "Annouce" + and deleted_ap_object.activity_object_ap_id + ): + related_object = await get_outbox_object_by_ap_id( + db_session, + deleted_ap_object.activity_object_ap_id, + ) + if related_object: + if related_object.is_from_outbox: + await db_session.execute( + update(models.OutboxObject) + .where( + models.OutboxObject.id == related_object.id, + ) + .values(announces_count=models.OutboxObject.announces_count - 1) + ) + # Delete any Like/Announce await db_session.execute( update(models.OutboxObject) @@ -916,7 +1001,11 @@ async def _revert_side_effect_for_deleted_object( # If it's a local replies, it was forwarded, so we also need to forward # the Delete activity if possible - if delete_activity.has_ld_signature and is_delete_needs_to_be_forwarded: + if ( + delete_activity.activity_object_ap_id == deleted_ap_object.ap_id + and delete_activity.has_ld_signature + and is_delete_needs_to_be_forwarded + ): logger.info("Forwarding Delete activity as it's a local reply") # Don't forward to the forwarding actor and the original Delete actor @@ -1638,11 +1727,26 @@ async def save_to_inbox( elif activity_ro.ap_type == "Move": await _handle_move_activity(db_session, actor, inbox_object) elif activity_ro.ap_type == "Delete": + object_to_delete: models.InboxObject | models.Actor | None + if relates_to_inbox_object: + object_to_delete = relates_to_inbox_object + elif inbox_object.activity_object_ap_id: + # If it's not a Delete for an inbox object, it may be related to + # an actor + try: + object_to_delete = await fetch_actor( + db_session, + inbox_object.activity_object_ap_id, + save_if_not_found=False, + ) + except ap.ObjectNotFoundError: + pass + await _handle_delete_activity( db_session, actor, inbox_object, - relates_to_inbox_object, + object_to_delete, forwarded_by_actor=forwarded_by_actor, ) elif activity_ro.ap_type == "Follow": diff --git a/app/models.py b/app/models.py index c013e05..0939266 100644 --- a/app/models.py +++ b/app/models.py @@ -52,6 +52,7 @@ class Actor(Base, BaseActor): handle = Column(String, nullable=True, index=True) is_blocked = Column(Boolean, nullable=False, default=False, server_default="0") + is_deleted = Column(Boolean, nullable=False, default=False, server_default="0") @property def is_from_db(self) -> bool: