Ignore:
Timestamp:
07/01/2026 07:14:12 PM (2 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
737ea05
Parents:
0688b5f
Message:

feat(fediverse): report a post/account to its home server (AS2 Flag)

From the interact page you can now report a fediverse post to its home server's
moderators. Sends the Mastodon-standard AS2 Flag (object = [reported account,
reported status], content = the reason) to the reported account's inbox.

  • src/services/ActivityPubService.js — sendReport(site, {objectUri, actorUri, reason}) resolves the reported account, builds the Flag and delivers it to their inbox.
  • src/routes/posts.js — POST /authorize_interaction/report (owner-only); GET passes reported and skips re-resolving on the confirmation view.
  • src/views/pages/authorize-interaction.ejs — a "Report this post" disclosure with a reason field, a "reported" confirmation, and scoped styles.
  • src/services/i18n.js — fedi.report_* + fedi.reported_* (nl/en/de).
  • test/activitypub-as2.test.js — allow the AS2 Flag activity type.
  • CHANGELOG(.nl/.de).md — "Report a post to the fediverse" under Unreleased.

Co-Authored-By: Claude <noreply@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/ActivityPubService.js

    r0688b5f r1c2dcba  
    21942194}
    21952195
     2196// Report a remote post or account to its home instance (moderation). Sends the Mastodon-standard
     2197// AS2 `Flag`: object = [reported account, reported status?], content = the reason, delivered to the
     2198// reported account's inbox so their instance's moderators receive it. objectUri = a post URL (its
     2199// author is resolved + included) OR pass actorUri to report an account directly.
     2200export async function sendReport(site, { objectUri, actorUri, reason }) {
     2201  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
     2202  if (!base || !site || !site.slug) return { error: 'config' };
     2203  let targetActor = actorUri || null;
     2204  let noteUri = null;
     2205  if (objectUri && /^https?:\/\//i.test(objectUri)) {
     2206    const note = await apGetJson(objectUri).catch(() => null);
     2207    if (note && note.id) { noteUri = note.id; if (!targetActor) targetActor = actorUriOf(note.attributedTo); }
     2208    else if (!targetActor) return { error: 'not_found' };
     2209  }
     2210  if (!targetActor || !/^https?:\/\//i.test(targetActor)) return { error: 'not_found' };
     2211  const actor = await fetchActor(targetActor).catch(() => null);
     2212  const inbox = actor && (actor.inbox || (actor.endpoints && actor.endpoints.sharedInbox)); // personal inbox → their moderators
     2213  if (!inbox) return { error: 'unreachable' };
     2214  const me = actorId(base, site.slug);
     2215  const keys = getOrCreateKeys(site.slug);
     2216  const object = [targetActor];
     2217  if (noteUri && noteUri !== targetActor) object.push(noteUri);
     2218  const flag = {
     2219    '@context': AP_CONTEXT,
     2220    id: `${me}#report-${Date.now()}-${rid()}`,
     2221    type: 'Flag',
     2222    actor: me,
     2223    content: String(reason == null ? '' : reason).slice(0, 3000),
     2224    object, // [account, status?] — Mastodon's Flag shape
     2225    to: [targetActor],
     2226  };
     2227  deliverWithRetry(site.slug, inbox, flag, `${me}#main-key`, keys.private_pem);
     2228  return { ok: true };
     2229}
     2230
    21962231export function isBlockedAny(actorUri) {
    21972232  if (!actorUri) return false;
     
    22492284  listOutbox, deliverOutboxDelete, deliverOutboxUpdate,
    22502285  webfingerResolve, followActor, resolveRemoteActor, unfollowActor, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, sendInteraction, voteOnPoll, voteOnRemotePoll,
    2251   parseOwnPoll, pollTally, ownPollView, deliverPollUpdate, maybeCrawlThread,
     2286  parseOwnPoll, pollTally, ownPollView, deliverPollUpdate, maybeCrawlThread, sendReport,
    22522287  autoBoostCount, boostedCount, markBoosted, unmarkBoosted, markLiked, unmarkLiked, getTimelineReaction, upsertBoostedNote, getCirkelPosts, getCirkelMembers, selfHealTimeline,
    22532288  getNotifications, listBlocks, isBlockedAny, blockTarget, unblock,
Note: See TracChangeset for help on using the changeset viewer.