Skip to content

Show fediverse replies to entries as comments - #494

Merged
laffer1 merged 5 commits into
masterfrom
activitypub-replies
Nov 2, 2026
Merged

laffer1 merged 5 commits into
masterfrom
activitypub-replies

Conversation

@laffer1

@laffer1 laffer1 commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator

Replies from Mastodon and other fediverse servers to a JustJournal entry now show as comments on it. Until now the inbox dropped them.

What is accepted

A Create of a Note or Article is stored when all of these hold:

  • its inReplyTo is one of our entry object ids, for an entry owned by the inbox's user;
  • federation is on for the site and for that owner;
  • the entry is publicly visible and allows comments;
  • attributedTo is the signed sender;
  • it is addressed to Public;
  • its id is on the sender's host.

Anything else gets the 202 that other ignored activities get. Replies to replies are ignored.

  • Repeats: the reply's ActivityPub id is unique, so the same reply delivered twice is stored once.
  • Author: name, @user@host and profile URL come from the sender's actor document, fetched through the SSRF-safe fetcher. The URL must be http(s). If the fetch fails temporarily, the inbox answers 503 so the sender retries.
  • Content: jsoup reduces it to text, paragraphs, line breaks and http(s) links, with rel="nofollow ugc noopener". Replies over 10,000 characters become escaped plain text and are cut. No remote avatars or images are shown.
  • Edits and deletes: only the reply's author can Update or Delete it. A signed Delete of the actor removes all their replies.

Where they show

  • After local comments on the entry page: the author's name links to their profile, followed by their handle. Everything is escaped.
  • In the comment counts on the entry list pages.
  • In GET /api/comment?entryId=, with remote, authorName and authorUrl.
  • The journal owner can delete a remote reply (DELETE /api/comment/remote/{id}). Anyone else gets 403.
  • They use the existing comment-notification email. The mail code moved into CommentMailService. That also fixes the entry link in the mail, which used to print the Settings object rather than the base URL.
  • Account deletion removes replies to the account's entries. Deleting a single entry removes its replies through the foreign key.

Storage

They go in a new table, activitypub_reply (V1_30), not in comments. Three things about comments ruled it out:

  • uid must be a local user, and much code calls getUser() on comments;
  • body and subject are latin1, so emoji would fail to insert;
  • the id is SMALLINT UNSIGNED, so remote spam could use up local comment ids.

The new table is utf8mb4. Its rows are deleted with their entry.

Open questions

  • Moderation: comments have no moderation status, so remote replies appear straight away. Blocking by actor or server, and rate limits, aren't built.
  • Deleting local comments: an entry's owner still can't delete local comments on it; only the comment's author can. That's existing behaviour, left as is.
  • Adjacent bug, not fixed here: CommentController.put lets any logged-in user edit any local comment by id.
  • Not covered:
    • Remote replies aren't counted in the site statistics or in the entry JSON comments.
    • Content warnings (summary) are dropped.

Testing

  • New: RemoteReplyServiceTests (34), InboxServiceRepliesTests (5), CommentMailServiceTests (2). Extended: comment, users, account, inbox and actor controller tests.
  • Affected classes: 203 tests, 0 failures. Full unit suite: 1750 tests, 0 failures.
  • Not verified on MySQL 8: the migration, the cascade, the unique key, and the JPA mapping.
  • This branch and Match an ActivityPub Undo that names the Follow by its id #493 both change InboxService and InboxControllerTests. Whichever merges second will need a small conflict fix.

🤖 Generated with Claude Code

Summary by Sourcery

Show validated fediverse replies as manageable comments throughout entry pages and comment APIs.

New Features:

  • Accept eligible ActivityPub replies to public, comment-enabled entries and store them as remote comments with author metadata and sanitized content.
  • Display remote replies alongside local comments, include them in comment counts and comment API responses, and allow entry owners to delete them.
  • Support author-only updates and deletes, actor-wide cleanup, duplicate suppression, and comment notification emails for remote replies.

Bug Fixes:

  • Correct comment notification links to use the configured base URL instead of the settings object representation.

Enhancements:

  • Store federated replies in a dedicated utf8mb4 table with uniqueness and entry-deletion cascading rather than mixing them with local comments.
  • Centralize comment notification email generation for local and remote comments.

Build:

  • Add the ActivityPub reply database migration.

Tests:

  • Add coverage for remote reply ingestion, validation, sanitization, lifecycle handling, presentation, API behavior, cleanup, and notification email generation.

Replies from Mastodon and other fediverse servers were dropped: the inbox
only understood Follow, Undo and Delete. A public Note or Article in reply
to one of a journal's entries is now stored and shown with the entry's
comments, when federation is on for the site and the journal, the entry
is publicly visible and takes comments, and the signed sender wrote it.

