Fix Accept header and supply actor outbox

Should help with Mitra among others. Refs #207.
This commit is contained in:
Andrew Godwin 2022-12-19 02:47:35 +00:00
parent ecde831e77
commit 3de188e406
7 changed files with 56 additions and 27 deletions

View file

@ -107,16 +107,11 @@ class PostAdmin(admin.ModelAdmin):
list_display = ["id", "type", "author", "state", "created"] list_display = ["id", "type", "author", "state", "created"]
list_filter = ("type", "local", "visibility", "state", "created") list_filter = ("type", "local", "visibility", "state", "created")
raw_id_fields = ["to", "mentions", "author", "emojis"] raw_id_fields = ["to", "mentions", "author", "emojis"]
actions = ["force_fetch", "reparse_hashtags"] actions = ["reparse_hashtags"]
search_fields = ["content"] search_fields = ["content"]
inlines = [PostAttachmentInline] inlines = [PostAttachmentInline]
readonly_fields = ["created", "updated", "state_changed", "object_json"] readonly_fields = ["created", "updated", "state_changed", "object_json"]
@admin.action(description="Force Fetch")
def force_fetch(self, request, queryset):
for instance in queryset:
instance.debug_fetch()
@admin.action(description="Reprocess content for hashtags") @admin.action(description="Reprocess content for hashtags")
def reparse_hashtags(self, request, queryset): def reparse_hashtags(self, request, queryset):
for instance in queryset: for instance in queryset:

View file

@ -5,7 +5,6 @@ from typing import Optional
import httpx import httpx
import urlman import urlman
from asgiref.sync import async_to_sync, sync_to_async from asgiref.sync import async_to_sync, sync_to_async
from django.conf import settings
from django.contrib.postgres.indexes import GinIndex from django.contrib.postgres.indexes import GinIndex
from django.db import models, transaction from django.db import models, transaction
from django.template import loader from django.template import loader
@ -874,24 +873,6 @@ class Post(StatorModel):
raise ValueError("Actor on delete does not match object") raise ValueError("Actor on delete does not match object")
post.delete() post.delete()
def debug_fetch(self):
"""
Fetches the Post from its original URL again and updates us with it
"""
response = httpx.get(
self.object_uri,
headers={
"Accept": "application/json",
"User-Agent": settings.TAKAHE_USER_AGENT,
},
follow_redirects=True,
)
if 200 <= response.status_code < 300:
return self.by_ap(
canonicalise(response.json(), include_security=True),
update=True,
)
### Mastodon API ### ### Mastodon API ###
def to_mastodon_json(self, interactions=None): def to_mastodon_json(self, interactions=None):

View file

@ -200,7 +200,9 @@ class HttpSignature:
body_bytes = b"" body_bytes = b""
# GET requests get implicit accept headers added # GET requests get implicit accept headers added
if method == "get": if method == "get":
headers["Accept"] = "application/activity+json" headers[
"Accept"
] = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
# Sign the headers # Sign the headers
signed_string = "\n".join( signed_string = "\n".join(
f"{name.lower()}: {value}" for name, value in headers.items() f"{name.lower()}: {value}" for name, value in headers.items()

View file

@ -156,6 +156,7 @@ urlpatterns = [
# Identity views # Identity views
path("@<handle>/", identity.ViewIdentity.as_view()), path("@<handle>/", identity.ViewIdentity.as_view()),
path("@<handle>/inbox/", activitypub.Inbox.as_view()), path("@<handle>/inbox/", activitypub.Inbox.as_view()),
path("@<handle>/outbox/", activitypub.Outbox.as_view()),
path("@<handle>/action/", identity.ActionIdentity.as_view()), path("@<handle>/action/", identity.ActionIdentity.as_view()),
path("@<handle>/rss/", identity.IdentityFeed()), path("@<handle>/rss/", identity.IdentityFeed()),
path("@<handle>/report/", report.SubmitReport.as_view()), path("@<handle>/report/", report.SubmitReport.as_view()),
@ -233,6 +234,7 @@ urlpatterns = [
path("nodeinfo/2.0/", activitypub.NodeInfo2.as_view()), path("nodeinfo/2.0/", activitypub.NodeInfo2.as_view()),
path("actor/", activitypub.SystemActorView.as_view()), path("actor/", activitypub.SystemActorView.as_view()),
path("actor/inbox/", activitypub.Inbox.as_view()), path("actor/inbox/", activitypub.Inbox.as_view()),
path("actor/outbox/", activitypub.EmptyOutbox.as_view()),
path("inbox/", activitypub.Inbox.as_view(), name="shared_inbox"), path("inbox/", activitypub.Inbox.as_view(), name="shared_inbox"),
# API/Oauth # API/Oauth
path("api/", api_router.urls), path("api/", api_router.urls),

View file

@ -335,6 +335,7 @@ class Identity(StatorModel):
"id": self.actor_uri, "id": self.actor_uri,
"type": "Person", "type": "Person",
"inbox": self.actor_uri + "inbox/", "inbox": self.actor_uri + "inbox/",
"outbox": self.actor_uri + "outbox/",
"preferredUsername": self.username, "preferredUsername": self.username,
"publicKey": { "publicKey": {
"id": self.public_key_id, "id": self.public_key_id,

View file

@ -43,6 +43,7 @@ class SystemActor:
"id": self.actor_uri, "id": self.actor_uri,
"type": "Application", "type": "Application",
"inbox": self.actor_uri + "inbox/", "inbox": self.actor_uri + "inbox/",
"outbox": self.actor_uri + "outbox/",
"endpoints": { "endpoints": {
"sharedInbox": f"https://{settings.MAIN_DOMAIN}/inbox/", "sharedInbox": f"https://{settings.MAIN_DOMAIN}/inbox/",
}, },

View file

@ -2,7 +2,7 @@ import json
from asgiref.sync import async_to_sync from asgiref.sync import async_to_sync
from django.conf import settings from django.conf import settings
from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse from django.http import Http404, HttpResponse, HttpResponseBadRequest, JsonResponse
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.generic import View from django.views.generic import View
@ -208,6 +208,53 @@ class Inbox(View):
return HttpResponse(status=202) return HttpResponse(status=202)
class Outbox(View):
"""
The ActivityPub outbox for an identity
"""
def get(self, request, handle):
self.identity = by_handle_or_404(
self.request,
handle,
local=False,
fetch=True,
)
# If this not a local actor, 404
if not self.identity.local:
raise Http404("Not a local identity")
# Return an ordered collection with the most recent 10 public posts
posts = list(self.identity.posts.not_hidden().public()[:10])
return JsonResponse(
canonicalise(
{
"type": "OrderedCollection",
"totalItems": len(posts),
"orderedItems": [post.to_ap() for post in posts],
}
),
content_type="application/activity+json",
)
class EmptyOutbox(View):
"""
A fixed-empty outbox for the system actor
"""
def get(self, request, *args, **kwargs):
return JsonResponse(
canonicalise(
{
"type": "OrderedCollection",
"totalItems": 0,
"orderedItems": [],
}
),
content_type="application/activity+json",
)
@method_decorator(cache_page(), name="dispatch") @method_decorator(cache_page(), name="dispatch")
class SystemActorView(View): class SystemActorView(View):
""" """