All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 41s
Phase 0+1 of AP integration. New 5th Quart microservice: Blueprints: - wellknown: WebFinger, NodeInfo 2.0, host-meta - actors: AP actor profiles (JSON-LD + HTML), outbox, inbox, followers - identity: username selection flow (creates ActorProfile + RSA keypair) - auth: magic link login/logout (ported from blog, self-contained) Services: - Registers SqlFederationService (real impl) for federation domain - Registers real impls for blog, calendar, market, cart - All cross-domain via shared service contracts Templates: - Actor profiles, username selection, platform home - Auth login/check-email (ported from blog) Infrastructure: - Dockerfile + entrypoint.sh (matches other apps) - CI/CD via Gitea Actions - shared/ as git submodule Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
115 lines
3.7 KiB
Python
115 lines
3.7 KiB
Python
"""Well-known federation endpoints: WebFinger, NodeInfo, host-meta.
|
|
|
|
Ported from ~/art-dag/activity-pub/app/routers/federation.py.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
|
|
from quart import Blueprint, request, abort, Response, g
|
|
|
|
from shared.services.registry import services
|
|
|
|
|
|
def _domain() -> str:
|
|
return os.getenv("AP_DOMAIN", "rose-ash.com")
|
|
|
|
|
|
def register(url_prefix=""):
|
|
bp = Blueprint("wellknown", __name__, url_prefix=url_prefix)
|
|
|
|
@bp.get("/.well-known/webfinger")
|
|
async def webfinger():
|
|
resource = request.args.get("resource", "")
|
|
if not resource.startswith("acct:"):
|
|
abort(400, "Invalid resource format")
|
|
|
|
parts = resource[5:].split("@")
|
|
if len(parts) != 2:
|
|
abort(400, "Invalid resource format")
|
|
|
|
username, domain = parts
|
|
if domain != _domain():
|
|
abort(404, "User not on this server")
|
|
|
|
actor = await services.federation.get_actor_by_username(g.s, username)
|
|
if not actor:
|
|
abort(404, "User not found")
|
|
|
|
domain = _domain()
|
|
return Response(
|
|
response=__import__("json").dumps({
|
|
"subject": resource,
|
|
"aliases": [f"https://{domain}/users/{username}"],
|
|
"links": [
|
|
{
|
|
"rel": "self",
|
|
"type": "application/activity+json",
|
|
"href": f"https://{domain}/users/{username}",
|
|
},
|
|
{
|
|
"rel": "http://webfinger.net/rel/profile-page",
|
|
"type": "text/html",
|
|
"href": f"https://{domain}/users/{username}",
|
|
},
|
|
],
|
|
}),
|
|
content_type="application/jrd+json",
|
|
)
|
|
|
|
@bp.get("/.well-known/nodeinfo")
|
|
async def nodeinfo_index():
|
|
domain = _domain()
|
|
return Response(
|
|
response=__import__("json").dumps({
|
|
"links": [
|
|
{
|
|
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
|
|
"href": f"https://{domain}/nodeinfo/2.0",
|
|
}
|
|
]
|
|
}),
|
|
content_type="application/json",
|
|
)
|
|
|
|
@bp.get("/nodeinfo/2.0")
|
|
async def nodeinfo():
|
|
stats = await services.federation.get_stats(g.s)
|
|
return Response(
|
|
response=__import__("json").dumps({
|
|
"version": "2.0",
|
|
"software": {
|
|
"name": "rose-ash",
|
|
"version": "1.0.0",
|
|
},
|
|
"protocols": ["activitypub"],
|
|
"usage": {
|
|
"users": {
|
|
"total": stats.get("actors", 0),
|
|
"activeMonth": stats.get("actors", 0),
|
|
},
|
|
"localPosts": stats.get("activities", 0),
|
|
},
|
|
"openRegistrations": False,
|
|
"metadata": {
|
|
"nodeName": "Rose Ash",
|
|
"nodeDescription": "Cooperative platform with ActivityPub federation",
|
|
},
|
|
}),
|
|
content_type="application/json",
|
|
)
|
|
|
|
@bp.get("/.well-known/host-meta")
|
|
async def host_meta():
|
|
domain = _domain()
|
|
xml = (
|
|
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
'<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">\n'
|
|
f' <Link rel="lrdd" type="application/xrd+xml" '
|
|
f'template="https://{domain}/.well-known/webfinger?resource={{uri}}"/>\n'
|
|
'</XRD>'
|
|
)
|
|
return Response(response=xml, content_type="application/xrd+xml")
|
|
|
|
return bp
|