Skip to content

Add JSON-LD namespace and content negotiation for AP objects - #1385

Draft
alphatownsman wants to merge 1 commit into
neodb-social:mainfrom
alphatownsman:neodb-ns
Draft

Add JSON-LD namespace and content negotiation for AP objects#1385
alphatownsman wants to merge 1 commit into
neodb-social:mainfrom
alphatownsman:neodb-ns

Conversation

@alphatownsman

Copy link
Copy Markdown
Member

Summary

  • Define neodb: namespace (https://neodb.social/ns#) for all custom ActivityPub types and properties
  • Add @context with proper JSON-LD namespace to catalog item and journal piece AP responses
  • Add content negotiation to review and collection views to serve AP JSON
  • Add /p/<uuid> endpoint so Rating, Comment, ShelfMember, Note, CollectionMember are dereferenceable AP objects
  • Register neodb namespace in outgoing federated Note context (neodb-takahe submodule)

Details

NeoDB publishes custom types (Review, Rating, Comment, Status, CollectionItem) and properties (withRegardTo, relatedWith, best, worst, value, etc.) in ActivityPub objects. Previously these were bare JSON keys without a JSON-LD namespace, making them invisible to JSON-LD-aware consumers. Catalog items were served as application/activity+json without any @context.

This PR:

  1. Adds @context with neodb: namespace to all AP object responses
  2. Makes journal pieces dereferenceable at their own URLs (previously /review/<uuid> only returned HTML, and /p/<uuid> was a 404)
  3. Includes the neodb context in the takahe canonicalise() default context so federated Notes carry proper namespace definitions

Note and Collection journal types are intentionally omitted from the neodb context to avoid conflicting with AS2 types of the same name.

Backward compatible: old servers use our context from the incoming document for JSON-LD processing, so all terms round-trip correctly.

Test plan

  • Verify catalog item AP response includes @context with neodb namespace
  • Verify /review/<uuid> with Accept: application/activity+json returns AP JSON
  • Verify /collection/<uuid> with Accept: application/activity+json returns AP JSON
  • Verify /p/<uuid> serves AP JSON for Rating/Comment/ShelfMember/Note pieces
  • Verify federated outgoing Notes include neodb terms in @context
  • Verify federation with an existing NeoDB instance still works (send and receive)

- Define neodb: namespace (https://neodb.social/ns#) for all custom
  ActivityPub types and properties used by NeoDB
- Add @context to catalog item AP responses (Movie, Edition, etc.)
- Add @context to journal piece standalone AP responses
- Add content negotiation to review and collection views so they
  serve AP JSON when Accept header requests it
- Add /p/<uuid> endpoint for serving Rating, Comment, ShelfMember,
  Note, and CollectionMember as dereferenceable AP objects
- Update neodb-takahe submodule to include neodb namespace in
  outgoing federated Note context

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces ActivityPub and JSON-LD context support for catalog items and journal pieces, allowing them to be served as standalone ActivityPub objects. It adds a new piece_retrieve view and updates the collection_retrieve and review_retrieve views to support content negotiation via the Accept header. The review feedback recommends making the content negotiation logic more robust by checking for specific ActivityPub mime types and suggests moving a local import to the top level for consistency.

collection = get_object_or_404(Collection, uid=get_uuid_or_404(collection_uuid))
if not collection.is_visible_to(request.user):
raise PermissionDenied(_("Insufficient permission"))
if request.headers.get("Accept", "").endswith("json"):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The content negotiation logic is a bit too broad. Checking if the Accept header endswith("json") can lead to incorrectly serving JSON for mime types like text/json. It would be more robust to check for specific ActivityPub-related mime types like application/activity+json and application/ld+json.

Suggested change
if request.headers.get("Accept", "").endswith("json"):
if any(mime in request.headers.get("Accept", "") for mime in ("application/activity+json", "application/ld+json")):

Comment thread journal/views/post.py
raise PermissionDenied(_("Insufficient permission"))
if request.method == "HEAD":
return HttpResponse()
if request.headers.get("Accept", "").endswith("json"):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The content negotiation logic is a bit too broad. Checking if the Accept header endswith("json") can lead to incorrectly serving JSON for mime types like text/json. It would be more robust to check for specific ActivityPub-related mime types like application/activity+json and application/ld+json.

Suggested change
if request.headers.get("Accept", "").endswith("json"):
if any(mime in request.headers.get("Accept", "") for mime in ("application/activity+json", "application/ld+json")):

Comment thread journal/views/review.py
Comment on lines +42 to +47
if request.headers.get("Accept", "").endswith("json"):
from django.http import JsonResponse

return JsonResponse(
piece.ap_object_response(), content_type="application/activity+json"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This block can be improved in two ways:

  1. The content negotiation logic is too broad. Checking if the Accept header endswith("json") can incorrectly match mime types like text/json. It's better to check for specific ActivityPub mime types like application/activity+json and application/ld+json.
  2. The from django.http import JsonResponse is a local import. For consistency and best practice, it should be moved to the top of the file with other imports.

Here's a suggested replacement for this block. Please also remember to add the import to the top of the file.

    if any(mime in request.headers.get("Accept", "") for mime in ("application/activity+json", "application/ld+json")):
        return JsonResponse(
            piece.ap_object_response(), content_type="application/activity+json"
        )

Comment thread journal/views/post.py
Comment on lines +61 to +63
return JsonResponse(
piece.ap_object_response(), content_type="application/activity+json"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Requests for FeaturedCollection or Like objects via the /p/<uuid> endpoint with an ActivityPub Accept header will crash because they are missing the required ap_object implementation.
Severity: HIGH

Suggested Fix

Implement the ap_object property on the FeaturedCollection and Like models to return a valid ActivityPub object representation. Alternatively, if these models are not meant to be served via this endpoint, add logic to piece_retrieve to explicitly exclude them or handle them gracefully instead of causing a server error.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: journal/views/post.py#L61-L63

Potential issue: The `piece_retrieve` view at `/p/<uuid>` can fetch any subclass of
`Piece`. However, the `FeaturedCollection` and `Like` models, which are `Piece`
subclasses, do not implement the required `ap_object` property as mandated by the base
class. When a request is made to the UUID of a `FeaturedCollection` or `Like` object
with an ActivityPub-related `Accept` header (e.g., one ending in `json`), the code
attempts to call the unimplemented `ap_object` property. This triggers an unhandled
`NotImplementedError`, resulting in an HTTP 500 server error and crashing the request.

Did we get this right? 馃憤 / 馃憥 to inform future reviews.

collection = get_object_or_404(Collection, uid=get_uuid_or_404(collection_uuid))
if not collection.is_visible_to(request.user):
raise PermissionDenied(_("Insufficient permission"))
if request.headers.get("Accept", "").endswith("json"):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The content negotiation logic uses a fragile .endswith("json") check on the Accept header, which fails for valid headers with parameters or multiple values, causing HTML to be served instead of JSON.
Severity: HIGH

Suggested Fix

Replace the .endswith("json") check with a more robust content negotiation method. For example, check if a specific media type like "application/activity+json" or "application/ld+json" is present in the Accept header string. A better approach would be to use a library or Django's built-in utilities that can properly parse Accept headers and respect quality factors.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: journal/views/collection.py#L96

Potential issue: The content negotiation logic in multiple views checks if the `Accept`
header string ends with `"json"` to determine whether to serve ActivityPub JSON. This
check is not robust. It will incorrectly fail for valid, spec-compliant `Accept` headers
that contain parameters (e.g., `application/ld+json; profile="..."`) or multiple media
types (e.g., `application/activity+json, text/html`). In these realistic scenarios, the
server will fall back to serving HTML instead of the requested JSON, which breaks object
dereferencing for ActivityPub federation.

Did we get this right? 馃憤 / 馃憥 to inform future reviews.

@alphatownsman
alphatownsman marked this pull request as draft April 5, 2026 02:52
@alphatownsman alphatownsman added the future-or-post-1.0 Unlikely to hit 1.0 label Apr 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

future-or-post-1.0 Unlikely to hit 1.0

Projects

Status: Wishlist

Development

Successfully merging this pull request may close these issues.

1 participant