Replies live in a new activitypub_reply table (V1_30) rather than in
comments: comments.uid is a local user that many places dereference, its
text columns are latin1 and its id is a SMALLINT, so remote replies there
would break local-user code, lose emoji and could use up local comment
ids. The reply's ActivityPub id is unique, so a repeated delivery stores
one row; rows go with their entry by a cascading foreign key.

- RemoteReplyService takes Create, Update and Delete of replies from
  InboxService; only the author may change or remove a reply. Deleting a
  remote actor, signed or gone, also removes its replies.
- Content is cleaned with a jsoup Safelist down to text, paragraphs,
  line breaks and http(s) links with rel="nofollow ugc noopener", and
  capped. Author name, @user@host and profile URL come from the actor
  document; no remote pictures are shown.
- Replies appear on the entry page, in comment counts and in the
  /api/comment listing (marked remote). The entry's owner can delete one
  through DELETE /api/comment/remote/{id}; nobody else can.
- The comment notification mail moved into CommentMailService so replies
  reuse it; its entry link now uses the baseuri setting's value instead
  of the Settings object's toString.
- Account deletion removes replies to the account's entries.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@sourcery-ai

sourcery-ai Bot commented Nov 2, 2026

Copy link
Copy Markdown

Sorry @laffer1, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 6 hours and 9 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@laffer1

laffer1 commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator Author

✅ Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
✅ Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@sourcery-ai

sourcery-ai Bot commented Nov 2, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR adds end-to-end support for displaying fediverse replies as comments by validating and sanitizing ActivityPub replies into dedicated storage, exposing them through pages and APIs with owner moderation controls, handling edits/deletes and actor/account cleanup, and reusing centralized comment notification mail.

Sequence diagram for storing a fediverse reply

sequenceDiagram
    participant Fediverse as Fediverse server
    participant InboxService
    participant RemoteReplyService
    participant SafeHttpFetcher
    participant RemoteReplyRepository
    participant CommentMailService

    Fediverse->>InboxService: Create
    InboxService->>RemoteReplyService: create(user, activity, sender)
    RemoteReplyService->>RemoteReplyService: validate reply and entry policy
    RemoteReplyService->>SafeHttpFetcher: signedGet(actor URL, maxBytes)
    SafeHttpFetcher-->>RemoteReplyService: actor document
    RemoteReplyService->>RemoteReplyService: clean(content)
    RemoteReplyService->>RemoteReplyRepository: saveAndFlush(reply)
    RemoteReplyService->>CommentMailService: commentAdded(entry, author, subject, body)
    CommentMailService-->>Fediverse: HANDLED

    alt actor fetch temporarily fails
        RemoteReplyService-->>Fediverse: RETRY
    else validation fails or reply is not eligible
        RemoteReplyService-->>Fediverse: IGNORED
    end
Loading

Sequence diagram for displaying and deleting a remote reply

sequenceDiagram
    actor Owner
    participant CommentController
    participant RemoteReplyRepository
    participant EntryRepository
    participant UsersController

    UsersController->>RemoteReplyRepository: findByEntryIdOrderByPublishedAscIdAsc(entryId)
    UsersController-->>Owner: Render remote replies with author profile and handle
    Owner->>CommentController: DELETE /api/comment/remote/{id}
    CommentController->>RemoteReplyRepository: findById(id)
    CommentController->>EntryRepository: findById(reply.entryId)
    CommentController->>RemoteReplyRepository: delete(reply)
    CommentController-->>Owner: deleted reply id
Loading

Entity relationship diagram for stored fediverse replies

