diff --git a/activities/admin.py b/activities/admin.py index f596f5f..76505af 100644 --- a/activities/admin.py +++ b/activities/admin.py @@ -107,16 +107,11 @@ class PostAdmin(admin.ModelAdmin): list_display = ["id", "type", "author", "state", "created"] list_filter = ("type", "local", "visibility", "state", "created") raw_id_fields = ["to", "mentions", "author", "emojis"] - actions = ["force_fetch", "reparse_hashtags"] + actions = ["reparse_hashtags"] search_fields = ["content"] inlines = [PostAttachmentInline] 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") def reparse_hashtags(self, request, queryset): for instance in queryset: diff --git a/activities/models/post.py b/activities/models/post.py index 3c3f50c..374823c 100644 --- a/activities/models/post.py +++ b/activities/models/post.py @@ -5,7 +5,6 @@ from typing import Optional import httpx import urlman from asgiref.sync import async_to_sync, sync_to_async -from django.conf import settings from django.contrib.postgres.indexes import GinIndex from django.db import models, transaction from django.template import loader @@ -874,24 +873,6 @@ class Post(StatorModel): raise ValueError("Actor on delete does not match object") 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 ### def to_mastodon_json(self, interactions=None): diff --git a/core/signatures.py b/core/signatures.py index 5829c40..12e9e97 100644 --- a/core/signatures.py +++ b/core/signatures.py @@ -200,7 +200,9 @@ class HttpSignature: body_bytes = b"" # GET requests get implicit accept headers added if method == "get": - headers["Accept"] = "application/activity+json" + headers[ + "Accept" + ] = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' # Sign the headers signed_string = "\n".join( f"{name.lower()}: {value}" for name, value in headers.items() diff --git a/takahe/urls.py b/takahe/urls.py index 9fad882..7c8231c 100644 --- a/takahe/urls.py +++ b/takahe/urls.py @@ -156,6 +156,7 @@ urlpatterns = [ # Identity views path("@/", identity.ViewIdentity.as_view()), path("@/inbox/", activitypub.Inbox.as_view()), + path("@/outbox/", activitypub.Outbox.as_view()), path("@/action/", identity.ActionIdentity.as_view()), path("@/rss/", identity.IdentityFeed()), path("@/report/", report.SubmitReport.as_view()), @@ -233,6 +234,7 @@ urlpatterns = [ path("nodeinfo/2.0/", activitypub.NodeInfo2.as_view()), path("actor/", activitypub.SystemActorView.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"), # API/Oauth path("api/", api_router.urls), diff --git a/users/models/identity.py b/users/models/identity.py index ba1bbb6..27a6ac8 100644 --- a/users/models/identity.py +++ b/users/models/identity.py @@ -335,6 +335,7 @@ class Identity(StatorModel): "id": self.actor_uri, "type": "Person", "inbox": self.actor_uri + "inbox/", + "outbox": self.actor_uri + "outbox/", "preferredUsername": self.username, "publicKey": { "id": self.public_key_id, diff --git a/users/models/system_actor.py b/users/models/system_actor.py index 5869f92..62b9996 100644 --- a/users/models/system_actor.py +++ b/users/models/system_actor.py @@ -43,6 +43,7 @@ class SystemActor: "id": self.actor_uri, "type": "Application", "inbox": self.actor_uri + "inbox/", + "outbox": self.actor_uri + "outbox/", "endpoints": { "sharedInbox": f"https://{settings.MAIN_DOMAIN}/inbox/", }, diff --git a/users/views/activitypub.py b/users/views/activitypub.py index 2d17afd..e027711 100644 --- a/users/views/activitypub.py +++ b/users/views/activitypub.py @@ -2,7 +2,7 @@ import json from asgiref.sync import async_to_sync 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.views.decorators.csrf import csrf_exempt from django.views.generic import View @@ -208,6 +208,53 @@ class Inbox(View): 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") class SystemActorView(View): """