Add support for blocking actors

main
Thomas Sileo 2022-07-31 10:35:11 +02:00
parent 7782a39638
commit cc086f3264
6 changed files with 148 additions and 4 deletions

View File

@ -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 ###

View File

@ -563,11 +563,41 @@ async def admin_actions_follow(
csrf_check: None = Depends(verify_csrf_token), csrf_check: None = Depends(verify_csrf_token),
db_session: AsyncSession = Depends(get_db_session), db_session: AsyncSession = Depends(get_db_session),
) -> RedirectResponse: ) -> RedirectResponse:
print(f"Following {ap_actor_id}") logger.info(f"Following {ap_actor_id}")
await send_follow(db_session, ap_actor_id) await send_follow(db_session, ap_actor_id)
return RedirectResponse(redirect_url, status_code=302) 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") @router.post("/actions/delete")
async def admin_actions_delete( async def admin_actions_delete(
request: Request, request: Request,

View File

@ -1271,6 +1271,10 @@ async def save_to_inbox(
await _process_transient_object(db_session, raw_object, actor) await _process_transient_object(db_session, raw_object, actor)
return None 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) raw_object_id = ap.get_id(raw_object)
forwarded_by_actor = None forwarded_by_actor = None

View File

@ -51,6 +51,8 @@ class Actor(Base, BaseActor):
handle = Column(String, nullable=True, index=True) handle = Column(String, nullable=True, index=True)
is_blocked = Column(Boolean, nullable=False, default=False, server_default="0")
@property @property
def is_from_db(self) -> bool: def is_from_db(self) -> bool:
return True return True

View File

@ -6,6 +6,24 @@
<input type="hidden" name="redirect_url" value="{{ request.url }}{% if permalink_id %}#{{ permalink_id }}{% endif %}"> <input type="hidden" name="redirect_url" value="{{ request.url }}{% if permalink_id %}#{{ permalink_id }}{% endif %}">
{% endmacro %} {% endmacro %}
{% macro admin_block_button(actor) %}
<form action="{{ request.url_for("admin_actions_block") }}" method="POST">
{{ embed_csrf_token() }}
{{ embed_redirect_url() }}
<input type="hidden" name="ap_actor_id" value="{{ actor.ap_id }}">
<input type="submit" value="block">
</form>
{% endmacro %}
{% macro admin_unblock_button(actor) %}
<form action="{{ request.url_for("admin_actions_unblock") }}" method="POST">
{{ embed_csrf_token() }}
{{ embed_redirect_url() }}
<input type="hidden" name="ap_actor_id" value="{{ actor.ap_id }}">
<input type="submit" value="unblock">
</form>
{% endmacro %}
{% macro admin_follow_button(actor) %} {% macro admin_follow_button(actor) %}
<form action="{{ request.url_for("admin_actions_follow") }}" method="POST"> <form action="{{ request.url_for("admin_actions_follow") }}" method="POST">
{{ embed_csrf_token() }} {{ embed_csrf_token() }}
@ -217,6 +235,14 @@
{% endif %} {% endif %}
</li> </li>
{% endif %} {% endif %}
{% if actor.is_from_db %}
{% if actor.is_blocked %}
<li>blocked</li>
<li>{{ admin_unblock_button(actor) }}</li>
{% else %}
<li>{{ admin_block_button(actor) }}</li>
{% endif %}
{% endif %}
</ul> </ul>
</nav> </nav>
</div> </div>
@ -255,11 +281,11 @@
<div class="activity-og-meta" style="display:flex;column-gap: 20px;margin:20px 0;"> <div class="activity-og-meta" style="display:flex;column-gap: 20px;margin:20px 0;">
{% if og_meta.image %} {% if og_meta.image %}
<div> <div>
<img src="{{ og_meta.image | media_proxy_url }}" style="max-width:200px;max-height:100px;"> <img src="{{ og_meta.image | media_proxy_url }}" style="max-width:200px;max-height:100px;">
</div> </div>
<div> <div>
<a href="{{ og_meta.url }}">{{ og_meta.title }}</a> <a href="{{ og_meta.url }}">{{ og_meta.title }}</a>
<small style="display:block;">{{ og_meta.site_name }}</small> <small style="display:block;">{{ og_meta.site_name }}</small>
</div> </div>
{% endif %} {% endif %}
</div> </div>

View File

@ -3,6 +3,7 @@ from uuid import uuid4
import httpx import httpx
import respx import respx
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from sqlalchemy import func
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@ -248,3 +249,52 @@ def test_inbox__create_already_deleted_object(
).scalar_one_or_none() ).scalar_one_or_none()
is 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
)