erDiagram
    ENTRY ||--o{ ACTIVITYPUB_REPLY : receives
    ENTRY {
        int id PK
    }
    ACTIVITYPUB_REPLY {
        int id PK
        int entry_id FK
        string object_id UK
        string actor_id
        string author_name
        string author_handle
        string author_url
        text content
        datetime published
    }
Loading

File-Level Changes

Change Details Files
Adds ActivityPub ingestion, validation, sanitization, persistence, lifecycle handling, and notification support for remote replies to local entries.
  • Routes Create, Update, and Delete inbox activities to a dedicated reply service.
  • Accepts only public Note/Article replies to eligible local entries from the signed actor, with same-host object IDs and federation/comment visibility checks.
  • Fetches actor metadata through the SSRF-safe signed fetcher, returns retryable failures, sanitizes reply HTML, truncates oversized content, and prevents duplicate storage.
  • Supports author-only edits/deletes and removes replies when an actor is deleted.
src/main/java/com/justjournal/services/activitypub/InboxService.java
src/main/java/com/justjournal/services/activitypub/RemoteReplyService.java
Introduces separate utf8mb4 storage and repository APIs for remote replies, including uniqueness and entry/actor lifecycle relationships.
  • Adds the activitypub_reply entity and repository with unique ActivityPub object IDs and ordered entry queries.
  • Adds a Flyway migration with utf8mb4 content, indexed actor/entry fields, and cascade deletion from entries.
  • Removes replies during account deletion before deleting the owner's entries.
src/main/java/com/justjournal/model/RemoteReply.java
src/main/java/com/justjournal/repository/RemoteReplyRepository.java
src/main/resources/db/migration/V1_30__activitypub_replies.sql
src/main/java/com/justjournal/ctl/api/AccountController.java
Integrates remote replies into web pages, comment APIs, ownership controls, and client-side deletion.
  • Displays escaped remote author metadata and sanitized content after local comments, and includes replies in entry comment counts.
  • Extends comment API responses with remote metadata and adds an owner-only remote-reply deletion endpoint.
  • Adds the browser request used by journal owners to delete remote replies.
src/main/java/com/justjournal/ctl/UsersController.java
src/main/java/com/justjournal/ctl/api/CommentController.java
src/main/java/com/justjournal/model/api/CommentTo.java
src/main/resources/static/js/jj.js
Centralizes comment notification mail and applies it to both local comments and remote replies.
  • Moves queue-mail construction into CommentMailService.
  • Uses the configured base URL correctly when building entry links.
  • Queues notifications for accepted remote replies using cleaned plain text and actor identity.
src/main/java/com/justjournal/services/CommentMailService.java
src/main/java/com/justjournal/ctl/api/CommentController.java
src/main/java/com/justjournal/services/activitypub/RemoteReplyService.java
Adds focused coverage for reply ingestion, security rules, rendering, APIs, deletion, mail, and account cleanup.
  • Tests acceptance/rejection conditions, SSRF-safe actor fetching, retry behavior, deduplication, sanitization, updates, deletes, and actor removal.
  • Tests page/API rendering, comment counts, owner authorization, notification links, and account deletion ordering.
  • Updates controller fixtures for the new service and repository dependencies.
src/test/java/com/justjournal/services/activitypub/RemoteReplyServiceTests.java
src/test/java/com/justjournal/services/activitypub/InboxServiceRepliesTests.java
src/test/java/com/justjournal/services/CommentMailServiceTests.java
src/test/java/com/justjournal/ctl/UsersControllerTests.java
src/test/java/com/justjournal/ctl/api/CommentControllerTests.java
src/test/java/com/justjournal/ctl/api/AccountControllerTests.java
src/test/java/com/justjournal/ctl/InboxControllerTests.java
src/test/java/com/justjournal/ctl/ActivityStreamControllerTests.java

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@deepsource-io

deepsource-io Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

DeepSource Code Review

We reviewed changes in 7ec2d66...93882c5 on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

PR Report Card

Overall Grade   Security  

Reliability  

Complexity  

Hygiene  

Code Review Summary

Analyzer Status Updated (UTC) Details
PHP Nov 2, 2026 7:39p.m. Review ↗
Java Nov 2, 2026 7:39p.m. Review ↗
Shell Nov 2, 2026 7:39p.m. Review ↗
JavaScript Nov 2, 2026 7:39p.m. Review ↗

Important

AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.

laffer1 and others added 4 commits November 2, 2026 14:23
CodeQL flagged Integer.parseInt in RemoteReplyService as a possible
uncaught NumberFormatException. The nine-digit pattern before it rules
that out, but a catch costs nothing and matches WebFingerController.

DeepSource reported two "incomplete assertion" issues in
InboxControllerTests on verify(httpSignatureService).verify(...): a
complete Mockito check of a mocked method that is itself named verify.
They are marked with skipcq rather than rewritten.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
jj.js hadn't changed since 2017, before DeepSource was set up. Adding a
function to it made DeepSource report all of its old issues (jQuery not
defined, var, alert) as introduced, failing the JavaScript check.
deleteRemoteComment now lives in the entry page's own script block, and
jj.js is as it was. confirmDelete still comes from jj.js, which loads
before anything can be clicked.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
DeepSource failed the Java check on two complexity warnings. create
now hands the acceptance checks, the object id check and the save to
helpers of their own. The comment count link, copied into three page
layouts, is one helper that also counts fediverse replies.

formatEntry was already far over the limit before this branch, and the
remote replies are listed inside it, so it is marked with skipcq rather
than split up here.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Both were over the cyclomatic complexity limit on master. This branch
only replaced their comment-count switches with appendCommentsLink, so
DeepSource now reports them as introduced and fails the check.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@laffer1
laffer1 merged commit e956390 into master Nov 2, 2026
11 checks passed
@laffer1
laffer1 deleted the activitypub-replies branch November 2, 2026 20:35
laffer1 added a commit that referenced this pull request Nov 2, 2026
Both branches dispatched Create, Update and Delete to their own service.
Replies to our entries are now tried first; whatever RemoteReplyService
ignores goes on to RemotePostService, which already skips replies to
our entries. HANDLED and RETRY from the replies stop there. A deleted
or gone actor loses its followers, replies and follows alike.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants