fixed reverse chronological ordering in a pair of FTs clogging the pipeline; added ActivityPub to project; new apps.ap for WebFinger, Actor, Outbox views; apps.lyric.models now contains ap_public_key, ap_private_key fields + ensure_keypair(); new apps.lyric migration accordingly; new in drama.models are to_activity() w. JoinGate, SelectRole, Create compat. & None verb support; new core.urls for /.well-known/webfinger + /ap/ included; cryptography installed, added to reqs.txt; 24 new green UTs & ITs; in sum, project is now read-only ActivityPub node
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
Disco DeDisco
2026-04-02 15:22:04 -04:00
parent 8538f76b13
commit ca38875660
17 changed files with 389 additions and 2 deletions

83
src/apps/ap/views.py Normal file
View File

@@ -0,0 +1,83 @@
import json
from django.http import HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404
from apps.lyric.models import User
AP_CONTEXT = [
"https://www.w3.org/ns/activitystreams",
{"earthman": "https://earthmanrpg.me/ns#"},
]
def _base_url(request):
return f"{request.scheme}://{request.get_host()}"
def _ap_response(data):
return HttpResponse(
json.dumps(data),
content_type="application/activity+json",
)
def webfinger(request):
resource = request.GET.get("resource", "")
if not resource:
return HttpResponse(status=400)
# Expect acct:username@host
if not resource.startswith("acct:"):
return HttpResponse(status=400)
username = resource[len("acct:"):].split("@")[0]
user = get_object_or_404(User, username=username)
base = _base_url(request)
data = {
"subject": resource,
"links": [
{
"rel": "self",
"type": "application/activity+json",
"href": f"{base}/ap/users/{user.username}/",
}
],
}
return HttpResponse(json.dumps(data), content_type="application/jrd+json")
def actor(request, username):
user = get_object_or_404(User, username=username)
user.ensure_keypair()
base = _base_url(request)
actor_url = f"{base}/ap/users/{username}/"
data = {
"@context": AP_CONTEXT,
"id": actor_url,
"type": "Person",
"preferredUsername": username,
"inbox": f"{actor_url}inbox/",
"outbox": f"{actor_url}outbox/",
"publicKey": {
"id": f"{actor_url}#main-key",
"owner": actor_url,
"publicKeyPem": user.ap_public_key,
},
}
return _ap_response(data)
def outbox(request, username):
user = get_object_or_404(User, username=username)
base = _base_url(request)
events = user.game_events.select_related("room").order_by("timestamp")
activities = [a for e in events if (a := e.to_activity(base)) is not None]
actor_url = f"{base}/ap/users/{username}/"
data = {
"@context": AP_CONTEXT,
"id": f"{actor_url}outbox/",
"type": "OrderedCollection",
"totalItems": len(activities),
"orderedItems": activities,
}
return _ap_response(data)