From cc086f32646cd1ca7b7c747d8e5e297e180caa42 Mon Sep 17 00:00:00 2001 From: Thomas Sileo Date: Sun, 31 Jul 2022 10:35:11 +0200 Subject: [PATCH] Add support for blocking actors --- ...0a65_add_is_blocked_attribute_on_actors.py | 32 ++++++++++++ app/admin.py | 32 +++++++++++- app/boxes.py | 4 ++ app/models.py | 2 + app/templates/utils.html | 32 ++++++++++-- tests/test_inbox.py | 50 +++++++++++++++++++ 6 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 alembic/versions/2022_07_31_0815-50d26a370a65_add_is_blocked_attribute_on_actors.py diff --git a/alembic/versions/2022_07_31_0815-50d26a370a65_add_is_blocked_attribute_on_actors.py b/alembic/versions/2022_07_31_0815-50d26a370a65_add_is_blocked_attribute_on_actors.py new file mode 100644 index 0000000..81ce1f6 --- /dev/null +++ b/alembic/versions/2022_07_31_0815-50d26a370a65_add_is_blocked_attribute_on_actors.py @@ -0,0 +1,32 @@ +"""Add is_blocked attribute on actors + +Revision ID: 50d26a370a65 +Revises: f5717d82b3ff +Create Date: 2022-07-31 08:15:27.226340+00:00 + +""" +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = '50d26a370a65' +down_revision = 'f5717d82b3ff' +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_blocked', 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_blocked') + + # ### end Alembic commands ### diff --git a/app/admin.py b/app/admin.py index 97f9036..a1e78d1 100644 --- a/app/admin.py +++ b/app/admin.py @@ -563,11 +563,41 @@ async def admin_actions_follow( csrf_check: None = Depends(verify_csrf_token), db_session: AsyncSession = Depends(get_db_session), ) -> RedirectResponse: - print(f"Following {ap_actor_id}") + logger.info(f"Following {ap_actor_id}") await send_follow(db_session, ap_actor_id) return RedirectResponse(redirect_url, status_code=302) +@router.post("/actions/block") +async def admin_actions_block( + request: Request, + ap_actor_id: str = Form(), + redirect_url: str = Form(), + csrf_check: None = Depends(verify_csrf_token), + db_session: AsyncSession = Depends(get_db_session), +) -> RedirectResponse: + logger.info(f"Blocking {ap_actor_id}") + actor = await fetch_actor(db_session, ap_actor_id) + actor.is_blocked = True + await db_session.commit() + return RedirectResponse(redirect_url, status_code=302) + + +@router.post("/actions/unblock") +async def admin_actions_unblock( + request: Request, + ap_actor_id: str = Form(), + redirect_url: str = Form(), + csrf_check: None = Depends(verify_csrf_token), + db_session: AsyncSession = Depends(get_db_session), +) -> RedirectResponse: + logger.info(f"Unblocking {ap_actor_id}") + actor = await fetch_actor(db_session, ap_actor_id) + actor.is_blocked = False + await db_session.commit() + return RedirectResponse(redirect_url, status_code=302) + + @router.post("/actions/delete") async def admin_actions_delete( request: Request, diff --git a/app/boxes.py b/app/boxes.py index c59a5fb..5a140ca 100644 --- a/app/boxes.py +++ b/app/boxes.py @@ -1271,6 +1271,10 @@ async def save_to_inbox( await _process_transient_object(db_session, raw_object, actor) return None + if actor.is_blocked: + logger.warning("Actor {actor.ap_id} is blocked, ignoring object") + return None + raw_object_id = ap.get_id(raw_object) forwarded_by_actor = None diff --git a/app/models.py b/app/models.py index 152d5b8..5d23245 100644 --- a/app/models.py +++ b/app/models.py @@ -51,6 +51,8 @@ class Actor(Base, BaseActor): handle = Column(String, nullable=True, index=True) + is_blocked = Column(Boolean, nullable=False, default=False, server_default="0") + @property def is_from_db(self) -> bool: return True diff --git a/app/templates/utils.html b/app/templates/utils.html index ba93ed2..5ba2ba5 100644 --- a/app/templates/utils.html +++ b/app/templates/utils.html @@ -6,6 +6,24 @@ {% endmacro %} +{% macro admin_block_button(actor) %} +
+ {{ embed_csrf_token() }} + {{ embed_redirect_url() }} + + +
+{% endmacro %} + +{% macro admin_unblock_button(actor) %} +
+ {{ embed_csrf_token() }} + {{ embed_redirect_url() }} + + +
+{% endmacro %} + {% macro admin_follow_button(actor) %}
{{ embed_csrf_token() }} @@ -217,6 +235,14 @@ {% endif %} {% endif %} + {% if actor.is_from_db %} + {% if actor.is_blocked %} +
  • blocked
  • +
  • {{ admin_unblock_button(actor) }}
  • + {% else %} +
  • {{ admin_block_button(actor) }}
  • + {% endif %} + {% endif %} @@ -255,11 +281,11 @@
    {% if og_meta.image %}
    - +
    - {{ og_meta.title }} - {{ og_meta.site_name }} + {{ og_meta.title }} + {{ og_meta.site_name }}
    {% endif %}
    diff --git a/tests/test_inbox.py b/tests/test_inbox.py index a4fd783..415db41 100644 --- a/tests/test_inbox.py +++ b/tests/test_inbox.py @@ -3,6 +3,7 @@ from uuid import uuid4 import httpx import respx from fastapi.testclient import TestClient +from sqlalchemy import func from sqlalchemy import select from sqlalchemy.orm import Session @@ -248,3 +249,52 @@ def test_inbox__create_already_deleted_object( ).scalar_one_or_none() is None ) + + +def test_inbox__actor_is_blocked( + db: Session, + client: TestClient, + respx_mock: respx.MockRouter, +) -> None: + # Given a remote actor + ra = setup_remote_actor(respx_mock) + + # Who is also a follower + follower = setup_remote_actor_as_follower(ra) + follower.actor.is_blocked = True + db.commit() + + create_activity = factories.build_create_activity( + factories.build_note_object( + from_remote_actor=ra, + outbox_public_id=str(uuid4()), + content="Hello", + to=[LOCAL_ACTOR.ap_id], + ) + ) + + # When receiving a Create activity + ro = RemoteObject(create_activity, ra) + + with mock_httpsig_checker(ra): + response = client.post( + "/inbox", + headers={"Content-Type": ap.AS_CTX}, + json=ro.ap_object, + ) + + # Then the server returns a 204 + assert response.status_code == 202 + + # And when processing the incoming activity from a blocked actor + run_async(process_next_incoming_activity) + + # Then the Create activity was discarded + assert ( + db.scalar( + select(func.count(models.InboxObject.id)).where( + models.InboxObject.ap_type != "Follow" + ) + ) + == 0 + )