From 72969fba662169b33d84abfa4e1171d421eb40db Mon Sep 17 00:00:00 2001 From: Nigini Oliveira Date: Sat, 14 Dec 2024 14:12:30 -0800 Subject: [PATCH] Moved all/most ActivityPub code to its own library. The goal is completely modularize it. --- activitypub/__init__.py | 0 {app => activitypub}/activitypub.py | 2 +- {app => activitypub}/actor.py | 51 +- {app => activitypub}/ap_object.py | 8 +- {app => activitypub}/boxes.py | 431 ++++++++--------- {app => activitypub}/incoming_activities.py | 33 +- activitypub/models.py | 452 +++++++++++++++++ {app => activitypub}/outgoing_activities.py | 47 +- activitypub/tests/__init__.py | 0 activitypub/tests/conftest.py | 42 ++ {tests => activitypub/tests}/factories.py | 27 +- {tests => activitypub/tests}/test_actor.py | 16 +- {tests => activitypub/tests}/test_inbox.py | 69 +-- {tests => activitypub/tests}/test_outbox.py | 41 +- .../test_process_outgoing_activities.py | 30 +- .../tests}/test_remote_actor_deletion.py | 20 +- ...236_add_a_slug_field_for_outbox_objects.py | 2 +- app/admin.py | 222 ++++----- app/customization.py | 4 +- app/httpsig.py | 15 +- app/ldsig.py | 2 +- app/lookup.py | 8 +- app/main.py | 234 ++++----- app/micropub.py | 10 +- app/models.py | 453 +----------------- app/prune.py | 49 +- app/source.py | 11 +- app/templates.py | 23 +- app/uploads.py | 11 +- app/utils/custom_index_handler.py | 2 +- app/utils/emoji.py | 2 +- app/utils/facepile.py | 2 +- app/utils/opengraph.py | 10 +- app/utils/stats.py | 23 +- app/webmentions.py | 15 +- tasks.py | 16 +- tests/conftest.py | 2 +- tests/test_admin.py | 2 +- tests/test_emoji.py | 5 +- tests/test_httpsig.py | 4 +- tests/test_ldsig.py | 4 +- tests/test_public.py | 4 +- tests/test_tags.py | 5 +- tests/utils.py | 30 +- 44 files changed, 1249 insertions(+), 1190 deletions(-) create mode 100644 activitypub/__init__.py rename {app => activitypub}/activitypub.py (99%) rename {app => activitypub}/actor.py (87%) rename {app => activitypub}/ap_object.py (98%) rename {app => activitypub}/boxes.py (85%) rename {app => activitypub}/incoming_activities.py (80%) create mode 100644 activitypub/models.py rename {app => activitypub}/outgoing_activities.py (86%) create mode 100644 activitypub/tests/__init__.py create mode 100644 activitypub/tests/conftest.py rename {tests => activitypub/tests}/factories.py (92%) rename {tests => activitypub/tests}/test_actor.py (77%) rename {tests => activitypub/tests}/test_inbox.py (81%) rename {tests => activitypub/tests}/test_outbox.py (88%) rename {tests => activitypub/tests}/test_process_outgoing_activities.py (88%) rename {tests => activitypub/tests}/test_remote_actor_deletion.py (81%) diff --git a/activitypub/__init__.py b/activitypub/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/activitypub.py b/activitypub/activitypub.py similarity index 99% rename from app/activitypub.py rename to activitypub/activitypub.py index 3a96e8bb..9a30e7f6 100644 --- a/app/activitypub.py +++ b/activitypub/activitypub.py @@ -18,7 +18,7 @@ from app.utils.url import check_url if TYPE_CHECKING: - from app.actor import Actor + from activitypub.actor import Actor RawObject = dict[str, Any] AS_CTX = "https://www.w3.org/ns/activitystreams" diff --git a/app/actor.py b/activitypub/actor.py similarity index 87% rename from app/actor.py rename to activitypub/actor.py index 24a2dcbd..a7e38a74 100644 --- a/app/actor.py +++ b/activitypub/actor.py @@ -11,7 +11,8 @@ from sqlalchemy import select from sqlalchemy.orm import joinedload -from app import activitypub as ap +import activitypub.models +from activitypub import activitypub as ap from app import media from app.config import BASE_URL from app.config import USER_AGENT @@ -22,7 +23,7 @@ from app.utils.datetime import now if typing.TYPE_CHECKING: - from app.models import Actor as ActorModel + from activitypub.models import Actor as ActorModel def _handle(raw_actor: ap.RawObject) -> str: @@ -216,7 +217,7 @@ async def save_actor(db_session: AsyncSession, ap_actor: ap.RawObject) -> "Actor if ap_type := ap_actor.get("type") not in ap.ACTOR_TYPES: raise ValueError(f"Invalid type {ap_type} for actor {ap_actor}") - actor = models.Actor( + actor = activitypub.models.Actor( ap_id=ap.get_id(ap_actor["id"]), ap_actor=ap_actor, ap_type=ap.as_list(ap_actor["type"])[0], @@ -239,8 +240,8 @@ async def fetch_actor( existing_actor = ( await db_session.scalars( - select(models.Actor).where( - models.Actor.ap_id == actor_id, + select(activitypub.models.Actor).where( + activitypub.models.Actor.ap_id == actor_id, ) ) ).one_or_none() @@ -273,8 +274,8 @@ async def fetch_actor( # (like Birdsite LIVE) , which mean we may already have it in DB existing_actor_by_url = ( await db_session.scalars( - select(models.Actor).where( - models.Actor.ap_id == ap.get_id(ap_actor), + select(activitypub.models.Actor).where( + activitypub.models.Actor.ap_id == ap.get_id(ap_actor), ) ) ).one_or_none() @@ -298,8 +299,8 @@ async def list_actors(db_session: AsyncSession, limit: int = 100) -> list["Actor return ( ( await db_session.scalars( - select(models.Actor) - .where(models.Actor.is_deleted.is_(False)) + select(activitypub.models.Actor) + .where(activitypub.models.Actor.is_deleted.is_(False)) .limit(limit) ) ) @@ -350,9 +351,9 @@ async def get_actors_metadata( follower.ap_actor_id: follower.inbox_object.ap_id for follower in ( await db_session.scalars( - select(models.Follower) - .where(models.Follower.ap_actor_id.in_(ap_actor_ids)) - .options(joinedload(models.Follower.inbox_object)) + select(activitypub.models.Follower) + .where(activitypub.models.Follower.ap_actor_id.in_(ap_actor_ids)) + .options(joinedload(activitypub.models.Follower.inbox_object)) ) ) .unique() @@ -361,37 +362,37 @@ async def get_actors_metadata( following = { following.ap_actor_id for following in await db_session.execute( - select(models.Following.ap_actor_id).where( - models.Following.ap_actor_id.in_(ap_actor_ids) + select(activitypub.models.Following.ap_actor_id).where( + activitypub.models.Following.ap_actor_id.in_(ap_actor_ids) ) ) } sent_follow_requests = { follow_req.ap_object["object"]: follow_req.ap_id for follow_req in await db_session.execute( - select(models.OutboxObject.ap_object, models.OutboxObject.ap_id).where( - models.OutboxObject.ap_type == "Follow", - models.OutboxObject.undone_by_outbox_object_id.is_(None), - models.OutboxObject.activity_object_ap_id.in_(ap_actor_ids), + select(activitypub.models.OutboxObject.ap_object, activitypub.models.OutboxObject.ap_id).where( + activitypub.models.OutboxObject.ap_type == "Follow", + activitypub.models.OutboxObject.undone_by_outbox_object_id.is_(None), + activitypub.models.OutboxObject.activity_object_ap_id.in_(ap_actor_ids), ) ) } rejected_follow_requests = { reject.activity_object_ap_id for reject in await db_session.execute( - select(models.InboxObject.activity_object_ap_id).where( - models.InboxObject.ap_type == "Reject", - models.InboxObject.ap_actor_id.in_(ap_actor_ids), + select(activitypub.models.InboxObject.activity_object_ap_id).where( + activitypub.models.InboxObject.ap_type == "Reject", + activitypub.models.InboxObject.ap_actor_id.in_(ap_actor_ids), ) ) } blocks = { block.ap_actor_id for block in await db_session.execute( - select(models.InboxObject.ap_actor_id).where( - models.InboxObject.ap_type == "Block", - models.InboxObject.undone_by_inbox_object_id.is_(None), - models.InboxObject.ap_actor_id.in_(ap_actor_ids), + select(activitypub.models.InboxObject.ap_actor_id).where( + activitypub.models.InboxObject.ap_type == "Block", + activitypub.models.InboxObject.undone_by_inbox_object_id.is_(None), + activitypub.models.InboxObject.ap_actor_id.in_(ap_actor_ids), ) ) } diff --git a/app/ap_object.py b/activitypub/ap_object.py similarity index 98% rename from app/ap_object.py rename to activitypub/ap_object.py index 21f58020..5208e386 100644 --- a/app/ap_object.py +++ b/activitypub/ap_object.py @@ -8,10 +8,10 @@ from bs4 import BeautifulSoup # type: ignore from mistletoe import markdown # type: ignore -from app import activitypub as ap -from app.actor import LOCAL_ACTOR -from app.actor import Actor -from app.actor import RemoteActor +from activitypub import activitypub as ap +from activitypub.actor import LOCAL_ACTOR +from activitypub.actor import Actor +from activitypub.actor import RemoteActor from app.config import ID from app.media import proxied_media_url from app.utils.datetime import now diff --git a/app/boxes.py b/activitypub/boxes.py similarity index 85% rename from app/boxes.py rename to activitypub/boxes.py index bf0eb97c..3db3d475 100644 --- a/app/boxes.py +++ b/activitypub/boxes.py @@ -17,17 +17,18 @@ from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import joinedload -from app import activitypub as ap +import activitypub.models +from activitypub import activitypub as ap from app import config from app import ldsig from app import models -from app.actor import LOCAL_ACTOR -from app.actor import Actor -from app.actor import RemoteActor -from app.actor import fetch_actor -from app.actor import save_actor -from app.actor import update_actor_if_needed -from app.ap_object import RemoteObject +from activitypub.actor import LOCAL_ACTOR +from activitypub.actor import Actor +from activitypub.actor import RemoteActor +from activitypub.actor import fetch_actor +from activitypub.actor import save_actor +from activitypub.actor import update_actor_if_needed +from activitypub.ap_object import RemoteObject from app.config import BASE_URL from app.config import ID from app.config import MANUALLY_APPROVES_FOLLOWERS @@ -35,7 +36,7 @@ from app.config import stream_visibility_callback from app.customization import ObjectInfo from app.database import AsyncSession -from app.outgoing_activities import new_outgoing_activity +from activitypub.outgoing_activities import new_outgoing_activity from app.source import dedup_tags from app.source import markdownify from app.uploads import upload_to_attachment @@ -48,7 +49,7 @@ from app.utils.text import slugify from app.utils.url import is_hostname_blocked -AnyboxObject = models.InboxObject | models.OutboxObject +AnyboxObject = activitypub.models.InboxObject | activitypub.models.OutboxObject def is_notification_enabled(notification_type: models.NotificationType) -> bool: @@ -81,10 +82,10 @@ async def save_outbox_object( is_transient: bool = False, conversation: str | None = None, slug: str | None = None, -) -> models.OutboxObject: +) -> activitypub.models.OutboxObject: ro = await RemoteObject.from_raw_object(raw_object) - outbox_object = models.OutboxObject( + outbox_object = activitypub.models.OutboxObject( public_id=public_id, ap_type=ro.ap_type, ap_id=ro.ap_id, @@ -114,9 +115,9 @@ async def send_unblock(db_session: AsyncSession, ap_actor_id: str) -> None: block_activity = ( await db_session.scalars( - select(models.OutboxObject).where( - models.OutboxObject.activity_object_ap_id == actor.ap_id, - models.OutboxObject.is_deleted.is_(False), + select(activitypub.models.OutboxObject).where( + activitypub.models.OutboxObject.activity_object_ap_id == actor.ap_id, + activitypub.models.OutboxObject.is_deleted.is_(False), ) ) ).one_or_none() @@ -136,10 +137,10 @@ async def send_block(db_session: AsyncSession, ap_actor_id: str) -> None: # 1. Unfollow the actor following = ( await db_session.scalars( - select(models.Following) - .options(joinedload(models.Following.outbox_object)) + select(activitypub.models.Following) + .options(joinedload(activitypub.models.Following.outbox_object)) .where( - models.Following.ap_actor_id == actor.ap_id, + activitypub.models.Following.ap_actor_id == actor.ap_id, ) ) ).one_or_none() @@ -149,10 +150,10 @@ async def send_block(db_session: AsyncSession, ap_actor_id: str) -> None: # 2. If the blocked actor is a follower, reject the follow request follower = ( await db_session.scalars( - select(models.Follower) - .options(joinedload(models.Follower.inbox_object)) + select(activitypub.models.Follower) + .options(joinedload(activitypub.models.Follower.inbox_object)) .where( - models.Follower.ap_actor_id == actor.ap_id, + activitypub.models.Follower.ap_actor_id == actor.ap_id, ) ) ).one_or_none() @@ -409,8 +410,8 @@ async def _send_undo(db_session: AsyncSession, ap_object_id: str) -> None: ) # Also remove the follow from the following collection await db_session.execute( - delete(models.Following).where( - models.Following.ap_actor_id == followed_actor.ap_id + delete(activitypub.models.Following).where( + activitypub.models.Following.ap_actor_id == followed_actor.ap_id ) ) elif outbox_object_to_undo.ap_type == "Like": @@ -584,7 +585,7 @@ async def send_create( db_session: AsyncSession, ap_type: str, source: str, - uploads: list[tuple[models.Upload, str, str | None]], + uploads: list[tuple[activitypub.models.Upload, str, str | None]], in_reply_to: str | None, visibility: ap.VisibilityEnum, content_warning: str | None = None, @@ -593,7 +594,7 @@ async def send_create( poll_answers: list[str] | None = None, poll_duration_in_minutes: int | None = None, name: str | None = None, -) -> tuple[str, models.OutboxObject]: +) -> tuple[str, activitypub.models.OutboxObject]: note_id = allocate_outbox_id() published = now().replace(microsecond=0).isoformat().replace("+00:00", "Z") context = f"{ID}/contexts/" + uuid.uuid4().hex @@ -708,14 +709,14 @@ async def send_create( for tag in tags: if tag["type"] == "Hashtag": - tagged_object = models.TaggedOutboxObject( + tagged_object = activitypub.models.TaggedOutboxObject( tag=tag["name"][1:].lower(), outbox_object_id=outbox_object.id, ) db_session.add(tagged_object) for upload, filename, alt in uploads: - outbox_object_attachment = models.OutboxObjectAttachment( + outbox_object_attachment = activitypub.models.OutboxObjectAttachment( filename=filename, alt=alt, outbox_object_id=outbox_object.id, @@ -751,17 +752,17 @@ async def send_create( ) if in_reply_to_object.is_from_outbox: await db_session.execute( - update(models.OutboxObject) + update(activitypub.models.OutboxObject) .where( - models.OutboxObject.ap_id == in_reply_to_object.ap_id, + activitypub.models.OutboxObject.ap_id == in_reply_to_object.ap_id, ) .values(replies_count=new_replies_count) ) elif in_reply_to_object.is_from_inbox: await db_session.execute( - update(models.InboxObject) + update(activitypub.models.InboxObject) .where( - models.InboxObject.ap_id == in_reply_to_object.ap_id, + activitypub.models.InboxObject.ap_id == in_reply_to_object.ap_id, ) .values(replies_count=new_replies_count) ) @@ -877,13 +878,13 @@ async def send_update( outbox_object.revisions = revisions await db_session.execute( - delete(models.TaggedOutboxObject).where( - models.TaggedOutboxObject.outbox_object_id == outbox_object.id + delete(activitypub.models.TaggedOutboxObject).where( + activitypub.models.TaggedOutboxObject.outbox_object_id == outbox_object.id ) ) for tag in tags: if tag["type"] == "Hashtag": - tagged_object = models.TaggedOutboxObject( + tagged_object = activitypub.models.TaggedOutboxObject( tag=tag["name"][1:].lower(), outbox_object_id=outbox_object.id, ) @@ -937,7 +938,7 @@ async def _compute_recipients( # Is it a known actor? known_actor = ( await db_session.execute( - select(models.Actor).where(models.Actor.ap_id == r) + select(activitypub.models.Actor).where(activitypub.models.Actor.ap_id == r) ) ).scalar_one_or_none() # type: ignore if known_actor: @@ -963,17 +964,17 @@ async def compute_all_known_recipients(db_session: AsyncSession) -> set[str]: actor.shared_inbox_url or actor.inbox_url for actor in ( await db_session.scalars( - select(models.Actor).where(models.Actor.is_deleted.is_(False)) + select(activitypub.models.Actor).where(activitypub.models.Actor.is_deleted.is_(False)) ) ).all() } -async def _get_following(db_session: AsyncSession) -> list[models.Following]: +async def _get_following(db_session: AsyncSession) -> list[activitypub.models.Following]: return ( ( await db_session.scalars( - select(models.Following).options(joinedload(models.Following.actor)) + select(activitypub.models.Following).options(joinedload(activitypub.models.Following.actor)) ) ) .unique() @@ -981,11 +982,11 @@ async def _get_following(db_session: AsyncSession) -> list[models.Following]: ) -async def _get_followers(db_session: AsyncSession) -> list[models.Follower]: +async def _get_followers(db_session: AsyncSession) -> list[activitypub.models.Follower]: return ( ( await db_session.scalars( - select(models.Follower).options(joinedload(models.Follower.actor)) + select(activitypub.models.Follower).options(joinedload(activitypub.models.Follower.actor)) ) ) .unique() @@ -995,7 +996,7 @@ async def _get_followers(db_session: AsyncSession) -> list[models.Follower]: async def _get_followers_recipients( db_session: AsyncSession, - skip_actors: list[models.Actor] | None = None, + skip_actors: list[activitypub.models.Actor] | None = None, ) -> set[str]: """Returns all the recipients from the local follower collection.""" actor_ap_ids_to_skip = [] @@ -1019,7 +1020,7 @@ async def get_notification_by_id( .where(models.Notification.id == notification_id) .options( joinedload(models.Notification.inbox_object).options( - joinedload(models.InboxObject.actor) + joinedload(activitypub.models.InboxObject.actor) ), ) ) @@ -1028,15 +1029,15 @@ async def get_notification_by_id( async def get_inbox_object_by_ap_id( db_session: AsyncSession, ap_id: str -) -> models.InboxObject | None: +) -> activitypub.models.InboxObject | None: return ( await db_session.execute( - select(models.InboxObject) - .where(models.InboxObject.ap_id == ap_id) + select(activitypub.models.InboxObject) + .where(activitypub.models.InboxObject.ap_id == ap_id) .options( - joinedload(models.InboxObject.actor), - joinedload(models.InboxObject.relates_to_inbox_object), - joinedload(models.InboxObject.relates_to_outbox_object), + joinedload(activitypub.models.InboxObject.actor), + joinedload(activitypub.models.InboxObject.relates_to_inbox_object), + joinedload(activitypub.models.InboxObject.relates_to_outbox_object), ) ) ).scalar_one_or_none() # type: ignore @@ -1044,18 +1045,18 @@ async def get_inbox_object_by_ap_id( async def get_inbox_delete_for_activity_object_ap_id( db_session: AsyncSession, activity_object_ap_id: str -) -> models.InboxObject | None: +) -> activitypub.models.InboxObject | None: return ( await db_session.execute( - select(models.InboxObject) + select(activitypub.models.InboxObject) .where( - models.InboxObject.ap_type == "Delete", - models.InboxObject.activity_object_ap_id == activity_object_ap_id, + activitypub.models.InboxObject.ap_type == "Delete", + activitypub.models.InboxObject.activity_object_ap_id == activity_object_ap_id, ) .options( - joinedload(models.InboxObject.actor), - joinedload(models.InboxObject.relates_to_inbox_object), - joinedload(models.InboxObject.relates_to_outbox_object), + joinedload(activitypub.models.InboxObject.actor), + joinedload(activitypub.models.InboxObject.relates_to_inbox_object), + joinedload(activitypub.models.InboxObject.relates_to_outbox_object), ) ) ).scalar_one_or_none() # type: ignore @@ -1063,23 +1064,23 @@ async def get_inbox_delete_for_activity_object_ap_id( async def get_outbox_object_by_ap_id( db_session: AsyncSession, ap_id: str -) -> models.OutboxObject | None: +) -> activitypub.models.OutboxObject | None: return ( ( await db_session.execute( - select(models.OutboxObject) - .where(models.OutboxObject.ap_id == ap_id) + select(activitypub.models.OutboxObject) + .where(activitypub.models.OutboxObject.ap_id == ap_id) .options( - joinedload(models.OutboxObject.outbox_object_attachments).options( - joinedload(models.OutboxObjectAttachment.upload) + joinedload(activitypub.models.OutboxObject.outbox_object_attachments).options( + joinedload(activitypub.models.OutboxObjectAttachment.upload) ), - joinedload(models.OutboxObject.relates_to_inbox_object).options( - joinedload(models.InboxObject.actor), + joinedload(activitypub.models.OutboxObject.relates_to_inbox_object).options( + joinedload(activitypub.models.InboxObject.actor), ), - joinedload(models.OutboxObject.relates_to_outbox_object).options( + joinedload(activitypub.models.OutboxObject.relates_to_outbox_object).options( joinedload( - models.OutboxObject.outbox_object_attachments - ).options(joinedload(models.OutboxObjectAttachment.upload)), + activitypub.models.OutboxObject.outbox_object_attachments + ).options(joinedload(activitypub.models.OutboxObjectAttachment.upload)), ), ) ) @@ -1093,20 +1094,20 @@ async def get_outbox_object_by_slug_and_short_id( db_session: AsyncSession, slug: str, short_id: str, -) -> models.OutboxObject | None: +) -> activitypub.models.OutboxObject | None: return ( ( await db_session.execute( - select(models.OutboxObject) + select(activitypub.models.OutboxObject) .options( - joinedload(models.OutboxObject.outbox_object_attachments).options( - joinedload(models.OutboxObjectAttachment.upload) + joinedload(activitypub.models.OutboxObject.outbox_object_attachments).options( + joinedload(activitypub.models.OutboxObjectAttachment.upload) ) ) .where( - models.OutboxObject.public_id.like(f"{short_id}%"), - models.OutboxObject.slug == slug, - models.OutboxObject.is_deleted.is_(False), + activitypub.models.OutboxObject.public_id.like(f"{short_id}%"), + activitypub.models.OutboxObject.slug == slug, + activitypub.models.OutboxObject.is_deleted.is_(False), ) ) ) @@ -1140,12 +1141,12 @@ async def get_webmention_by_id( async def _handle_delete_activity( db_session: AsyncSession, - from_actor: models.Actor, - delete_activity: models.InboxObject, - relates_to_inbox_object: models.InboxObject | None, - forwarded_by_actor: models.Actor | None, + from_actor: activitypub.models.Actor, + delete_activity: activitypub.models.InboxObject, + relates_to_inbox_object: activitypub.models.InboxObject | None, + forwarded_by_actor: activitypub.models.Actor | None, ) -> None: - ap_object_to_delete: models.InboxObject | models.Actor | None = None + ap_object_to_delete: activitypub.models.InboxObject | activitypub.models.Actor | None = None if relates_to_inbox_object: ap_object_to_delete = relates_to_inbox_object elif delete_activity.activity_object_ap_id: @@ -1167,7 +1168,7 @@ async def _handle_delete_activity( ) return - if isinstance(ap_object_to_delete, models.InboxObject): + if isinstance(ap_object_to_delete, activitypub.models.InboxObject): if from_actor.ap_id != ap_object_to_delete.actor.ap_id: logger.warning( "Actor mismatch between the activity and the object: " @@ -1185,7 +1186,7 @@ async def _handle_delete_activity( forwarded_by_actor, ) ap_object_to_delete.is_deleted = True - elif isinstance(ap_object_to_delete, models.Actor): + elif isinstance(ap_object_to_delete, activitypub.models.Actor): if from_actor.ap_id != ap_object_to_delete.ap_id: logger.warning( "Actor mismatch between the activity and the object: " @@ -1196,8 +1197,8 @@ async def _handle_delete_activity( 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, + select(activitypub.models.Follower).where( + activitypub.models.Follower.ap_actor_id == ap_object_to_delete.ap_id, ) ) ).one_or_none() @@ -1208,11 +1209,11 @@ async def _handle_delete_activity( # Also mark Follow activities for this actor as deleted follow_activities = ( await db_session.scalars( - select(models.OutboxObject).where( - models.OutboxObject.ap_type == "Follow", - models.OutboxObject.relates_to_actor_id + select(activitypub.models.OutboxObject).where( + activitypub.models.OutboxObject.ap_type == "Follow", + activitypub.models.OutboxObject.relates_to_actor_id == ap_object_to_delete.id, - models.OutboxObject.is_deleted.is_(False), + activitypub.models.OutboxObject.is_deleted.is_(False), ) ) ).all() @@ -1224,8 +1225,8 @@ async def _handle_delete_activity( following = ( await db_session.scalars( - select(models.Following).where( - models.Following.ap_actor_id == ap_object_to_delete.ap_id, + select(activitypub.models.Following).where( + activitypub.models.Following.ap_actor_id == ap_object_to_delete.ap_id, ) ) ).one_or_none() @@ -1238,9 +1239,9 @@ async def _handle_delete_activity( 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), + select(activitypub.models.InboxObject).where( + activitypub.models.InboxObject.actor_id == ap_object_to_delete.id, + activitypub.models.InboxObject.is_deleted.is_(False), ) ) ).all() @@ -1265,18 +1266,18 @@ async def _get_replies_count( ) -> int: return ( await db_session.scalar( - select(func.count(models.InboxObject.id)).where( - func.json_extract(models.InboxObject.ap_object, "$.inReplyTo") + select(func.count(activitypub.models.InboxObject.id)).where( + func.json_extract(activitypub.models.InboxObject.ap_object, "$.inReplyTo") == replied_object_ap_id, - models.InboxObject.is_deleted.is_(False), + activitypub.models.InboxObject.is_deleted.is_(False), ) ) ) + ( await db_session.scalar( - select(func.count(models.OutboxObject.id)).where( - func.json_extract(models.OutboxObject.ap_object, "$.inReplyTo") + select(func.count(activitypub.models.OutboxObject.id)).where( + func.json_extract(activitypub.models.OutboxObject.ap_object, "$.inReplyTo") == replied_object_ap_id, - models.OutboxObject.is_deleted.is_(False), + activitypub.models.OutboxObject.is_deleted.is_(False), ) ) ) @@ -1284,7 +1285,7 @@ async def _get_replies_count( async def _get_outbox_replies_count( db_session: AsyncSession, - outbox_object: models.OutboxObject, + outbox_object: activitypub.models.OutboxObject, ) -> int: return (await _get_replies_count(db_session, outbox_object.ap_id)) + ( await db_session.scalar( @@ -1299,14 +1300,14 @@ async def _get_outbox_replies_count( async def _get_outbox_likes_count( db_session: AsyncSession, - outbox_object: models.OutboxObject, + outbox_object: activitypub.models.OutboxObject, ) -> int: return ( await db_session.scalar( - select(func.count(models.InboxObject.id)).where( - models.InboxObject.ap_type == "Like", - models.InboxObject.relates_to_outbox_object_id == outbox_object.id, - models.InboxObject.is_deleted.is_(False), + select(func.count(activitypub.models.InboxObject.id)).where( + activitypub.models.InboxObject.ap_type == "Like", + activitypub.models.InboxObject.relates_to_outbox_object_id == outbox_object.id, + activitypub.models.InboxObject.is_deleted.is_(False), ) ) ) + ( @@ -1322,14 +1323,14 @@ async def _get_outbox_likes_count( async def _get_outbox_announces_count( db_session: AsyncSession, - outbox_object: models.OutboxObject, + outbox_object: activitypub.models.OutboxObject, ) -> int: return ( await db_session.scalar( - select(func.count(models.InboxObject.id)).where( - models.InboxObject.ap_type == "Announce", - models.InboxObject.relates_to_outbox_object_id == outbox_object.id, - models.InboxObject.is_deleted.is_(False), + select(func.count(activitypub.models.InboxObject.id)).where( + activitypub.models.InboxObject.ap_type == "Announce", + activitypub.models.InboxObject.relates_to_outbox_object_id == outbox_object.id, + activitypub.models.InboxObject.is_deleted.is_(False), ) ) ) + ( @@ -1345,9 +1346,9 @@ async def _get_outbox_announces_count( async def _revert_side_effect_for_deleted_object( db_session: AsyncSession, - delete_activity: models.InboxObject | None, - deleted_ap_object: models.InboxObject, - forwarded_by_actor: models.Actor | None, + delete_activity: activitypub.models.InboxObject | None, + deleted_ap_object: activitypub.models.InboxObject, + forwarded_by_actor: activitypub.models.Actor | None, ) -> None: is_delete_needs_to_be_forwarded = False @@ -1378,9 +1379,9 @@ async def _revert_side_effect_for_deleted_object( ) await db_session.execute( - update(models.OutboxObject) + update(activitypub.models.OutboxObject) .where( - models.OutboxObject.id == replied_object.id, + activitypub.models.OutboxObject.id == replied_object.id, ) .values(replies_count=new_replies_count - 1) ) @@ -1390,9 +1391,9 @@ async def _revert_side_effect_for_deleted_object( ) await db_session.execute( - update(models.InboxObject) + update(activitypub.models.InboxObject) .where( - models.InboxObject.id == replied_object.id, + activitypub.models.InboxObject.id == replied_object.id, ) .values(replies_count=new_replies_count - 1) ) @@ -1406,9 +1407,9 @@ async def _revert_side_effect_for_deleted_object( if related_object.is_from_outbox: likes_count = await _get_outbox_likes_count(db_session, related_object) await db_session.execute( - update(models.OutboxObject) + update(activitypub.models.OutboxObject) .where( - models.OutboxObject.id == related_object.id, + activitypub.models.OutboxObject.id == related_object.id, ) .values(likes_count=likes_count - 1) ) @@ -1426,18 +1427,18 @@ async def _revert_side_effect_for_deleted_object( db_session, related_object ) await db_session.execute( - update(models.OutboxObject) + update(activitypub.models.OutboxObject) .where( - models.OutboxObject.id == related_object.id, + activitypub.models.OutboxObject.id == related_object.id, ) .values(announces_count=announces_count - 1) ) # Delete any Like/Announce await db_session.execute( - update(models.OutboxObject) + update(activitypub.models.OutboxObject) .where( - models.OutboxObject.activity_object_ap_id == deleted_ap_object.ap_id, + activitypub.models.OutboxObject.activity_object_ap_id == deleted_ap_object.ap_id, ) .values(is_deleted=True) ) @@ -1471,8 +1472,8 @@ async def _revert_side_effect_for_deleted_object( async def _handle_follow_follow_activity( db_session: AsyncSession, - from_actor: models.Actor, - follow_activity: models.InboxObject, + from_actor: activitypub.models.Actor, + follow_activity: activitypub.models.InboxObject, ) -> None: if follow_activity.activity_object_ap_id != LOCAL_ACTOR.ap_id: logger.warning( @@ -1496,7 +1497,7 @@ async def _handle_follow_follow_activity( async def _get_incoming_follow_from_notification_id( db_session: AsyncSession, notification_id: int, -) -> tuple[models.Notification, models.InboxObject]: +) -> tuple[models.Notification, activitypub.models.InboxObject]: notif = await get_notification_by_id(db_session, notification_id) if notif is None: raise ValueError(f"Notification {notification_id=} not found") @@ -1528,11 +1529,11 @@ async def send_accept( async def _send_accept( db_session: AsyncSession, - from_actor: models.Actor, - inbox_object: models.InboxObject, + from_actor: activitypub.models.Actor, + inbox_object: activitypub.models.InboxObject, ) -> None: - follower = models.Follower( + follower = activitypub.models.Follower( actor_id=from_actor.id, inbox_object_id=inbox_object.id, ap_actor_id=from_actor.ap_id, @@ -1584,8 +1585,8 @@ async def send_reject( async def _send_reject( db_session: AsyncSession, - from_actor: models.Actor, - inbox_object: models.InboxObject, + from_actor: activitypub.models.Actor, + inbox_object: activitypub.models.InboxObject, ) -> None: # Reply with an Accept reply_id = allocate_outbox_id() @@ -1613,9 +1614,9 @@ async def _send_reject( async def _handle_undo_activity( db_session: AsyncSession, - from_actor: models.Actor, - undo_activity: models.InboxObject, - ap_activity_to_undo: models.InboxObject, + from_actor: activitypub.models.Actor, + undo_activity: activitypub.models.InboxObject, + ap_activity_to_undo: activitypub.models.InboxObject, ) -> None: if from_actor.ap_id != ap_activity_to_undo.actor.ap_id: logger.warning( @@ -1630,8 +1631,8 @@ async def _handle_undo_activity( if ap_activity_to_undo.ap_type == "Follow": logger.info(f"Undo follow from {from_actor.ap_id}") await db_session.execute( - delete(models.Follower).where( - models.Follower.inbox_object_id == ap_activity_to_undo.id + delete(activitypub.models.Follower).where( + activitypub.models.Follower.inbox_object_id == ap_activity_to_undo.id ) ) if is_notification_enabled(models.NotificationType.UNFOLLOW): @@ -1685,7 +1686,7 @@ async def _handle_undo_activity( if announced_obj_from_outbox: logger.info("Found in the oubox") announced_obj_from_outbox.announces_count = ( - models.OutboxObject.announces_count - 1 + activitypub.models.OutboxObject.announces_count - 1 ) if is_notification_enabled(models.NotificationType.UNDO_ANNOUNCE): notif = models.Notification( @@ -1711,8 +1712,8 @@ async def _handle_undo_activity( async def _handle_move_activity( db_session: AsyncSession, - from_actor: models.Actor, - move_activity: models.InboxObject, + from_actor: activitypub.models.Actor, + move_activity: activitypub.models.InboxObject, ) -> None: logger.info("Processing Move activity") @@ -1745,9 +1746,9 @@ async def _handle_move_activity( # Unfollow the old account following = ( await db_session.execute( - select(models.Following) - .where(models.Following.ap_actor_id == old_actor_id) - .options(joinedload(models.Following.outbox_object)) + select(activitypub.models.Following) + .where(activitypub.models.Following.ap_actor_id == old_actor_id) + .options(joinedload(activitypub.models.Following.outbox_object)) ) ).scalar_one_or_none() if not following: @@ -1759,7 +1760,7 @@ async def _handle_move_activity( # Follow the new one if not ( await db_session.execute( - select(models.Following).where(models.Following.ap_actor_id == new_actor_id) + select(activitypub.models.Following).where(activitypub.models.Following.ap_actor_id == new_actor_id) ) ).scalar(): await _send_follow(db_session, new_actor_id) @@ -1777,8 +1778,8 @@ async def _handle_move_activity( async def _handle_update_activity( db_session: AsyncSession, - from_actor: models.Actor, - update_activity: models.InboxObject, + from_actor: activitypub.models.Actor, + update_activity: activitypub.models.InboxObject, ) -> None: logger.info("Processing Update activity") wrapped_object = await ap.get_object(update_activity.ap_object) @@ -1829,10 +1830,10 @@ async def _handle_update_activity( async def _handle_create_activity( db_session: AsyncSession, - from_actor: models.Actor, - create_activity: models.InboxObject, - forwarded_by_actor: models.Actor | None = None, - relates_to_inbox_object: models.InboxObject | None = None, + from_actor: activitypub.models.Actor, + create_activity: activitypub.models.InboxObject, + forwarded_by_actor: activitypub.models.Actor | None = None, + relates_to_inbox_object: activitypub.models.InboxObject | None = None, ) -> None: logger.info("Processing Create activity") @@ -1885,8 +1886,8 @@ async def _handle_create_activity( async def _handle_read_activity( db_session: AsyncSession, - from_actor: models.Actor, - read_activity: models.InboxObject, + from_actor: activitypub.models.Actor, + read_activity: activitypub.models.InboxObject, ) -> None: logger.info("Processing Read activity") @@ -1914,10 +1915,10 @@ async def _handle_read_activity( async def _process_note_object( db_session: AsyncSession, - parent_activity: models.InboxObject, - from_actor: models.Actor, + parent_activity: activitypub.models.InboxObject, + from_actor: activitypub.models.Actor, ro: RemoteObject, - forwarded_by_actor: models.Actor | None = None, + forwarded_by_actor: activitypub.models.Actor | None = None, ) -> None: if parent_activity.ap_type not in ["Create", "Read"]: raise ValueError(f"Unexpected parent activity {parent_activity.ap_id}") @@ -1951,7 +1952,7 @@ async def _process_note_object( remote_object=ro, ) - inbox_object = models.InboxObject( + inbox_object = activitypub.models.InboxObject( server=urlparse(ro.ap_id).hostname, actor_id=from_actor.id, ap_actor_id=from_actor.ap_id, @@ -1998,9 +1999,9 @@ async def _process_note_object( ) await db_session.execute( - update(models.OutboxObject) + update(activitypub.models.OutboxObject) .where( - models.OutboxObject.id == replied_object.id, + activitypub.models.OutboxObject.id == replied_object.id, ) .values(replies_count=new_replies_count) ) @@ -2010,9 +2011,9 @@ async def _process_note_object( ) await db_session.execute( - update(models.InboxObject) + update(activitypub.models.InboxObject) .where( - models.InboxObject.id == replied_object.id, + activitypub.models.InboxObject.id == replied_object.id, ) .values(replies_count=new_replies_count) ) @@ -2053,8 +2054,8 @@ async def _process_note_object( async def _handle_vote_answer( db_session: AsyncSession, - answer: models.InboxObject, - question: models.OutboxObject, + answer: activitypub.models.InboxObject, + question: activitypub.models.OutboxObject, ) -> None: logger.info(f"Processing poll answer for {question.ap_id}: {answer.ap_id}") @@ -2071,7 +2072,7 @@ async def _handle_vote_answer( return answer.is_transient = True - poll_answer = models.PollAnswer( + poll_answer = activitypub.models.PollAnswer( outbox_object_id=question.id, poll_type="oneOf" if question.is_one_of_poll else "anyOf", inbox_object_id=answer.id, @@ -2082,18 +2083,18 @@ async def _handle_vote_answer( await db_session.flush() voters_count = await db_session.scalar( - select(func.count(func.distinct(models.PollAnswer.actor_id))).where( - models.PollAnswer.outbox_object_id == question.id + select(func.count(func.distinct(activitypub.models.PollAnswer.actor_id))).where( + activitypub.models.PollAnswer.outbox_object_id == question.id ) ) all_answers = await db_session.execute( select( - func.count(models.PollAnswer.name).label("answer_count"), - models.PollAnswer.name, + func.count(activitypub.models.PollAnswer.name).label("answer_count"), + activitypub.models.PollAnswer.name, ) - .where(models.PollAnswer.outbox_object_id == question.id) - .group_by(models.PollAnswer.name) + .where(activitypub.models.PollAnswer.outbox_object_id == question.id) + .group_by(activitypub.models.PollAnswer.name) ) all_answers_count = {a["name"]: a["answer_count"] for a in all_answers} @@ -2130,15 +2131,15 @@ async def _handle_vote_answer( async def _handle_announce_activity( db_session: AsyncSession, - actor: models.Actor, - announce_activity: models.InboxObject, - relates_to_outbox_object: models.OutboxObject | None, - relates_to_inbox_object: models.InboxObject | None, + actor: activitypub.models.Actor, + announce_activity: activitypub.models.InboxObject, + relates_to_outbox_object: activitypub.models.OutboxObject | None, + relates_to_inbox_object: activitypub.models.InboxObject | None, ): if relates_to_outbox_object: # This is an announce for a local object relates_to_outbox_object.announces_count = ( - models.OutboxObject.announces_count + 1 + activitypub.models.OutboxObject.announces_count + 1 ) if is_notification_enabled(models.NotificationType.ANNOUNCE): @@ -2173,12 +2174,12 @@ async def _handle_announce_activity( ) or ( dup_count := ( await db_session.scalar( - select(func.count(models.InboxObject.id)).where( - models.InboxObject.ap_type == "Announce", - models.InboxObject.ap_published_at > now() - skip_delta, - models.InboxObject.relates_to_inbox_object_id + select(func.count(activitypub.models.InboxObject.id)).where( + activitypub.models.InboxObject.ap_type == "Announce", + activitypub.models.InboxObject.ap_published_at > now() - skip_delta, + activitypub.models.InboxObject.relates_to_inbox_object_id == relates_to_inbox_object.id, - models.InboxObject.is_hidden_from_stream.is_(False), + activitypub.models.InboxObject.is_hidden_from_stream.is_(False), ) ) ) @@ -2206,7 +2207,7 @@ async def _handle_announce_activity( ) if not announced_actor.is_blocked: announced_object = RemoteObject(announced_raw_object, announced_actor) - announced_inbox_object = models.InboxObject( + announced_inbox_object = activitypub.models.InboxObject( server=urlparse(announced_object.ap_id).hostname, actor_id=announced_actor.id, ap_actor_id=announced_actor.ap_id, @@ -2232,10 +2233,10 @@ async def _handle_announce_activity( async def _handle_like_activity( db_session: AsyncSession, - actor: models.Actor, - like_activity: models.InboxObject, - relates_to_outbox_object: models.OutboxObject | None, - relates_to_inbox_object: models.InboxObject | None, + actor: activitypub.models.Actor, + like_activity: activitypub.models.InboxObject, + relates_to_outbox_object: activitypub.models.OutboxObject | None, + relates_to_inbox_object: activitypub.models.InboxObject | None, ): if not relates_to_outbox_object: logger.info( @@ -2261,8 +2262,8 @@ async def _handle_like_activity( async def _handle_block_activity( db_session: AsyncSession, - actor: models.Actor, - block_activity: models.InboxObject, + actor: activitypub.models.Actor, + block_activity: activitypub.models.InboxObject, ): if block_activity.activity_object_ap_id != LOCAL_ACTOR.ap_id: logger.warning( @@ -2285,7 +2286,7 @@ async def _handle_block_activity( async def _process_transient_object( db_session: AsyncSession, raw_object: ap.RawObject, - from_actor: models.Actor, + from_actor: activitypub.models.Actor, ) -> None: # TODO: track featured/pinned objects for actors ap_type = raw_object["type"] @@ -2383,8 +2384,8 @@ async def save_to_inbox( if ( await db_session.scalar( - select(func.count(models.InboxObject.id)).where( - models.InboxObject.ap_id == raw_object_id + select(func.count(activitypub.models.InboxObject.id)).where( + activitypub.models.InboxObject.ap_id == raw_object_id ) ) > 0 @@ -2400,8 +2401,8 @@ async def save_to_inbox( activity_ro = RemoteObject(raw_object, actor=actor) - relates_to_inbox_object: models.InboxObject | None = None - relates_to_outbox_object: models.OutboxObject | None = None + relates_to_inbox_object: activitypub.models.InboxObject | None = None + relates_to_outbox_object: activitypub.models.OutboxObject | None = None if activity_ro.activity_object_ap_id: if activity_ro.activity_object_ap_id.startswith(BASE_URL): relates_to_outbox_object = await get_outbox_object_by_ap_id( @@ -2414,7 +2415,7 @@ async def save_to_inbox( activity_ro.activity_object_ap_id, ) - inbox_object = models.InboxObject( + inbox_object = activitypub.models.InboxObject( server=urlparse(activity_ro.ap_id).hostname, actor_id=actor.id, ap_actor_id=actor.ap_id, @@ -2491,7 +2492,7 @@ async def save_to_inbox( db_session.add(notif) if activity_ro.ap_type == "Accept": - following = models.Following( + following = activitypub.models.Following( actor_id=actor.id, outbox_object_id=relates_to_outbox_object.id, ap_actor_id=actor.ap_id, @@ -2506,8 +2507,8 @@ async def save_to_inbox( elif activity_ro.ap_type == "Reject": maybe_following = ( await db_session.scalars( - select(models.Following).where( - models.Following.ap_actor_id == actor.ap_id, + select(activitypub.models.Following).where( + activitypub.models.Following.ap_actor_id == actor.ap_id, ) ) ).one_or_none() @@ -2563,7 +2564,7 @@ async def save_to_inbox( async def _prefetch_actor_outbox( db_session: AsyncSession, - actor: models.Actor, + actor: activitypub.models.Actor, ) -> None: """Try to fetch some notes to fill the stream""" saved = 0 @@ -2593,7 +2594,7 @@ async def _prefetch_actor_outbox( async def save_object_to_inbox( db_session: AsyncSession, raw_object: ap.RawObject, -) -> models.InboxObject: +) -> activitypub.models.InboxObject: """Used to save unknown object before intetacting with them, i.e. to like an object that was looked up, or prefill the inbox when an actor accepted a follow request.""" @@ -2605,7 +2606,7 @@ async def save_object_to_inbox( if "published" in ro.ap_object: ap_published_at = parse_isoformat(ro.ap_object["published"]) - inbox_object = models.InboxObject( + inbox_object = activitypub.models.InboxObject( server=urlparse(ro.ap_id).hostname, actor_id=obj_actor.id, ap_actor_id=obj_actor.ap_id, @@ -2631,9 +2632,9 @@ async def save_object_to_inbox( async def public_outbox_objects_count(db_session: AsyncSession) -> int: return await db_session.scalar( - select(func.count(models.OutboxObject.id)).where( - models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC, - models.OutboxObject.is_deleted.is_(False), + select(func.count(activitypub.models.OutboxObject.id)).where( + activitypub.models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC, + activitypub.models.OutboxObject.is_deleted.is_(False), ) ) @@ -2644,8 +2645,8 @@ async def fetch_actor_collection(db_session: AsyncSession, url: str) -> list[Act followers = ( ( await db_session.scalars( - select(models.Follower).options( - joinedload(models.Follower.actor) + select(activitypub.models.Follower).options( + joinedload(activitypub.models.Follower.actor) ) ) ) @@ -2697,23 +2698,23 @@ async def get_replies_tree( tree_nodes.extend( ( await db_session.scalars( - select(models.InboxObject) + select(activitypub.models.InboxObject) .where( ( - models.InboxObject.conversation - == requested_object.conversation + activitypub.models.InboxObject.conversation + == requested_object.conversation ) | ( - models.InboxObject.ap_context - == requested_object.conversation + activitypub.models.InboxObject.ap_context + == requested_object.conversation ), - models.InboxObject.ap_type.in_( + activitypub.models.InboxObject.ap_type.in_( ["Note", "Page", "Article", "Question"] ), - models.InboxObject.is_deleted.is_(False), - models.InboxObject.visibility.in_(allowed_visibility), + activitypub.models.InboxObject.is_deleted.is_(False), + activitypub.models.InboxObject.visibility.in_(allowed_visibility), ) - .options(joinedload(models.InboxObject.actor)) + .options(joinedload(activitypub.models.InboxObject.actor)) ) ) .unique() @@ -2723,20 +2724,20 @@ async def get_replies_tree( tree_nodes.extend( ( await db_session.scalars( - select(models.OutboxObject) + select(activitypub.models.OutboxObject) .where( - models.OutboxObject.conversation + activitypub.models.OutboxObject.conversation == requested_object.conversation, - models.OutboxObject.is_deleted.is_(False), - models.OutboxObject.ap_type.in_( + activitypub.models.OutboxObject.is_deleted.is_(False), + activitypub.models.OutboxObject.ap_type.in_( ["Note", "Page", "Article", "Question"] ), - models.OutboxObject.visibility.in_(allowed_visibility), + activitypub.models.OutboxObject.visibility.in_(allowed_visibility), ) .options( joinedload( - models.OutboxObject.outbox_object_attachments - ).options(joinedload(models.OutboxObjectAttachment.upload)) + activitypub.models.OutboxObject.outbox_object_attachments + ).options(joinedload(activitypub.models.OutboxObjectAttachment.upload)) ) ) ) diff --git a/app/incoming_activities.py b/activitypub/incoming_activities.py similarity index 80% rename from app/incoming_activities.py rename to activitypub/incoming_activities.py index 94dd0d6e..e214f66c 100644 --- a/app/incoming_activities.py +++ b/activitypub/incoming_activities.py @@ -7,11 +7,12 @@ from sqlalchemy import func from sqlalchemy import select -from app import activitypub as ap +import activitypub.models +from activitypub import activitypub as ap from app import httpsig from app import ldsig from app import models -from app.boxes import save_to_inbox +from activitypub.boxes import save_to_inbox from app.database import AsyncSession from app.utils.datetime import now from app.utils.workers import Worker @@ -23,7 +24,7 @@ async def new_ap_incoming_activity( db_session: AsyncSession, httpsig_info: httpsig.HTTPSigInfo, raw_object: ap.RawObject, -) -> models.IncomingActivity | None: +) -> activitypub.models.IncomingActivity | None: ap_id: str if "id" not in raw_object or ap.as_list(raw_object["type"])[0] in ap.ACTOR_TYPES: if "@context" not in raw_object: @@ -37,7 +38,7 @@ async def new_ap_incoming_activity( # TODO(ts): dedup first - incoming_activity = models.IncomingActivity( + incoming_activity = activitypub.models.IncomingActivity( sent_by_ap_actor_id=httpsig_info.signed_by_ap_actor_id, ap_id=ap_id, ap_object=raw_object, @@ -54,7 +55,7 @@ def _exp_backoff(tries: int) -> datetime: def _set_next_try( - outgoing_activity: models.IncomingActivity, + outgoing_activity: activitypub.models.IncomingActivity, next_try: datetime | None = None, ) -> None: if not outgoing_activity.tries: @@ -69,14 +70,14 @@ def _set_next_try( async def fetch_next_incoming_activity( db_session: AsyncSession, -) -> models.IncomingActivity | None: +) -> activitypub.models.IncomingActivity | None: where = [ - models.IncomingActivity.next_try <= now(), - models.IncomingActivity.is_errored.is_(False), - models.IncomingActivity.is_processed.is_(False), + activitypub.models.IncomingActivity.next_try <= now(), + activitypub.models.IncomingActivity.is_errored.is_(False), + activitypub.models.IncomingActivity.is_processed.is_(False), ] q_count = await db_session.scalar( - select(func.count(models.IncomingActivity.id)).where(*where) + select(func.count(activitypub.models.IncomingActivity.id)).where(*where) ) if q_count > 0: logger.info(f"{q_count} incoming activities ready to process") @@ -86,10 +87,10 @@ async def fetch_next_incoming_activity( next_activity = ( await db_session.execute( - select(models.IncomingActivity) + select(activitypub.models.IncomingActivity) .where(*where) .limit(1) - .order_by(models.IncomingActivity.next_try.asc()) + .order_by(activitypub.models.IncomingActivity.next_try.asc()) ) ).scalar_one() @@ -98,7 +99,7 @@ async def fetch_next_incoming_activity( async def process_next_incoming_activity( db_session: AsyncSession, - next_activity: models.IncomingActivity, + next_activity: activitypub.models.IncomingActivity, ) -> None: logger.info( f"incoming_activity={next_activity.ap_object}/" @@ -142,18 +143,18 @@ async def process_next_incoming_activity( return None -class IncomingActivityWorker(Worker[models.IncomingActivity]): +class IncomingActivityWorker(Worker[activitypub.models.IncomingActivity]): async def process_message( self, db_session: AsyncSession, - next_activity: models.IncomingActivity, + next_activity: activitypub.models.IncomingActivity, ) -> None: await process_next_incoming_activity(db_session, next_activity) async def get_next_message( self, db_session: AsyncSession, - ) -> models.IncomingActivity | None: + ) -> activitypub.models.IncomingActivity | None: return await fetch_next_incoming_activity(db_session) diff --git a/activitypub/models.py b/activitypub/models.py new file mode 100644 index 00000000..44b8d59e --- /dev/null +++ b/activitypub/models.py @@ -0,0 +1,452 @@ +from typing import Optional, Any, Union + +from sqlalchemy import Column, Integer, DateTime, String, JSON, Boolean, ForeignKey, Enum, UniqueConstraint, Index, text +from sqlalchemy.orm import Mapped, relationship + +from activitypub import activitypub as ap +from activitypub.actor import Actor as BaseActor, LOCAL_ACTOR +from activitypub.ap_object import Object as BaseObject, Attachment +from app.config import BASE_URL +from app.database import Base +from app.utils.datetime import now + + +class Actor(Base, BaseActor): + __tablename__ = "actor" + + id = Column(Integer, primary_key=True, index=True) + created_at = Column(DateTime(timezone=True), nullable=False, default=now) + updated_at = Column(DateTime(timezone=True), nullable=False, default=now) + + ap_id: Mapped[str] = Column(String, unique=True, nullable=False, index=True) + ap_actor: Mapped[ap.RawObject] = Column(JSON, nullable=False) + ap_type = Column(String, nullable=False) + + 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") + + are_announces_hidden_from_stream = Column( + Boolean, nullable=False, default=False, server_default="0" + ) + + @property + def is_from_db(self) -> bool: + return True + + +class InboxObject(Base, BaseObject): + __tablename__ = "inbox" + + id = Column(Integer, primary_key=True, index=True) + created_at = Column(DateTime(timezone=True), nullable=False, default=now) + updated_at = Column(DateTime(timezone=True), nullable=False, default=now) + + actor_id = Column(Integer, ForeignKey("actor.id"), nullable=False) + actor: Mapped[Actor] = relationship(Actor, uselist=False) + + server = Column(String, nullable=False) + + is_hidden_from_stream = Column(Boolean, nullable=False, default=False) + + ap_actor_id = Column(String, nullable=False) + ap_type = Column(String, nullable=False, index=True) + ap_id: Mapped[str] = Column(String, nullable=False, unique=True, index=True) + ap_context = Column(String, nullable=True) + ap_published_at = Column(DateTime(timezone=True), nullable=False) + ap_object: Mapped[ap.RawObject] = Column(JSON, nullable=False) + + # Only set for activities + activity_object_ap_id = Column(String, nullable=True, index=True) + + visibility = Column(Enum(ap.VisibilityEnum), nullable=False) + conversation = Column(String, nullable=True) + + has_local_mention = Column( + Boolean, nullable=False, default=False, server_default="0" + ) + + # Used for Like, Announce and Undo activities + relates_to_inbox_object_id = Column( + Integer, + ForeignKey("inbox.id"), + nullable=True, + ) + relates_to_inbox_object: Mapped[Optional["InboxObject"]] = relationship( + "InboxObject", + foreign_keys=relates_to_inbox_object_id, + remote_side=id, + uselist=False, + ) + relates_to_outbox_object_id = Column( + Integer, + ForeignKey("outbox.id"), + nullable=True, + ) + relates_to_outbox_object: Mapped[Optional["OutboxObject"]] = relationship( + "OutboxObject", + foreign_keys=[relates_to_outbox_object_id], + uselist=False, + ) + + undone_by_inbox_object_id = Column(Integer, ForeignKey("inbox.id"), nullable=True) + + # Link the oubox AP ID to allow undo without any extra query + liked_via_outbox_object_ap_id = Column(String, nullable=True) + announced_via_outbox_object_ap_id = Column(String, nullable=True) + voted_for_answers: Mapped[list[str] | None] = Column(JSON, nullable=True) + + is_bookmarked = Column(Boolean, nullable=False, default=False) + + # Used to mark deleted objects, but also activities that were undone + is_deleted = Column(Boolean, nullable=False, default=False) + is_transient = Column(Boolean, nullable=False, default=False, server_default="0") + + replies_count: Mapped[int] = Column(Integer, nullable=False, default=0) + + og_meta: Mapped[list[dict[str, Any]] | None] = Column(JSON, nullable=True) + + @property + def relates_to_anybox_object(self) -> Union["InboxObject", "OutboxObject"] | None: + if self.relates_to_inbox_object_id: + return self.relates_to_inbox_object + elif self.relates_to_outbox_object_id: + return self.relates_to_outbox_object + else: + return None + + @property + def is_from_db(self) -> bool: + return True + + @property + def is_from_inbox(self) -> bool: + return True + + +class OutboxObject(Base, BaseObject): + __tablename__ = "outbox" + + id = Column(Integer, primary_key=True, index=True) + created_at = Column(DateTime(timezone=True), nullable=False, default=now) + updated_at = Column(DateTime(timezone=True), nullable=False, default=now) + + is_hidden_from_homepage = Column(Boolean, nullable=False, default=False) + + public_id = Column(String, nullable=False, index=True) + slug = Column(String, nullable=True, index=True) + + ap_type = Column(String, nullable=False, index=True) + ap_id: Mapped[str] = Column(String, nullable=False, unique=True, index=True) + ap_context = Column(String, nullable=True) + ap_object: Mapped[ap.RawObject] = Column(JSON, nullable=False) + + activity_object_ap_id = Column(String, nullable=True, index=True) + + # Source content for activities (like Notes) + source = Column(String, nullable=True) + revisions: Mapped[list[dict[str, Any]] | None] = Column(JSON, nullable=True) + + ap_published_at = Column(DateTime(timezone=True), nullable=False, default=now) + visibility = Column(Enum(ap.VisibilityEnum), nullable=False) + conversation = Column(String, nullable=True) + + likes_count = Column(Integer, nullable=False, default=0) + announces_count = Column(Integer, nullable=False, default=0) + replies_count: Mapped[int] = Column(Integer, nullable=False, default=0) + webmentions_count: Mapped[int] = Column( + Integer, nullable=False, default=0, server_default="0" + ) + # reactions: Mapped[list[dict[str, Any]] | None] = Column(JSON, nullable=True) + + og_meta: Mapped[list[dict[str, Any]] | None] = Column(JSON, nullable=True) + + # For the featured collection + is_pinned = Column(Boolean, nullable=False, default=False) + is_transient = Column(Boolean, nullable=False, default=False, server_default="0") + + # Never actually delete from the outbox + is_deleted = Column(Boolean, nullable=False, default=False) + + # Used for Create, Like, Announce and Undo activities + relates_to_inbox_object_id = Column( + Integer, + ForeignKey("inbox.id"), + nullable=True, + ) + relates_to_inbox_object: Mapped[Optional["InboxObject"]] = relationship( + "InboxObject", + foreign_keys=[relates_to_inbox_object_id], + uselist=False, + ) + relates_to_outbox_object_id = Column( + Integer, + ForeignKey("outbox.id"), + nullable=True, + ) + relates_to_outbox_object: Mapped[Optional["OutboxObject"]] = relationship( + "OutboxObject", + foreign_keys=[relates_to_outbox_object_id], + remote_side=id, + uselist=False, + ) + # For Follow activies + relates_to_actor_id = Column( + Integer, + ForeignKey("actor.id"), + nullable=True, + ) + relates_to_actor: Mapped[Optional["Actor"]] = relationship( + "Actor", + foreign_keys=[relates_to_actor_id], + uselist=False, + ) + + undone_by_outbox_object_id = Column(Integer, ForeignKey("outbox.id"), nullable=True) + + @property + def actor(self) -> BaseActor: + return LOCAL_ACTOR + + outbox_object_attachments: Mapped[list["OutboxObjectAttachment"]] = relationship( + "OutboxObjectAttachment", uselist=True, backref="outbox_object" + ) + + @property + def attachments(self) -> list[Attachment]: + out = [] + for attachment in self.outbox_object_attachments: + url = ( + BASE_URL + + f"/attachments/{attachment.upload.content_hash}/{attachment.filename}" + ) + out.append( + Attachment.parse_obj( + { + "type": "Document", + "mediaType": attachment.upload.content_type, + "name": attachment.alt or attachment.filename, + "url": url, + "width": attachment.upload.width, + "height": attachment.upload.height, + "proxiedUrl": url, + "resizedUrl": ( + BASE_URL + + ( + "/attachments/thumbnails/" + f"{attachment.upload.content_hash}" + f"/{attachment.filename}" + ) + if attachment.upload.has_thumbnail + else None + ), + } + ) + ) + return out + + @property + def relates_to_anybox_object(self) -> Union["InboxObject", "OutboxObject"] | None: + if self.relates_to_inbox_object_id: + return self.relates_to_inbox_object + elif self.relates_to_outbox_object_id: + return self.relates_to_outbox_object + else: + return None + + @property + def is_from_db(self) -> bool: + return True + + @property + def is_from_outbox(self) -> bool: + return True + + @property + def url(self) -> str | None: + # XXX: rewrite old URL here for compat + if self.ap_type == "Article" and self.slug and self.public_id: + return f"{BASE_URL}/articles/{self.public_id[:7]}/{self.slug}" + return super().url + + +class Follower(Base): + __tablename__ = "follower" + + id = Column(Integer, primary_key=True, index=True) + created_at = Column(DateTime(timezone=True), nullable=False, default=now) + updated_at = Column(DateTime(timezone=True), nullable=False, default=now) + + actor_id = Column(Integer, ForeignKey("actor.id"), nullable=False, unique=True) + actor: Mapped[Actor] = relationship(Actor, uselist=False) + + inbox_object_id = Column(Integer, ForeignKey("inbox.id"), nullable=False) + inbox_object = relationship(InboxObject, uselist=False) + + ap_actor_id = Column(String, nullable=False, unique=True) + + +class Following(Base): + __tablename__ = "following" + + id = Column(Integer, primary_key=True, index=True) + created_at = Column(DateTime(timezone=True), nullable=False, default=now) + updated_at = Column(DateTime(timezone=True), nullable=False, default=now) + + actor_id = Column(Integer, ForeignKey("actor.id"), nullable=False, unique=True) + actor = relationship(Actor, uselist=False) + + outbox_object_id = Column(Integer, ForeignKey("outbox.id"), nullable=False) + outbox_object = relationship(OutboxObject, uselist=False) + + ap_actor_id = Column(String, nullable=False, unique=True) + + +class IncomingActivity(Base): + __tablename__ = "incoming_activity" + + id = Column(Integer, primary_key=True, index=True) + created_at = Column(DateTime(timezone=True), nullable=False, default=now) + + # An incoming activity can be a webmention + webmention_source = Column(String, nullable=True) + # or an AP object + sent_by_ap_actor_id = Column(String, nullable=True) + ap_id = Column(String, nullable=True, index=True) + ap_object: Mapped[ap.RawObject] = Column(JSON, nullable=True) + + tries: Mapped[int] = Column(Integer, nullable=False, default=0) + next_try = Column(DateTime(timezone=True), nullable=True, default=now) + + last_try = Column(DateTime(timezone=True), nullable=True) + + is_processed = Column(Boolean, nullable=False, default=False) + is_errored = Column(Boolean, nullable=False, default=False) + error = Column(String, nullable=True) + + +class OutgoingActivity(Base): + __tablename__ = "outgoing_activity" + + id = Column(Integer, primary_key=True, index=True) + created_at = Column(DateTime(timezone=True), nullable=False, default=now) + + recipient = Column(String, nullable=False) + + outbox_object_id = Column(Integer, ForeignKey("outbox.id"), nullable=True) + outbox_object = relationship(OutboxObject, uselist=False) + + # Can also reference an inbox object if it needds to be forwarded + inbox_object_id = Column(Integer, ForeignKey("inbox.id"), nullable=True) + inbox_object = relationship(InboxObject, uselist=False) + + # The source will be the outbox object URL + webmention_target = Column(String, nullable=True) + + tries = Column(Integer, nullable=False, default=0) + next_try = Column(DateTime(timezone=True), nullable=True, default=now) + + last_try = Column(DateTime(timezone=True), nullable=True) + last_status_code = Column(Integer, nullable=True) + last_response = Column(String, nullable=True) + + is_sent = Column(Boolean, nullable=False, default=False) + is_errored = Column(Boolean, nullable=False, default=False) + error = Column(String, nullable=True) + + @property + def anybox_object(self) -> OutboxObject | InboxObject: + if self.outbox_object_id: + return self.outbox_object # type: ignore + elif self.inbox_object_id: + return self.inbox_object # type: ignore + else: + raise ValueError("Should never happen") + + +class TaggedOutboxObject(Base): + __tablename__ = "tagged_outbox_object" + __table_args__ = ( + UniqueConstraint("outbox_object_id", "tag", name="uix_tagged_object"), + ) + + id = Column(Integer, primary_key=True, index=True) + + outbox_object_id = Column(Integer, ForeignKey("outbox.id"), nullable=False) + outbox_object = relationship(OutboxObject, uselist=False) + + tag = Column(String, nullable=False, index=True) + + +class Upload(Base): + __tablename__ = "upload" + + id = Column(Integer, primary_key=True, index=True) + created_at = Column(DateTime(timezone=True), nullable=False, default=now) + + content_type: Mapped[str] = Column(String, nullable=False) + content_hash = Column(String, nullable=False, unique=True) + + has_thumbnail = Column(Boolean, nullable=False) + + # Only set for images + blurhash = Column(String, nullable=True) + width = Column(Integer, nullable=True) + height = Column(Integer, nullable=True) + + @property + def is_image(self) -> bool: + return self.content_type.startswith("image") + + +class OutboxObjectAttachment(Base): + __tablename__ = "outbox_object_attachment" + + id = Column(Integer, primary_key=True, index=True) + created_at = Column(DateTime(timezone=True), nullable=False, default=now) + filename = Column(String, nullable=False) + alt = Column(String, nullable=True) + + outbox_object_id = Column(Integer, ForeignKey("outbox.id"), nullable=False) + + upload_id = Column(Integer, ForeignKey("upload.id"), nullable=False) + upload: Mapped["Upload"] = relationship(Upload, uselist=False) + + +class PollAnswer(Base): + __tablename__ = "poll_answer" + __table_args__ = ( + # Enforce a single answer for poll/actor/answer + UniqueConstraint( + "outbox_object_id", + "name", + "actor_id", + name="uix_outbox_object_id_name_actor_id", + ), + # Enforce an actor can only vote once on a "oneOf" Question + Index( + "uix_one_of_outbox_object_id_actor_id", + "outbox_object_id", + "actor_id", + unique=True, + sqlite_where=text('poll_type = "oneOf"'), + ), + ) + + id = Column(Integer, primary_key=True, index=True) + created_at = Column(DateTime(timezone=True), nullable=False, default=now) + + outbox_object_id = Column(Integer, ForeignKey("outbox.id"), nullable=False) + outbox_object = relationship(OutboxObject, uselist=False) + + # oneOf|anyOf + poll_type = Column(String, nullable=False) + + inbox_object_id = Column(Integer, ForeignKey("inbox.id"), nullable=False) + inbox_object = relationship(InboxObject, uselist=False) + + actor_id = Column(Integer, ForeignKey("actor.id"), nullable=False) + actor = relationship(Actor, uselist=False) + + name = Column(String, nullable=False) diff --git a/app/outgoing_activities.py b/activitypub/outgoing_activities.py similarity index 86% rename from app/outgoing_activities.py rename to activitypub/outgoing_activities.py index 022334d5..65baa3d5 100644 --- a/app/outgoing_activities.py +++ b/activitypub/outgoing_activities.py @@ -13,12 +13,13 @@ from sqlalchemy import select from sqlalchemy.orm import joinedload -from app import activitypub as ap +import activitypub.models +from activitypub import activitypub as ap from app import config from app import ldsig from app import models -from app.actor import LOCAL_ACTOR -from app.actor import _actor_hash +from activitypub.actor import LOCAL_ACTOR +from activitypub.actor import _actor_hash from app.config import KEY_PATH from app.database import AsyncSession from app.key import Key @@ -66,10 +67,10 @@ async def _send_actor_update_if_needed( logger.info("Will send an Update for the local actor") - from app.boxes import allocate_outbox_id - from app.boxes import compute_all_known_recipients - from app.boxes import outbox_object_id - from app.boxes import save_outbox_object + from activitypub.boxes import allocate_outbox_id + from activitypub.boxes import compute_all_known_recipients + from activitypub.boxes import outbox_object_id + from activitypub.boxes import save_outbox_object update_activity_id = allocate_outbox_id() update_activity = { @@ -103,7 +104,7 @@ async def new_outgoing_activity( outbox_object_id: int | None = None, inbox_object_id: int | None = None, webmention_target: str | None = None, -) -> models.OutgoingActivity: +) -> activitypub.models.OutgoingActivity: if outbox_object_id is None and inbox_object_id is None: raise ValueError("Must reference at least one inbox/outbox activity") if webmention_target and outbox_object_id is None: @@ -111,7 +112,7 @@ async def new_outgoing_activity( if outbox_object_id and inbox_object_id: raise ValueError("Cannot reference both inbox/outbox activities") - outgoing_activity = models.OutgoingActivity( + outgoing_activity = activitypub.models.OutgoingActivity( recipient=recipient, outbox_object_id=outbox_object_id, inbox_object_id=inbox_object_id, @@ -145,7 +146,7 @@ def _exp_backoff(tries: int) -> datetime: def _set_next_try( - outgoing_activity: models.OutgoingActivity, + outgoing_activity: activitypub.models.OutgoingActivity, next_try: datetime | None = None, ) -> None: if not outgoing_activity.tries: @@ -160,14 +161,14 @@ def _set_next_try( async def fetch_next_outgoing_activity( db_session: AsyncSession, -) -> models.OutgoingActivity | None: +) -> activitypub.models.OutgoingActivity | None: where = [ - models.OutgoingActivity.next_try <= now(), - models.OutgoingActivity.is_errored.is_(False), - models.OutgoingActivity.is_sent.is_(False), + activitypub.models.OutgoingActivity.next_try <= now(), + activitypub.models.OutgoingActivity.is_errored.is_(False), + activitypub.models.OutgoingActivity.is_sent.is_(False), ] q_count = await db_session.scalar( - select(func.count(models.OutgoingActivity.id)).where(*where) + select(func.count(activitypub.models.OutgoingActivity.id)).where(*where) ) if q_count > 0: logger.info(f"{q_count} outgoing activities ready to process") @@ -177,14 +178,14 @@ async def fetch_next_outgoing_activity( next_activity = ( await db_session.execute( - select(models.OutgoingActivity) + select(activitypub.models.OutgoingActivity) .where(*where) .limit(1) .options( - joinedload(models.OutgoingActivity.inbox_object), - joinedload(models.OutgoingActivity.outbox_object), + joinedload(activitypub.models.OutgoingActivity.inbox_object), + joinedload(activitypub.models.OutgoingActivity.outbox_object), ) - .order_by(models.OutgoingActivity.next_try) + .order_by(activitypub.models.OutgoingActivity.next_try) ) ).scalar_one() return next_activity @@ -192,7 +193,7 @@ async def fetch_next_outgoing_activity( async def process_next_outgoing_activity( db_session: AsyncSession, - next_activity: models.OutgoingActivity, + next_activity: activitypub.models.OutgoingActivity, ) -> None: next_activity.tries = next_activity.tries + 1 # type: ignore next_activity.last_try = now() @@ -269,18 +270,18 @@ async def process_next_outgoing_activity( return None -class OutgoingActivityWorker(Worker[models.OutgoingActivity]): +class OutgoingActivityWorker(Worker[activitypub.models.OutgoingActivity]): async def process_message( self, db_session: AsyncSession, - next_activity: models.OutgoingActivity, + next_activity: activitypub.models.OutgoingActivity, ) -> None: await process_next_outgoing_activity(db_session, next_activity) async def get_next_message( self, db_session: AsyncSession, - ) -> models.OutgoingActivity | None: + ) -> activitypub.models.OutgoingActivity | None: return await fetch_next_outgoing_activity(db_session) async def startup(self, db_session: AsyncSession) -> None: diff --git a/activitypub/tests/__init__.py b/activitypub/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/activitypub/tests/conftest.py b/activitypub/tests/conftest.py new file mode 100644 index 00000000..87d15530 --- /dev/null +++ b/activitypub/tests/conftest.py @@ -0,0 +1,42 @@ +import os +from typing import Generator + +import pytest +import pytest_asyncio +from fastapi.testclient import TestClient + +from app.database import Base +from app.database import async_engine +from app.database import async_session +from app.database import engine +from app.main import app +from activitypub.tests.factories import _Session + +os.environ["MICROBLOGPUB_CONFIG_FILE"] = "tests.toml" + + +@pytest_asyncio.fixture +async def async_db_session(): + async with async_session() as session: + async with async_engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + yield session + async with async_engine.begin() as conn: + await conn.run_sync(Base.metadata.drop_all) + + +@pytest.fixture +def db() -> Generator: + Base.metadata.create_all(bind=engine) + with _Session() as db_session: + try: + yield db_session + finally: + db_session.close() + Base.metadata.drop_all(bind=engine) + + +@pytest.fixture +def client(db) -> Generator: + with TestClient(app) as c: + yield c diff --git a/tests/factories.py b/activitypub/tests/factories.py similarity index 92% rename from tests/factories.py rename to activitypub/tests/factories.py index e8056470..6a2c54f0 100644 --- a/tests/factories.py +++ b/activitypub/tests/factories.py @@ -6,11 +6,10 @@ from dateutil.parser import isoparse from sqlalchemy import orm -from app import activitypub as ap -from app import actor -from app import models -from app.actor import RemoteActor -from app.ap_object import RemoteObject +import activitypub.models +from activitypub import activitypub as ap, actor +from activitypub.actor import RemoteActor +from activitypub.ap_object import RemoteObject from app.database import SessionLocal from app.utils.datetime import now @@ -37,7 +36,7 @@ def build_follow_activity( def build_delete_activity( - from_remote_actor: actor.RemoteActor | models.Actor, + from_remote_actor: actor.RemoteActor | activitypub.models.Actor, deleted_object_ap_id: str, outbox_public_id: str | None = None, ) -> ap.RawObject: @@ -98,7 +97,7 @@ def build_move_activity( def build_note_object( - from_remote_actor: actor.RemoteActor | models.Actor, + from_remote_actor: actor.RemoteActor | activitypub.models.Actor, outbox_public_id: str | None = None, content: str = "Hello", to: list[str] | None = None, @@ -191,7 +190,7 @@ class Params: class ActorFactory(factory.alchemy.SQLAlchemyModelFactory): class Meta(BaseModelMeta): - model = models.Actor + model = activitypub.models.Actor # ap_actor # ap_id @@ -208,7 +207,7 @@ def from_remote_actor(cls, ra): class OutboxObjectFactory(factory.alchemy.SQLAlchemyModelFactory): class Meta(BaseModelMeta): - model = models.OutboxObject + model = activitypub.models.OutboxObject # public_id # relates_to_inbox_object_id @@ -231,7 +230,7 @@ def from_remote_object(cls, public_id, ro): class OutgoingActivityFactory(factory.alchemy.SQLAlchemyModelFactory): class Meta(BaseModelMeta): - model = models.OutgoingActivity + model = activitypub.models.OutgoingActivity # recipient # outbox_object_id @@ -239,13 +238,13 @@ class Meta(BaseModelMeta): class InboxObjectFactory(factory.alchemy.SQLAlchemyModelFactory): class Meta(BaseModelMeta): - model = models.InboxObject + model = activitypub.models.InboxObject @classmethod def from_remote_object( cls, ro: RemoteObject, - actor: models.Actor, + actor: activitypub.models.Actor, relates_to_inbox_object_id: int | None = None, relates_to_outbox_object_id: int | None = None, ): @@ -272,9 +271,9 @@ def from_remote_object( class FollowerFactory(factory.alchemy.SQLAlchemyModelFactory): class Meta(BaseModelMeta): - model = models.Follower + model = activitypub.models.Follower class FollowingFactory(factory.alchemy.SQLAlchemyModelFactory): class Meta(BaseModelMeta): - model = models.Following + model = activitypub.models.Following diff --git a/tests/test_actor.py b/activitypub/tests/test_actor.py similarity index 77% rename from tests/test_actor.py rename to activitypub/tests/test_actor.py index 487f28f7..141002ca 100644 --- a/tests/test_actor.py +++ b/activitypub/tests/test_actor.py @@ -5,10 +5,10 @@ from sqlalchemy import select from sqlalchemy.orm import Session -from app import models -from app.actor import fetch_actor -from app.database import AsyncSession -from tests import factories +import activitypub.models +from activitypub.actor import fetch_actor +from sqlalchemy.ext.asyncio import AsyncSession +from activitypub.tests import factories @pytest.mark.asyncio @@ -31,7 +31,7 @@ async def test_fetch_actor(async_db_session: AsyncSession, respx_mock) -> None: # Then it has been fetched and saved in DB assert respx.calls.call_count == 2 assert ( - await async_db_session.execute(select(models.Actor)) + await async_db_session.execute(select(activitypub.models.Actor)) ).scalar_one().ap_id == saved_actor.ap_id # When fetching it a second time @@ -40,9 +40,10 @@ async def test_fetch_actor(async_db_session: AsyncSession, respx_mock) -> None: # Then it's read from the DB assert actor_from_db.ap_id == ra.ap_id assert ( - await async_db_session.execute(select(func.count(models.Actor.id))) + await async_db_session.execute(select(func.count(activitypub.models.Actor.id))) ).scalar_one() == 1 assert respx.calls.call_count == 2 + await async_db_session.close() def test_sqlalchemy_factory(db: Session) -> None: @@ -56,4 +57,5 @@ def test_sqlalchemy_factory(db: Session) -> None: ap_actor=ra.ap_actor, ap_id=ra.ap_id, ) - assert actor_in_db.id == db.execute(select(models.Actor)).scalar_one().id + assert actor_in_db.id == db.execute(select(activitypub.models.Actor)).scalar_one().id + db.close() diff --git a/tests/test_inbox.py b/activitypub/tests/test_inbox.py similarity index 81% rename from tests/test_inbox.py rename to activitypub/tests/test_inbox.py index 325ab7b6..18a9096e 100644 --- a/tests/test_inbox.py +++ b/activitypub/tests/test_inbox.py @@ -8,11 +8,12 @@ from sqlalchemy import select from sqlalchemy.orm import Session -from app import activitypub as ap +import activitypub.models +from activitypub import activitypub as ap from app import models -from app.actor import LOCAL_ACTOR -from app.ap_object import RemoteObject -from tests import factories +from activitypub.actor import LOCAL_ACTOR +from activitypub.ap_object import RemoteObject +from activitypub.tests import factories from tests.utils import mock_httpsig_checker from tests.utils import run_process_next_incoming_activity from tests.utils import setup_inbox_delete @@ -67,26 +68,26 @@ def test_inbox_incoming_follow_request( run_process_next_incoming_activity() # And the actor was saved in DB - saved_actor = db.execute(select(models.Actor)).scalar_one() + saved_actor = db.execute(select(activitypub.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() + inbox_object = db.execute(select(activitypub.models.InboxObject)).scalar_one() assert inbox_object.ap_object == follow_activity.ap_object # And a follower was internally created - follower = db.execute(select(models.Follower)).scalar_one() + follower = db.execute(select(activitypub.models.Follower)).scalar_one() assert follower.ap_actor_id == ra.ap_id assert follower.actor_id == saved_actor.id assert follower.inbox_object_id == inbox_object.id # And an Accept activity was created in the outbox - outbox_object = db.execute(select(models.OutboxObject)).scalar_one() + outbox_object = db.execute(select(activitypub.models.OutboxObject)).scalar_one() assert outbox_object.ap_type == "Accept" assert outbox_object.activity_object_ap_id == follow_activity.ap_id # And an outgoing activity was created to track the Accept activity delivery - outgoing_activity = db.execute(select(models.OutgoingActivity)).scalar_one() + outgoing_activity = db.execute(select(activitypub.models.OutgoingActivity)).scalar_one() assert outgoing_activity.outbox_object_id == outbox_object.id @@ -121,19 +122,19 @@ def test_inbox_incoming_follow_request__manually_approves_followers( # Then the server returns a 202 assert response.status_code == 202 - with mock.patch("app.boxes.MANUALLY_APPROVES_FOLLOWERS", True): + with mock.patch("activitypub.boxes.MANUALLY_APPROVES_FOLLOWERS", True): run_process_next_incoming_activity() # And the actor was saved in DB - saved_actor = db.execute(select(models.Actor)).scalar_one() + saved_actor = db.execute(select(activitypub.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() + inbox_object = db.execute(select(activitypub.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 + assert db.scalar(select(func.count(activitypub.models.Follower.id))) == 0 def test_inbox_accept_follow_request( @@ -180,13 +181,13 @@ def test_inbox_accept_follow_request( run_process_next_incoming_activity() # And the Accept activity was saved in the inbox - inbox_activity = db.execute(select(models.InboxObject)).scalar_one() + inbox_activity = db.execute(select(activitypub.models.InboxObject)).scalar_one() assert inbox_activity.ap_type == "Accept" assert inbox_activity.relates_to_outbox_object_id == outbox_object.id assert inbox_activity.actor_id == actor_in_db.id # And a following entry was created internally - following = db.execute(select(models.Following)).scalar_one() + following = db.execute(select(activitypub.models.Following)).scalar_one() assert following.ap_actor_id == actor_in_db.ap_id @@ -227,15 +228,15 @@ def test_inbox__create_from_follower( run_process_next_incoming_activity() # Then the Create activity was saved - create_activity_from_inbox: models.InboxObject | None = db.execute( - select(models.InboxObject).where(models.InboxObject.ap_type == "Create") + create_activity_from_inbox: activitypub.models.InboxObject | None = db.execute( + select(activitypub.models.InboxObject).where(activitypub.models.InboxObject.ap_type == "Create") ).scalar_one_or_none() assert create_activity_from_inbox assert create_activity_from_inbox.ap_id == ro.ap_id # And the Note object was created - note_activity_from_inbox: models.InboxObject | None = db.execute( - select(models.InboxObject).where(models.InboxObject.ap_type == "Note") + note_activity_from_inbox: activitypub.models.InboxObject | None = db.execute( + select(activitypub.models.InboxObject).where(activitypub.models.InboxObject.ap_type == "Note") ).scalar_one_or_none() assert note_activity_from_inbox assert note_activity_from_inbox.ap_id == ro.activity_object_ap_id @@ -281,8 +282,8 @@ def test_inbox__create_already_deleted_object( run_process_next_incoming_activity() # Then the Create activity was saved - create_activity_from_inbox: models.InboxObject | None = db.execute( - select(models.InboxObject).where(models.InboxObject.ap_type == "Create") + create_activity_from_inbox: activitypub.models.InboxObject | None = db.execute( + select(activitypub.models.InboxObject).where(activitypub.models.InboxObject.ap_type == "Create") ).scalar_one_or_none() assert create_activity_from_inbox assert create_activity_from_inbox.ap_id == ro.ap_id @@ -292,7 +293,7 @@ def test_inbox__create_already_deleted_object( # And the Note wasn't created assert ( db.execute( - select(models.InboxObject).where(models.InboxObject.ap_type == "Note") + select(activitypub.models.InboxObject).where(activitypub.models.InboxObject.ap_type == "Note") ).scalar_one_or_none() is None ) @@ -339,8 +340,8 @@ def test_inbox__actor_is_blocked( # Then the Create activity was discarded assert ( db.scalar( - select(func.count(models.InboxObject.id)).where( - models.InboxObject.ap_type != "Follow" + select(func.count(activitypub.models.InboxObject.id)).where( + activitypub.models.InboxObject.ap_type != "Follow" ) ) == 0 @@ -386,19 +387,19 @@ def test_inbox__move_activity( run_process_next_incoming_activity() # And the Move activity was saved in the inbox - inbox_activity = db.execute(select(models.InboxObject)).scalar_one() + inbox_activity = db.execute(select(activitypub.models.InboxObject)).scalar_one() assert inbox_activity.ap_type == "Move" assert inbox_activity.actor_id == old_actor.id # And the following actor was deleted - assert db.scalar(select(func.count(models.Following.id))) == 0 + assert db.scalar(select(func.count(activitypub.models.Following.id))) == 0 # And the follow was undone assert ( db.scalar( - select(func.count(models.OutboxObject.id)).where( - models.OutboxObject.ap_type == "Undo", - models.OutboxObject.activity_object_ap_id == follow_id, + select(func.count(activitypub.models.OutboxObject.id)).where( + activitypub.models.OutboxObject.ap_type == "Undo", + activitypub.models.OutboxObject.activity_object_ap_id == follow_id, ) ) == 1 @@ -407,9 +408,9 @@ def test_inbox__move_activity( # And the new account was followed assert ( db.scalar( - select(func.count(models.OutboxObject.id)).where( - models.OutboxObject.ap_type == "Follow", - models.OutboxObject.activity_object_ap_id == new_ra.ap_id, + select(func.count(activitypub.models.OutboxObject.id)).where( + activitypub.models.OutboxObject.ap_type == "Follow", + activitypub.models.OutboxObject.activity_object_ap_id == new_ra.ap_id, ) ) == 1 @@ -457,12 +458,12 @@ def test_inbox__block_activity( run_process_next_incoming_activity() # And the actor was saved in DB - saved_actor = db.execute(select(models.Actor)).scalar_one() + saved_actor = db.execute(select(activitypub.models.Actor)).scalar_one() assert saved_actor.ap_id == ra.ap_id # And the Block activity was saved in the inbox inbox_activity = db.execute( - select(models.InboxObject).where(models.InboxObject.ap_type == "Block") + select(activitypub.models.InboxObject).where(activitypub.models.InboxObject.ap_type == "Block") ).scalar_one() # And a notification was created diff --git a/tests/test_outbox.py b/activitypub/tests/test_outbox.py similarity index 88% rename from tests/test_outbox.py rename to activitypub/tests/test_outbox.py index c8efedb6..17139ab8 100644 --- a/tests/test_outbox.py +++ b/activitypub/tests/test_outbox.py @@ -5,10 +5,11 @@ from sqlalchemy import select from sqlalchemy.orm import Session -from app import activitypub as ap +import activitypub.models +from activitypub import activitypub as ap from app import models from app import webfinger -from app.actor import LOCAL_ACTOR +from activitypub.actor import LOCAL_ACTOR from app.config import generate_csrf_token from tests.utils import generate_admin_session_cookies from tests.utils import setup_inbox_note @@ -54,12 +55,12 @@ def test_send_follow_request( assert response.headers.get("Location") == "http://testserver/" # And the Follow activity was created in the outbox - outbox_object = db.execute(select(models.OutboxObject)).scalar_one() + outbox_object = db.execute(select(activitypub.models.OutboxObject)).scalar_one() assert outbox_object.ap_type == "Follow" assert outbox_object.activity_object_ap_id == ra.ap_id # And an outgoing activity was queued - outgoing_activity = db.execute(select(models.OutgoingActivity)).scalar_one() + outgoing_activity = db.execute(select(activitypub.models.OutgoingActivity)).scalar_one() assert outgoing_activity.outbox_object_id == outbox_object.id assert outgoing_activity.recipient == ra.inbox_url @@ -113,13 +114,13 @@ def test_send_delete__reverts_side_effects( # And the Delete activity was created in the outbox outbox_object = db.execute( - select(models.OutboxObject).where(models.OutboxObject.ap_type == "Delete") + select(activitypub.models.OutboxObject).where(activitypub.models.OutboxObject.ap_type == "Delete") ).scalar_one() assert outbox_object.ap_type == "Delete" assert outbox_object.activity_object_ap_id == outbox_note2.ap_id # And an outgoing activity was queued - outgoing_activity = db.execute(select(models.OutgoingActivity)).scalar_one() + outgoing_activity = db.execute(select(activitypub.models.OutgoingActivity)).scalar_one() assert outgoing_activity.outbox_object_id == outbox_object.id assert outgoing_activity.recipient == ra.inbox_url @@ -179,7 +180,7 @@ def test_send_create_activity__with_attachment( assert response.status_code == 302 # And the Follow activity was created in the outbox - outbox_object = db.execute(select(models.OutboxObject)).scalar_one() + outbox_object = db.execute(select(activitypub.models.OutboxObject)).scalar_one() assert outbox_object.ap_type == "Note" assert outbox_object.summary is None assert outbox_object.content == "

hello

\n" @@ -191,12 +192,12 @@ def test_send_create_activity__with_attachment( assert attachment_response.status_code == 200 assert attachment_response.content == b"hello" - upload = db.execute(select(models.Upload)).scalar_one() + upload = db.execute(select(activitypub.models.Upload)).scalar_one() assert upload.content_hash == ( "324dcf027dd4a30a932c441f365a25e86b173defa4b8e58948253471b81b72cf" ) - outbox_attachment = db.execute(select(models.OutboxObjectAttachment)).scalar_one() + outbox_attachment = db.execute(select(activitypub.models.OutboxObjectAttachment)).scalar_one() assert outbox_attachment.upload_id == upload.id assert outbox_attachment.outbox_object_id == outbox_object.id assert outbox_attachment.filename == "attachment.txt" @@ -228,7 +229,7 @@ def test_send_create_activity__no_content_with_cw_and_attachments( assert response.status_code == 302 # And the Follow activity was created in the outbox - outbox_object = db.execute(select(models.OutboxObject)).scalar_one() + outbox_object = db.execute(select(activitypub.models.OutboxObject)).scalar_one() assert outbox_object.ap_type == "Note" assert outbox_object.summary is None assert outbox_object.content == "

cw

\n" @@ -260,11 +261,11 @@ def test_send_create_activity__no_followers_and_with_mention( assert response.status_code == 302 # And the Follow activity was created in the outbox - outbox_object = db.execute(select(models.OutboxObject)).scalar_one() + outbox_object = db.execute(select(activitypub.models.OutboxObject)).scalar_one() assert outbox_object.ap_type == "Note" # And an outgoing activity was queued - outgoing_activity = db.execute(select(models.OutgoingActivity)).scalar_one() + outgoing_activity = db.execute(select(activitypub.models.OutgoingActivity)).scalar_one() assert outgoing_activity.outbox_object_id == outbox_object.id assert outgoing_activity.recipient == ra.inbox_url @@ -297,11 +298,11 @@ def test_send_create_activity__with_followers( assert response.status_code == 302 # And the Follow activity was created in the outbox - outbox_object = db.execute(select(models.OutboxObject)).scalar_one() + outbox_object = db.execute(select(activitypub.models.OutboxObject)).scalar_one() assert outbox_object.ap_type == "Note" # And an outgoing activity was queued - outgoing_activity = db.execute(select(models.OutgoingActivity)).scalar_one() + outgoing_activity = db.execute(select(activitypub.models.OutgoingActivity)).scalar_one() assert outgoing_activity.outbox_object_id == outbox_object.id assert outgoing_activity.recipient == follower.actor.inbox_url @@ -338,7 +339,7 @@ def test_send_create_activity__question__one_of( assert response.status_code == 302 # And the Follow activity was created in the outbox - outbox_object = db.execute(select(models.OutboxObject)).scalar_one() + outbox_object = db.execute(select(activitypub.models.OutboxObject)).scalar_one() assert outbox_object.ap_type == "Question" assert outbox_object.is_one_of_poll is True assert len(outbox_object.poll_items) == 2 @@ -346,7 +347,7 @@ def test_send_create_activity__question__one_of( assert outbox_object.is_poll_ended is False # And an outgoing activity was queued - outgoing_activity = db.execute(select(models.OutgoingActivity)).scalar_one() + outgoing_activity = db.execute(select(activitypub.models.OutgoingActivity)).scalar_one() assert outgoing_activity.outbox_object_id == outbox_object.id assert outgoing_activity.recipient == follower.actor.inbox_url @@ -385,7 +386,7 @@ def test_send_create_activity__question__any_of( assert response.status_code == 302 # And the Follow activity was created in the outbox - outbox_object = db.execute(select(models.OutboxObject)).scalar_one() + outbox_object = db.execute(select(activitypub.models.OutboxObject)).scalar_one() assert outbox_object.ap_type == "Question" assert outbox_object.is_one_of_poll is False assert len(outbox_object.poll_items) == 4 @@ -393,7 +394,7 @@ def test_send_create_activity__question__any_of( assert outbox_object.is_poll_ended is False # And an outgoing activity was queued - outgoing_activity = db.execute(select(models.OutgoingActivity)).scalar_one() + outgoing_activity = db.execute(select(activitypub.models.OutgoingActivity)).scalar_one() assert outgoing_activity.outbox_object_id == outbox_object.id assert outgoing_activity.recipient == follower.actor.inbox_url @@ -427,11 +428,11 @@ def test_send_create_activity__article( assert response.status_code == 302 # And the Follow activity was created in the outbox - outbox_object = db.execute(select(models.OutboxObject)).scalar_one() + outbox_object = db.execute(select(activitypub.models.OutboxObject)).scalar_one() assert outbox_object.ap_type == "Article" assert outbox_object.ap_object["name"] == "Article" # And an outgoing activity was queued - outgoing_activity = db.execute(select(models.OutgoingActivity)).scalar_one() + outgoing_activity = db.execute(select(activitypub.models.OutgoingActivity)).scalar_one() assert outgoing_activity.outbox_object_id == outbox_object.id assert outgoing_activity.recipient == follower.actor.inbox_url diff --git a/tests/test_process_outgoing_activities.py b/activitypub/tests/test_process_outgoing_activities.py similarity index 88% rename from tests/test_process_outgoing_activities.py rename to activitypub/tests/test_process_outgoing_activities.py index 7da510ed..3962f69d 100644 --- a/tests/test_process_outgoing_activities.py +++ b/activitypub/tests/test_process_outgoing_activities.py @@ -6,18 +6,18 @@ from fastapi.testclient import TestClient from sqlalchemy import select -from app import models -from app.actor import LOCAL_ACTOR -from app.ap_object import RemoteObject +import activitypub.models +from activitypub.actor import LOCAL_ACTOR +from activitypub.ap_object import RemoteObject from app.database import AsyncSession -from app.outgoing_activities import _MAX_RETRIES -from app.outgoing_activities import fetch_next_outgoing_activity -from app.outgoing_activities import new_outgoing_activity -from app.outgoing_activities import process_next_outgoing_activity -from tests import factories +from activitypub.outgoing_activities import _MAX_RETRIES +from activitypub.outgoing_activities import fetch_next_outgoing_activity +from activitypub.outgoing_activities import new_outgoing_activity +from activitypub.outgoing_activities import process_next_outgoing_activity +from activitypub.tests import factories -def _setup_outbox_object() -> models.OutboxObject: +def _setup_outbox_object() -> activitypub.models.OutboxObject: ra = factories.RemoteActorFactory( base_url="https://example.com", username="toto", @@ -59,7 +59,7 @@ async def test_new_outgoing_activity( await async_db_session.commit() assert ( - await async_db_session.execute(select(models.OutgoingActivity)) + await async_db_session.execute(select(activitypub.models.OutgoingActivity)) ).scalar_one() == outgoing_activity assert outgoing_activity.outbox_object_id == outbox_object.id assert outgoing_activity.recipient == inbox_url @@ -101,7 +101,7 @@ async def test_process_next_outgoing_activity__server_200( assert respx_mock.calls.call_count == 1 outgoing_activity = ( - await async_db_session.execute(select(models.OutgoingActivity)) + await async_db_session.execute(select(activitypub.models.OutgoingActivity)) ).scalar_one() assert outgoing_activity.is_sent is True assert outgoing_activity.last_status_code == 204 @@ -136,7 +136,7 @@ async def test_process_next_outgoing_activity__webmention( assert respx_mock.calls.call_count == 1 outgoing_activity = ( - await async_db_session.execute(select(models.OutgoingActivity)) + await async_db_session.execute(select(activitypub.models.OutgoingActivity)) ).scalar_one() assert outgoing_activity.is_sent is True assert outgoing_activity.last_status_code == 204 @@ -172,7 +172,7 @@ async def test_process_next_outgoing_activity__error_500( assert respx_mock.calls.call_count == 1 outgoing_activity = ( - await async_db_session.execute(select(models.OutgoingActivity)) + await async_db_session.execute(select(activitypub.models.OutgoingActivity)) ).scalar_one() assert outgoing_activity.is_sent is False assert outgoing_activity.last_status_code == 500 @@ -210,7 +210,7 @@ async def test_process_next_outgoing_activity__errored( assert respx_mock.calls.call_count == 1 outgoing_activity = ( - await async_db_session.execute(select(models.OutgoingActivity)) + await async_db_session.execute(select(activitypub.models.OutgoingActivity)) ).scalar_one() assert outgoing_activity.is_sent is False assert outgoing_activity.last_status_code == 500 @@ -248,7 +248,7 @@ async def test_process_next_outgoing_activity__connect_error( assert respx_mock.calls.call_count == 1 outgoing_activity = ( - await async_db_session.execute(select(models.OutgoingActivity)) + await async_db_session.execute(select(activitypub.models.OutgoingActivity)) ).scalar_one() assert outgoing_activity.is_sent is False assert outgoing_activity.error is not None diff --git a/tests/test_remote_actor_deletion.py b/activitypub/tests/test_remote_actor_deletion.py similarity index 81% rename from tests/test_remote_actor_deletion.py rename to activitypub/tests/test_remote_actor_deletion.py index 34ca73f4..04d181c6 100644 --- a/tests/test_remote_actor_deletion.py +++ b/activitypub/tests/test_remote_actor_deletion.py @@ -5,10 +5,10 @@ from sqlalchemy import select from sqlalchemy.orm import Session -from app import activitypub as ap -from app import models -from app.ap_object import RemoteObject -from tests import factories +import activitypub.models +from activitypub import activitypub as ap +from activitypub.ap_object import RemoteObject +from activitypub.tests import factories from tests.utils import mock_httpsig_checker from tests.utils import run_process_next_incoming_activity from tests.utils import setup_remote_actor @@ -47,7 +47,7 @@ def test_inbox__incoming_delete_for_unknown_actor( assert response.status_code == 202 # And no incoming activity was created - assert db.scalar(select(func.count(models.IncomingActivity.id))) == 0 + assert db.scalar(select(func.count(activitypub.models.IncomingActivity.id))) == 0 def test_inbox__incoming_delete_for_known_actor( @@ -90,19 +90,19 @@ def test_inbox__incoming_delete_for_known_actor( # Then every inbox object from the actor was deleted assert ( db.scalar( - select(func.count(models.InboxObject.id)).where( - models.InboxObject.actor_id == actor.id, - models.InboxObject.is_deleted.is_(False), + select(func.count(activitypub.models.InboxObject.id)).where( + activitypub.models.InboxObject.actor_id == actor.id, + activitypub.models.InboxObject.is_deleted.is_(False), ) ) == 0 ) # And the following actor was deleted - assert db.scalar(select(func.count(models.Following.id))) == 0 + assert db.scalar(select(func.count(activitypub.models.Following.id))) == 0 # And the follower actor was deleted too - assert db.scalar(select(func.count(models.Follower.id))) == 0 + assert db.scalar(select(func.count(activitypub.models.Follower.id))) == 0 # And the actor was marked in deleted db.refresh(actor) diff --git a/alembic/versions/2022_10_30_1409-b28c0551c236_add_a_slug_field_for_outbox_objects.py b/alembic/versions/2022_10_30_1409-b28c0551c236_add_a_slug_field_for_outbox_objects.py index d48f18c4..8ab9b454 100644 --- a/alembic/versions/2022_10_30_1409-b28c0551c236_add_a_slug_field_for_outbox_objects.py +++ b/alembic/versions/2022_10_30_1409-b28c0551c236_add_a_slug_field_for_outbox_objects.py @@ -27,7 +27,7 @@ def upgrade() -> None: # ### end Alembic commands ### # Backfill the slug for existing articles - from app.models import OutboxObject + from activitypub.models import OutboxObject from app.utils.text import slugify sess = Session(op.get_bind()) articles = sess.execute(select(OutboxObject).where( diff --git a/app/admin.py b/app/admin.py index 4ba2c8b6..e812addb 100644 --- a/app/admin.py +++ b/app/admin.py @@ -18,19 +18,19 @@ from sqlalchemy import select from sqlalchemy.orm import joinedload -from app import activitypub as ap -from app import boxes +import activitypub.models +from activitypub import activitypub as ap, boxes from app import models from app import templates -from app.actor import LOCAL_ACTOR -from app.actor import fetch_actor -from app.actor import get_actors_metadata -from app.actor import list_actors -from app.boxes import get_inbox_object_by_ap_id -from app.boxes import get_outbox_object_by_ap_id -from app.boxes import send_block -from app.boxes import send_follow -from app.boxes import send_unblock +from activitypub.actor import LOCAL_ACTOR +from activitypub.actor import fetch_actor +from activitypub.actor import get_actors_metadata +from activitypub.actor import list_actors +from activitypub.boxes import get_inbox_object_by_ap_id +from activitypub.boxes import get_outbox_object_by_ap_id +from activitypub.boxes import send_block +from activitypub.boxes import send_follow +from activitypub.boxes import send_unblock from app.config import EMOJIS from app.config import SESSION_TIMEOUT from app.config import generate_csrf_token @@ -236,24 +236,24 @@ async def admin_bookmarks( stream = ( ( await db_session.scalars( - select(models.InboxObject) + select(activitypub.models.InboxObject) .where( - models.InboxObject.ap_type.in_( + activitypub.models.InboxObject.ap_type.in_( ["Note", "Article", "Video", "Announce"] ), - models.InboxObject.is_bookmarked.is_(True), - models.InboxObject.is_deleted.is_(False), + activitypub.models.InboxObject.is_bookmarked.is_(True), + activitypub.models.InboxObject.is_deleted.is_(False), ) .options( - joinedload(models.InboxObject.relates_to_inbox_object), - joinedload(models.InboxObject.relates_to_outbox_object).options( + joinedload(activitypub.models.InboxObject.relates_to_inbox_object), + joinedload(activitypub.models.InboxObject.relates_to_outbox_object).options( joinedload( - models.OutboxObject.outbox_object_attachments - ).options(joinedload(models.OutboxObjectAttachment.upload)), + activitypub.models.OutboxObject.outbox_object_attachments + ).options(joinedload(activitypub.models.OutboxObjectAttachment.upload)), ), - joinedload(models.InboxObject.actor), + joinedload(activitypub.models.InboxObject.actor), ) - .order_by(models.InboxObject.ap_published_at.desc()) + .order_by(activitypub.models.InboxObject.ap_published_at.desc()) .limit(20) ) ) @@ -277,35 +277,35 @@ async def admin_stream( cursor: str | None = None, ) -> templates.TemplateResponse: where = [ - models.InboxObject.is_hidden_from_stream.is_(False), - models.InboxObject.is_deleted.is_(False), + activitypub.models.InboxObject.is_hidden_from_stream.is_(False), + activitypub.models.InboxObject.is_deleted.is_(False), ] if cursor: where.append( - models.InboxObject.ap_published_at < pagination.decode_cursor(cursor) + activitypub.models.InboxObject.ap_published_at < pagination.decode_cursor(cursor) ) page_size = 20 remaining_count = await db_session.scalar( - select(func.count(models.InboxObject.id)).where(*where) + select(func.count(activitypub.models.InboxObject.id)).where(*where) ) - q = select(models.InboxObject).where(*where) + q = select(activitypub.models.InboxObject).where(*where) inbox = ( ( await db_session.scalars( q.options( - joinedload(models.InboxObject.relates_to_inbox_object).options( - joinedload(models.InboxObject.actor) + joinedload(activitypub.models.InboxObject.relates_to_inbox_object).options( + joinedload(activitypub.models.InboxObject.actor) ), - joinedload(models.InboxObject.relates_to_outbox_object).options( + joinedload(activitypub.models.InboxObject.relates_to_outbox_object).options( joinedload( - models.OutboxObject.outbox_object_attachments - ).options(joinedload(models.OutboxObjectAttachment.upload)), + activitypub.models.OutboxObject.outbox_object_attachments + ).options(joinedload(activitypub.models.OutboxObjectAttachment.upload)), ), - joinedload(models.InboxObject.actor), + joinedload(activitypub.models.InboxObject.actor), ) - .order_by(models.InboxObject.ap_published_at.desc()) + .order_by(activitypub.models.InboxObject.ap_published_at.desc()) .limit(20) ) ) @@ -349,7 +349,7 @@ async def admin_inbox( cursor: str | None = None, ) -> templates.TemplateResponse: where = [ - models.InboxObject.ap_type.not_in( + activitypub.models.InboxObject.ap_type.not_in( [ "Accept", "Delete", @@ -363,37 +363,37 @@ async def admin_inbox( "EmojiReact", ] ), - models.InboxObject.is_deleted.is_(False), - models.InboxObject.is_transient.is_(False), + activitypub.models.InboxObject.is_deleted.is_(False), + activitypub.models.InboxObject.is_transient.is_(False), ] if filter_by: - where.append(models.InboxObject.ap_type == filter_by) + where.append(activitypub.models.InboxObject.ap_type == filter_by) if cursor: where.append( - models.InboxObject.ap_published_at < pagination.decode_cursor(cursor) + activitypub.models.InboxObject.ap_published_at < pagination.decode_cursor(cursor) ) page_size = 20 remaining_count = await db_session.scalar( - select(func.count(models.InboxObject.id)).where(*where) + select(func.count(activitypub.models.InboxObject.id)).where(*where) ) - q = select(models.InboxObject).where(*where) + q = select(activitypub.models.InboxObject).where(*where) inbox = ( ( await db_session.scalars( q.options( - joinedload(models.InboxObject.relates_to_inbox_object).options( - joinedload(models.InboxObject.actor) + joinedload(activitypub.models.InboxObject.relates_to_inbox_object).options( + joinedload(activitypub.models.InboxObject.actor) ), - joinedload(models.InboxObject.relates_to_outbox_object).options( + joinedload(activitypub.models.InboxObject.relates_to_outbox_object).options( joinedload( - models.OutboxObject.outbox_object_attachments - ).options(joinedload(models.OutboxObjectAttachment.upload)), + activitypub.models.OutboxObject.outbox_object_attachments + ).options(joinedload(activitypub.models.OutboxObjectAttachment.upload)), ), - joinedload(models.InboxObject.actor), + joinedload(activitypub.models.InboxObject.actor), ) - .order_by(models.InboxObject.ap_published_at.desc()) + .order_by(activitypub.models.InboxObject.ap_published_at.desc()) .limit(20) ) ) @@ -442,21 +442,21 @@ async def admin_direct_messages( ( await db_session.execute( select( - models.InboxObject.ap_context, - models.InboxObject.actor_id, + activitypub.models.InboxObject.ap_context, + activitypub.models.InboxObject.actor_id, func.count(1).label("count"), - func.max(models.InboxObject.ap_published_at).label( + func.max(activitypub.models.InboxObject.ap_published_at).label( "most_recent_date" ), ) .where( - models.InboxObject.visibility == ap.VisibilityEnum.DIRECT, - models.InboxObject.ap_context.is_not(None), + activitypub.models.InboxObject.visibility == ap.VisibilityEnum.DIRECT, + activitypub.models.InboxObject.ap_context.is_not(None), # Skip transient object like poll relies - models.InboxObject.is_transient.is_(False), - models.InboxObject.is_deleted.is_(False), + activitypub.models.InboxObject.is_transient.is_(False), + activitypub.models.InboxObject.is_deleted.is_(False), ) - .group_by(models.InboxObject.ap_context, models.InboxObject.actor_id) + .group_by(activitypub.models.InboxObject.ap_context, activitypub.models.InboxObject.actor_id) ) ) .unique() @@ -466,20 +466,20 @@ async def admin_direct_messages( ( await db_session.execute( select( - models.OutboxObject.ap_context, + activitypub.models.OutboxObject.ap_context, func.count(1).label("count"), - func.max(models.OutboxObject.ap_published_at).label( + func.max(activitypub.models.OutboxObject.ap_published_at).label( "most_recent_date" ), ) .where( - models.OutboxObject.visibility == ap.VisibilityEnum.DIRECT, - models.OutboxObject.ap_context.is_not(None), + activitypub.models.OutboxObject.visibility == ap.VisibilityEnum.DIRECT, + activitypub.models.OutboxObject.ap_context.is_not(None), # Skip transient object like poll relies - models.OutboxObject.is_transient.is_(False), - models.OutboxObject.is_deleted.is_(False), + activitypub.models.OutboxObject.is_transient.is_(False), + activitypub.models.OutboxObject.is_deleted.is_(False), ) - .group_by(models.OutboxObject.ap_context) + .group_by(activitypub.models.OutboxObject.ap_context) ) ) .unique() @@ -526,16 +526,16 @@ async def admin_direct_messages( if convo["most_recent_from_inbox"] > convo["most_recent_from_outbox"]: convos_with_last_from_inbox.append( and_( - models.InboxObject.ap_context == context, - models.InboxObject.ap_published_at + activitypub.models.InboxObject.ap_context == context, + activitypub.models.InboxObject.ap_published_at == convo["most_recent_from_inbox"], ) ) else: convos_with_last_from_outbox.append( and_( - models.OutboxObject.ap_context == context, - models.OutboxObject.ap_published_at + activitypub.models.OutboxObject.ap_context == context, + activitypub.models.OutboxObject.ap_published_at == convo["most_recent_from_outbox"], ) ) @@ -543,10 +543,10 @@ async def admin_direct_messages( ( ( await db_session.scalars( - select(models.InboxObject) + select(activitypub.models.InboxObject) .where(or_(*convos_with_last_from_inbox)) .options( - joinedload(models.InboxObject.actor), + joinedload(activitypub.models.InboxObject.actor), ) ) ) @@ -560,12 +560,12 @@ async def admin_direct_messages( ( ( await db_session.scalars( - select(models.OutboxObject) + select(activitypub.models.OutboxObject) .where(or_(*convos_with_last_from_outbox)) .options( joinedload( - models.OutboxObject.outbox_object_attachments - ).options(joinedload(models.OutboxObjectAttachment.upload)), + activitypub.models.OutboxObject.outbox_object_attachments + ).options(joinedload(activitypub.models.OutboxObjectAttachment.upload)), ) ) ) @@ -587,7 +587,7 @@ async def admin_direct_messages( actors = list( ( await db_session.execute( - select(models.Actor).where(models.Actor.id.in_(convo["actor_ids"])) + select(activitypub.models.Actor).where(activitypub.models.Actor.id.in_(convo["actor_ids"])) ) ).scalars() ) @@ -596,8 +596,8 @@ async def admin_direct_messages( if not actors and anybox_object.is_from_outbox: actors = ( # type: ignore await db_session.execute( - select(models.Actor).where( - models.Actor.ap_id.in_( + select(activitypub.models.Actor).where( + activitypub.models.Actor.ap_id.in_( mention["href"] for mention in anybox_object.tags if mention["type"] == "Mention" @@ -625,37 +625,37 @@ async def admin_outbox( cursor: str | None = None, ) -> templates.TemplateResponse: where = [ - models.OutboxObject.ap_type.not_in(["Accept", "Delete", "Update"]), - models.OutboxObject.is_deleted.is_(False), - models.OutboxObject.is_transient.is_(False), + activitypub.models.OutboxObject.ap_type.not_in(["Accept", "Delete", "Update"]), + activitypub.models.OutboxObject.is_deleted.is_(False), + activitypub.models.OutboxObject.is_transient.is_(False), ] if filter_by: - where.append(models.OutboxObject.ap_type == filter_by) + where.append(activitypub.models.OutboxObject.ap_type == filter_by) if cursor: where.append( - models.OutboxObject.ap_published_at < pagination.decode_cursor(cursor) + activitypub.models.OutboxObject.ap_published_at < pagination.decode_cursor(cursor) ) page_size = 20 remaining_count = await db_session.scalar( - select(func.count(models.OutboxObject.id)).where(*where) + select(func.count(activitypub.models.OutboxObject.id)).where(*where) ) - q = select(models.OutboxObject).where(*where) + q = select(activitypub.models.OutboxObject).where(*where) outbox = ( ( await db_session.scalars( q.options( - joinedload(models.OutboxObject.relates_to_inbox_object).options( - joinedload(models.InboxObject.actor), + joinedload(activitypub.models.OutboxObject.relates_to_inbox_object).options( + joinedload(activitypub.models.InboxObject.actor), ), - joinedload(models.OutboxObject.relates_to_outbox_object), - joinedload(models.OutboxObject.relates_to_actor), - joinedload(models.OutboxObject.outbox_object_attachments).options( - joinedload(models.OutboxObjectAttachment.upload) + joinedload(activitypub.models.OutboxObject.relates_to_outbox_object), + joinedload(activitypub.models.OutboxObject.relates_to_actor), + joinedload(activitypub.models.OutboxObject.outbox_object_attachments).options( + joinedload(activitypub.models.OutboxObjectAttachment.upload) ), ) - .order_by(models.OutboxObject.ap_published_at.desc()) + .order_by(activitypub.models.OutboxObject.ap_published_at.desc()) .limit(page_size) ) ) @@ -714,12 +714,12 @@ async def get_notifications( .options( joinedload(models.Notification.actor), joinedload(models.Notification.inbox_object).options( - joinedload(models.InboxObject.actor) + joinedload(activitypub.models.InboxObject.actor) ), joinedload(models.Notification.outbox_object).options( joinedload( - models.OutboxObject.outbox_object_attachments - ).options(joinedload(models.OutboxObjectAttachment.upload)), + activitypub.models.OutboxObject.outbox_object_attachments + ).options(joinedload(activitypub.models.OutboxObjectAttachment.upload)), ), joinedload(models.Notification.webmention), ) @@ -804,7 +804,7 @@ async def admin_profile( # TODO: show featured/pinned actor = ( await db_session.execute( - select(models.Actor).where(models.Actor.ap_id == actor_id) + select(activitypub.models.Actor).where(activitypub.models.Actor.ap_id == actor_id) ) ).scalar_one_or_none() if not actor: @@ -813,38 +813,38 @@ async def admin_profile( actors_metadata = await get_actors_metadata(db_session, [actor]) where = [ - models.InboxObject.is_deleted.is_(False), - models.InboxObject.actor_id == actor.id, - models.InboxObject.ap_type.in_( + activitypub.models.InboxObject.is_deleted.is_(False), + activitypub.models.InboxObject.actor_id == actor.id, + activitypub.models.InboxObject.ap_type.in_( ["Note", "Article", "Video", "Page", "Announce"] ), ] if cursor: decoded_cursor = pagination.decode_cursor(cursor) - where.append(models.InboxObject.ap_published_at < decoded_cursor) + where.append(activitypub.models.InboxObject.ap_published_at < decoded_cursor) page_size = 20 remaining_count = await db_session.scalar( - select(func.count(models.InboxObject.id)).where(*where) + select(func.count(activitypub.models.InboxObject.id)).where(*where) ) inbox_objects = ( ( await db_session.scalars( - select(models.InboxObject) + select(activitypub.models.InboxObject) .where(*where) .options( - joinedload(models.InboxObject.relates_to_inbox_object).options( - joinedload(models.InboxObject.actor) + joinedload(activitypub.models.InboxObject.relates_to_inbox_object).options( + joinedload(activitypub.models.InboxObject.actor) ), - joinedload(models.InboxObject.relates_to_outbox_object).options( + joinedload(activitypub.models.InboxObject.relates_to_outbox_object).options( joinedload( - models.OutboxObject.outbox_object_attachments - ).options(joinedload(models.OutboxObjectAttachment.upload)), + activitypub.models.OutboxObject.outbox_object_attachments + ).options(joinedload(activitypub.models.OutboxObjectAttachment.upload)), ), - joinedload(models.InboxObject.actor), + joinedload(activitypub.models.InboxObject.actor), ) - .order_by(models.InboxObject.ap_published_at.desc()) + .order_by(activitypub.models.InboxObject.ap_published_at.desc()) .limit(page_size) ) ) @@ -1231,9 +1231,9 @@ async def admin_edit_text( maybe_object = ( ( await db_session.execute( - select(models.OutboxObject).where( - models.OutboxObject.public_id == public_id, - models.OutboxObject.is_deleted.is_(False), + select(activitypub.models.OutboxObject).where( + activitypub.models.OutboxObject.public_id == public_id, + activitypub.models.OutboxObject.is_deleted.is_(False), ) ) ) @@ -1270,9 +1270,9 @@ async def admin_actions_edit_text( maybe_object = ( ( await db_session.execute( - select(models.OutboxObject).where( - models.OutboxObject.public_id == public_id, - models.OutboxObject.is_deleted.is_(False), + select(activitypub.models.OutboxObject).where( + activitypub.models.OutboxObject.public_id == public_id, + activitypub.models.OutboxObject.is_deleted.is_(False), ) ) ) diff --git a/app/customization.py b/app/customization.py index 8a4f0395..68c6a806 100644 --- a/app/customization.py +++ b/app/customization.py @@ -11,7 +11,7 @@ from starlette.responses import JSONResponse if TYPE_CHECKING: - from app.ap_object import RemoteObject + from activitypub.ap_object import RemoteObject _DATA_DIR = Path().parent.resolve() / "data" @@ -77,7 +77,7 @@ class ActivityPubResponse(JSONResponse): def _custom_page_handler(path: str, html_page: HTMLPage) -> Any: from app import templates - from app.actor import LOCAL_ACTOR + from activitypub.actor import LOCAL_ACTOR from app.config import is_activitypub_requested from app.database import AsyncSession from app.database import get_db_session diff --git a/app/httpsig.py b/app/httpsig.py index e9f9e690..cc286045 100644 --- a/app/httpsig.py +++ b/app/httpsig.py @@ -21,7 +21,8 @@ from loguru import logger from sqlalchemy import select -from app import activitypub as ap +import activitypub.models +from activitypub import activitypub as ap from app import config from app.config import KEY_PATH from app.database import AsyncSession @@ -103,7 +104,7 @@ async def _get_public_key( existing_actor = ( await db_session.scalars( - select(models.Actor).where(models.Actor.ap_id == key_id.split("#")[0]) + select(activitypub.models.Actor).where(activitypub.models.Actor.ap_id == key_id.split("#")[0]) ) ).one_or_none() if not should_skip_cache: @@ -115,9 +116,9 @@ async def _get_public_key( return k # Fetch it - from app import activitypub as ap - from app.actor import RemoteActor - from app.actor import update_actor_if_needed + from activitypub import activitypub as ap + from activitypub.actor import RemoteActor + from activitypub.actor import update_actor_if_needed # Without signing the request as if it's the first contact, the 2 servers # might race to fetch each other key @@ -212,8 +213,8 @@ async def httpsig_checker( and actor_id == ap.get_id(activity["object"]) and not ( await db_session.scalars( - select(models.Actor).where( - models.Actor.ap_id == actor_id, + select(activitypub.models.Actor).where( + activitypub.models.Actor.ap_id == actor_id, ) ) ).one_or_none() diff --git a/app/ldsig.py b/app/ldsig.py index 2eea9de9..197c5f78 100644 --- a/app/ldsig.py +++ b/app/ldsig.py @@ -9,7 +9,7 @@ from loguru import logger from pyld import jsonld # type: ignore -from app import activitypub as ap +from activitypub import activitypub as ap from app.database import AsyncSession from app.httpsig import _get_public_key diff --git a/app/lookup.py b/app/lookup.py index c1b6ef1e..640782fd 100644 --- a/app/lookup.py +++ b/app/lookup.py @@ -1,10 +1,10 @@ import mf2py # type: ignore -from app import activitypub as ap +from activitypub import activitypub as ap from app import webfinger -from app.actor import Actor -from app.actor import RemoteActor -from app.ap_object import RemoteObject +from activitypub.actor import Actor +from activitypub.actor import RemoteActor +from activitypub.ap_object import RemoteObject from app.database import AsyncSession from app.source import _MENTION_REGEX diff --git a/app/main.py b/app/main.py index bce0edae..7ef2d36b 100644 --- a/app/main.py +++ b/app/main.py @@ -42,9 +42,9 @@ from starlette.types import Message from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware # type: ignore -from app import activitypub as ap +import activitypub.models +from activitypub import activitypub as ap, boxes from app import admin -from app import boxes from app import config from app import httpsig from app import indieauth @@ -53,9 +53,9 @@ from app import models from app import templates from app import webmentions -from app.actor import LOCAL_ACTOR -from app.actor import get_actors_metadata -from app.boxes import public_outbox_objects_count +from activitypub.actor import LOCAL_ACTOR +from activitypub.actor import get_actors_metadata +from activitypub.boxes import public_outbox_objects_count from app.config import BASE_URL from app.config import DEBUG from app.config import DOMAIN @@ -69,7 +69,7 @@ from app.database import AsyncSession from app.database import async_session from app.database import get_db_session -from app.incoming_activities import new_ap_incoming_activity +from activitypub.incoming_activities import new_ap_incoming_activity from app.templates import is_current_user_admin from app.uploads import UPLOAD_DIR from app.utils import pagination @@ -295,34 +295,34 @@ async def index( page = page or 1 where = ( - models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC, - models.OutboxObject.is_deleted.is_(False), - models.OutboxObject.is_hidden_from_homepage.is_(False), - models.OutboxObject.ap_type.in_(["Announce", "Note", "Video", "Question"]), + activitypub.models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC, + activitypub.models.OutboxObject.is_deleted.is_(False), + activitypub.models.OutboxObject.is_hidden_from_homepage.is_(False), + activitypub.models.OutboxObject.ap_type.in_(["Announce", "Note", "Video", "Question"]), ) - q = select(models.OutboxObject).where(*where) + q = select(activitypub.models.OutboxObject).where(*where) total_count = await db_session.scalar( - select(func.count(models.OutboxObject.id)).where(*where) + select(func.count(activitypub.models.OutboxObject.id)).where(*where) ) page_size = 20 page_offset = (page - 1) * page_size outbox_objects_result = await db_session.scalars( q.options( - joinedload(models.OutboxObject.outbox_object_attachments).options( - joinedload(models.OutboxObjectAttachment.upload) + joinedload(activitypub.models.OutboxObject.outbox_object_attachments).options( + joinedload(activitypub.models.OutboxObjectAttachment.upload) ), - joinedload(models.OutboxObject.relates_to_inbox_object).options( - joinedload(models.InboxObject.actor), + joinedload(activitypub.models.OutboxObject.relates_to_inbox_object).options( + joinedload(activitypub.models.InboxObject.actor), ), - joinedload(models.OutboxObject.relates_to_outbox_object).options( - joinedload(models.OutboxObject.outbox_object_attachments).options( - joinedload(models.OutboxObjectAttachment.upload) + joinedload(activitypub.models.OutboxObject.relates_to_outbox_object).options( + joinedload(activitypub.models.OutboxObject.outbox_object_attachments).options( + joinedload(activitypub.models.OutboxObjectAttachment.upload) ), ), ) - .order_by(models.OutboxObject.is_pinned.desc()) - .order_by(models.OutboxObject.ap_published_at.desc()) + .order_by(activitypub.models.OutboxObject.is_pinned.desc()) + .order_by(activitypub.models.OutboxObject.ap_published_at.desc()) .offset(page_offset) .limit(page_size) ) @@ -352,27 +352,27 @@ async def articles( # TODO: special ActivityPub collection for Article where = ( - models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC, - models.OutboxObject.is_deleted.is_(False), - models.OutboxObject.is_hidden_from_homepage.is_(False), - models.OutboxObject.ap_type == "Article", + activitypub.models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC, + activitypub.models.OutboxObject.is_deleted.is_(False), + activitypub.models.OutboxObject.is_hidden_from_homepage.is_(False), + activitypub.models.OutboxObject.ap_type == "Article", ) - q = select(models.OutboxObject).where(*where) + q = select(activitypub.models.OutboxObject).where(*where) outbox_objects_result = await db_session.scalars( q.options( - joinedload(models.OutboxObject.outbox_object_attachments).options( - joinedload(models.OutboxObjectAttachment.upload) + joinedload(activitypub.models.OutboxObject.outbox_object_attachments).options( + joinedload(activitypub.models.OutboxObjectAttachment.upload) ), - joinedload(models.OutboxObject.relates_to_inbox_object).options( - joinedload(models.InboxObject.actor), + joinedload(activitypub.models.OutboxObject.relates_to_inbox_object).options( + joinedload(activitypub.models.InboxObject.actor), ), - joinedload(models.OutboxObject.relates_to_outbox_object).options( - joinedload(models.OutboxObject.outbox_object_attachments).options( - joinedload(models.OutboxObjectAttachment.upload) + joinedload(activitypub.models.OutboxObject.relates_to_outbox_object).options( + joinedload(activitypub.models.OutboxObject.outbox_object_attachments).options( + joinedload(activitypub.models.OutboxObjectAttachment.upload) ), ), - ).order_by(models.OutboxObject.ap_published_at.desc()) + ).order_by(activitypub.models.OutboxObject.ap_published_at.desc()) ) outbox_objects = outbox_objects_result.unique().all() @@ -389,7 +389,7 @@ async def articles( async def _build_followx_collection( db_session: AsyncSession, - model_cls: Type[models.Following | models.Follower], + model_cls: Type[activitypub.models.Following | activitypub.models.Follower], path: str, page: bool | None, next_cursor: str | None, @@ -444,7 +444,7 @@ async def _build_followx_collection( async def _empty_followx_collection( db_session: AsyncSession, - model_cls: Type[models.Following | models.Follower], + model_cls: Type[activitypub.models.Following | activitypub.models.Follower], path: str, ) -> ap.RawObject: total_items = await db_session.scalar(select(func.count(model_cls.id))) @@ -475,7 +475,7 @@ async def followers( return ActivityPubResponse( await _empty_followx_collection( db_session=db_session, - model_cls=models.Follower, + model_cls=activitypub.models.Follower, path="/followers", ) ) @@ -483,7 +483,7 @@ async def followers( return ActivityPubResponse( await _build_followx_collection( db_session=db_session, - model_cls=models.Follower, + model_cls=activitypub.models.Follower, path="/followers", page=page, next_cursor=next_cursor, @@ -495,9 +495,9 @@ async def followers( # We only show the most recent 100 followers on the public website followers_result = await db_session.scalars( - select(models.Follower) - .options(joinedload(models.Follower.actor)) - .order_by(models.Follower.created_at.desc()) + select(activitypub.models.Follower) + .options(joinedload(activitypub.models.Follower.actor)) + .order_by(activitypub.models.Follower.created_at.desc()) .limit(100) ) followers = followers_result.unique().all() @@ -536,7 +536,7 @@ async def following( return ActivityPubResponse( await _empty_followx_collection( db_session=db_session, - model_cls=models.Following, + model_cls=activitypub.models.Following, path="/following", ) ) @@ -544,7 +544,7 @@ async def following( return ActivityPubResponse( await _build_followx_collection( db_session=db_session, - model_cls=models.Following, + model_cls=activitypub.models.Following, path="/following", page=page, next_cursor=next_cursor, @@ -558,9 +558,9 @@ async def following( following = ( ( await db_session.scalars( - select(models.Following) - .options(joinedload(models.Following.actor)) - .order_by(models.Following.created_at.desc()) + select(activitypub.models.Following) + .options(joinedload(activitypub.models.Following.actor)) + .order_by(activitypub.models.Following.created_at.desc()) .limit(100) ) ) @@ -599,19 +599,19 @@ async def outbox( # Default restrictions unless the request is authenticated with an access token restricted_where = [ - models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC, - models.OutboxObject.ap_type.in_(["Create", "Note", "Article", "Announce"]), + activitypub.models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC, + activitypub.models.OutboxObject.ap_type.in_(["Create", "Note", "Article", "Announce"]), ] # By design, we only show the last 20 public activities in the oubox outbox_objects = ( await db_session.scalars( - select(models.OutboxObject) + select(activitypub.models.OutboxObject) .where( - models.OutboxObject.is_deleted.is_(False), + activitypub.models.OutboxObject.is_deleted.is_(False), *([] if maybe_access_token_info else restricted_where), ) - .order_by(models.OutboxObject.ap_published_at.desc()) + .order_by(activitypub.models.OutboxObject.ap_published_at.desc()) .limit(20) ) ).all() @@ -680,13 +680,13 @@ async def featured( ) -> ActivityPubResponse: outbox_objects = ( await db_session.scalars( - select(models.OutboxObject) + select(activitypub.models.OutboxObject) .filter( - models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC, - models.OutboxObject.is_deleted.is_(False), - models.OutboxObject.is_pinned.is_(True), + activitypub.models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC, + activitypub.models.OutboxObject.is_deleted.is_(False), + activitypub.models.OutboxObject.is_pinned.is_(True), ) - .order_by(models.OutboxObject.ap_published_at.desc()) + .order_by(activitypub.models.OutboxObject.ap_published_at.desc()) .limit(5) ) ).all() @@ -704,7 +704,7 @@ async def featured( async def _check_outbox_object_acl( request: Request, db_session: AsyncSession, - ap_object: models.OutboxObject, + ap_object: activitypub.models.OutboxObject, httpsig_info: httpsig.HTTPSigInfo, ) -> None: if templates.is_current_user_admin(request): @@ -743,19 +743,19 @@ async def _check_outbox_object_acl( async def _fetch_likes( db_session: AsyncSession, - outbox_object: models.OutboxObject, -) -> list[models.InboxObject]: + outbox_object: activitypub.models.OutboxObject, +) -> list[activitypub.models.InboxObject]: return ( ( await db_session.scalars( - select(models.InboxObject) + select(activitypub.models.InboxObject) .where( - models.InboxObject.ap_type == "Like", - models.InboxObject.activity_object_ap_id == outbox_object.ap_id, - models.InboxObject.is_deleted.is_(False), + activitypub.models.InboxObject.ap_type == "Like", + activitypub.models.InboxObject.activity_object_ap_id == outbox_object.ap_id, + activitypub.models.InboxObject.is_deleted.is_(False), ) - .options(joinedload(models.InboxObject.actor)) - .order_by(models.InboxObject.ap_published_at.desc()) + .options(joinedload(activitypub.models.InboxObject.actor)) + .order_by(activitypub.models.InboxObject.ap_published_at.desc()) .limit(10) ) ) @@ -766,19 +766,19 @@ async def _fetch_likes( async def _fetch_shares( db_session: AsyncSession, - outbox_object: models.OutboxObject, -) -> list[models.InboxObject]: + outbox_object: activitypub.models.OutboxObject, +) -> list[activitypub.models.InboxObject]: return ( ( await db_session.scalars( - select(models.InboxObject) + select(activitypub.models.InboxObject) .filter( - models.InboxObject.ap_type == "Announce", - models.InboxObject.activity_object_ap_id == outbox_object.ap_id, - models.InboxObject.is_deleted.is_(False), + activitypub.models.InboxObject.ap_type == "Announce", + activitypub.models.InboxObject.activity_object_ap_id == outbox_object.ap_id, + activitypub.models.InboxObject.is_deleted.is_(False), ) - .options(joinedload(models.InboxObject.actor)) - .order_by(models.InboxObject.ap_published_at.desc()) + .options(joinedload(activitypub.models.InboxObject.actor)) + .order_by(activitypub.models.InboxObject.ap_published_at.desc()) .limit(10) ) ) @@ -789,7 +789,7 @@ async def _fetch_shares( async def _fetch_webmentions( db_session: AsyncSession, - outbox_object: models.OutboxObject, + outbox_object: activitypub.models.OutboxObject, ) -> list[models.Webmention]: return ( await db_session.scalars( @@ -813,15 +813,15 @@ async def outbox_by_public_id( maybe_object = ( ( await db_session.execute( - select(models.OutboxObject) + select(activitypub.models.OutboxObject) .options( - joinedload(models.OutboxObject.outbox_object_attachments).options( - joinedload(models.OutboxObjectAttachment.upload) + joinedload(activitypub.models.OutboxObject.outbox_object_attachments).options( + joinedload(activitypub.models.OutboxObjectAttachment.upload) ) ) .where( - models.OutboxObject.public_id == public_id, - models.OutboxObject.is_deleted.is_(False), + activitypub.models.OutboxObject.public_id == public_id, + activitypub.models.OutboxObject.is_deleted.is_(False), ) ) ) @@ -889,7 +889,7 @@ def _filter_webmentions( def _merge_faces_from_inbox_object_and_webmentions( - inbox_objects: list[models.InboxObject], + inbox_objects: list[activitypub.models.InboxObject], webmentions: list[models.Webmention], webmention_type: models.WebmentionType, ) -> list[Face]: @@ -991,9 +991,9 @@ async def outbox_activity_by_public_id( ) -> ActivityPubResponse: maybe_object = ( await db_session.execute( - select(models.OutboxObject).where( - models.OutboxObject.public_id == public_id, - models.OutboxObject.is_deleted.is_(False), + select(activitypub.models.OutboxObject).where( + activitypub.models.OutboxObject.public_id == public_id, + activitypub.models.OutboxObject.is_deleted.is_(False), ) ) ).scalar_one_or_none() @@ -1013,13 +1013,13 @@ async def tag_by_name( _: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker), ) -> ActivityPubResponse | templates.TemplateResponse: where = [ - models.TaggedOutboxObject.tag == tag.lower(), - models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC, - models.OutboxObject.is_deleted.is_(False), + activitypub.models.TaggedOutboxObject.tag == tag.lower(), + activitypub.models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC, + activitypub.models.OutboxObject.is_deleted.is_(False), ] tagged_count = await db_session.scalar( - select(func.count(models.OutboxObject.id)) - .join(models.TaggedOutboxObject) + select(func.count(activitypub.models.OutboxObject.id)) + .join(activitypub.models.TaggedOutboxObject) .where(*where) ) if is_activitypub_requested(request): @@ -1027,13 +1027,13 @@ async def tag_by_name( raise HTTPException(status_code=404) outbox_object_ids = await db_session.execute( - select(models.OutboxObject.ap_id) + select(activitypub.models.OutboxObject.ap_id) .join( - models.TaggedOutboxObject, - models.TaggedOutboxObject.outbox_object_id == models.OutboxObject.id, + activitypub.models.TaggedOutboxObject, + activitypub.models.TaggedOutboxObject.outbox_object_id == activitypub.models.OutboxObject.id, ) .where(*where) - .order_by(models.OutboxObject.ap_published_at.desc()) + .order_by(activitypub.models.OutboxObject.ap_published_at.desc()) .limit(20) ) return ActivityPubResponse( @@ -1049,18 +1049,18 @@ async def tag_by_name( ) outbox_objects_result = await db_session.scalars( - select(models.OutboxObject) + select(activitypub.models.OutboxObject) .where(*where) .join( - models.TaggedOutboxObject, - models.TaggedOutboxObject.outbox_object_id == models.OutboxObject.id, + activitypub.models.TaggedOutboxObject, + activitypub.models.TaggedOutboxObject.outbox_object_id == activitypub.models.OutboxObject.id, ) .options( - joinedload(models.OutboxObject.outbox_object_attachments).options( - joinedload(models.OutboxObjectAttachment.upload) + joinedload(activitypub.models.OutboxObject.outbox_object_attachments).options( + joinedload(activitypub.models.OutboxObjectAttachment.upload) ) ) - .order_by(models.OutboxObject.ap_published_at.desc()) + .order_by(activitypub.models.OutboxObject.ap_published_at.desc()) .limit(20) ) outbox_objects = outbox_objects_result.unique().all() @@ -1098,12 +1098,12 @@ async def get_inbox( next_cursor: str | None = None, ) -> ActivityPubResponse: where = [ - models.InboxObject.ap_type.in_( + activitypub.models.InboxObject.ap_type.in_( ["Create", "Follow", "Like", "Announce", "Undo", "Update"] ) ] total_items = await db_session.scalar( - select(func.count(models.InboxObject.id)).where(*where) + select(func.count(activitypub.models.InboxObject.id)).where(*where) ) if not page and not next_cursor: @@ -1118,13 +1118,13 @@ async def get_inbox( ) q = ( - select(models.InboxObject) + select(activitypub.models.InboxObject) .where(*where) - .order_by(models.InboxObject.created_at.desc()) + .order_by(activitypub.models.InboxObject.created_at.desc()) ) # type: ignore if next_cursor: q = q.where( - models.InboxObject.created_at + activitypub.models.InboxObject.created_at < pagination.decode_cursor(next_cursor) # type: ignore ) q = q.limit(20) @@ -1134,8 +1134,8 @@ async def get_inbox( if ( items and await db_session.scalar( - select(func.count(models.InboxObject.id)).where( - *where, models.InboxObject.created_at < items[-1].created_at + select(func.count(activitypub.models.InboxObject.id)).where( + *where, activitypub.models.InboxObject.created_at < items[-1].created_at ) ) > 0 @@ -1538,8 +1538,8 @@ async def serve_attachment( ): upload = ( await db_session.execute( - select(models.Upload).where( - models.Upload.content_hash == content_hash, + select(activitypub.models.Upload).where( + activitypub.models.Upload.content_hash == content_hash, ) ) ).scalar_one_or_none() @@ -1562,8 +1562,8 @@ async def serve_attachment_thumbnail( ): upload = ( await db_session.execute( - select(models.Upload).where( - models.Upload.content_hash == content_hash, + select(activitypub.models.Upload).where( + activitypub.models.Upload.content_hash == content_hash, ) ) ).scalar_one_or_none() @@ -1596,22 +1596,22 @@ async def robots_file(): Disallow: /remote_follow""" -async def _get_outbox_for_feed(db_session: AsyncSession) -> list[models.OutboxObject]: +async def _get_outbox_for_feed(db_session: AsyncSession) -> list[activitypub.models.OutboxObject]: return ( ( await db_session.scalars( - select(models.OutboxObject) + select(activitypub.models.OutboxObject) .where( - models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC, - models.OutboxObject.is_deleted.is_(False), - models.OutboxObject.ap_type.in_(["Note", "Article", "Video"]), + activitypub.models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC, + activitypub.models.OutboxObject.is_deleted.is_(False), + activitypub.models.OutboxObject.ap_type.in_(["Note", "Article", "Video"]), ) .options( - joinedload(models.OutboxObject.outbox_object_attachments).options( - joinedload(models.OutboxObjectAttachment.upload) + joinedload(activitypub.models.OutboxObject.outbox_object_attachments).options( + joinedload(activitypub.models.OutboxObjectAttachment.upload) ) ) - .order_by(models.OutboxObject.ap_published_at.desc()) + .order_by(activitypub.models.OutboxObject.ap_published_at.desc()) .limit(20) ) ) diff --git a/app/micropub.py b/app/micropub.py index 3f0d5250..648060f9 100644 --- a/app/micropub.py +++ b/app/micropub.py @@ -7,11 +7,11 @@ from fastapi.responses import RedirectResponse from loguru import logger -from app import activitypub as ap -from app.boxes import get_outbox_object_by_ap_id -from app.boxes import send_create -from app.boxes import send_delete -from app.boxes import send_update +from activitypub import activitypub as ap +from activitypub.boxes import get_outbox_object_by_ap_id +from activitypub.boxes import send_create +from activitypub.boxes import send_delete +from activitypub.boxes import send_update from app.database import AsyncSession from app.database import get_db_session from app.indieauth import AccessTokenInfo diff --git a/app/models.py b/app/models.py index f1b4caaa..01407d46 100644 --- a/app/models.py +++ b/app/models.py @@ -1,8 +1,6 @@ import enum from datetime import datetime from typing import Any -from typing import Optional -from typing import Union import pydantic from loguru import logger @@ -12,21 +10,15 @@ from sqlalchemy import DateTime from sqlalchemy import Enum from sqlalchemy import ForeignKey -from sqlalchemy import Index from sqlalchemy import Integer from sqlalchemy import String from sqlalchemy import Table from sqlalchemy import UniqueConstraint -from sqlalchemy import text from sqlalchemy.orm import Mapped from sqlalchemy.orm import relationship -from app import activitypub as ap -from app.actor import LOCAL_ACTOR -from app.actor import Actor as BaseActor -from app.ap_object import Attachment -from app.ap_object import Object as BaseObject -from app.config import BASE_URL +from activitypub import activitypub as ap +from activitypub.models import Actor, InboxObject, OutboxObject from app.database import Base from app.database import metadata_obj from app.utils import webmentions @@ -39,409 +31,6 @@ class ObjectRevision(pydantic.BaseModel): updated_at: str -class Actor(Base, BaseActor): - __tablename__ = "actor" - - id = Column(Integer, primary_key=True, index=True) - created_at = Column(DateTime(timezone=True), nullable=False, default=now) - updated_at = Column(DateTime(timezone=True), nullable=False, default=now) - - ap_id: Mapped[str] = Column(String, unique=True, nullable=False, index=True) - ap_actor: Mapped[ap.RawObject] = Column(JSON, nullable=False) - ap_type = Column(String, nullable=False) - - 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") - - are_announces_hidden_from_stream = Column( - Boolean, nullable=False, default=False, server_default="0" - ) - - @property - def is_from_db(self) -> bool: - return True - - -class InboxObject(Base, BaseObject): - __tablename__ = "inbox" - - id = Column(Integer, primary_key=True, index=True) - created_at = Column(DateTime(timezone=True), nullable=False, default=now) - updated_at = Column(DateTime(timezone=True), nullable=False, default=now) - - actor_id = Column(Integer, ForeignKey("actor.id"), nullable=False) - actor: Mapped[Actor] = relationship(Actor, uselist=False) - - server = Column(String, nullable=False) - - is_hidden_from_stream = Column(Boolean, nullable=False, default=False) - - ap_actor_id = Column(String, nullable=False) - ap_type = Column(String, nullable=False, index=True) - ap_id: Mapped[str] = Column(String, nullable=False, unique=True, index=True) - ap_context = Column(String, nullable=True) - ap_published_at = Column(DateTime(timezone=True), nullable=False) - ap_object: Mapped[ap.RawObject] = Column(JSON, nullable=False) - - # Only set for activities - activity_object_ap_id = Column(String, nullable=True, index=True) - - visibility = Column(Enum(ap.VisibilityEnum), nullable=False) - conversation = Column(String, nullable=True) - - has_local_mention = Column( - Boolean, nullable=False, default=False, server_default="0" - ) - - # Used for Like, Announce and Undo activities - relates_to_inbox_object_id = Column( - Integer, - ForeignKey("inbox.id"), - nullable=True, - ) - relates_to_inbox_object: Mapped[Optional["InboxObject"]] = relationship( - "InboxObject", - foreign_keys=relates_to_inbox_object_id, - remote_side=id, - uselist=False, - ) - relates_to_outbox_object_id = Column( - Integer, - ForeignKey("outbox.id"), - nullable=True, - ) - relates_to_outbox_object: Mapped[Optional["OutboxObject"]] = relationship( - "OutboxObject", - foreign_keys=[relates_to_outbox_object_id], - uselist=False, - ) - - undone_by_inbox_object_id = Column(Integer, ForeignKey("inbox.id"), nullable=True) - - # Link the oubox AP ID to allow undo without any extra query - liked_via_outbox_object_ap_id = Column(String, nullable=True) - announced_via_outbox_object_ap_id = Column(String, nullable=True) - voted_for_answers: Mapped[list[str] | None] = Column(JSON, nullable=True) - - is_bookmarked = Column(Boolean, nullable=False, default=False) - - # Used to mark deleted objects, but also activities that were undone - is_deleted = Column(Boolean, nullable=False, default=False) - is_transient = Column(Boolean, nullable=False, default=False, server_default="0") - - replies_count: Mapped[int] = Column(Integer, nullable=False, default=0) - - og_meta: Mapped[list[dict[str, Any]] | None] = Column(JSON, nullable=True) - - @property - def relates_to_anybox_object(self) -> Union["InboxObject", "OutboxObject"] | None: - if self.relates_to_inbox_object_id: - return self.relates_to_inbox_object - elif self.relates_to_outbox_object_id: - return self.relates_to_outbox_object - else: - return None - - @property - def is_from_db(self) -> bool: - return True - - @property - def is_from_inbox(self) -> bool: - return True - - -class OutboxObject(Base, BaseObject): - __tablename__ = "outbox" - - id = Column(Integer, primary_key=True, index=True) - created_at = Column(DateTime(timezone=True), nullable=False, default=now) - updated_at = Column(DateTime(timezone=True), nullable=False, default=now) - - is_hidden_from_homepage = Column(Boolean, nullable=False, default=False) - - public_id = Column(String, nullable=False, index=True) - slug = Column(String, nullable=True, index=True) - - ap_type = Column(String, nullable=False, index=True) - ap_id: Mapped[str] = Column(String, nullable=False, unique=True, index=True) - ap_context = Column(String, nullable=True) - ap_object: Mapped[ap.RawObject] = Column(JSON, nullable=False) - - activity_object_ap_id = Column(String, nullable=True, index=True) - - # Source content for activities (like Notes) - source = Column(String, nullable=True) - revisions: Mapped[list[dict[str, Any]] | None] = Column(JSON, nullable=True) - - ap_published_at = Column(DateTime(timezone=True), nullable=False, default=now) - visibility = Column(Enum(ap.VisibilityEnum), nullable=False) - conversation = Column(String, nullable=True) - - likes_count = Column(Integer, nullable=False, default=0) - announces_count = Column(Integer, nullable=False, default=0) - replies_count: Mapped[int] = Column(Integer, nullable=False, default=0) - webmentions_count: Mapped[int] = Column( - Integer, nullable=False, default=0, server_default="0" - ) - # reactions: Mapped[list[dict[str, Any]] | None] = Column(JSON, nullable=True) - - og_meta: Mapped[list[dict[str, Any]] | None] = Column(JSON, nullable=True) - - # For the featured collection - is_pinned = Column(Boolean, nullable=False, default=False) - is_transient = Column(Boolean, nullable=False, default=False, server_default="0") - - # Never actually delete from the outbox - is_deleted = Column(Boolean, nullable=False, default=False) - - # Used for Create, Like, Announce and Undo activities - relates_to_inbox_object_id = Column( - Integer, - ForeignKey("inbox.id"), - nullable=True, - ) - relates_to_inbox_object: Mapped[Optional["InboxObject"]] = relationship( - "InboxObject", - foreign_keys=[relates_to_inbox_object_id], - uselist=False, - ) - relates_to_outbox_object_id = Column( - Integer, - ForeignKey("outbox.id"), - nullable=True, - ) - relates_to_outbox_object: Mapped[Optional["OutboxObject"]] = relationship( - "OutboxObject", - foreign_keys=[relates_to_outbox_object_id], - remote_side=id, - uselist=False, - ) - # For Follow activies - relates_to_actor_id = Column( - Integer, - ForeignKey("actor.id"), - nullable=True, - ) - relates_to_actor: Mapped[Optional["Actor"]] = relationship( - "Actor", - foreign_keys=[relates_to_actor_id], - uselist=False, - ) - - undone_by_outbox_object_id = Column(Integer, ForeignKey("outbox.id"), nullable=True) - - @property - def actor(self) -> BaseActor: - return LOCAL_ACTOR - - outbox_object_attachments: Mapped[list["OutboxObjectAttachment"]] = relationship( - "OutboxObjectAttachment", uselist=True, backref="outbox_object" - ) - - @property - def attachments(self) -> list[Attachment]: - out = [] - for attachment in self.outbox_object_attachments: - url = ( - BASE_URL - + f"/attachments/{attachment.upload.content_hash}/{attachment.filename}" - ) - out.append( - Attachment.parse_obj( - { - "type": "Document", - "mediaType": attachment.upload.content_type, - "name": attachment.alt or attachment.filename, - "url": url, - "width": attachment.upload.width, - "height": attachment.upload.height, - "proxiedUrl": url, - "resizedUrl": ( - BASE_URL - + ( - "/attachments/thumbnails/" - f"{attachment.upload.content_hash}" - f"/{attachment.filename}" - ) - if attachment.upload.has_thumbnail - else None - ), - } - ) - ) - return out - - @property - def relates_to_anybox_object(self) -> Union["InboxObject", "OutboxObject"] | None: - if self.relates_to_inbox_object_id: - return self.relates_to_inbox_object - elif self.relates_to_outbox_object_id: - return self.relates_to_outbox_object - else: - return None - - @property - def is_from_db(self) -> bool: - return True - - @property - def is_from_outbox(self) -> bool: - return True - - @property - def url(self) -> str | None: - # XXX: rewrite old URL here for compat - if self.ap_type == "Article" and self.slug and self.public_id: - return f"{BASE_URL}/articles/{self.public_id[:7]}/{self.slug}" - return super().url - - -class Follower(Base): - __tablename__ = "follower" - - id = Column(Integer, primary_key=True, index=True) - created_at = Column(DateTime(timezone=True), nullable=False, default=now) - updated_at = Column(DateTime(timezone=True), nullable=False, default=now) - - actor_id = Column(Integer, ForeignKey("actor.id"), nullable=False, unique=True) - actor: Mapped[Actor] = relationship(Actor, uselist=False) - - inbox_object_id = Column(Integer, ForeignKey("inbox.id"), nullable=False) - inbox_object = relationship(InboxObject, uselist=False) - - ap_actor_id = Column(String, nullable=False, unique=True) - - -class Following(Base): - __tablename__ = "following" - - id = Column(Integer, primary_key=True, index=True) - created_at = Column(DateTime(timezone=True), nullable=False, default=now) - updated_at = Column(DateTime(timezone=True), nullable=False, default=now) - - actor_id = Column(Integer, ForeignKey("actor.id"), nullable=False, unique=True) - actor = relationship(Actor, uselist=False) - - outbox_object_id = Column(Integer, ForeignKey("outbox.id"), nullable=False) - outbox_object = relationship(OutboxObject, uselist=False) - - ap_actor_id = Column(String, nullable=False, unique=True) - - -class IncomingActivity(Base): - __tablename__ = "incoming_activity" - - id = Column(Integer, primary_key=True, index=True) - created_at = Column(DateTime(timezone=True), nullable=False, default=now) - - # An incoming activity can be a webmention - webmention_source = Column(String, nullable=True) - # or an AP object - sent_by_ap_actor_id = Column(String, nullable=True) - ap_id = Column(String, nullable=True, index=True) - ap_object: Mapped[ap.RawObject] = Column(JSON, nullable=True) - - tries: Mapped[int] = Column(Integer, nullable=False, default=0) - next_try = Column(DateTime(timezone=True), nullable=True, default=now) - - last_try = Column(DateTime(timezone=True), nullable=True) - - is_processed = Column(Boolean, nullable=False, default=False) - is_errored = Column(Boolean, nullable=False, default=False) - error = Column(String, nullable=True) - - -class OutgoingActivity(Base): - __tablename__ = "outgoing_activity" - - id = Column(Integer, primary_key=True, index=True) - created_at = Column(DateTime(timezone=True), nullable=False, default=now) - - recipient = Column(String, nullable=False) - - outbox_object_id = Column(Integer, ForeignKey("outbox.id"), nullable=True) - outbox_object = relationship(OutboxObject, uselist=False) - - # Can also reference an inbox object if it needds to be forwarded - inbox_object_id = Column(Integer, ForeignKey("inbox.id"), nullable=True) - inbox_object = relationship(InboxObject, uselist=False) - - # The source will be the outbox object URL - webmention_target = Column(String, nullable=True) - - tries = Column(Integer, nullable=False, default=0) - next_try = Column(DateTime(timezone=True), nullable=True, default=now) - - last_try = Column(DateTime(timezone=True), nullable=True) - last_status_code = Column(Integer, nullable=True) - last_response = Column(String, nullable=True) - - is_sent = Column(Boolean, nullable=False, default=False) - is_errored = Column(Boolean, nullable=False, default=False) - error = Column(String, nullable=True) - - @property - def anybox_object(self) -> OutboxObject | InboxObject: - if self.outbox_object_id: - return self.outbox_object # type: ignore - elif self.inbox_object_id: - return self.inbox_object # type: ignore - else: - raise ValueError("Should never happen") - - -class TaggedOutboxObject(Base): - __tablename__ = "tagged_outbox_object" - __table_args__ = ( - UniqueConstraint("outbox_object_id", "tag", name="uix_tagged_object"), - ) - - id = Column(Integer, primary_key=True, index=True) - - outbox_object_id = Column(Integer, ForeignKey("outbox.id"), nullable=False) - outbox_object = relationship(OutboxObject, uselist=False) - - tag = Column(String, nullable=False, index=True) - - -class Upload(Base): - __tablename__ = "upload" - - id = Column(Integer, primary_key=True, index=True) - created_at = Column(DateTime(timezone=True), nullable=False, default=now) - - content_type: Mapped[str] = Column(String, nullable=False) - content_hash = Column(String, nullable=False, unique=True) - - has_thumbnail = Column(Boolean, nullable=False) - - # Only set for images - blurhash = Column(String, nullable=True) - width = Column(Integer, nullable=True) - height = Column(Integer, nullable=True) - - @property - def is_image(self) -> bool: - return self.content_type.startswith("image") - - -class OutboxObjectAttachment(Base): - __tablename__ = "outbox_object_attachment" - - id = Column(Integer, primary_key=True, index=True) - created_at = Column(DateTime(timezone=True), nullable=False, default=now) - filename = Column(String, nullable=False) - alt = Column(String, nullable=True) - - outbox_object_id = Column(Integer, ForeignKey("outbox.id"), nullable=False) - - upload_id = Column(Integer, ForeignKey("upload.id"), nullable=False) - upload: Mapped["Upload"] = relationship(Upload, uselist=False) - - class IndieAuthAuthorizationRequest(Base): __tablename__ = "indieauth_authorization_request" @@ -545,44 +134,6 @@ def as_facepile_item(self) -> webmentions.Webmention | None: return None -class PollAnswer(Base): - __tablename__ = "poll_answer" - __table_args__ = ( - # Enforce a single answer for poll/actor/answer - UniqueConstraint( - "outbox_object_id", - "name", - "actor_id", - name="uix_outbox_object_id_name_actor_id", - ), - # Enforce an actor can only vote once on a "oneOf" Question - Index( - "uix_one_of_outbox_object_id_actor_id", - "outbox_object_id", - "actor_id", - unique=True, - sqlite_where=text('poll_type = "oneOf"'), - ), - ) - - id = Column(Integer, primary_key=True, index=True) - created_at = Column(DateTime(timezone=True), nullable=False, default=now) - - outbox_object_id = Column(Integer, ForeignKey("outbox.id"), nullable=False) - outbox_object = relationship(OutboxObject, uselist=False) - - # oneOf|anyOf - poll_type = Column(String, nullable=False) - - inbox_object_id = Column(Integer, ForeignKey("inbox.id"), nullable=False) - inbox_object = relationship(InboxObject, uselist=False) - - actor_id = Column(Integer, ForeignKey("actor.id"), nullable=False) - actor = relationship(Actor, uselist=False) - - name = Column(String, nullable=False) - - @enum.unique class NotificationType(str, enum.Enum): NEW_FOLLOWER = "new_follower" diff --git a/app/prune.py b/app/prune.py index 75ca89b6..f06dcfaf 100644 --- a/app/prune.py +++ b/app/prune.py @@ -8,7 +8,8 @@ from sqlalchemy import or_ from sqlalchemy import select -from app import activitypub as ap +import activitypub.models +from activitypub import activitypub as ap from app import models from app.config import BASE_URL from app.config import INBOX_RETENTION_DAYS @@ -36,12 +37,12 @@ async def _prune_old_incoming_activities( db_session: AsyncSession, ) -> None: result = await db_session.execute( - delete(models.IncomingActivity) + delete(activitypub.models.IncomingActivity) .where( - models.IncomingActivity.created_at + activitypub.models.IncomingActivity.created_at < now() - timedelta(days=INBOX_RETENTION_DAYS), # Keep failed activity for debug - models.IncomingActivity.is_errored.is_(False), + activitypub.models.IncomingActivity.is_errored.is_(False), ) .execution_options(synchronize_session=False) ) @@ -52,12 +53,12 @@ async def _prune_old_outgoing_activities( db_session: AsyncSession, ) -> None: result = await db_session.execute( - delete(models.OutgoingActivity) + delete(activitypub.models.OutgoingActivity) .where( - models.OutgoingActivity.created_at + activitypub.models.OutgoingActivity.created_at < now() - timedelta(days=INBOX_RETENTION_DAYS), # Keep failed activity for debug - models.OutgoingActivity.is_errored.is_(False), + activitypub.models.OutgoingActivity.is_errored.is_(False), ) .execution_options(synchronize_session=False) ) @@ -67,45 +68,45 @@ async def _prune_old_outgoing_activities( async def _prune_old_inbox_objects( db_session: AsyncSession, ) -> None: - outbox_conversation = select(func.distinct(models.OutboxObject.conversation)).where( - models.OutboxObject.conversation.is_not(None), - models.OutboxObject.conversation.not_like(f"{BASE_URL}%"), + outbox_conversation = select(func.distinct(activitypub.models.OutboxObject.conversation)).where( + activitypub.models.OutboxObject.conversation.is_not(None), + activitypub.models.OutboxObject.conversation.not_like(f"{BASE_URL}%"), ) result = await db_session.execute( - delete(models.InboxObject) + delete(activitypub.models.InboxObject) .where( # Keep bookmarked objects - models.InboxObject.is_bookmarked.is_(False), + activitypub.models.InboxObject.is_bookmarked.is_(False), # Keep liked objects - models.InboxObject.liked_via_outbox_object_ap_id.is_(None), + activitypub.models.InboxObject.liked_via_outbox_object_ap_id.is_(None), # Keep announced objects - models.InboxObject.announced_via_outbox_object_ap_id.is_(None), + activitypub.models.InboxObject.announced_via_outbox_object_ap_id.is_(None), # Keep objects mentioning the local actor - models.InboxObject.has_local_mention.is_(False), + activitypub.models.InboxObject.has_local_mention.is_(False), # Keep objects related to local conversations (i.e. don't break the # public website) or_( - models.InboxObject.conversation.not_like(f"{BASE_URL}%"), - models.InboxObject.conversation.is_(None), - models.InboxObject.conversation.not_in(outbox_conversation), + activitypub.models.InboxObject.conversation.not_like(f"{BASE_URL}%"), + activitypub.models.InboxObject.conversation.is_(None), + activitypub.models.InboxObject.conversation.not_in(outbox_conversation), ), # Keep activities related to the outbox (like Like/Announce/Follow...) or_( # XXX: no `/` here because the local ID does not have one - models.InboxObject.activity_object_ap_id.not_like(f"{BASE_URL}%"), - models.InboxObject.activity_object_ap_id.is_(None), + activitypub.models.InboxObject.activity_object_ap_id.not_like(f"{BASE_URL}%"), + activitypub.models.InboxObject.activity_object_ap_id.is_(None), ), # Keep direct messages not_( and_( - models.InboxObject.visibility == ap.VisibilityEnum.DIRECT, - models.InboxObject.ap_type.in_(["Note"]), + activitypub.models.InboxObject.visibility == ap.VisibilityEnum.DIRECT, + activitypub.models.InboxObject.ap_type.in_(["Note"]), ) ), # Keep Move object as they are linked to notifications - models.InboxObject.ap_type.not_in(["Move"]), + activitypub.models.InboxObject.ap_type.not_in(["Move"]), # Filter by retention days - models.InboxObject.ap_published_at + activitypub.models.InboxObject.ap_published_at < now() - timedelta(days=INBOX_RETENTION_DAYS), ) .execution_options(synchronize_session=False) diff --git a/app/source.py b/app/source.py index 20e98eea..0ac6911a 100644 --- a/app/source.py +++ b/app/source.py @@ -11,6 +11,7 @@ from pygments.util import ClassNotFound # type: ignore from sqlalchemy import select +import activitypub.models from app import webfinger from app.config import BASE_URL from app.config import CODE_HIGHLIGHTING_THEME @@ -18,7 +19,7 @@ from app.utils import emoji if typing.TYPE_CHECKING: - from app.actor import Actor + from activitypub.actor import Actor _FORMATTER = HtmlFormatter(style=CODE_HIGHLIGHTING_THEME) _HASHTAG_REGEX = re.compile(r"(#[\d\w]+)") @@ -121,7 +122,7 @@ async def _prefetch_mentioned_actors( content: str, ) -> dict[str, "Actor"]: from app import models - from app.actor import fetch_actor + from activitypub.actor import fetch_actor actors = {} @@ -137,9 +138,9 @@ async def _prefetch_mentioned_actors( _, username, domain = mention.split("@") actor = ( await db_session.execute( - select(models.Actor).where( - models.Actor.handle == mention, - models.Actor.is_deleted.is_(False), + select(activitypub.models.Actor).where( + activitypub.models.Actor.handle == mention, + activitypub.models.Actor.is_deleted.is_(False), ) ) ).scalar_one_or_none() diff --git a/app/templates.py b/app/templates.py index baee6598..353e94f7 100644 --- a/app/templates.py +++ b/app/templates.py @@ -18,12 +18,13 @@ from sqlalchemy import select from starlette.templating import _TemplateResponse as TemplateResponse -from app import activitypub as ap +import activitypub.models +from activitypub import activitypub as ap from app import config from app import models -from app.actor import LOCAL_ACTOR -from app.ap_object import Attachment -from app.ap_object import Object +from activitypub.actor import LOCAL_ACTOR +from activitypub.ap_object import Attachment +from activitypub.ap_object import Object from app.config import BASE_URL from app.config import CUSTOM_FOOTER from app.config import DEBUG @@ -114,19 +115,19 @@ async def render_template( else 0 ), "articles_count": await db_session.scalar( - select(func.count(models.OutboxObject.id)).where( - models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC, - models.OutboxObject.is_deleted.is_(False), - models.OutboxObject.is_hidden_from_homepage.is_(False), - models.OutboxObject.ap_type == "Article", + select(func.count(activitypub.models.OutboxObject.id)).where( + activitypub.models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC, + activitypub.models.OutboxObject.is_deleted.is_(False), + activitypub.models.OutboxObject.is_hidden_from_homepage.is_(False), + activitypub.models.OutboxObject.ap_type == "Article", ) ), "local_actor": LOCAL_ACTOR, "followers_count": await db_session.scalar( - select(func.count(models.Follower.id)) + select(func.count(activitypub.models.Follower.id)) ), "following_count": await db_session.scalar( - select(func.count(models.Following.id)) + select(func.count(activitypub.models.Following.id)) ), "actor_types": ap.ACTOR_TYPES, "custom_footer": CUSTOM_FOOTER, diff --git a/app/uploads.py b/app/uploads.py index 048c74c3..002674b0 100644 --- a/app/uploads.py +++ b/app/uploads.py @@ -8,7 +8,8 @@ from PIL import ImageOps from sqlalchemy import select -from app import activitypub as ap +import activitypub.models +from activitypub import activitypub as ap from app import models from app.config import BASE_URL from app.config import ROOT_DIR @@ -17,7 +18,7 @@ UPLOAD_DIR = ROOT_DIR / "data" / "uploads" -async def save_upload(db_session: AsyncSession, f: UploadFile) -> models.Upload | None: +async def save_upload(db_session: AsyncSession, f: UploadFile) -> activitypub.models.Upload | None: # Compute the hash h = hashlib.blake2b(digest_size=32) while True: @@ -31,7 +32,7 @@ async def save_upload(db_session: AsyncSession, f: UploadFile) -> models.Upload existing_upload = ( await db_session.execute( - select(models.Upload).where(models.Upload.content_hash == content_hash) + select(activitypub.models.Upload).where(activitypub.models.Upload.content_hash == content_hash) ) ).scalar_one_or_none() if existing_upload: @@ -90,7 +91,7 @@ async def save_upload(db_session: AsyncSession, f: UploadFile) -> models.Upload break dest.write(buf) - new_upload = models.Upload( + new_upload = activitypub.models.Upload( content_type=f.content_type, content_hash=content_hash, has_thumbnail=has_thumbnail, @@ -107,7 +108,7 @@ async def save_upload(db_session: AsyncSession, f: UploadFile) -> models.Upload def upload_to_attachment( - upload: models.Upload, + upload: activitypub.models.Upload, filename: str, alt_text: str | None, ) -> ap.RawObject: diff --git a/app/utils/custom_index_handler.py b/app/utils/custom_index_handler.py index eb776e29..a6598cb9 100644 --- a/app/utils/custom_index_handler.py +++ b/app/utils/custom_index_handler.py @@ -6,7 +6,7 @@ from fastapi import Request from fastapi.responses import JSONResponse -from app.actor import LOCAL_ACTOR +from activitypub.actor import LOCAL_ACTOR from app.config import is_activitypub_requested from app.database import AsyncSession from app.database import get_db_session diff --git a/app/utils/emoji.py b/app/utils/emoji.py index da4867cb..23b20bdc 100644 --- a/app/utils/emoji.py +++ b/app/utils/emoji.py @@ -4,7 +4,7 @@ from pathlib import Path if typing.TYPE_CHECKING: - from app.activitypub import RawObject + from activitypub.activitypub import RawObject EMOJI_REGEX = re.compile(r"(:[\d\w]+:)") diff --git a/app/utils/facepile.py b/app/utils/facepile.py index a4a15954..1a0f6d40 100644 --- a/app/utils/facepile.py +++ b/app/utils/facepile.py @@ -7,7 +7,7 @@ from loguru import logger from app import media -from app.models import InboxObject +from activitypub.models import InboxObject from app.models import Webmention from app.utils.datetime import parse_isoformat from app.utils.url import must_make_abs diff --git a/app/utils/opengraph.py b/app/utils/opengraph.py index e9e7e341..a83d3bc2 100644 --- a/app/utils/opengraph.py +++ b/app/utils/opengraph.py @@ -12,14 +12,12 @@ from pebble import concurrent # type: ignore from pydantic import BaseModel -from app import activitypub as ap -from app import ap_object +from activitypub import activitypub as ap, ap_object from app import config -from app.actor import LOCAL_ACTOR -from app.actor import fetch_actor +from activitypub.actor import LOCAL_ACTOR +from activitypub.actor import fetch_actor from app.database import AsyncSession -from app.models import InboxObject -from app.models import OutboxObject +from activitypub.models import InboxObject, OutboxObject from app.utils.url import is_url_valid from app.utils.url import make_abs diff --git a/app/utils/stats.py b/app/utils/stats.py index c9e318b5..c4a7b831 100644 --- a/app/utils/stats.py +++ b/app/utils/stats.py @@ -9,6 +9,7 @@ from sqlalchemy.orm import joinedload from tabulate import tabulate +import activitypub.models from app import models from app.config import ROOT_DIR from app.database import AsyncSession @@ -61,14 +62,14 @@ async def _get_stats(f) -> OutgoingActivityStatsItem: row = ( await db_session.execute( select( - func.count(models.OutgoingActivity.id).label("total_count"), + func.count(activitypub.models.OutgoingActivity.id).label("total_count"), func.sum( case( [ ( or_( - models.OutgoingActivity.next_try > now(), - models.OutgoingActivity.tries == 0, + activitypub.models.OutgoingActivity.next_try > now(), + activitypub.models.OutgoingActivity.tries == 0, ), 1, ), @@ -79,7 +80,7 @@ async def _get_stats(f) -> OutgoingActivityStatsItem: func.sum( case( [ - (models.OutgoingActivity.is_sent.is_(True), 1), + (activitypub.models.OutgoingActivity.is_sent.is_(True), 1), ], else_=0, ) @@ -87,7 +88,7 @@ async def _get_stats(f) -> OutgoingActivityStatsItem: func.sum( case( [ - (models.OutgoingActivity.is_errored.is_(True), 1), + (activitypub.models.OutgoingActivity.is_errored.is_(True), 1), ], else_=0, ) @@ -102,9 +103,9 @@ async def _get_stats(f) -> OutgoingActivityStatsItem: errored_count=row.errored_count or 0, ) - from_inbox = await _get_stats(models.OutgoingActivity.inbox_object_id.is_not(None)) + from_inbox = await _get_stats(activitypub.models.OutgoingActivity.inbox_object_id.is_not(None)) from_outbox = await _get_stats( - models.OutgoingActivity.outbox_object_id.is_not(None) + activitypub.models.OutgoingActivity.outbox_object_id.is_not(None) ) return OutgoingActivityStats( @@ -127,12 +128,12 @@ async def _get_stats(): outgoing_activities = ( ( await db_session.scalars( - select(models.OutgoingActivity) + select(activitypub.models.OutgoingActivity) .options( - joinedload(models.OutgoingActivity.inbox_object), - joinedload(models.OutgoingActivity.outbox_object), + joinedload(activitypub.models.OutgoingActivity.inbox_object), + joinedload(activitypub.models.OutgoingActivity.outbox_object), ) - .order_by(models.OutgoingActivity.last_try.desc()) + .order_by(activitypub.models.OutgoingActivity.last_try.desc()) .limit(10) ) ) diff --git a/app/webmentions.py b/app/webmentions.py index 12b99a6e..19ee8ad2 100644 --- a/app/webmentions.py +++ b/app/webmentions.py @@ -11,13 +11,14 @@ from sqlalchemy import func from sqlalchemy import select +import activitypub.models from app import models -from app.boxes import _get_outbox_announces_count -from app.boxes import _get_outbox_likes_count -from app.boxes import _get_outbox_replies_count -from app.boxes import get_outbox_object_by_ap_id -from app.boxes import get_outbox_object_by_slug_and_short_id -from app.boxes import is_notification_enabled +from activitypub.boxes import _get_outbox_announces_count +from activitypub.boxes import _get_outbox_likes_count +from activitypub.boxes import _get_outbox_replies_count +from activitypub.boxes import get_outbox_object_by_ap_id +from activitypub.boxes import get_outbox_object_by_slug_and_short_id +from activitypub.boxes import is_notification_enabled from app.database import AsyncSession from app.database import get_db_session from app.utils import microformats @@ -205,7 +206,7 @@ async def webmention_endpoint( async def _handle_webmention_side_effects( db_session: AsyncSession, webmention: models.Webmention, - mentioned_object: models.OutboxObject, + mentioned_object: activitypub.models.OutboxObject, ) -> None: if webmention.webmention_type == models.WebmentionType.UNKNOWN: # TODO: recount everything diff --git a/tasks.py b/tasks.py index 7bafa765..72309062 100644 --- a/tasks.py +++ b/tasks.py @@ -103,7 +103,7 @@ def uvicorn(ctx): @task def process_outgoing_activities(ctx): # type: (Context) -> None - from app.outgoing_activities import loop + from activitypub.outgoing_activities import loop asyncio.run(loop()) @@ -111,7 +111,7 @@ def process_outgoing_activities(ctx): @task def process_incoming_activities(ctx): # type: (Context) -> None - from app.incoming_activities import loop + from activitypub.incoming_activities import loop asyncio.run(loop()) @@ -264,9 +264,9 @@ def move_to(ctx, moved_to): from loguru import logger - from app.actor import LOCAL_ACTOR - from app.actor import fetch_actor - from app.boxes import send_move + from activitypub.actor import LOCAL_ACTOR + from activitypub.actor import fetch_actor + from activitypub.boxes import send_move from app.database import async_session from app.source import _MENTION_REGEX from app.webfinger import get_actor_url @@ -314,7 +314,7 @@ def self_destruct(ctx): # type: (Context) -> None from loguru import logger - from app.boxes import send_self_destruct + from activitypub.boxes import send_self_destruct from app.database import async_session logger.disable("app") @@ -392,8 +392,8 @@ def import_mastodon_following_accounts(ctx, path): # type: (Context, str) -> None from loguru import logger - from app.boxes import _get_following - from app.boxes import _send_follow + from activitypub.boxes import _get_following + from activitypub.boxes import _send_follow from app.database import async_session from app.utils.mastodon import get_actor_urls_from_following_accounts_csv_file diff --git a/tests/conftest.py b/tests/conftest.py index 2049270c..87d15530 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,7 @@ from app.database import async_session from app.database import engine from app.main import app -from tests.factories import _Session +from activitypub.tests.factories import _Session os.environ["MICROBLOGPUB_CONFIG_FILE"] = "tests.toml" diff --git a/tests/test_admin.py b/tests/test_admin.py index bb7635da..80a6fdd3 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -3,7 +3,7 @@ import starlette from fastapi.testclient import TestClient -from app import activitypub as ap +from activitypub import activitypub as ap from app.config import generate_csrf_token from app.main import app from tests.utils import generate_admin_session_cookies diff --git a/tests/test_emoji.py b/tests/test_emoji.py index 9a903387..558b7c19 100644 --- a/tests/test_emoji.py +++ b/tests/test_emoji.py @@ -1,7 +1,8 @@ from fastapi.testclient import TestClient from sqlalchemy.orm import Session -from app import activitypub as ap +import activitypub.models +from activitypub import activitypub as ap from app import models from app.config import generate_csrf_token from app.utils.emoji import EMOJIS_BY_NAME @@ -45,7 +46,7 @@ def test_emoji_note_with_emoji(db: Session, client: TestClient) -> None: assert response.status_code == 302 # And the Follow activity was created in the outbox - outbox_object = db.query(models.OutboxObject).one() + outbox_object = db.query(activitypub.models.OutboxObject).one() assert outbox_object.ap_type == "Note" assert len(outbox_object.tags) == 1 emoji_tag = outbox_object.tags[0] diff --git a/tests/test_httpsig.py b/tests/test_httpsig.py index a36775a0..18cdeb76 100644 --- a/tests/test_httpsig.py +++ b/tests/test_httpsig.py @@ -6,13 +6,13 @@ import respx from fastapi.testclient import TestClient -from app import activitypub as ap +from activitypub import activitypub as ap from app import httpsig from app.database import AsyncSession from app.httpsig import _KEY_CACHE from app.httpsig import HTTPSigInfo from app.key import Key -from tests import factories +from activitypub.tests import factories _test_app = fastapi.FastAPI() diff --git a/tests/test_ldsig.py b/tests/test_ldsig.py index 62ea5251..bfd3f2ba 100644 --- a/tests/test_ldsig.py +++ b/tests/test_ldsig.py @@ -4,11 +4,11 @@ import pytest from respx import MockRouter -from app import activitypub as ap +from activitypub import activitypub as ap from app import ldsig from app.database import AsyncSession from app.key import Key -from tests import factories +from activitypub.tests import factories _SAMPLE_CREATE = { "type": "Create", diff --git a/tests/test_public.py b/tests/test_public.py index 1c94f3df..30fbe41c 100644 --- a/tests/test_public.py +++ b/tests/test_public.py @@ -4,8 +4,8 @@ from fastapi.testclient import TestClient from sqlalchemy.orm import Session -from app import activitypub as ap -from app.actor import LOCAL_ACTOR +from activitypub import activitypub as ap +from activitypub.actor import LOCAL_ACTOR _ACCEPTED_AP_HEADERS = [ "application/activity+json", diff --git a/tests/test_tags.py b/tests/test_tags.py index e98812be..ad5abc1e 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -2,7 +2,8 @@ from sqlalchemy import select from sqlalchemy.orm import Session -from app import activitypub as ap +import activitypub.models +from activitypub import activitypub as ap from app import models from app.config import generate_csrf_token from tests.utils import generate_admin_session_cookies @@ -37,7 +38,7 @@ def test_tags__note_with_tag(db: Session, client: TestClient) -> None: assert response.status_code == 302 # And the Follow activity was created in the outbox - outbox_object = db.execute(select(models.OutboxObject)).scalar_one() + outbox_object = db.execute(select(activitypub.models.OutboxObject)).scalar_one() assert outbox_object.ap_type == "Note" assert len(outbox_object.tags) == 1 emoji_tag = outbox_object.tags[0] diff --git a/tests/utils.py b/tests/utils.py index fc829d1e..132e90c3 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -7,19 +7,19 @@ import httpx import respx -from app import activitypub as ap -from app import actor +import activitypub.models +from activitypub import activitypub as ap, actor from app import httpsig from app import models -from app.actor import LOCAL_ACTOR -from app.ap_object import RemoteObject +from activitypub.actor import LOCAL_ACTOR +from activitypub.ap_object import RemoteObject from app.config import session_serializer from app.database import AsyncSession from app.database import async_session -from app.incoming_activities import fetch_next_incoming_activity -from app.incoming_activities import process_next_incoming_activity +from activitypub.incoming_activities import fetch_next_incoming_activity +from activitypub.incoming_activities import process_next_incoming_activity from app.main import app -from tests import factories +from activitypub.tests import factories @contextmanager @@ -75,7 +75,7 @@ def setup_remote_actor( return ra -def setup_remote_actor_as_follower(ra: actor.RemoteActor) -> models.Follower: +def setup_remote_actor_as_follower(ra: actor.RemoteActor) -> activitypub.models.Follower: actor = factories.ActorFactory.from_remote_actor(ra) follow_id = uuid4().hex @@ -99,7 +99,7 @@ def setup_remote_actor_as_follower(ra: actor.RemoteActor) -> models.Follower: return follower -def setup_remote_actor_as_following(ra: actor.RemoteActor) -> models.Following: +def setup_remote_actor_as_following(ra: actor.RemoteActor) -> activitypub.models.Following: actor = factories.ActorFactory.from_remote_actor(ra) follow_id = uuid4().hex @@ -125,7 +125,7 @@ def setup_remote_actor_as_following(ra: actor.RemoteActor) -> models.Following: def setup_remote_actor_as_following_and_follower( ra: actor.RemoteActor, -) -> tuple[models.Following, models.Follower]: +) -> tuple[activitypub.models.Following, activitypub.models.Follower]: actor = factories.ActorFactory.from_remote_actor(ra) follow_id = uuid4().hex @@ -175,7 +175,7 @@ def setup_outbox_note( cc: list[str] | None = None, tags: list[ap.RawObject] | None = None, in_reply_to: str | None = None, -) -> models.OutboxObject: +) -> activitypub.models.OutboxObject: note_id = uuid4().hex note_from_outbox = RemoteObject( factories.build_note_object( @@ -193,13 +193,13 @@ def setup_outbox_note( def setup_inbox_note( - actor: models.Actor, + actor: activitypub.models.Actor, content: str = "Hello", to: list[str] | None = None, cc: list[str] | None = None, tags: list[ap.RawObject] | None = None, in_reply_to: str | None = None, -) -> models.OutboxObject: +) -> activitypub.models.OutboxObject: note_id = uuid4().hex note_from_outbox = RemoteObject( factories.build_note_object( @@ -217,8 +217,8 @@ def setup_inbox_note( def setup_inbox_delete( - actor: models.Actor, deleted_object_ap_id: str -) -> models.InboxObject: + actor: activitypub.models.Actor, deleted_object_ap_id: str +) -> activitypub.models.InboxObject: follow_from_inbox = RemoteObject( factories.build_delete_activity( from_remote_actor=actor,