From d321d07a495853fbfe19fea8362d41a430a094c3 Mon Sep 17 00:00:00 2001 From: SouthFox Date: Sun, 2 Apr 2023 00:13:16 +0800 Subject: [PATCH] refactor/actor model --- app/actor.py | 44 ++++++---------- app/boxes.py | 138 +++++++++++++++++++++++++++++++++++++++++++++----- app/models.py | 2 +- 3 files changed, 142 insertions(+), 42 deletions(-) diff --git a/app/actor.py b/app/actor.py index 27199d3..773d4c6 100644 --- a/app/actor.py +++ b/app/actor.py @@ -13,26 +13,6 @@ if typing.TYPE_CHECKING: 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 : str = ap_type # type: ignore - - @property - def ap_actor(self) -> ap.RawObject: - return self._ap_actor - - @property - def inbox_url(self) -> str: - return self.ap_actor["inbox"] - - @property - def ap_type(self) -> str: - return self._ap_type - async def fetch_actor( db_session : AsyncSession, @@ -51,15 +31,6 @@ async def fetch_actor( exist_actor = await save_actor(ap_object, db_session) return exist_actor else: - try: - _actor = await ap.fetch(actor_id) - exist_actor = await save_actor(_actor, db_session) - except json.JSONDecodeError: - raise ValueError - except KeyError: - logger.warning("actor gone? ") - raise KeyError - return exist_actor async def save_actor( @@ -89,3 +60,18 @@ def _handle ( handle = '@' + ap_object["preferredUsername"] + '@' + ap_id.hostname return handle + + + +async def get_public_key( + db_session: AsyncSession, + key_id: str +) -> str: + + existing_actor = ( + await db_session.scalars( + select(models.Actor).where(models.Actor.ap_id == key_id.split("#")[0]) + ) + ).one_or_none() + public_key = existing_actor.ap_object["publicKey"]["publicKeyPem"] + return public_key diff --git a/app/boxes.py b/app/boxes.py index 38f4cbd..6a9150c 100644 --- a/app/boxes.py +++ b/app/boxes.py @@ -5,14 +5,16 @@ import uuid from sqlalchemy.orm import session from app import models +from app import ldsig 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.config import BASE_URL, ID from app.models import Actor from app.actor import fetch_actor +from app.httpsig import k import app.activitypub as ap @@ -23,6 +25,7 @@ from sqlalchemy.orm import joinedload from sqlalchemy.exc import IntegrityError from loguru import logger from uuid import uuid4 +from datetime import datetime @@ -133,6 +136,8 @@ async def process_incoming( await db_session.flush() await db_session.refresh(following) return True + # elif "Creat" == ap_object["type"]: + return False @@ -173,18 +178,21 @@ async def _send_accept( await db_session.rollback() logger.warning("existing follower in db!") - reply_id = allocate_outbox_id() + try: + 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 + 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 + except Exception as e: + logger.error(e) async def _handle_undo( @@ -260,6 +268,112 @@ async def _send_follow( ) +async def _send_create( + db_session: AsyncSession, + ap_type: str, + content: str, + visibility: ap.VisibilityEnum, + published: str | None = None, +) -> bool: + object_id = build_object_id(allocate_outbox_id()) + if not published: + published = now().replace(microsecond=0).isoformat().replace("+00:00", "Z") + + to = [] + cc = [] + + if visibility == ap.VisibilityEnum.PUBLIC: + to = [ap.AS_PUBLIC] + cc = [f"{BASE_URL}/followers"] + else: + raise ValueError(f"Unsupport visibility {visibility}") + + ap_object = { + "@context": ap.AS_EXTENDED_CTX, + "type": ap_type, + "id": object_id, + "attributedTo": ID, + "content": content, + "to": to, + "cc": cc, + "published": published, + # "context": context, + # "conversation": context, + "url": object_id, + "tag": [], + "summary": None, + "inReplyTo": None, + "sensitive": False, + "attachment": [], + } + + outbox_object = await save_to_outbox( + db_session, + object_id, + ap_object, + ) + + recipients = await _compute_recipients(db_session, ap_object) + ap_object = ap.wrap_ap_object(ap_object) + + if ap_object["type"] == "Create": + if ap.VisibilityEnum.PUBLIC == outbox_object.visibility: + ldsig.generate_signature(ap_object, k) + + for r in recipients: + await ap.post( + r, + ap_object, + ) + + return True + + +async def _compute_recipients( + db_session: AsyncSession, + ap_object: dict, +) -> set[str]: + + async def process_collection( + db_session, + url) -> list[Actor]: + if url == BASE_URL + "/followers": + followers = ( + ( + await db_session.scalars( + select(models.Follower).options( + joinedload(models.Follower.actor) + ) + ) + ) + .unique() + .all() + ) + else: + raise ValueError(f"{url}) not supported") + + return [follower.actor for follower in followers] + + _recipients = [] + for field in ["to", "cc", "bcc", "bto"]: + if field in ap_object: + _recipients.extend(ap_object[field]) + + recipients = set() + logger.info(f"{_recipients}") + for r in _recipients: + if r in [ap.AS_PUBLIC, ID]: + continue + + if r.startswith(BASE_URL): + for actor in await process_collection(db_session, r): + recipients.add(actor.share_inbox_url) + + continue + + return recipients + + async def save_to_inbox( db_session : AsyncSession, inbox_id : str, diff --git a/app/models.py b/app/models.py index 9b3e2ed..d02125d 100644 --- a/app/models.py +++ b/app/models.py @@ -7,7 +7,7 @@ from typing import Union from app import activitypub as ap from app.database import Base from app.database import metadata_obj -from app.actor import BaseActor +from app.activitypub import BaseActor from sqlalchemy import Column from sqlalchemy import Boolean