microblog.pub/tests/factories.py

281 lines
8.0 KiB
Python

from urllib.parse import urlparse
from uuid import uuid4
import factory # type: ignore
from Crypto.PublicKey import RSA
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
from app.database import SessionLocal
from app.utils.datetime import now
_Session = orm.scoped_session(SessionLocal)
def generate_key() -> tuple[str, str]:
k = RSA.generate(1024)
return k.exportKey("PEM").decode(), k.publickey().exportKey("PEM").decode()
def build_follow_activity(
from_remote_actor: actor.RemoteActor,
for_remote_actor: actor.RemoteActor,
outbox_public_id: str | None = None,
) -> ap.RawObject:
return {
"@context": ap.AS_CTX,
"type": "Follow",
"id": from_remote_actor.ap_id + "/follow/" + (outbox_public_id or uuid4().hex),
"actor": from_remote_actor.ap_id,
"object": for_remote_actor.ap_id,
}
def build_delete_activity(
from_remote_actor: actor.RemoteActor | models.Actor,
deleted_object_ap_id: str,
outbox_public_id: str | None = None,
) -> ap.RawObject:
return {
"@context": ap.AS_CTX,
"type": "Delete",
"id": (
from_remote_actor.ap_id # type: ignore
+ "/follow/"
+ (outbox_public_id or uuid4().hex)
),
"actor": from_remote_actor.ap_id,
"object": deleted_object_ap_id,
}
def build_accept_activity(
from_remote_actor: actor.RemoteActor,
for_remote_object: RemoteObject,
outbox_public_id: str | None = None,
) -> ap.RawObject:
return {
"@context": ap.AS_CTX,
"type": "Accept",
"id": from_remote_actor.ap_id + "/accept/" + (outbox_public_id or uuid4().hex),
"actor": from_remote_actor.ap_id,
"object": for_remote_object.ap_id,
}
def build_block_activity(
from_remote_actor: actor.RemoteActor,
for_remote_actor: actor.RemoteActor,
outbox_public_id: str | None = None,
) -> ap.RawObject:
return {
"@context": ap.AS_CTX,
"type": "Block",
"id": from_remote_actor.ap_id + "/block/" + (outbox_public_id or uuid4().hex),
"actor": from_remote_actor.ap_id,
"object": for_remote_actor.ap_id,
}
def build_move_activity(
from_remote_actor: actor.RemoteActor,
for_remote_object: actor.RemoteActor,
outbox_public_id: str | None = None,
) -> ap.RawObject:
return {
"@context": ap.AS_CTX,
"type": "Move",
"id": from_remote_actor.ap_id + "/move/" + (outbox_public_id or uuid4().hex),
"actor": from_remote_actor.ap_id,
"object": from_remote_actor.ap_id,
"target": for_remote_object.ap_id,
}
def build_note_object(
from_remote_actor: actor.RemoteActor | models.Actor,
outbox_public_id: str | None = None,
content: str = "Hello",
to: list[str] = None,
cc: list[str] = None,
tags: list[ap.RawObject] = None,
in_reply_to: str | None = None,
) -> ap.RawObject:
published = now().replace(microsecond=0).isoformat().replace("+00:00", "Z")
context = from_remote_actor.ap_id + "/ctx/" + uuid4().hex
note_id = outbox_public_id or uuid4().hex
return {
"@context": ap.AS_CTX,
"type": "Note",
"id": from_remote_actor.ap_id + "/note/" + note_id,
"attributedTo": from_remote_actor.ap_id,
"content": content,
"to": to or [ap.AS_PUBLIC],
"cc": cc or [],
"published": published,
"context": context,
"conversation": context,
"url": from_remote_actor.ap_id + "/note/" + note_id,
"tag": tags or [],
"summary": None,
"sensitive": False,
"inReplyTo": in_reply_to,
}
def build_create_activity(obj: ap.RawObject) -> ap.RawObject:
return {
"@context": ap.AS_EXTENDED_CTX,
"actor": obj["attributedTo"],
"to": obj.get("to", []),
"cc": obj.get("cc", []),
"id": obj["id"] + "/activity",
"object": ap.remove_context(obj),
"published": obj["published"],
"type": "Create",
}
class BaseModelMeta:
sqlalchemy_session = _Session
sqlalchemy_session_persistence = "commit"
class RemoteActorFactory(factory.Factory):
class Meta:
model = RemoteActor
exclude = (
"base_url",
"username",
"public_key",
"also_known_as",
)
class Params:
icon_url = None
summary = "I like unit tests"
also_known_as: list[str] = []
ap_actor = factory.LazyAttribute(
lambda o: {
"@context": ap.AS_CTX,
"type": "Person",
"id": o.base_url,
"following": o.base_url + "/following",
"followers": o.base_url + "/followers",
# "featured": ID + "/featured",
"inbox": o.base_url + "/inbox",
"outbox": o.base_url + "/outbox",
"preferredUsername": o.username,
"name": o.username,
"summary": o.summary,
"endpoints": {},
"url": o.base_url,
"manuallyApprovesFollowers": False,
"attachment": [],
"icon": {},
"publicKey": {
"id": f"{o.base_url}#main-key",
"owner": o.base_url,
"publicKeyPem": o.public_key,
},
"alsoKnownAs": o.also_known_as,
}
)
class ActorFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta(BaseModelMeta):
model = models.Actor
# ap_actor
# ap_id
ap_type = "Person"
@classmethod
def from_remote_actor(cls, ra):
return cls(
ap_type=ra.ap_type,
ap_actor=ra.ap_actor,
ap_id=ra.ap_id,
)
class OutboxObjectFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta(BaseModelMeta):
model = models.OutboxObject
# public_id
# relates_to_inbox_object_id
# relates_to_outbox_object_id
@classmethod
def from_remote_object(cls, public_id, ro):
return cls(
public_id=public_id,
ap_type=ro.ap_type,
ap_id=ro.ap_id,
ap_context=ro.ap_context,
ap_object=ro.ap_object,
visibility=ro.visibility,
og_meta=ro.og_meta,
activity_object_ap_id=ro.activity_object_ap_id,
is_hidden_from_homepage=True if ro.in_reply_to else False,
)
class OutgoingActivityFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta(BaseModelMeta):
model = models.OutgoingActivity
# recipient
# outbox_object_id
class InboxObjectFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta(BaseModelMeta):
model = models.InboxObject
@classmethod
def from_remote_object(
cls,
ro: RemoteObject,
actor: models.Actor,
relates_to_inbox_object_id: int | None = None,
relates_to_outbox_object_id: int | None = None,
):
ap_published_at = now()
if "published" in ro.ap_object:
ap_published_at = isoparse(ro.ap_object["published"])
return cls(
server=urlparse(ro.ap_id).hostname,
actor_id=actor.id,
ap_actor_id=actor.ap_id,
ap_type=ro.ap_type,
ap_id=ro.ap_id,
ap_context=ro.ap_context,
ap_published_at=ap_published_at,
ap_object=ro.ap_object,
visibility=ro.visibility,
relates_to_inbox_object_id=relates_to_inbox_object_id,
relates_to_outbox_object_id=relates_to_outbox_object_id,
activity_object_ap_id=ro.activity_object_ap_id,
# Hide replies from the stream
is_hidden_from_stream=True if ro.in_reply_to else False,
)
class FollowerFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta(BaseModelMeta):
model = models.Follower
class FollowingFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta(BaseModelMeta):
model = models.Following