#!/usr/bin/env python3 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, OutboxObject, 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 import app.activitypub as ap from urllib.parse import urlparse from sqlalchemy import select from sqlalchemy import delete from sqlalchemy.orm import joinedload from sqlalchemy.exc import IntegrityError from loguru import logger from uuid import uuid4 def allocate_outbox_id() -> str: return str(uuid.uuid4()) def build_object_id(id) -> str: return f"{BASE_URL}/tail/{id}" async def save_incoming( db_session: AsyncSession, payload: dict, ) -> models.IncomingActivity | None: ap_id: str if "@context" not in payload: logger.warning(f"invalid object: {payload}") return None if "id" in payload: ap_id = payload["id"] else: ap_id = str(uuid4()) incoming_activity = models.IncomingActivity( ap_id=ap_id, ap_object=payload, ) if await process_incoming(db_session, payload): await db_session.commit() await db_session.flush() return incoming_activity db_session.add(incoming_activity) await db_session.commit() await db_session.refresh(incoming_activity) return incoming_activity async def process_incoming( db_session: AsyncSession, ap_object: dict, ) -> bool: actor = await fetch_actor(db_session, ap_object["actor"]) def build_object( object, relates_to_inbox_object_id = None, relates_to_outbox_object_id = None, ) -> 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"], relates_to_inbox_object_id=relates_to_inbox_object_id, relates_to_outbox_object_id=relates_to_outbox_object_id, ) return inbox_object if "Follow" == ap_object["type"]: inbox_object = build_object(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 elif "Undo" == ap_object["type"]: #inbox_object = build_object(ap_object) #db_session.add(inbox_object) if await _handle_undo(db_session, ap_object): return True elif ap_object["type"] in ["Accept", "Rejact"]: relate_following_object = (await db_session.execute( select(models.OutboxObject) .where(models.OutboxObject.ap_id == ap_object["object"]) .options( joinedload(models.OutboxObject.relates_to_inbox_object).options( joinedload(models.InboxObject.actor) ) ) ) ).unique().scalar_one_or_none() if "Accept" == ap_object["type"]: inbox_object = build_object(ap_object) db_session.add(inbox_object) await db_session.flush() await db_session.refresh(inbox_object) following = models.Following( actor_id=relate_following_object.relates_to_actor.id, #type: ignore outbox_object_id=relate_following_object.id, #type: ignore ap_actor_id=ap_object["actor"], #type: ignore ) db_session.add(following) await db_session.flush() await db_session.refresh(following) return True return False async def _handle_follow( db_session : AsyncSession, actor : Actor, inbox_object : InboxObject, ) -> bool: if ME["id"] != inbox_object.ap_object["object"]: #type: ignore # await db_session.delete(ap_object) 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, actor, inbox_object) return True async def _send_accept( db_session: AsyncSession, actor : Actor, inbox_object : InboxObject, ) -> None : 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 = actor.inbox_url # type: ignore out = { "@context": ap.AS_CTX, "id": build_object_id(reply_id), "type": "Accept", "actor": ME["id"], "object": inbox_object.ap_object["id"], #type: ignore } #TODO outcoming await ap.post(url, out) # type: ignore async def _handle_undo( db_session : AsyncSession, inbox_object : dict ) -> bool: if inbox_object["object"]["object"] != ME["id"]: logger.warning("Wrong undo object! " + inbox_object["object"]["actor"]) return False if "Follow" == inbox_object["object"]["type"]: relate_object = (await db_session.execute( select(models.InboxObject) .where(models.InboxObject.ap_id == inbox_object["object"]["id"]) .options( joinedload(models.InboxObject.actor), joinedload(models.InboxObject.relates_to_inbox_object), joinedload(models.InboxObject.relates_to_outbox_object), ) ) ).scalar_one_or_none() # type: ignore if relate_object: relate_object.undo_id=inbox_object["object"]["id"] relate_object.is_deleted=True await db_session.execute( delete(models.Follower).where( models.Follower.ap_actor_id == inbox_object["actor"] ) ) logger.info("undo follow " + inbox_object["actor"]) return True return False async def send_follow( db_session : AsyncSession, acct : str ): await _send_follow(db_session, acct) await db_session.commit() await db_session.flush() async def _send_follow( db_session : AsyncSession, actor_url : str, ): actor = await fetch_actor(db_session, actor_url) follow_id = build_object_id(allocate_outbox_id()) out = { "@context": ap.AS_CTX, "id": follow_id, "type": "Follow", "actor": ME["id"], "object": actor.ap_id, } await save_to_outbox( db_session, follow_id, out, relates_to_actor_id=actor.id, # type: ignore ) await ap.post( actor.inbox_url, out, ) async def save_to_inbox( db_session : AsyncSession, inbox_id : str, ap_object : dict, relates_to_inbox_object_id: int | None = None, relates_to_outbox_object_id: int | None = None, relates_to_actor_id: int | None = None, ) -> InboxObject: ap_type = ap_object["type"] ap_id = ap_object["id"] visibility = handle_visibility(ap_object) inbox_object = OutboxObject( public_id=inbox_id, ap_object=ap_object, ap_id=ap_id, ap_type=ap_type, visibility =visibility, relates_to_inbox_object_id=relates_to_inbox_object_id, relates_to_outbox_object_id=relates_to_outbox_object_id, relates_to_actor_id=relates_to_actor_id, ) db_session.add(inbox_object) await db_session.flush() await db_session.refresh(inbox_object) return inbox_object async def save_to_outbox( db_session : AsyncSession, outbox_id : str, ap_object : dict, relates_to_inbox_object_id: int | None = None, relates_to_outbox_object_id: int | None = None, relates_to_actor_id: int | None = None, ) -> OutboxObject: ap_type = ap_object["type"] ap_id = ap_object["id"] visibility = handle_visibility(ap_object) outbox_object = OutboxObject( public_id=outbox_id, ap_object=ap_object, ap_id=ap_id, ap_type=ap_type, visibility =visibility, relates_to_inbox_object_id=relates_to_inbox_object_id, relates_to_outbox_object_id=relates_to_outbox_object_id, relates_to_actor_id=relates_to_actor_id, ) db_session.add(outbox_object) await db_session.flush() await db_session.refresh(outbox_object) return outbox_object