diff --git a/app/activitypub.py b/app/activitypub.py index 571e16e..d0dd416 100644 --- a/app/activitypub.py +++ b/app/activitypub.py @@ -81,6 +81,20 @@ class VisibilityEnum(str, enum.Enum): FOLLOWERS_ONLY = "followers-only" DIRECT = "direct" + +def handle_visibility( + ap_object: dict +) -> VisibilityEnum: + to = ap_object.get("to", []) + cc = ap_object.get("cc", []) + if AS_PUBLIC in to: + return VisibilityEnum.PUBLIC + elif AS_PUBLIC in cc: + return VisibilityEnum.UNLISTED + else: + return VisibilityEnum.DIRECT + + async def post( url: str, payload : dict, @@ -100,6 +114,7 @@ async def post( return resp + async def fetch( url: str, ) -> dict: diff --git a/app/actor.py b/app/actor.py index 8df61d6..062032f 100644 --- a/app/actor.py +++ b/app/actor.py @@ -7,6 +7,22 @@ from urllib.parse import urlparse import app.activitypub as ap +class BaseActor: + def __init__(self, ap_actor: ap.RawObject) -> None: + if (ap_type := ap_actor.get("type")) not in ap.ACTOR_TYPES: + raise ValueError(f"Unexpected actor type: {ap_type}") + + self._ap_actor = ap_actor + self._ap_type = ap_type + + @property + def ap_actor(self) -> ap.RawObject: + return self._ap_actor + + @property + def inbox_url(self) -> str: + return self.ap_actor["inbox"] + async def fetch_actor( actor_id : str, db_session : AsyncSession, diff --git a/app/boxes.py b/app/boxes.py index 43724d1..9284117 100644 --- a/app/boxes.py +++ b/app/boxes.py @@ -2,18 +2,24 @@ from typing import Any import uuid +from sqlalchemy.orm import session + from app import models from app.database import AsyncSession +from app.models import InboxObject, now from app.activitypub import ME +from app.activitypub import handle_visibility from app.config import MANUALLY_APPROVES_FOLLOWERS from app.config import BASE_URL from app.models import Actor from app.actor import fetch_actor +from app.actor import BaseActor import app.activitypub as ap - +from urllib.parse import urlparse from sqlalchemy import select +from sqlalchemy.exc import IntegrityError from loguru import logger from uuid import uuid4 @@ -26,6 +32,7 @@ def allocate_outbox_id() -> str: def build_object_id(id) -> str: return f"{BASE_URL}/tail/{id}" + async def save_incoming( db_session: AsyncSession, payload: dict, @@ -59,8 +66,29 @@ async def process_incoming( ) -> bool: actor = await fetch_actor(ap_object["actor"], db_session) + def save_to_db(object) -> InboxObject: + inbox_object = models.InboxObject( + actor_id=actor.id, + server=urlparse(object["id"]).hostname, + is_hidden_from_stream=True, + ap_actor_id=actor.ap_id, + ap_type=object["type"], + ap_id=object["id"], + ap_published_at=now(), + ap_object=object, + visibility=handle_visibility(object), + activity_object_ap_id=object["actor"] + #TODO relates + ) + return inbox_object + if "Follow" == ap_object["type"]: - if await _handle_follow(db_session, actor.ap_actor["inbox"], ap_object): + inbox_object = save_to_db(ap_object) + db_session.add(inbox_object) + await db_session.flush() + await db_session.refresh(inbox_object) + + if await _handle_follow(db_session, actor, inbox_object): return True return False @@ -69,37 +97,50 @@ async def process_incoming( async def _handle_follow( db_session : AsyncSession, - inbox_url : str | Any, - ap_object : dict, + actor : Actor, + inbox_object : InboxObject, ) -> bool: - if ME["id"] != ap_object["object"]: + if ME["id"] != inbox_object.ap_object["object"]: #type: ignore # await db_session.delete(ap_object) - logger.warning("no match follow object!" + ap_object["id"]) + logger.warning("no match follow object!" + inbox_object.ap_object["id"]) #type: ignore return False if MANUALLY_APPROVES_FOLLOWERS: # TODO return False - await _send_accept(db_session, inbox_url, ap_object) + await _send_accept(db_session, actor, inbox_object) return True async def _send_accept( db_session: AsyncSession, - inbox_url : str | Any, - ap_object : dict, + actor : Actor, + inbox_object : InboxObject, ) -> None : + actor = BaseActor(actor.ap_actor) + follower = models.Follower( + actor_id=inbox_object.actor_id, + inbox_object_id=inbox_object.id, + ap_actor_id=inbox_object.ap_object["actor"], #type: ignore + ) + + try: + db_session.add(follower) + await db_session.flush() + except IntegrityError: + await db_session.rollback() + logger.warning("existing follower in db!") reply_id = allocate_outbox_id() - url = inbox_url + url = actor.inbox_url # type: ignore out = { "@context": ap.AS_CTX, "id": build_object_id(reply_id), "type": "Accept", "actor": ME["id"], - "object": ap_object["id"], + "object": inbox_object.ap_object["id"], #type: ignore } #TODO outcoming - await ap.post(url, out) + await ap.post(url, out) # type: ignore