Add support for blocking actors
This commit is contained in:
parent
7782a39638
commit
cc086f3264
6 changed files with 148 additions and 4 deletions
|
@ -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 ###
|
32
app/admin.py
32
app/admin.py
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in a new issue