From 2c62f0f948d1b96455f82eeb276a791227083470 Mon Sep 17 00:00:00 2001 From: SouthFox Date: Fri, 10 Jan 2025 17:41:33 +0800 Subject: [PATCH] [chore] use black format code --- app/activitypub.py | 13 ++- app/actor.py | 27 ++---- app/boxes.py | 186 ++++++++++++++++++-------------------- app/config.py | 16 ++-- app/database.py | 5 +- app/httpsig.py | 39 ++++---- app/ldsig.py | 10 +- app/main.py | 18 ++-- app/models.py | 16 +++- app/orgpython/document.py | 61 +++++++------ app/orgpython/inline.py | 91 +++++++++---------- app/workers.py | 2 + tests/conftest.py | 3 +- tests/factories.py | 3 +- tests/test.toml | 1 - tests/test_inbox.py | 17 ++-- tests/test_main.py | 12 +-- tests/test_outbox.py | 44 ++++----- tests/utils.py | 4 +- 19 files changed, 266 insertions(+), 302 deletions(-) diff --git a/app/activitypub.py b/app/activitypub.py index 1ac0d15..7611e33 100644 --- a/app/activitypub.py +++ b/app/activitypub.py @@ -61,9 +61,9 @@ ME = { }, "url": config.ID + "/", # the path is important for Mastodon compat "manuallyApprovesFollowers": config.CONFIG.manually_approves_followers, - "attachment": [], # TODO media support + "attachment": [], # TODO media support "icon": { - "mediaType": "image/png", # TODO media support + "mediaType": "image/png", # TODO media support "type": "Image", "url": config.CONFIG.icon_url, }, @@ -72,16 +72,17 @@ ME = { "owner": config.ID, "publicKeyPem": get_pubkey_as_pem(config.KEY_PATH), }, - "tag": [] # TODO tag support + "tag": [], # TODO tag support } + class BaseActor: def __init__(self, ap_actor: RawObject, **_) -> None: if (ap_type := ap_actor.get("type")) not in ACTOR_TYPES: raise ValueError(f"Unexpected actor type: {ap_type}") self._ap_actor = ap_actor - self._ap_type : str = ap_type # type: ignore + self._ap_type: str = ap_type # type: ignore @property def ap_actor(self) -> RawObject: @@ -101,8 +102,7 @@ class BaseActor: @property def share_inbox_url(self) -> str: - return self.ap_actor.get("endpoints", {}).get("sharedInbox") \ - or self.inbox_url + return self.ap_actor.get("endpoints", {}).get("sharedInbox") or self.inbox_url class VisibilityEnum(str, enum.Enum): @@ -125,7 +125,6 @@ def handle_visibility(ap_object: dict) -> VisibilityEnum: def wrap_ap_object(ap_object: dict) -> dict: if ap_object["type"] in ["Note"]: - if "@context" in ap_object: del ap_object["@context"] diff --git a/app/actor.py b/app/actor.py index bb94750..835dce2 100644 --- a/app/actor.py +++ b/app/actor.py @@ -7,7 +7,7 @@ from urllib.parse import urlparse from sqlalchemy import select from loguru import logger -from app.hyap import fetch # type: ignore +from app.hyap import fetch # type: ignore from app import models from app.database import AsyncSession @@ -17,16 +17,14 @@ if typing.TYPE_CHECKING: async def fetch_actor( - db_session: AsyncSession, - actor_id: str, + db_session: AsyncSession, + actor_id: str, ) -> "ActorModel": """Fetch actor on db, if not exist will be grabed and stored in db.""" exist_actor = ( await db_session.scalars( - select(models.Actor).where( - models.Actor.ap_id == actor_id - ) + select(models.Actor).where(models.Actor.ap_id == actor_id) ) ).one_or_none() @@ -38,10 +36,7 @@ async def fetch_actor( return exist_actor -async def save_actor( - ap_object: dict, - db_session: AsyncSession -) -> "ActorModel": +async def save_actor(ap_object: dict, db_session: AsyncSession) -> "ActorModel": """Save actor to db.""" logger.info("save actor " + ap_object["id"]) actor = models.Actor( @@ -58,26 +53,22 @@ async def save_actor( def _handle( - ap_object: dict, + ap_object: dict, ) -> str: ap_id = urlparse(ap_object["id"]) if not ap_id.hostname: raise ValueError(f"Invalid actor ID {ap_id}") - handle = '@' + ap_object["preferredUsername"] + '@' + ap_id.hostname + handle = "@" + ap_object["preferredUsername"] + "@" + ap_id.hostname return handle -async def get_public_key( - db_session: AsyncSession, - key_id: str -) -> str: +async def get_public_key(db_session: AsyncSession, key_id: str) -> str: """Give key id and reutrn public key.""" existing_actor = ( await db_session.scalars( - select(models.Actor).where( - models.Actor.ap_id == key_id.split("#")[0]) + select(models.Actor).where(models.Actor.ap_id == key_id.split("#")[0]) ) ).one_or_none() public_key = existing_actor.ap_actor["publicKey"]["publicKeyPem"] diff --git a/app/boxes.py b/app/boxes.py index 6c76956..7e67133 100644 --- a/app/boxes.py +++ b/app/boxes.py @@ -17,7 +17,7 @@ from app.actor import fetch_actor from app.httpsig import k import app.activitypub as ap -from app.hyap import post # type: ignore +from app.hyap import post # type: ignore from urllib.parse import urlparse from sqlalchemy import select @@ -29,7 +29,6 @@ from uuid import uuid4 from datetime import datetime - def allocate_outbox_id() -> str: return str(uuid.uuid4()) @@ -61,7 +60,7 @@ async def get_outbox_object( async def get_inbox_object( db_session: AsyncSession, - ap_id: str, + ap_id: str, ) -> InboxObject | None: return ( await db_session.execute( @@ -77,8 +76,8 @@ async def get_inbox_object( async def save_incoming( - db_session: AsyncSession, - payload: dict, + db_session: AsyncSession, + payload: dict, ) -> models.IncomingActivity | None: ap_id: str if "@context" not in payload: @@ -107,8 +106,8 @@ async def save_incoming( async def process_incoming( - db_session: AsyncSession, - ap_object: dict[str, Any], + db_session: AsyncSession, + ap_object: dict[str, Any], ) -> bool: actor = await fetch_actor(db_session, ap_object["actor"]) @@ -122,8 +121,7 @@ async def process_incoming( ap_object["object"]["id"], ) relates_to_inbox_object = await get_inbox_object( - db_session, - ap_object["object"]["id"] + db_session, ap_object["object"]["id"] ) else: if ap_object["object"].startswith(BASE_URL): @@ -132,15 +130,13 @@ async def process_incoming( ap_object["object"], ) relates_to_inbox_object = await get_inbox_object( - db_session, - ap_object["object"] + db_session, ap_object["object"] ) - def build_object( - object, - relates_to_inbox_object = None, - relates_to_outbox_object = None, + object, + relates_to_inbox_object=None, + relates_to_outbox_object=None, ) -> InboxObject: inbox_object = models.InboxObject( actor_id=actor.id, @@ -172,8 +168,8 @@ async def process_incoming( return True return False elif "Undo" == ap_object["type"]: - #inbox_object = build_object(ap_object) - #db_session.add(inbox_object) + # 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"]: @@ -181,16 +177,21 @@ async def process_incoming( if isinstance(follow_id, dict): follow_id = follow_id["id"] - relate_following_object = (await db_session.execute( - select(models.OutboxObject) - .where(models.OutboxObject.ap_id == follow_id) - .options( - joinedload(models.OutboxObject.relates_to_inbox_object).options( - joinedload(models.InboxObject.actor) - ) + relate_following_object = ( + ( + await db_session.execute( + select(models.OutboxObject) + .where(models.OutboxObject.ap_id == follow_id) + .options( + joinedload(models.OutboxObject.relates_to_inbox_object).options( + joinedload(models.InboxObject.actor) + ) + ) + ) ) + .unique() + .scalar_one_or_none() ) - ).unique().scalar_one_or_none() if "Accept" == ap_object["type"]: try: @@ -253,13 +254,13 @@ async def process_incoming( async def _handle_follow( - db_session: AsyncSession, - actor: Actor, - inbox_object: InboxObject, + db_session: AsyncSession, + actor: Actor, + inbox_object: InboxObject, ) -> bool: - if ME["id"] != inbox_object.ap_object["object"]: #type: ignore + 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 + logger.warning("no match follow object!" + inbox_object.ap_object["id"]) # type: ignore return False if MANUALLY_APPROVES_FOLLOWERS: @@ -270,14 +271,14 @@ async def _handle_follow( async def _send_accept( - db_session: AsyncSession, - actor: Actor, - inbox_object: InboxObject, + 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 + ap_actor_id=inbox_object.ap_object["actor"], # type: ignore ) try: @@ -290,52 +291,49 @@ async def _send_accept( try: reply_id = allocate_outbox_id() - url = actor.inbox_url # 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 + "object": inbox_object.ap_object["id"], # type: ignore } - #TODO outcoming + # TODO outcoming await save_to_outbox( db_session, reply_id, out, - relates_to_actor_id=actor.id, # type: ignore + relates_to_actor_id=actor.id, # type: ignore ) - await post(url, out) # type: ignore + await post(url, out) # type: ignore except Exception as e: logger.error(e) -async def _handle_undo( - db_session: AsyncSession, - inbox_object: dict -) -> bool: +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"]) + 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), + 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 + relate_object.undo_id = inbox_object["object"]["id"] + relate_object.is_deleted = True await db_session.execute( delete(models.Follower).where( @@ -348,23 +346,20 @@ async def _handle_undo( return False -async def send_follow( - db_session : AsyncSession, - acct : str -): +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 _handle_announce( - db_session: AsyncSession, - inbox_object: InboxObject, - relates_to_outbox_object: models.OutboxObject | None, - relates_to_inbox_object: models.InboxObject | None, -) -> bool : + db_session: AsyncSession, + inbox_object: InboxObject, + relates_to_outbox_object: models.OutboxObject | None, + relates_to_inbox_object: models.InboxObject | None, +) -> bool: if relates_to_outbox_object: - relates_to_outbox_object.announces_count = ( # type: ignore + relates_to_outbox_object.announces_count = ( # type: ignore models.OutboxObject.announces_count + 1 ) logger.info(f"announces +1 {relates_to_outbox_object.ap_id}") @@ -378,9 +373,9 @@ async def _handle_like( inbox_object: InboxObject, relates_to_outbox_object: models.OutboxObject | None, relates_to_inbox_object: models.InboxObject | None, -) -> bool : +) -> bool: if relates_to_outbox_object: - relates_to_outbox_object.likes_count = ( # type: ignore + relates_to_outbox_object.likes_count = ( # type: ignore models.OutboxObject.likes_count + 1 ) logger.info(f"likes +1 {relates_to_outbox_object.ap_id}") @@ -390,8 +385,8 @@ async def _handle_like( async def _send_follow( - db_session : AsyncSession, - actor_url : str, + db_session: AsyncSession, + actor_url: str, ): actor = await fetch_actor(db_session, actor_url) @@ -408,8 +403,8 @@ async def _send_follow( db_session, follow_id, out, - relates_to_actor_id=actor.id, # type: ignore - activity_object_ap_id=actor.ap_id + relates_to_actor_id=actor.id, # type: ignore + activity_object_ap_id=actor.ap_id, ) await post( @@ -419,11 +414,11 @@ async def _send_follow( async def _send_create( - db_session: AsyncSession, - ap_type: str, - content: str, - visibility: ap.VisibilityEnum, - published: str | None = None, + db_session: AsyncSession, + ap_type: str, + content: str, + visibility: ap.VisibilityEnum, + published: str | None = None, ) -> bool: object_id = allocate_outbox_id() if not published: @@ -485,18 +480,14 @@ async def _send_create( except Exception as e: logger.error(e) - return True async def _compute_recipients( - db_session: AsyncSession, - ap_object: dict, + db_session: AsyncSession, + ap_object: dict, ) -> set[str]: - - async def process_collection( - db_session, - url) -> list[Actor]: + async def process_collection(db_session, url) -> list[Actor]: if url == BASE_URL + "/followers": followers = ( ( @@ -535,14 +526,13 @@ async def _compute_recipients( 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, + 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) @@ -552,7 +542,7 @@ async def save_to_inbox( ap_object=ap_object, ap_id=ap_id, ap_type=ap_type, - visibility =visibility, + 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, @@ -566,13 +556,13 @@ async def save_to_inbox( async def save_to_outbox( - db_session : AsyncSession, - outbox_id : str, - ap_object : dict, - activity_object_ap_id: str | None = None, - relates_to_inbox_object_id: int | None = None, - relates_to_outbox_object_id: int | None = None, - relates_to_actor_id: int | None = None, + db_session: AsyncSession, + outbox_id: str, + ap_object: dict, + activity_object_ap_id: str | None = None, + 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"] @@ -583,7 +573,7 @@ async def save_to_outbox( ap_object=ap_object, ap_id=ap_id, ap_type=ap_type, - visibility =visibility, + visibility=visibility, activity_object_ap_id=activity_object_ap_id, relates_to_inbox_object_id=relates_to_inbox_object_id, relates_to_outbox_object_id=relates_to_outbox_object_id, diff --git a/app/config.py b/app/config.py index 1e660b8..d8b2f7b 100644 --- a/app/config.py +++ b/app/config.py @@ -35,28 +35,30 @@ class Config(pydantic.BaseModel): ROOT_DIR = Path().parent.resolve() _CONFIG_FILE = os.getenv("FOXHOLE_CONFIG_FILE", "config.toml") + def load_config() -> Config: try: return Config.parse_obj( tomli.loads((ROOT_DIR / "data" / _CONFIG_FILE).read_text()) ) except FileNotFoundError: - raise ValueError( - f"{_CONFIG_FILE} is missing" - ) + raise ValueError(f"{_CONFIG_FILE} is missing") + def get_version_commit() -> str: import subprocess + try: return ( - '+' + - subprocess.check_output(["git", "rev-parse", "--short=8", "HEAD"]) + "+" + + subprocess.check_output(["git", "rev-parse", "--short=8", "HEAD"]) .split()[0] .decode() ) except Exception: return "+dev" + CONFIG = load_config() DOMAIN = CONFIG.domain @@ -67,9 +69,9 @@ _SCHEME = "https" if CONFIG.https else "http" ID = f"{_SCHEME}://{DOMAIN}" BASE_URL = ID USERNAME = CONFIG.username -KEY_PATH = (ROOT_DIR / os.getenv("FOXHOLE_KEY_PATH", "data/key.pem")) +KEY_PATH = ROOT_DIR / os.getenv("FOXHOLE_KEY_PATH", "data/key.pem") -VERSION = "Foxhole-"+ MAIN_VERSION + get_version_commit() +VERSION = "Foxhole-" + MAIN_VERSION + get_version_commit() USER_AGENT = "Fediverse Application/" + VERSION AP_CONTENT_TYPE = "application/activity+json" diff --git a/app/database.py b/app/database.py index 2d1338a..16d5147 100644 --- a/app/database.py +++ b/app/database.py @@ -25,13 +25,16 @@ async_engine = create_async_engine( ) async_session = sessionmaker(async_engine, class_=AsyncSession, expire_on_commit=False) + class Base(DeclarativeBase): pass + metadata_obj = MetaData() + async def get_db_session() -> AsyncGenerator[AsyncSession, None]: - async with async_session() as session: # type: ignore + async with async_session() as session: # type: ignore try: yield session finally: diff --git a/app/httpsig.py b/app/httpsig.py index 0e67281..1e7a844 100644 --- a/app/httpsig.py +++ b/app/httpsig.py @@ -12,36 +12,30 @@ from Crypto.Signature import PKCS1_v1_5 from Crypto.PublicKey import RSA - class HttpSignature: """ calculation and verification of HTTP signatures """ @classmethod - def calculation_digest( - cls, - body : bytes, - algorithm : str ="sha-256" - )-> str : + def calculation_digest(cls, body: bytes, algorithm: str = "sha-256") -> str: """ Calculates the digest header value for a given HTTP body """ if "sha-256" == algorithm: h = SHA256.new() h.update(body) - return "SHA-256=" + \ - base64.b64encode(h.digest()).decode("utf-8") + return "SHA-256=" + base64.b64encode(h.digest()).decode("utf-8") else: raise ValueError(f"No support algorithm {algorithm}") @classmethod def verify_signature( - cls, - signature_string : str, - signature : bytes, - pubkey, - ) -> bool : + cls, + signature_string: str, + signature: bytes, + pubkey, + ) -> bool: pubkey = RSA.importKey(pubkey) signer = PKCS1_v1_5.new(pubkey) digest = SHA256.new() @@ -65,13 +59,13 @@ class HttpSignature: @classmethod def build_signature_string( - cls, - method : str, - path : str, - signed_headers : list, - body_digest : str | None, - headers, - ) -> str : + cls, + method: str, + path: str, + signed_headers: list, + body_digest: str | None, + headers, + ) -> str: signed_string = [] for signed_header in signed_headers: if signed_header == "(request-target)": @@ -87,9 +81,7 @@ class HTTPXSigAuth(httpx.Auth): def __init__(self, key) -> None: self.key = key - def auth_flow( - self, r: httpx.Request - ): + def auth_flow(self, r: httpx.Request): bodydigest = None if r.content: bh = hashlib.new("sha256") @@ -122,6 +114,7 @@ class HTTPXSigAuth(httpx.Auth): r.headers["signature"] = sig_value yield r + k = KEY_PATH.read_text() k = RSA.importKey(k) auth = HTTPXSigAuth(k) diff --git a/app/ldsig.py b/app/ldsig.py index e0c10f4..83241ef 100644 --- a/app/ldsig.py +++ b/app/ldsig.py @@ -16,8 +16,8 @@ from app.database import AsyncSession from app.actor import get_public_key +requests_loader = pyld.documentloader.requests.requests_document_loader() # type: ignore -requests_loader = pyld.documentloader.requests.requests_document_loader() # type: ignore def _loader(url, options): if options is None: @@ -47,7 +47,7 @@ def _options_hash(doc: ap.RawObject) -> str: doc, {"algorithm": "URDNA2015", "format": "application/nquads"} ) doc_hash = hashlib.new("sha256") - doc_hash.update(normalized.encode("utf-8")) # type: ignore + doc_hash.update(normalized.encode("utf-8")) # type: ignore return doc_hash.hexdigest() @@ -59,7 +59,7 @@ def _doc_hash(doc: ap.RawObject) -> str: doc, {"algorithm": "URDNA2015", "format": "application/nquads"} ) doc_hash = hashlib.new("sha256") - doc_hash.update(normalized.encode("utf-8")) # type: ignore + doc_hash.update(normalized.encode("utf-8")) # type: ignore return doc_hash.hexdigest() @@ -80,7 +80,9 @@ async def verify_signature( signer = PKCS1_v1_5.new(pubkey) digest = SHA256.new() digest.update(to_be_signed.encode("utf-8")) - return signer.verify(digest, base64.b64decode(signature)) # pylint: disable=not-callable + return signer.verify( + digest, base64.b64decode(signature) + ) # pylint: disable=not-callable def generate_signature(doc: ap.RawObject, key) -> None: diff --git a/app/main.py b/app/main.py index b9acd2b..21b038e 100644 --- a/app/main.py +++ b/app/main.py @@ -35,14 +35,12 @@ from app.hysql import get_index_status def _check_0rtt_early_data(request: Request) -> None: """Disable TLS1.3 0-RTT requests for non-GET.""" - if request.headers.get("Early-Data", None) == "1" \ - and request.method != "GET": + if request.headers.get("Early-Data", None) == "1" and request.method != "GET": raise fastapi.HTTPException(status_code=425, detail="Too early") app = FastAPI( - docs_url=None, redoc_url=None, - dependencies=[Depends(_check_0rtt_early_data)] + docs_url=None, redoc_url=None, dependencies=[Depends(_check_0rtt_early_data)] ) templates = Jinja2Templates(directory="templates") @@ -55,6 +53,7 @@ logger.add("output.log", level="DEBUG") # pylint: disable=too-few-public-methods class ActivityPubResponse(JSONResponse): """Simple wrap JSONresponse return ActivityPub response.""" + media_type = "application/activity+json" @@ -114,8 +113,8 @@ async def inbox( @app.post("/outbox") async def outbox( - request: Request, - db_session: AsyncSession = Depends(get_db_session), + request: Request, + db_session: AsyncSession = Depends(get_db_session), ) -> Response: """ActivityPub outbox endpoint, now only process client post request.""" payload = await request.json() @@ -137,12 +136,7 @@ async def outbox( logger.info("True token") note_content = to_html(payload["content"]).replace("\n", "") - await _send_create( - db_session, - "Note", - note_content, - payload["visibility"] - ) + await _send_create(db_session, "Note", note_content, payload["visibility"]) return Response(status_code=200) diff --git a/app/models.py b/app/models.py index da646ff..f105a3f 100644 --- a/app/models.py +++ b/app/models.py @@ -42,8 +42,12 @@ class Actor(Base, BaseActor): handle = mapped_column(String, nullable=True, index=True) - is_blocked = mapped_column(Boolean, nullable=False, default=False, server_default="0") - is_deleted = mapped_column(Boolean, nullable=False, default=False, server_default="0") + is_blocked = mapped_column( + Boolean, nullable=False, default=False, server_default="0" + ) + is_deleted = mapped_column( + Boolean, nullable=False, default=False, server_default="0" + ) class InboxObject(Base, BaseObject): @@ -179,7 +183,9 @@ class Follower(Base): created_at = mapped_column(DateTime(timezone=True), nullable=False, default=now) updated_at = mapped_column(DateTime(timezone=True), nullable=False, default=now) - actor_id = mapped_column(Integer, ForeignKey("actor.id"), nullable=False, unique=True) + actor_id = mapped_column( + Integer, ForeignKey("actor.id"), nullable=False, unique=True + ) actor: Mapped[Actor] = relationship(Actor, uselist=False) inbox_object_id = mapped_column(Integer, ForeignKey("inbox.id"), nullable=False) @@ -195,7 +201,9 @@ class Following(Base): created_at = mapped_column(DateTime(timezone=True), nullable=False, default=now) updated_at = mapped_column(DateTime(timezone=True), nullable=False, default=now) - actor_id = mapped_column(Integer, ForeignKey("actor.id"), nullable=False, unique=True) + actor_id = mapped_column( + Integer, ForeignKey("actor.id"), nullable=False, unique=True + ) actor: Mapped[Actor] = relationship(Actor, uselist=False) outbox_object_id = mapped_column(Integer, ForeignKey("outbox.id"), nullable=False) diff --git a/app/orgpython/document.py b/app/orgpython/document.py index f961544..8eae177 100644 --- a/app/orgpython/document.py +++ b/app/orgpython/document.py @@ -36,7 +36,8 @@ LIST_STATUS_REGEXP = re.compile(r"\[( |X|-)\]\s") LIST_LEVEL_REGEXP = re.compile(r"(\s*)(.+)$") HEADLINE_REGEXP = re.compile( - r"^(\*+)(?:\s+(.+?))?(?:\s+\[#(.+)\])?(\s+.*?)(?:\s+:(.+):)?$") + r"^(\*+)(?:\s+(.+?))?(?:\s+\[#(.+)\])?(\s+.*?)(?:\s+:(.+):)?$" +) KEYWORD_REGEXP = re.compile(r"^(\s*)#\+([^:]+):(\s+(.*)|$)") COMMENT_REGEXP = re.compile(r"^(\s*)#(.*)") ATTRIBUTE_REGEXP = re.compile(r"(?:^|\s+)(:[-\w]+)\s+(.*)$") @@ -172,7 +173,7 @@ class Parser(object): num = index + 1 while num < end: if node.matchend(num, lines): - node.preparse(lines[index + 1:num]) + node.preparse(lines[index + 1 : num]) return node, num num += 1 return None, index @@ -188,7 +189,7 @@ class Parser(object): if node.matchend(num, lines): break num += 1 - node.preparse(lines[index + 1:num]) + node.preparse(lines[index + 1 : num]) return node, num def parse_headline(self, index, lines): @@ -260,7 +261,7 @@ class Parser(object): def __str__(self): str_children = [str(child) for child in self.children] - return self.__class__.__name__ + '(' + ','.join(str_children) + ')' + return self.__class__.__name__ + "(" + ",".join(str_children) + ")" def __repr__(self): return self.__str__() @@ -268,13 +269,14 @@ class Parser(object): class Headline(Parser): def __init__( - self, - title, - stars=1, - keyword=None, - priority=None, - tags=[], - todo_keywords=TODO_KEYWORDS): + self, + title, + stars=1, + keyword=None, + priority=None, + tags=[], + todo_keywords=TODO_KEYWORDS, + ): super(Headline, self).__init__() self.title = title self.stars = stars @@ -311,7 +313,7 @@ class Headline(Parser): ) def id(self): - hid = 'org-{0}'.format(sha1(self.title.encode()).hexdigest()[:10]) + hid = "org-{0}".format(sha1(self.title.encode()).hexdigest()[:10]) if self.properties: return self.properties.get("CUSTOM_ID", hid) return hid @@ -319,18 +321,18 @@ class Headline(Parser): def toc(self): b = "" if self.keyword: - b = b + "{0}".format(self.keyword) + b = b + '{0}'.format(self.keyword) if self.priority: - b = b + "{0}".format(self.priority) + b = b + '{0}'.format(self.priority) b = b + self.inlinetext(self.title).to_html() for tag in self.tags: - b = b + "{0}".format(tag) + b = b + '{0}'.format(tag) return b.strip() def to_html(self): - b = "{2}".format( + b = '{2}'.format( self.stars, self.id(), self.toc(), @@ -413,13 +415,13 @@ class Block(Parser): class Center(Block): def __init__(self, params=""): super(Center, self).__init__("center", params) - self.element = "
\n{0}\n
" + self.element = '
\n{0}\n
' class Verse(Block): def __init__(self, params=""): super(Verse, self).__init__("verse", params) - self.element = "

\n{0}\n

" + self.element = '

\n{0}\n

' def add_child(self, node): self.children.append(node) @@ -453,7 +455,7 @@ class Src(Block): super(Src, self).__init__("src", params) self.language = language self.highlight_code = highlight - self.element = "
\n{1}\n
" + self.element = '
\n{1}\n
' self.needparse = False self.escape = False self.parsed_nodes = () @@ -482,7 +484,7 @@ class Example(Src): class BlockResult(Parser): def __init__(self): super(BlockResult, self).__init__() - self.element = "
\n{0}\n
" + self.element = '
\n{0}\n
' @classmethod def match(cls, line): @@ -512,7 +514,7 @@ class ListItem(Parser): content = line status_match = LIST_STATUS_REGEXP.match(line) if status_match: - status, content = status_match[1], content[len("[ ] "):] + status, content = status_match[1], content[len("[ ] ") :] node = cls(status) node.add_child(node.inlinetext(content)) @@ -524,8 +526,7 @@ class ListItem(Parser): if self.checkbox == "HTML": if self.status == "X": - node = self.inlinetext( - '') + node = self.inlinetext('') else: node = self.inlinetext('') node.needparse = False @@ -641,7 +642,7 @@ class TableRow(Parser): self.is_sep = False self.header = header self.element = "\n{0}\n" - self.parsed_nodes = ("tablecolumn", ) + self.parsed_nodes = ("tablecolumn",) @classmethod def match(cls, line): @@ -671,7 +672,7 @@ class Table(Parser): super(Table, self).__init__() self.element = "\n{0}\n
" self.keyword = keyword - self.parsed_nodes = ("tablerow", ) + self.parsed_nodes = ("tablerow",) @classmethod def match(cls, line): @@ -777,7 +778,7 @@ class Section(Parser): def to_html(self): text = "
  • " - text += "{1}".format( + text += '{1}'.format( self.headline.id(), self.headline.toc(), ) @@ -785,7 +786,8 @@ class Section(Parser): return text + "
  • " text += "\n\n".format( - "\n".join([child.to_html() for child in self.children])) + "\n".join([child.to_html() for child in self.children]) + ) return text @@ -794,9 +796,10 @@ class Toc(Parser): super(Toc, self).__init__() self.element = ( '
    ' - '

    Table of Contents

    ' + "

    Table of Contents

    " '
    ' - '\n
      \n{0}\n
    \n
    ') + "\n\n" + ) def add_child(self, node): last = self.last_child() diff --git a/app/orgpython/inline.py b/app/orgpython/inline.py index 74f3084..9756f95 100644 --- a/app/orgpython/inline.py +++ b/app/orgpython/inline.py @@ -16,83 +16,73 @@ import os # _inline_regexp = r"(^|.*?(?") + r"^<(\d{4}-\d{2}-\d{2})( [A-Za-z]+)?( \d{2}:\d{2})?( \+\d+[dwmy])?>" +) _html_escape = ( ("&", "&"), ("'", "'"), ("<", "<"), (">", ">"), - ("\"", """), + ('"', """), ) # https://github.com/tsroten/zhon/blob/develop/zhon/hanzi.py _chinese_non_stops = ( # Fullwidth ASCII variants - '\uFF02\uFF03\uFF04\uFF05\uFF06\uFF07\uFF08\uFF09\uFF0A\uFF0B\uFF0C\uFF0D' - '\uFF0F\uFF1A\uFF1B\uFF1C\uFF1D\uFF1E\uFF20\uFF3B\uFF3C\uFF3D\uFF3E\uFF3F' - '\uFF40\uFF5B\uFF5C\uFF5D\uFF5E\uFF5F\uFF60' - + "\uFF02\uFF03\uFF04\uFF05\uFF06\uFF07\uFF08\uFF09\uFF0A\uFF0B\uFF0C\uFF0D" + "\uFF0F\uFF1A\uFF1B\uFF1C\uFF1D\uFF1E\uFF20\uFF3B\uFF3C\uFF3D\uFF3E\uFF3F" + "\uFF40\uFF5B\uFF5C\uFF5D\uFF5E\uFF5F\uFF60" # Halfwidth CJK punctuation - '\uFF62\uFF63\uFF64' - + "\uFF62\uFF63\uFF64" # CJK symbols and punctuation - '\u3000\u3001\u3003' - + "\u3000\u3001\u3003" # CJK angle and corner brackets - '\u3008\u3009\u300A\u300B\u300C\u300D\u300E\u300F\u3010\u3011' - + "\u3008\u3009\u300A\u300B\u300C\u300D\u300E\u300F\u3010\u3011" # CJK brackets and symbols/punctuation - '\u3014\u3015\u3016\u3017\u3018\u3019\u301A\u301B\u301C\u301D\u301E\u301F' - + "\u3014\u3015\u3016\u3017\u3018\u3019\u301A\u301B\u301C\u301D\u301E\u301F" # Other CJK symbols - '\u3030' - + "\u3030" # Special CJK indicators - '\u303E\u303F' - + "\u303E\u303F" # Dashes - '\u2013\u2014' - + "\u2013\u2014" # Quotation marks and apostrophe - '\u2018\u2019\u201B\u201C\u201D\u201E\u201F' - + "\u2018\u2019\u201B\u201C\u201D\u201E\u201F" # General punctuation - '\u2026\u2027' - + "\u2026\u2027" # Overscores and underscores - '\uFE4F' - + "\uFE4F" # Small form variants - '\uFE51\uFE54' - + "\uFE51\uFE54" # Latin punctuation - '\u00B7') + "\u00B7" +) _chinese_stops = ( - '\uFF01' # Fullwidth exclamation mark - '\uFF1F' # Fullwidth question mark - '\uFF61' # Halfwidth ideographic full stop - '\u3002' # Ideographic full stop + "\uFF01" # Fullwidth exclamation mark + "\uFF1F" # Fullwidth question mark + "\uFF61" # Halfwidth ideographic full stop + "\u3002" # Ideographic full stop ) @@ -103,7 +93,7 @@ def html_escape(text): def match_chinese(ch): - if '\u4e00' <= ch <= '\u9fff': + if "\u4e00" <= ch <= "\u9fff": return True if ch in _chinese_stops: return True @@ -186,18 +176,17 @@ class InlineParser(object): ) char_map = dict(chars) single_char = lines[index] - double_char = lines[index:index + 2] + double_char = lines[index : index + 2] for char in chars: c1 = len(char[0]) == 1 and char[0] == single_char c2 = len(char[0]) == 2 and char[0] == double_char if c1 or c2: - node, num = getattr(self, "parse_" + char_map[char[0]])( - index, lines) + node, num = getattr(self, "parse_" + char_map[char[0]])(index, lines) if node: return node, num - if lines[index:index + 3] == "[fn": + if lines[index : index + 3] == "[fn": node, num = self.parse_fn(index, lines) if node: return node, num @@ -232,7 +221,7 @@ class InlineParser(object): return text def __str__(self): - return '{}({})'.format(self.__class__.__name__, self.content.strip()) + return "{}({})".format(self.__class__.__name__, self.content.strip()) def __repr__(self): return self.__str__() @@ -308,7 +297,7 @@ class Verbatim(InlineParser): class Underline(InlineParser): def __init__(self, content): super(Underline, self).__init__(content) - self.element = "{0}" + self.element = '{0}' @classmethod def match(cls, line, index): @@ -350,9 +339,9 @@ class Link(InlineParser): def to_html(self): if self.is_img(): - return "".format(self.content) + return ''.format(self.content) if self.is_vedio(): - return "".format(self.content) + return ''.format(self.content) if self.desc: return '{1}'.format(self.content, self.desc) return '{1}'.format(self.content, self.content) @@ -361,7 +350,9 @@ class Link(InlineParser): class Fn(InlineParser): def __init__(self, content): super(Fn, self).__init__(content) - self.element = '{0}' + self.element = ( + '{0}' + ) @classmethod def match(cls, line, index): diff --git a/app/workers.py b/app/workers.py index 8292d5d..72b3a0e 100644 --- a/app/workers.py +++ b/app/workers.py @@ -7,6 +7,8 @@ from typing import TypeVar from loguru import logger T = TypeVar("T") + + class Worker(Generic[T]): def __init__(self) -> None: self._loop = asyncio.get_event_loop() diff --git a/tests/conftest.py b/tests/conftest.py index bd5ff70..eff7191 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,10 +14,9 @@ from app.database import SessionLocal from sqlalchemy import orm - @pytest_asyncio.fixture async def async_db_session(): - async with async_session() as session: # type: ignore + async with async_session() as session: # type: ignore async with async_engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) yield session diff --git a/tests/factories.py b/tests/factories.py index 17b5e36..c9daf5a 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -19,7 +19,7 @@ class BaseModelMeta: sqlalchemy_session_persistence = "commit" -class RemoteActorFactory(factory.Factory): # pyright: disable +class RemoteActorFactory(factory.Factory): # pyright: disable class Meta: model = ap.BaseActor exclude = ( @@ -64,7 +64,6 @@ class ActorFactory(factory.alchemy.SQLAlchemyModelFactory): ap_id = "stub" - async def inbox_prechecker( request: fastapi.Request, ) -> bool: diff --git a/tests/test.toml b/tests/test.toml index c3d3137..e18bef2 100644 --- a/tests/test.toml +++ b/tests/test.toml @@ -1,4 +1,3 @@ - domain = "test.foxhole.me" username = "testfox" name = "test233" diff --git a/tests/test_inbox.py b/tests/test_inbox.py index 3de94ee..49d2fac 100644 --- a/tests/test_inbox.py +++ b/tests/test_inbox.py @@ -18,7 +18,6 @@ from app import models from sqlalchemy import select - def test_inbox_follow_request( db: Session, client: TestClient, @@ -26,10 +25,10 @@ def test_inbox_follow_request( ) -> None: # build test actor ra = build_remote_actor() - ap_id = ra.ap_id # type: ignore + ap_id = ra.ap_id # type: ignore # mock request - respx_mock.get(ap_id).mock(return_value=httpx.Response(200,json=ra.ap_actor)) + respx_mock.get(ap_id).mock(return_value=httpx.Response(200, json=ra.ap_actor)) respx_mock.post(ap_id + "/inbox").mock(return_value=httpx.Response(202)) # send follower request @@ -99,11 +98,11 @@ def test_inbox_announce_activity( # build test actor ra = build_remote_actor() - ap_id = ra.ap_id # type: ignore + ap_id = ra.ap_id # type: ignore app.dependency_overrides[precheck.inbox_prechecker] = factories.inbox_prechecker # mock request - respx_mock.get(ap_id).mock(return_value=httpx.Response(200,json=ra.ap_actor)) + respx_mock.get(ap_id).mock(return_value=httpx.Response(200, json=ra.ap_actor)) respx_mock.post(ap_id + "/inbox").mock(return_value=httpx.Response(202)) from app.models import now @@ -135,7 +134,7 @@ def test_inbox_announce_activity( ap_object=note_object, ap_id=note_id, ap_type="Note", - visibility =VisibilityEnum.PUBLIC, + visibility=VisibilityEnum.PUBLIC, ) db.add(outbox_object) @@ -168,11 +167,11 @@ def test_inbox_like_activity( # build test actor ra = build_remote_actor() - ap_id = ra.ap_id # type: ignore + ap_id = ra.ap_id # type: ignore app.dependency_overrides[precheck.inbox_prechecker] = factories.inbox_prechecker # mock request - respx_mock.get(ap_id).mock(return_value=httpx.Response(200,json=ra.ap_actor)) + respx_mock.get(ap_id).mock(return_value=httpx.Response(200, json=ra.ap_actor)) respx_mock.post(ap_id + "/inbox").mock(return_value=httpx.Response(202)) from app.models import now @@ -204,7 +203,7 @@ def test_inbox_like_activity( ap_object=note_object, ap_id=note_id, ap_type="Note", - visibility =VisibilityEnum.PUBLIC, + visibility=VisibilityEnum.PUBLIC, ) db.add(outbox_object) diff --git a/tests/test_main.py b/tests/test_main.py index f530cc5..3c00133 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -31,7 +31,6 @@ def test_index__ap(db: Session, client: TestClient, accept: str): assert response.json() == ap.ME - @pytest.mark.asyncio async def test_follow_only_status( client: TestClient, @@ -39,19 +38,18 @@ async def test_follow_only_status( ) -> None: # build test actor ra = build_remote_actor() - remote_ap_id = ra.ap_id # type: ignore + remote_ap_id = ra.ap_id # type: ignore # mock request respx_mock.get(remote_ap_id).mock( - return_value=httpx.Response(200,json=ra.ap_actor)) - respx_mock.post(remote_ap_id + "/inbox").mock( - return_value=httpx.Response(202)) - + return_value=httpx.Response(200, json=ra.ap_actor) + ) + respx_mock.post(remote_ap_id + "/inbox").mock(return_value=httpx.Response(202)) response = client.post( "/outbox", headers={"Authorization": "Basic test-token"}, - content='{"visibility": "followers-only","content": "note content"}' + content='{"visibility": "followers-only","content": "note content"}', ) assert response.status_code == 200 diff --git a/tests/test_outbox.py b/tests/test_outbox.py index f18102c..2a51757 100644 --- a/tests/test_outbox.py +++ b/tests/test_outbox.py @@ -27,17 +27,17 @@ async def test_outbox_send_follow_request( ) -> None: # build test actor ra = build_remote_actor() - remote_ap_id = ra.ap_id # type: ignore + remote_ap_id = ra.ap_id # type: ignore # mock request respx_mock.get(remote_ap_id).mock( - return_value=httpx.Response(200,json=ra.ap_actor)) - respx_mock.post(remote_ap_id + "/inbox").mock( - return_value=httpx.Response(202)) + return_value=httpx.Response(200, json=ra.ap_actor) + ) + respx_mock.post(remote_ap_id + "/inbox").mock(return_value=httpx.Response(202)) from app.boxes import send_follow - async with async_db_session as db_session: #type: ignore + async with async_db_session as db_session: # type: ignore await send_follow(db_session, remote_ap_id) # And the Follow activity was created in the outbox @@ -77,17 +77,17 @@ async def test_outbox_send_follow_request_nest_accept( ) -> None: # build test actor ra = build_remote_actor() - remote_ap_id = ra.ap_id # type: ignore + remote_ap_id = ra.ap_id # type: ignore # mock request respx_mock.get(remote_ap_id).mock( - return_value=httpx.Response(200,json=ra.ap_actor)) - respx_mock.post(remote_ap_id + "/inbox").mock( - return_value=httpx.Response(202)) + return_value=httpx.Response(200, json=ra.ap_actor) + ) + respx_mock.post(remote_ap_id + "/inbox").mock(return_value=httpx.Response(202)) from app.boxes import send_follow - async with async_db_session as db_session: #type: ignore + async with async_db_session as db_session: # type: ignore await send_follow(db_session, remote_ap_id) # And the Follow activity was created in the outbox @@ -132,9 +132,9 @@ async def test_outbox_send_create_activity( # mock request respx_mock.get(remote_ap_id).mock( - return_value=httpx.Response(200,json=ra.ap_actor)) - respx_mock.post(remote_ap_id + "/inbox").mock( - return_value=httpx.Response(202)) + return_value=httpx.Response(200, json=ra.ap_actor) + ) + respx_mock.post(remote_ap_id + "/inbox").mock(return_value=httpx.Response(202)) from app.boxes import _send_create from app.activitypub import VisibilityEnum @@ -145,12 +145,7 @@ async def test_outbox_send_create_activity( content = "*Blod Text* =code Text= \n" content = to_html(content) - await _send_create( - db_session, - "Note", - content, - VisibilityEnum.PUBLIC - ) + await _send_create(db_session, "Note", content, VisibilityEnum.PUBLIC) # And the Follow activity was created in the outbox outbox_object = db.execute(select(models.OutboxObject)).scalar_one() @@ -167,21 +162,20 @@ async def test_outbox_send_unlisted_note( ) -> None: # build test actor ra = build_remote_actor() - remote_ap_id = ra.ap_id # type: ignore + remote_ap_id = ra.ap_id # type: ignore # mock request respx_mock.get(remote_ap_id).mock( - return_value=httpx.Response(200,json=ra.ap_actor)) - respx_mock.post(remote_ap_id + "/inbox").mock( - return_value=httpx.Response(202)) + return_value=httpx.Response(200, json=ra.ap_actor) + ) + respx_mock.post(remote_ap_id + "/inbox").mock(return_value=httpx.Response(202)) from app.activitypub import VisibilityEnum - response = client.post( "/outbox", headers={"Authorization": "Basic test-token"}, - content='{"visibility": "unlisted","content": "note content"}' + content='{"visibility": "unlisted","content": "note content"}', ) assert response.status_code == 200 diff --git a/tests/utils.py b/tests/utils.py index 258cc3a..819a353 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -4,7 +4,6 @@ from app.main import app from app.utils import precheck - def build_remote_actor(): ra = factories.RemoteActorFactory( base_url="https://example.com", @@ -12,7 +11,6 @@ def build_remote_actor(): public_key="pk", ) - app.dependency_overrides[precheck.inbox_prechecker] = \ - factories.inbox_prechecker + app.dependency_overrides[precheck.inbox_prechecker] = factories.inbox_prechecker return ra