Start support for manually approving followers
This commit is contained in:
parent
9f3956db67
commit
a1a9ec3f7c
10 changed files with 272 additions and 10 deletions
|
@ -0,0 +1,34 @@
|
||||||
|
"""Tweak notification model
|
||||||
|
|
||||||
|
Revision ID: 1702e88016db
|
||||||
|
Revises: 50d26a370a65
|
||||||
|
Create Date: 2022-08-02 15:19:57.221421+00:00
|
||||||
|
|
||||||
|
"""
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '1702e88016db'
|
||||||
|
down_revision = '50d26a370a65'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('notifications', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('is_accepted', sa.Boolean(), nullable=True))
|
||||||
|
batch_op.add_column(sa.Column('is_rejected', sa.Boolean(), nullable=True))
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('notifications', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('is_rejected')
|
||||||
|
batch_op.drop_column('is_accepted')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
|
@ -95,7 +95,7 @@ ME = {
|
||||||
+ "/inbox",
|
+ "/inbox",
|
||||||
},
|
},
|
||||||
"url": config.ID,
|
"url": config.ID,
|
||||||
"manuallyApprovesFollowers": False,
|
"manuallyApprovesFollowers": config.CONFIG.manually_approves_followers,
|
||||||
"attachment": [],
|
"attachment": [],
|
||||||
"icon": {
|
"icon": {
|
||||||
"mediaType": mimetypes.guess_type(config.CONFIG.icon_url)[0],
|
"mediaType": mimetypes.guess_type(config.CONFIG.icon_url)[0],
|
||||||
|
|
|
@ -218,6 +218,7 @@ async def get_actors_metadata(
|
||||||
select(models.OutboxObject.ap_object, models.OutboxObject.ap_id).where(
|
select(models.OutboxObject.ap_object, models.OutboxObject.ap_id).where(
|
||||||
models.OutboxObject.ap_type == "Follow",
|
models.OutboxObject.ap_type == "Follow",
|
||||||
models.OutboxObject.undone_by_outbox_object_id.is_(None),
|
models.OutboxObject.undone_by_outbox_object_id.is_(None),
|
||||||
|
models.OutboxObject.activity_object_ap_id.in_(ap_actor_ids),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
24
app/admin.py
24
app/admin.py
|
@ -616,6 +616,30 @@ async def admin_actions_delete(
|
||||||
return RedirectResponse(redirect_url, status_code=302)
|
return RedirectResponse(redirect_url, status_code=302)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/actions/accept_incoming_follow")
|
||||||
|
async def admin_actions_accept_incoming_follow(
|
||||||
|
request: Request,
|
||||||
|
notification_id: int = Form(),
|
||||||
|
redirect_url: str = Form(),
|
||||||
|
csrf_check: None = Depends(verify_csrf_token),
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
) -> RedirectResponse:
|
||||||
|
await boxes.send_accept(db_session, notification_id)
|
||||||
|
return RedirectResponse(redirect_url, status_code=302)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/actions/reject_incoming_follow")
|
||||||
|
async def admin_actions_reject_incoming_follow(
|
||||||
|
request: Request,
|
||||||
|
notification_id: int = Form(),
|
||||||
|
redirect_url: str = Form(),
|
||||||
|
csrf_check: None = Depends(verify_csrf_token),
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
) -> RedirectResponse:
|
||||||
|
await boxes.send_reject(db_session, notification_id)
|
||||||
|
return RedirectResponse(redirect_url, status_code=302)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/actions/like")
|
@router.post("/actions/like")
|
||||||
async def admin_actions_like(
|
async def admin_actions_like(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|
115
app/boxes.py
115
app/boxes.py
|
@ -27,6 +27,7 @@ from app.actor import save_actor
|
||||||
from app.ap_object import RemoteObject
|
from app.ap_object import RemoteObject
|
||||||
from app.config import BASE_URL
|
from app.config import BASE_URL
|
||||||
from app.config import ID
|
from app.config import ID
|
||||||
|
from app.config import MANUALLY_APPROVES_FOLLOWERS
|
||||||
from app.database import AsyncSession
|
from app.database import AsyncSession
|
||||||
from app.outgoing_activities import new_outgoing_activity
|
from app.outgoing_activities import new_outgoing_activity
|
||||||
from app.source import markdownify
|
from app.source import markdownify
|
||||||
|
@ -654,6 +655,22 @@ async def _get_followers_recipients(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def get_notification_by_id(
|
||||||
|
db_session: AsyncSession, notification_id: int
|
||||||
|
) -> models.Notification | None:
|
||||||
|
return (
|
||||||
|
await db_session.execute(
|
||||||
|
select(models.Notification)
|
||||||
|
.where(models.Notification.id == notification_id)
|
||||||
|
.options(
|
||||||
|
joinedload(models.Notification.inbox_object).options(
|
||||||
|
joinedload(models.InboxObject.actor)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).scalar_one_or_none() # type: ignore
|
||||||
|
|
||||||
|
|
||||||
async def get_inbox_object_by_ap_id(
|
async def get_inbox_object_by_ap_id(
|
||||||
db_session: AsyncSession, ap_id: str
|
db_session: AsyncSession, ap_id: str
|
||||||
) -> models.InboxObject | None:
|
) -> models.InboxObject | None:
|
||||||
|
@ -832,6 +849,57 @@ async def _handle_follow_follow_activity(
|
||||||
from_actor: models.Actor,
|
from_actor: models.Actor,
|
||||||
inbox_object: models.InboxObject,
|
inbox_object: models.InboxObject,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
if MANUALLY_APPROVES_FOLLOWERS:
|
||||||
|
notif = models.Notification(
|
||||||
|
notification_type=models.NotificationType.PENDING_INCOMING_FOLLOWER,
|
||||||
|
actor_id=from_actor.id,
|
||||||
|
inbox_object_id=inbox_object.id,
|
||||||
|
)
|
||||||
|
db_session.add(notif)
|
||||||
|
return None
|
||||||
|
|
||||||
|
await _send_accept(db_session, from_actor, inbox_object)
|
||||||
|
|
||||||
|
|
||||||
|
async def _get_incoming_follow_from_notification_id(
|
||||||
|
db_session: AsyncSession,
|
||||||
|
notification_id: int,
|
||||||
|
) -> tuple[models.Notification, models.InboxObject]:
|
||||||
|
notif = await get_notification_by_id(db_session, notification_id)
|
||||||
|
if notif is None:
|
||||||
|
raise ValueError(f"Notification {notification_id=} not found")
|
||||||
|
|
||||||
|
if notif.inbox_object is None:
|
||||||
|
raise ValueError("Should never happen")
|
||||||
|
|
||||||
|
if ap_type := notif.inbox_object.ap_type != "Follow":
|
||||||
|
raise ValueError(f"Unexpected {ap_type=}")
|
||||||
|
|
||||||
|
return notif, notif.inbox_object
|
||||||
|
|
||||||
|
|
||||||
|
async def send_accept(
|
||||||
|
db_session: AsyncSession,
|
||||||
|
notification_id: int,
|
||||||
|
) -> None:
|
||||||
|
notif, incoming_follow_request = await _get_incoming_follow_from_notification_id(
|
||||||
|
db_session, notification_id
|
||||||
|
)
|
||||||
|
|
||||||
|
await _send_accept(
|
||||||
|
db_session, incoming_follow_request.actor, incoming_follow_request
|
||||||
|
)
|
||||||
|
notif.is_accepted = True
|
||||||
|
|
||||||
|
await db_session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def _send_accept(
|
||||||
|
db_session: AsyncSession,
|
||||||
|
from_actor: models.Actor,
|
||||||
|
inbox_object: models.InboxObject,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
follower = models.Follower(
|
follower = models.Follower(
|
||||||
actor_id=from_actor.id,
|
actor_id=from_actor.id,
|
||||||
inbox_object_id=inbox_object.id,
|
inbox_object_id=inbox_object.id,
|
||||||
|
@ -852,7 +920,9 @@ async def _handle_follow_follow_activity(
|
||||||
"actor": ID,
|
"actor": ID,
|
||||||
"object": inbox_object.ap_id,
|
"object": inbox_object.ap_id,
|
||||||
}
|
}
|
||||||
outbox_activity = await save_outbox_object(db_session, reply_id, reply)
|
outbox_activity = await save_outbox_object(
|
||||||
|
db_session, reply_id, reply, relates_to_inbox_object_id=inbox_object.id
|
||||||
|
)
|
||||||
if not outbox_activity.id:
|
if not outbox_activity.id:
|
||||||
raise ValueError("Should never happen")
|
raise ValueError("Should never happen")
|
||||||
await new_outgoing_activity(db_session, from_actor.inbox_url, outbox_activity.id)
|
await new_outgoing_activity(db_session, from_actor.inbox_url, outbox_activity.id)
|
||||||
|
@ -864,6 +934,49 @@ async def _handle_follow_follow_activity(
|
||||||
db_session.add(notif)
|
db_session.add(notif)
|
||||||
|
|
||||||
|
|
||||||
|
async def send_reject(
|
||||||
|
db_session: AsyncSession,
|
||||||
|
notification_id: int,
|
||||||
|
) -> None:
|
||||||
|
notif, incoming_follow_request = await _get_incoming_follow_from_notification_id(
|
||||||
|
db_session, notification_id
|
||||||
|
)
|
||||||
|
|
||||||
|
await _send_reject(
|
||||||
|
db_session, incoming_follow_request.actor, incoming_follow_request
|
||||||
|
)
|
||||||
|
notif.is_rejected = True
|
||||||
|
await db_session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def _send_reject(
|
||||||
|
db_session: AsyncSession,
|
||||||
|
from_actor: models.Actor,
|
||||||
|
inbox_object: models.InboxObject,
|
||||||
|
) -> None:
|
||||||
|
# Reply with an Accept
|
||||||
|
reply_id = allocate_outbox_id()
|
||||||
|
reply = {
|
||||||
|
"@context": ap.AS_CTX,
|
||||||
|
"id": outbox_object_id(reply_id),
|
||||||
|
"type": "Reject",
|
||||||
|
"actor": ID,
|
||||||
|
"object": inbox_object.ap_id,
|
||||||
|
}
|
||||||
|
outbox_activity = await save_outbox_object(
|
||||||
|
db_session, reply_id, reply, relates_to_inbox_object_id=inbox_object.id
|
||||||
|
)
|
||||||
|
if not outbox_activity.id:
|
||||||
|
raise ValueError("Should never happen")
|
||||||
|
await new_outgoing_activity(db_session, from_actor.inbox_url, outbox_activity.id)
|
||||||
|
|
||||||
|
notif = models.Notification(
|
||||||
|
notification_type=models.NotificationType.REJECTED_FOLLOWER,
|
||||||
|
actor_id=from_actor.id,
|
||||||
|
)
|
||||||
|
db_session.add(notif)
|
||||||
|
|
||||||
|
|
||||||
async def _handle_undo_activity(
|
async def _handle_undo_activity(
|
||||||
db_session: AsyncSession,
|
db_session: AsyncSession,
|
||||||
from_actor: models.Actor,
|
from_actor: models.Actor,
|
||||||
|
|
|
@ -42,6 +42,7 @@ class Config(pydantic.BaseModel):
|
||||||
secret: str
|
secret: str
|
||||||
debug: bool = False
|
debug: bool = False
|
||||||
trusted_hosts: list[str] = ["127.0.0.1"]
|
trusted_hosts: list[str] = ["127.0.0.1"]
|
||||||
|
manually_approves_followers: bool = False
|
||||||
|
|
||||||
# Config items to make tests easier
|
# Config items to make tests easier
|
||||||
sqlalchemy_database: str | None = None
|
sqlalchemy_database: str | None = None
|
||||||
|
@ -82,6 +83,7 @@ DOMAIN = CONFIG.domain
|
||||||
_SCHEME = "https" if CONFIG.https else "http"
|
_SCHEME = "https" if CONFIG.https else "http"
|
||||||
ID = f"{_SCHEME}://{DOMAIN}"
|
ID = f"{_SCHEME}://{DOMAIN}"
|
||||||
USERNAME = CONFIG.username
|
USERNAME = CONFIG.username
|
||||||
|
MANUALLY_APPROVES_FOLLOWERS = CONFIG.manually_approves_followers
|
||||||
BASE_URL = ID
|
BASE_URL = ID
|
||||||
DEBUG = CONFIG.debug
|
DEBUG = CONFIG.debug
|
||||||
DB_PATH = CONFIG.sqlalchemy_database or ROOT_DIR / "data" / "microblogpub.db"
|
DB_PATH = CONFIG.sqlalchemy_database or ROOT_DIR / "data" / "microblogpub.db"
|
||||||
|
|
|
@ -523,6 +523,8 @@ class PollAnswer(Base):
|
||||||
@enum.unique
|
@enum.unique
|
||||||
class NotificationType(str, enum.Enum):
|
class NotificationType(str, enum.Enum):
|
||||||
NEW_FOLLOWER = "new_follower"
|
NEW_FOLLOWER = "new_follower"
|
||||||
|
PENDING_INCOMING_FOLLOWER = "pending_incoming_follower"
|
||||||
|
REJECTED_FOLLOWER = "rejected_follower"
|
||||||
UNFOLLOW = "unfollow"
|
UNFOLLOW = "unfollow"
|
||||||
|
|
||||||
FOLLOW_REQUEST_ACCEPTED = "follow_request_accepted"
|
FOLLOW_REQUEST_ACCEPTED = "follow_request_accepted"
|
||||||
|
@ -563,6 +565,9 @@ class Notification(Base):
|
||||||
)
|
)
|
||||||
webmention = relationship(Webmention, uselist=False)
|
webmention = relationship(Webmention, uselist=False)
|
||||||
|
|
||||||
|
is_accepted = Column(Boolean, nullable=True)
|
||||||
|
is_rejected = Column(Boolean, nullable=True)
|
||||||
|
|
||||||
|
|
||||||
outbox_fts = Table(
|
outbox_fts = Table(
|
||||||
"outbox_fts",
|
"outbox_fts",
|
||||||
|
|
|
@ -22,6 +22,10 @@
|
||||||
{%- if notif.notification_type.value == "new_follower" %}
|
{%- if notif.notification_type.value == "new_follower" %}
|
||||||
{{ notif_actor_action(notif, "followed you") }}
|
{{ notif_actor_action(notif, "followed you") }}
|
||||||
{{ utils.display_actor(notif.actor, actors_metadata) }}
|
{{ utils.display_actor(notif.actor, actors_metadata) }}
|
||||||
|
{%- elif notif.notification_type.value == "pending_incoming_follower" %}
|
||||||
|
{{ notif_actor_action(notif, "sent a follow request") }}
|
||||||
|
{{ utils.display_actor(notif.actor, actors_metadata, pending_incoming_follow_notif=notif) }}
|
||||||
|
{% elif notif.notification_type.value == "rejected_follower" %}
|
||||||
{% elif notif.notification_type.value == "unfollow" %}
|
{% elif notif.notification_type.value == "unfollow" %}
|
||||||
{{ notif_actor_action(notif, "unfollowed you") }}
|
{{ notif_actor_action(notif, "unfollowed you") }}
|
||||||
{{ utils.display_actor(notif.actor, actors_metadata) }}
|
{{ utils.display_actor(notif.actor, actors_metadata) }}
|
||||||
|
|
|
@ -33,6 +33,24 @@
|
||||||
</form>
|
</form>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro admin_accept_incoming_follow_button(notif) %}
|
||||||
|
<form action="{{ request.url_for("admin_actions_accept_incoming_follow") }}" method="POST">
|
||||||
|
{{ embed_csrf_token() }}
|
||||||
|
{{ embed_redirect_url() }}
|
||||||
|
<input type="hidden" name="notification_id" value="{{ notif.id }}">
|
||||||
|
<input type="submit" value="accept follow">
|
||||||
|
</form>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro admin_reject_incoming_follow_button(notif) %}
|
||||||
|
<form action="{{ request.url_for("admin_actions_reject_incoming_follow") }}" method="POST">
|
||||||
|
{{ embed_csrf_token() }}
|
||||||
|
{{ embed_redirect_url() }}
|
||||||
|
<input type="hidden" name="notification_id" value="{{ notif.id }}">
|
||||||
|
<input type="submit" value="reject follow">
|
||||||
|
</form>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro admin_like_button(ap_object_id, permalink_id) %}
|
{% macro admin_like_button(ap_object_id, permalink_id) %}
|
||||||
<form action="{{ request.url_for("admin_actions_like") }}" method="POST">
|
<form action="{{ request.url_for("admin_actions_like") }}" method="POST">
|
||||||
{{ embed_csrf_token() }}
|
{{ embed_csrf_token() }}
|
||||||
|
@ -197,7 +215,7 @@
|
||||||
|
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro display_actor(actor, actors_metadata={}, embedded=False, with_details=False) %}
|
{% macro display_actor(actor, actors_metadata={}, embedded=False, with_details=False, pending_incoming_follow_notif=None) %}
|
||||||
{% set metadata = actors_metadata.get(actor.ap_id) %}
|
{% set metadata = actors_metadata.get(actor.ap_id) %}
|
||||||
|
|
||||||
{% if not embedded %}
|
{% if not embedded %}
|
||||||
|
@ -243,6 +261,20 @@
|
||||||
<li>{{ admin_block_button(actor) }}</li>
|
<li>{{ admin_block_button(actor) }}</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if pending_incoming_follow_notif %}
|
||||||
|
{% if not pending_incoming_follow_notif.is_accepted and not pending_incoming_follow_notif.is_rejected %}
|
||||||
|
<li>
|
||||||
|
{{ admin_accept_incoming_follow_button(pending_incoming_follow_notif) }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{{ admin_reject_incoming_follow_button(pending_incoming_follow_notif) }}
|
||||||
|
</li>
|
||||||
|
{% elif pending_incoming_follow_notif.is_accepted %}
|
||||||
|
<li>accepted</li>
|
||||||
|
{% else %}
|
||||||
|
<li>rejected</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from unittest import mock
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
@ -32,7 +33,7 @@ def test_inbox_requires_httpsig(
|
||||||
assert response.json()["detail"] == "Invalid HTTP sig"
|
assert response.json()["detail"] == "Invalid HTTP sig"
|
||||||
|
|
||||||
|
|
||||||
def test_inbox_follow_request(
|
def test_inbox_incoming_follow_request(
|
||||||
db: Session,
|
db: Session,
|
||||||
client: TestClient,
|
client: TestClient,
|
||||||
respx_mock: respx.MockRouter,
|
respx_mock: respx.MockRouter,
|
||||||
|
@ -66,11 +67,11 @@ def test_inbox_follow_request(
|
||||||
run_async(process_next_incoming_activity)
|
run_async(process_next_incoming_activity)
|
||||||
|
|
||||||
# And the actor was saved in DB
|
# And the actor was saved in DB
|
||||||
saved_actor = db.query(models.Actor).one()
|
saved_actor = db.execute(select(models.Actor)).scalar_one()
|
||||||
assert saved_actor.ap_id == ra.ap_id
|
assert saved_actor.ap_id == ra.ap_id
|
||||||
|
|
||||||
# And the Follow activity was saved in the inbox
|
# And the Follow activity was saved in the inbox
|
||||||
inbox_object = db.query(models.InboxObject).one()
|
inbox_object = db.execute(select(models.InboxObject)).scalar_one()
|
||||||
assert inbox_object.ap_object == follow_activity.ap_object
|
assert inbox_object.ap_object == follow_activity.ap_object
|
||||||
|
|
||||||
# And a follower was internally created
|
# And a follower was internally created
|
||||||
|
@ -80,15 +81,61 @@ def test_inbox_follow_request(
|
||||||
assert follower.inbox_object_id == inbox_object.id
|
assert follower.inbox_object_id == inbox_object.id
|
||||||
|
|
||||||
# And an Accept activity was created in the outbox
|
# And an Accept activity was created in the outbox
|
||||||
outbox_object = db.query(models.OutboxObject).one()
|
outbox_object = db.execute(select(models.OutboxObject)).scalar_one()
|
||||||
assert outbox_object.ap_type == "Accept"
|
assert outbox_object.ap_type == "Accept"
|
||||||
assert outbox_object.activity_object_ap_id == follow_activity.ap_id
|
assert outbox_object.activity_object_ap_id == follow_activity.ap_id
|
||||||
|
|
||||||
# And an outgoing activity was created to track the Accept activity delivery
|
# And an outgoing activity was created to track the Accept activity delivery
|
||||||
outgoing_activity = db.query(models.OutgoingActivity).one()
|
outgoing_activity = db.execute(select(models.OutgoingActivity)).scalar_one()
|
||||||
assert outgoing_activity.outbox_object_id == outbox_object.id
|
assert outgoing_activity.outbox_object_id == outbox_object.id
|
||||||
|
|
||||||
|
|
||||||
|
def test_inbox_incoming_follow_request__manually_approves_followers(
|
||||||
|
db: Session,
|
||||||
|
client: TestClient,
|
||||||
|
respx_mock: respx.MockRouter,
|
||||||
|
) -> None:
|
||||||
|
# Given a remote actor
|
||||||
|
ra = factories.RemoteActorFactory(
|
||||||
|
base_url="https://example.com",
|
||||||
|
username="toto",
|
||||||
|
public_key="pk",
|
||||||
|
)
|
||||||
|
respx_mock.get(ra.ap_id).mock(return_value=httpx.Response(200, json=ra.ap_actor))
|
||||||
|
|
||||||
|
# When receiving a Follow activity
|
||||||
|
follow_activity = RemoteObject(
|
||||||
|
factories.build_follow_activity(
|
||||||
|
from_remote_actor=ra,
|
||||||
|
for_remote_actor=LOCAL_ACTOR,
|
||||||
|
),
|
||||||
|
ra,
|
||||||
|
)
|
||||||
|
with mock_httpsig_checker(ra):
|
||||||
|
response = client.post(
|
||||||
|
"/inbox",
|
||||||
|
headers={"Content-Type": ap.AS_CTX},
|
||||||
|
json=follow_activity.ap_object,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then the server returns a 204
|
||||||
|
assert response.status_code == 202
|
||||||
|
|
||||||
|
with mock.patch("app.boxes.MANUALLY_APPROVES_FOLLOWERS", True):
|
||||||
|
run_async(process_next_incoming_activity)
|
||||||
|
|
||||||
|
# And the actor was saved in DB
|
||||||
|
saved_actor = db.execute(select(models.Actor)).scalar_one()
|
||||||
|
assert saved_actor.ap_id == ra.ap_id
|
||||||
|
|
||||||
|
# And the Follow activity was saved in the inbox
|
||||||
|
inbox_object = db.execute(select(models.InboxObject)).scalar_one()
|
||||||
|
assert inbox_object.ap_object == follow_activity.ap_object
|
||||||
|
|
||||||
|
# And no follower was internally created
|
||||||
|
assert db.scalar(select(func.count(models.Follower.id))) == 0
|
||||||
|
|
||||||
|
|
||||||
def test_inbox_accept_follow_request(
|
def test_inbox_accept_follow_request(
|
||||||
db: Session,
|
db: Session,
|
||||||
client: TestClient,
|
client: TestClient,
|
||||||
|
@ -133,13 +180,13 @@ def test_inbox_accept_follow_request(
|
||||||
run_async(process_next_incoming_activity)
|
run_async(process_next_incoming_activity)
|
||||||
|
|
||||||
# And the Accept activity was saved in the inbox
|
# And the Accept activity was saved in the inbox
|
||||||
inbox_activity = db.query(models.InboxObject).one()
|
inbox_activity = db.execute(select(models.InboxObject)).scalar_one()
|
||||||
assert inbox_activity.ap_type == "Accept"
|
assert inbox_activity.ap_type == "Accept"
|
||||||
assert inbox_activity.relates_to_outbox_object_id == outbox_object.id
|
assert inbox_activity.relates_to_outbox_object_id == outbox_object.id
|
||||||
assert inbox_activity.actor_id == actor_in_db.id
|
assert inbox_activity.actor_id == actor_in_db.id
|
||||||
|
|
||||||
# And a following entry was created internally
|
# And a following entry was created internally
|
||||||
following = db.query(models.Following).one()
|
following = db.execute(select(models.Following)).scalar_one()
|
||||||
assert following.ap_actor_id == actor_in_db.ap_id
|
assert following.ap_actor_id == actor_in_db.ap_id
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue