"""WebFinger client for resolving remote AP actor profiles.""" from __future__ import annotations import logging import httpx log = logging.getLogger(__name__) AP_CONTENT_TYPE = "application/activity+json" async def resolve_actor(acct: str) -> dict | None: """Resolve user@domain to actor JSON via WebFinger + actor fetch. Args: acct: Handle in the form ``user@domain`` (no leading ``@``). Returns: Actor JSON-LD dict, or None if resolution fails. """ acct = acct.lstrip("@") if "@" not in acct: return None _, domain = acct.rsplit("@", 1) webfinger_url = f"https://{domain}/.well-known/webfinger" try: async with httpx.AsyncClient(timeout=10, follow_redirects=True) as client: # Step 1: WebFinger lookup resp = await client.get( webfinger_url, params={"resource": f"acct:{acct}"}, headers={"Accept": "application/jrd+json, application/json"}, ) if resp.status_code != 200: log.debug("WebFinger %s returned %d", webfinger_url, resp.status_code) return None data = resp.json() # Find self link with AP content type actor_url = None for link in data.get("links", []): if link.get("rel") == "self" and link.get("type") in ( AP_CONTENT_TYPE, "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", ): actor_url = link.get("href") break if not actor_url: log.debug("No AP self link in WebFinger response for %s", acct) return None # Step 2: Fetch actor JSON resp = await client.get( actor_url, headers={"Accept": AP_CONTENT_TYPE}, ) if resp.status_code == 200: return resp.json() log.debug("Actor fetch %s returned %d", actor_url, resp.status_code) except Exception: log.exception("WebFinger resolution failed for %s", acct) return None