176 Commits

Author SHA1 Message Date
svemagie
318720c90d merge: upstream 42c0959..6436763 — own posts in ap_timeline with synthesized content for interactions
- 42c0959: feat: add own posts to ap_timeline after syndication
- f1ad18d: fix: synthesize timeline content for likes/bookmarks/reposts
- 6436763: fix: include interaction target URL in timeline content

Upstream's implementation supersedes our 42f8c2d: uses buildTimelineContent()
for synthesized display content on interaction types, reads real profile data,
normalizes media, idempotent via $setOnInsert.
2026-03-27 20:33:26 +01:00
svemagie
b54146ce5b fix(oauth): echo state parameter back in authorization redirect
OAuth 2.0 requires the server to echo the state parameter in the
callback redirect. Mastodon clients (e.g. murmel.social) send a
state value and fail with 'missing parameters' if it is absent.

Thread state through: GET query → session store → hidden form field
→ POST body → callback redirect (approve and deny paths).
2026-03-27 16:47:42 +01:00
Ricardo
6436763dab fix: include interaction target URL in timeline content
Interaction types with comment text (e.g. repost with a comment) were
showing only the comment, losing the repost-of/bookmark-of/like-of URL.
Now always includes the target URL for interaction types, combining it
with any body text.
2026-03-27 16:26:13 +01:00
Ricardo
f1ad18d92d fix: synthesize timeline content for likes/bookmarks/reposts
Interaction types (likes, bookmarks, reposts) have no body content in
their JF2 properties. The timeline entry was created with empty content,
showing blank posts in Phanpy/Moshidon. Now synthesizes display content
(e.g. "Liked: https://...") matching backfill-timeline.js behavior.
2026-03-27 15:32:15 +01:00
Ricardo
42c0959c8a feat: add own posts to ap_timeline after syndication
Own Micropub posts weren't appearing in Mastodon Client API timelines
(Phanpy/Moshidon) because there was no mechanism to add them to
ap_timeline. The inbox round-trip doesn't work (we don't follow
ourselves) and startup backfill only runs once.

Now the AP syndicator adds the post to ap_timeline after successful
delivery, using addTimelineItem ($setOnInsert — idempotent). Content
flows directly from Micropub properties with proper HTML links.
2026-03-27 14:03:41 +01:00
svemagie
9b6db9865c fix(logging): suppress fedify inbox HTTP Signature verification noise 2026-03-27 10:15:05 +01:00
svemagie
69ae731dab fix(rate-limit): suppress ERR_ERL_PERMISSIVE_TRUST_PROXY behind nginx reverse proxy 2026-03-27 09:43:09 +01:00
svemagie
b595734087 fix(accounts): add missing tokenRequired/scopeRequired imports dropped during merge 2026-03-27 09:35:37 +01:00
svemagie
230bfd105e merge: upstream c1a6f7e — Fedify 2.1.0, 5 FEPs, security/perf audit, v3.9.x
Upstream commits merged (0820067..c1a6f7e):
- Fedify 2.1.0 upgrade (FEP-5feb, FEP-f1d5/0151, FEP-4f05 Tombstone,
  FEP-3b86 Activity Intents, FEP-8fcf Collection Sync)
- Comprehensive security/perf audit: XSS/CSRF fixes, OAuth scopes,
  rate limiting, secret hashing, token expiry/rotation, SSRF fix
- Architecture refactoring: syndicator.js, batch-broadcast.js,
  init-indexes.js, federation-actions.js; index.js -35%
- CSS split into 15 feature-scoped files + reader-interactions.js
- Mastodon API status creation: content-warning field, linkify fix

Fork-specific resolutions:
- syndicator.js: added addTimelineItem mirror for own Micropub posts
- syndicator.js: fixed missing await on jf2ToAS2Activity (async fn)
- statuses.js: kept DM path, pin/unpin routes, edit post route,
  processStatusContent (used by edit), addTimelineItem/lookupWithSecurity/
  addNotification imports
- compose.js: kept addNotification + added federation-actions.js imports
- enrich-accounts.js: kept cache-first approach for avatar updates
- ap-notification-card.njk: kept DM lock icon (🔒) for isDirect mentions
2026-03-27 09:30:34 +01:00
Ricardo
35ab840a56 feat: upgrade Fedify to 2.1.0 + implement 5 FEPs
Fedify 2.1.0 upgrade:
- Upgrade @fedify/fedify, @fedify/redis, @fedify/debugger to ^2.1.0
- Remove as:Endpoints type-stripping workaround (fixed upstream, fedify#576)
- Wire onUnverifiedActivity handler for Delete from actors with gone keys

FEP implementations:
- FEP-5feb: Add indexable + discoverable to actor (search indexing consent)
- FEP-f1d5/0151: Enrich NodeInfo 2.1 with metadata, staff accounts, repo info
- FEP-4f05: Soft delete with Tombstone — deleted posts serve 410 + Tombstone
  JSON-LD with formerType, published, deleted timestamps. New ap_tombstones
  collection + lib/storage/tombstones.js
- FEP-3b86: Activity Intents — WebFinger links for Follow/Create/Like/Announce
  intents, authorize_interaction routes by intent parameter
- FEP-8fcf: Collection Sync outbound via Fedify syncCollection (documented
  that receiving side is not yet implemented)
2026-03-26 17:33:28 +01:00
Ricardo
1bfeabeaf3 fix: Mastodon API status creation — links, CW, timeline timing
- Provide content as {text, html} with linkified URLs (Micropub's
  markdown-it doesn't have linkify enabled)
- Use content-warning field (not summary) to match native reader and
  AP syndicator expectations
- Remove premature addTimelineItem — post appears in timeline after
  syndication round-trip, not immediately
- Remove processStatusContent (unused after addTimelineItem removal)
- Remove addTimelineItem import
2026-03-26 15:33:38 +01:00
Ricardo
12454749ad fix: comprehensive security, performance, and architecture audit fixes
27 issues fixed from multi-dimensional code review (4 Critical, 6 High, 11 Medium, 6 Low):

Security (Critical):
- Escape HTML in OAuth authorization page to prevent XSS (C1)
- Add CSRF protection to OAuth authorize flow (C2)
- Replace bypassable regex sanitizer with sanitize-html library (C3)
- Enforce OAuth scopes on all Mastodon API routes (C4)

Security (Medium/Low):
- Fix SSRF via DNS resolution before private IP check (M1)
- Add rate limiting to API, auth, and app registration endpoints (M2)
- Validate redirect_uri on POST /oauth/authorize (M4)
- Fix custom emoji URL injection with scheme validation + escaping (M5)
- Remove data: scheme from allowed image sources (L6)
- Add access token expiry (1hr) and refresh token rotation (90d) (M3)
- Hash client secrets before storage (L3)

Architecture:
- Extract batch-broadcast.js — shared delivery logic (H1a)
- Extract init-indexes.js — MongoDB index creation (H1b)
- Extract syndicator.js — syndication logic (H1c)
- Create federation-actions.js facade for controllers (M6)
- index.js reduced from 1810 to ~1169 lines (35%)

Performance:
- Cache moderation data with 30s TTL + write invalidation (H6)
- Increase inbox queue throughput to 10 items/sec (H5)
- Make account enrichment non-blocking with fire-and-forget (H4)
- Remove ephemeral getReplies/getLikes/getShares from ingest (M11)
- Fix LRU caches to use true LRU eviction (L1)
- Fix N+1 backfill queries with batch $in lookup (L2)

UI/UX:
- Split 3441-line reader.css into 15 feature-scoped files (H2)
- Extract inline Alpine.js interaction component (H3)
- Reduce sidebar navigation from 7 to 3 items (M7)
- Add ARIA live regions for dynamic content updates (M8)
- Extract shared CW/non-CW content partial (M9)
- Document form handling pattern convention (M10)
- Add accessible labels to functional emoji icons (L4)
- Convert profile editor to Alpine.js (L5)

Audit: documentation-central/audits/2026-03-24-activitypub-code-review.md
Plan: documentation-central/plans/2026-03-24-activitypub-audit-fixes.md
2026-03-25 07:41:20 +01:00
svemagie
bd3a623488 fix(linkify): strip trailing punctuation from auto-linked URLs
URLs at the end of a sentence followed by . , ; : ) etc. were capturing
the punctuation character as part of the URL, producing broken links
(e.g. https://example.com. instead of https://example.com).

Fix in both places where URL linkification happens:
- lib/jf2-to-as2.js linkifyUrls() — used when federating posts via AP
- lib/mastodon/routes/statuses.js processStatusContent() — used when
  creating posts via the Mastodon Client API

Both now use a replacement callback that strips trailing [.,;:!?)\]'"]
from the captured URL before inserting it into the <a> tag.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 19:56:49 +01:00
svemagie
e319c348d0 feat(mastodon-api): implement PUT /api/v1/statuses/:id (edit post)
Adds the Mastodon Client API edit endpoint, which was returning 501 so
"Beitrag bearbeiten" / edit post always failed.

Flow:
1. Look up timeline item by cursor ID; 403 if not own post
2. Build Micropub replace operation for content/summary/sensitive/language
   and call postData.update() + postContent.update() to update MongoDB
   posts collection and the content file on disk
3. Patch ap_timeline with new content, summary, sensitive, and updatedAt
   (serializeStatus reads updatedAt → edited_at field)
4. Broadcast AP Update(Note) to all followers via shared inbox so remote
   servers can display the edit indicator
5. Return serialized status with edited_at set

Also adds Update to the top-level @fedify/fedify/vocab import and updates
the module-level comment block to list the new route.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 11:32:48 +01:00
svemagie
b5ebf6a1e4 feat(mastodon-api): implement POST /api/v1/statuses/:id/pin and /unpin
Adds the Mastodon Client API endpoints for pinning and unpinning posts:

- POST /api/v1/statuses/:id/pin — upserts a document into ap_featured
  (same store the admin UI uses), enforces the 5-post maximum, fires
  broadcastActorUpdate() so remote servers re-fetch the featured collection
- POST /api/v1/statuses/:id/unpin — removes from ap_featured, broadcasts update
- loadItemInteractions() now also queries ap_featured and returns pinnedIds
- GET /api/v1/statuses/:id response now reflects actual pin state
- broadcastActorUpdate wired into mastodon pluginOptions in index.js

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 11:16:15 +01:00
svemagie
2660a1a604 fix(mastodon-api): favourite fails for pre-UTC-normalization timeline items
findTimelineItemById's string range query used $gte/$lte on ISO strings,
which fails for items stored with timezone offsets (e.g. "+01:00"): the
string "2026-03-21T16:33:50+01:00" is lexicographically outside the
range ["2026-03-21T15:33:50Z", "2026-03-21T15:33:51Z"].

Replace with a $or that covers both cases:
- BSON Date stored (Micropub): direct Date range comparison
- String stored with any timezone: $dateFromString parses the offset
  correctly, $toLong converts to ms-since-epoch, numeric range always
  matches regardless of timezone format

Items received before extractObjectData's UTC normalization (a259c79)
was deployed are stored with the original server's timezone offset and
now resolve correctly through this fallback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 10:53:20 +01:00
svemagie
b33932f1f6 merge: upstream c2920ca raw signed fetch fallback for author resolution
Merges upstream fix that adds Strategy 1b to resolveAuthor: a raw signed
HTTP fetch for servers (e.g. wafrn) that return AP JSON without @context,
which Fedify's JSON-LD processor would otherwise reject.

Combined with our 5-second timeout wrapper so both improvements apply:
- privateKey/keyId now passed to resolveAuthor for the signed raw fetch
- timeout still guards all three strategies against slow/unreachable remotes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 09:32:33 +01:00
svemagie
01f6f81bda fix(mastodon-api): favourite/reblog blocks on unbound resolveAuthor HTTP requests
likePost, unlikePost and boostPost all call resolveAuthor() which makes
up to 3 signed HTTP requests to the remote server (post fetch, author
actor fetch, getAttributedTo) with no timeout. If the remote server is
slow or unreachable, the favourite/reblog HTTP response hangs until the
Node.js socket default times out (~2 min). Mastodon clients (Phanpy,
Elk) give up much sooner and show "Failed to load post".

Fix: wrap every resolveAuthor() call in a Promise.race() with a 5 s
timeout. The interaction is still recorded in ap_interactions and the
Like/Announce activity is still sent when recipient resolution succeeds
within the window; if it times out, AP delivery is silently skipped
(the local record is kept — the client sees a successful ).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 08:39:46 +01:00
svemagie
da89554ef9 fix(mastodon): profile avatar lost after first enrichment; actor published non-UTC
Two profile display fixes:

1. Avatar not persisting across requests: resolveRemoteAccount fetches
   the correct avatar via lookupWithSecurity, but only updated the
   in-memory serialized status — never the DB or the cache. On the next
   request serializeStatus rebuilt the account from item.author.photo
   (empty if the actor was on a Secure Mode server when the item arrived),
   and enrichAccountStats skipped re-fetching because follower counts
   were already > 0. Fix: include avatarUrl in cacheAccountStats; in
   collectAccount always check the cache first (for avatar + createdAt)
   regardless of whether counts are already populated.

2. actor.published may not be UTC: Temporal.Instant.toString() preserves
   the original timezone offset from the AP actor object; wrap in
   new Date(...).toISOString() so created_at is always UTC ISO 8601.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 08:30:25 +01:00
svemagie
a259c79a31 fix(mastodon-api): favourite/like 404 for items with BSON Date or timezone-offset published
Three-layer fix for findTimelineItemById cursor mismatches:

1. encodeCursor returns "" (falsy) for invalid dates — serializeStatus
   now correctly falls back to item._id.toString() instead of using "0"
   as an opaque ID that can never be looked up.

2. findTimelineItemById adds two new fallbacks after the existing string
   lookups fail:
   - BSON Date lookup: Micropub pipeline (postData.create) may store
     published as a JavaScript Date → MongoDB BSON Date; string
     comparison never matches.
   - ±999 ms ISO range query: some AP servers send published with a
     timezone offset (e.g. +01:00). String(Temporal.Instant) preserves
     the original offset; decodeCursor normalizes to UTC, so the stored
     string and the lookup string differ.

3. timeline-store.js extractObjectData now normalizes published via
   new Date(String(...)).toISOString() before storing, ensuring all
   future items are stored in UTC ISO format and the exact-match lookup
   succeeds without needing the range fallback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 08:30:14 +01:00
Ricardo
c2920cafd8 fix: raw signed fetch fallback for author resolution
Servers like wafrn return AP JSON without @context, causing Fedify's
JSON-LD processor to reject the document. Strategy 1b in resolveAuthor
does a direct signed GET, extracts attributedTo/actor from plain JSON,
then resolves the actor via lookupWithSecurity.

Also: _loadRsaPrivateKey now imports with extractable=true (required
by Fedify's signRequest), and loadRsaKey is wired through to all
Mastodon API interaction helpers.
2026-03-23 07:56:34 +01:00
svemagie
6c13eb85a5 fix(mastodon-api): pass createdAt for follower/following accounts; URL-type AP lookup
- In accounts.js: all places that build an actor object from ap_followers or
  ap_following docs now include `createdAt: f.createdAt || undefined`.
  Previously the field was omitted, causing serializeAccount() to fall back to
  `new Date().toISOString()`, making every follower/following appear to have
  joined "just now" in the Mastodon client.
  Affected: GET /api/v1/accounts/:id/followers, /following, /lookup, and the
  resolveActorData() fallback used by GET /api/v1/accounts/:id.

- In resolve-account.js: HTTP actor URLs are now passed to lookupWithSecurity()
  as a native URL object instead of a bare string, matching Fedify's preferred
  type. The acct:user@domain WebFinger path remains a string (new URL() would
  misparse the @ as a user-info separator under WHATWG rules).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 07:20:50 +01:00
svemagie
ed18446e05 fix(mastodon): use lookupWithSecurity for remote profile resolution
Replace direct ctx.lookupObject() call in resolveRemoteAccount with
lookupWithSecurity() so servers that reject signed GETs are retried
unsigned. Also add 5 s Promise.race timeouts to followers/following/
outbox collection fetches to prevent profile loads from hanging on
slow remote servers.

Fixes missing profile pictures and zero follower stats in Mastodon
client views.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 21:24:03 +01:00
svemagie
7b838ea295 fix(mastodon-api): pass collections object (not raw collection) to addTimelineItem
addTimelineItem(collections, item) destructures { ap_timeline } from its
first argument. Passing collections.ap_timeline directly gave it a raw
MongoDB collection with no ap_timeline property, causing
"Cannot read properties of undefined (reading 'updateOne')".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 21:03:49 +01:00
svemagie
481603391e fix(mastodon-api): DM response "no data" — return full serialized status
The minimal bare JSON returned for visibility=direct DMs caused clients
(Phanpy, Elk) to show "no data" — they expect a full Mastodon Status
entity. Fix: build a proper ap_timeline document, store it with
visibility=direct (home/public timelines already exclude direct items),
and serialize it via serializeStatus() before returning. Also store the
DM in ap_notifications for the thread view as before.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 20:57:39 +01:00
svemagie
99964e9c3c fix(mastodon-api): DM with visibility=direct created public blog post
Intercept visibility="direct" in POST /api/v1/statuses before the
Micropub pipeline. Resolve the @mention via WebFinger, build a
Create/Note AP activity addressed only to the recipient (no public
addressing), send via ctx.sendActivity(), and store in ap_notifications
for the DM thread view. No blog post is created.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 20:45:12 +01:00
svemagie
0a686d7ab4 fix: support plain-object collections in resolveAuthor (Mastodon Client API like/reblog)
resolveAuthor() called collections.get("ap_timeline") assuming a Map, but
the Mastodon Client API passes collections as a plain object
(req.app.locals.mastodonCollections). This caused "collection.get is not a
function" on every favourite/reblog action from Mastodon clients (Phanpy,
Elk, etc.). Now checks typeof collections.get before deciding which access
pattern to use.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 20:28:12 +01:00
Ricardo
fee1706d38 fix: direct follow workaround for tags.pub identity/v1 context rejection
tags.pub's activitypub-bot (activitystrea.ms parser) rejects any activity
body containing the https://w3id.org/identity/v1 JSON-LD context with
400 Invalid request body. Fedify 2.0 adds this context via LD Signatures
(RsaSignature2017) on all outbound activities.

Workaround: lib/direct-follow.js sends Follow/Undo(Follow) with a minimal
body (no LD Sig, no proof) using draft-cavage HTTP Signatures, scoped only
to tags.pub via DIRECT_FOLLOW_HOSTS set.

Also removes [federation-diag] inbox POST logging (no longer needed).

Upstream: https://github.com/social-web-foundation/tags.pub/issues/10
2026-03-22 20:00:46 +01:00
Ricardo
c71fd691a3 fix: direct follow workaround for tags.pub identity/v1 context rejection
tags.pub's activitypub-bot (activitystrea.ms parser) rejects any activity
body containing the https://w3id.org/identity/v1 JSON-LD context with
400 Invalid request body. Fedify 2.0 adds this context via LD Signatures
(RsaSignature2017) on all outbound activities.

Workaround: lib/direct-follow.js sends Follow/Undo(Follow) with a minimal
body (no LD Sig, no proof) using draft-cavage HTTP Signatures, scoped only
to tags.pub via DIRECT_FOLLOW_HOSTS set.

Also removes [federation-diag] inbox POST logging (no longer needed).

Upstream: https://github.com/social-web-foundation/tags.pub/issues/10
2026-03-22 19:40:12 +01:00
svemagie
0c077c2588 fix: remove duplicate cachedUrl declaration in resolveActorUrl (merge artifact)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 16:33:14 +01:00
svemagie
95564a3e73 fix: remove duplicate getActorUrlFromId import in accounts.js (merge artifact)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 16:30:00 +01:00
svemagie
b3eb579696 fix: remove duplicate remoteActorId import in account-cache.js (merge artifact)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 15:56:14 +01:00
svemagie
6089df0c27 Merge remote-tracking branch 'upstream/main' 2026-03-22 15:51:46 +01:00
Ricardo
4298a6a307 fix: remove RSA Multikey from assertionMethod to fix tags.pub signature verification
The Fedify 2.0 migration added assertionMethods = keyPairs.map(k => k.multikey),
which places the RSA Multikey (id: #main-key) into assertionMethod alongside the
Ed25519 Multikey (id: #key-2).

This creates a keyId collision: the RSA CryptographicKey in publicKey and the RSA
Multikey in assertionMethod both use #main-key. Servers that traverse JSON-LD
properties alphabetically (assertionMethod before publicKey) find the Multikey
first — which lacks publicKeyPem — and return "public key not found".

Fix: filter assertionMethods to only Ed25519 keys (Object Integrity Proofs).
RSA keys already have their correct representation in publicKey (HTTP Signatures).
This matches Mastodon's behavior and is semantically correct per the two key systems.
2026-03-22 15:31:29 +01:00
Ricardo
4495667ed9 fix: remove RSA Multikey from assertionMethod to fix tags.pub signature verification
The Fedify 2.0 migration added assertionMethods = keyPairs.map(k => k.multikey),
which places the RSA Multikey (id: #main-key) into assertionMethod alongside the
Ed25519 Multikey (id: #key-2).

This creates a keyId collision: the RSA CryptographicKey in publicKey and the RSA
Multikey in assertionMethod both use #main-key. Servers that traverse JSON-LD
properties alphabetically (assertionMethod before publicKey) find the Multikey
first — which lacks publicKeyPem — and return "public key not found".

Fix: filter assertionMethods to only Ed25519 keys (Object Integrity Proofs).
RSA keys already have their correct representation in publicKey (HTTP Signatures).
This matches Mastodon's behavior and is semantically correct per the two key systems.
2026-03-22 15:00:14 +01:00
Ricardo
c5733e3551 fix: isTagFollowed false positive for global-only follows; # stripping in getTagsPubActorUrl
- isTagFollowed() now checks doc?.followedAt instead of !!doc, so it
  correctly returns false for global-only follows (document exists but
  no local followedAt)
- getTagsPubActorUrl() strips leading # so URLs like ?tag=%23indieweb
  don't produce invalid https://tags.pub/user/#indieweb actor URLs
- Remove stale "Task 5" plan reference comment in tag timeline template
2026-03-22 11:26:30 +01:00
Ricardo
a84c6f1abd feat: tags.pub global hashtag discovery integration (v3.8.0)
- Add setGlobalFollow/removeGlobalFollow/getFollowedTagsWithState to
  followed-tags storage; unfollowTag now preserves global follow state
- Add followTagGloballyController/unfollowTagGloballyController that
  send AP Follow/Undo via Fedify to tags.pub actor URLs
- Register POST /admin/reader/follow-tag-global and unfollow-tag-global
  routes with plugin reference for Fedify access
- Tag timeline controller passes isGloballyFollowed + error query param
- Tag timeline template adds global follow/unfollow buttons with globe
  indicator and inline error display
- Wire GET /api/v1/followed_tags to return real data with globalFollow state
- Add i18n keys: followGlobally, unfollowGlobally, globallyFollowing,
  globalFollowError
2026-03-22 11:26:30 +01:00
Ricardo
f69776b183 fix: isTagFollowed false positive for global-only follows; # stripping in getTagsPubActorUrl
- isTagFollowed() now checks doc?.followedAt instead of !!doc, so it
  correctly returns false for global-only follows (document exists but
  no local followedAt)
- getTagsPubActorUrl() strips leading # so URLs like ?tag=%23indieweb
  don't produce invalid https://tags.pub/user/#indieweb actor URLs
- Remove stale "Task 5" plan reference comment in tag timeline template
2026-03-22 00:25:29 +01:00
Ricardo
944917b3f0 feat: tags.pub global hashtag discovery integration (v3.8.0)
- Add setGlobalFollow/removeGlobalFollow/getFollowedTagsWithState to
  followed-tags storage; unfollowTag now preserves global follow state
- Add followTagGloballyController/unfollowTagGloballyController that
  send AP Follow/Undo via Fedify to tags.pub actor URLs
- Register POST /admin/reader/follow-tag-global and unfollow-tag-global
  routes with plugin reference for Fedify access
- Tag timeline controller passes isGloballyFollowed + error query param
- Tag timeline template adds global follow/unfollow buttons with globe
  indicator and inline error display
- Wire GET /api/v1/followed_tags to return real data with globalFollow state
- Add i18n keys: followGlobally, unfollowGlobally, globallyFollowing,
  globalFollowError
2026-03-22 00:22:47 +01:00
svemagie
97a902bda1 feat: merge upstream v3.7.1–v3.7.5 into svemagie/main
All five 3.7.x releases published 2026-03-21 in one pass.

Changes from upstream:

- lib/lookup-helpers.js: lookupWithSecurity → async with signed→unsigned
  fallback (handles servers like tags.pub that return 400 on signed GETs)

- lib/mastodon/helpers/account-cache.js: add reverse lookup map
  (hashId → actorUrl) populated by cacheAccountStats(); export
  getActorUrlFromId() for follow/unfollow resolution

- lib/mastodon/helpers/enrich-accounts.js: NEW — enrichAccountStats()
  enriches embedded account objects in serialized statuses with real
  follower/following/post counts; Phanpy never calls /accounts/:id so
  counts were always 0 without this

- lib/mastodon/routes/timelines.js: call enrichAccountStats() after
  serialising home, public, and hashtag timelines

- lib/mastodon/routes/statuses.js: processStatusContent() linkifies bare
  URLs and converts @user@domain mentions to <a> links; extractMentions()
  builds mention list; date lookup now tries both .000Z and bare Z suffixes

- lib/mastodon/routes/stubs.js: /api/v1/domain_blocks now returns real
  blocked-server hostnames from ap_blocked_servers instead of []

- lib/mastodon/routes/accounts.js: /accounts/relationships computes
  domain_blocking using ap_blocked_servers; resolveActorUrl() falls back
  to getActorUrlFromId() cache for timeline-author resolution

- lib/controllers/federation-mgmt.js: fetch blocked servers, blocked
  accounts, and muted accounts in parallel; pass to template

- views/activitypub-federation-mgmt.njk: add Moderation section showing
  blocked servers, blocked accounts, and muted accounts

- package.json: bump version 3.6.8 → 3.7.5

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 20:22:04 +01:00
Ricardo
c30657ef71 feat: surface moderation data in federation admin + Mastodon API
1. Federation admin page (/admin/federation): new Moderation section
   showing blocked servers (with hostnames), blocked accounts, and
   muted accounts/keywords

2. GET /api/v1/domain_blocks: returns actual blocked server hostnames
   from ap_blocked_servers (was stub returning [])

3. Relationship responses: domain_blocking field now checks if the
   account's domain matches a blocked server hostname (was always false)
2026-03-21 20:03:19 +01:00
Ricardo
76e9ba0b35 fix: centralize unsigned fallback in lookupWithSecurity
Some servers (e.g., tags.pub) return 400 for signed GET requests.
Previously only followActor had an unsigned fallback — all other
callers (resolve, unfollowActor, profile viewer, messages, post
detail, OG unfurl) would silently fail.

Fix: moved the fallback logic into lookupWithSecurity itself. When
an authenticated documentLoader is provided and the lookup fails,
it automatically retries without the loader (unsigned GET). This
fixes ALL AP resolution paths in one place — resolve, follow,
unfollow, profile viewing, message sending, quote fetching.

Removed individual fallbacks in followActor and resolve controller
since the central helper now handles it.
2026-03-21 19:16:05 +01:00
Ricardo
94c4546234 feat: linkify URLs and extract @mentions in status creation
Mastodon clients send plain text — the server must convert bare URLs
and @user@domain mentions into HTML links. Previously, URLs appeared
as plain text and mentions were not stored as mention objects.

- Bare URLs (http/https) are wrapped in <a> tags
- @user@domain patterns are converted to profile links with h-card markup
- Mentions are extracted into the mentions[] array with name and URL
- Only processes content that doesn't already contain <a> tags
  (avoids double-linkifying Micropub-rendered content)
2026-03-21 19:01:05 +01:00
Ricardo
ccb9cc99a2 fix: follow/unfollow fails for remotely resolved profiles
POST /accounts/:id/follow returned 404 for actors resolved via Fedify
(like @_followback@tags.pub) because resolveActorUrl only checked local
data (followers/following/timeline). These actors aren't in local
collections — they were resolved on-demand via WebFinger.

Fix: add reverse lookup map (accountId hash → actorUrl) to the account
cache. When resolveRemoteAccount resolves a profile, the hash-to-URL
mapping is stored alongside the stats. resolveActorUrl checks this
cache before scanning local collections.
2026-03-21 17:50:48 +01:00
Ricardo
30eff8e6c7 fix: status lookup fails due to published date format mismatch
findTimelineItemById decoded the cursor (ms-since-epoch) back to an ISO
date via toISOString() which produces "2026-03-21T15:33:50.000Z". But
the stored published dates lack the .000Z milliseconds suffix — they're
"2026-03-21T15:33:50Z". The exact string match failed for every single
status, breaking /statuses/:id, /statuses/:id/context, and all
interaction endpoints (favourite, boost, bookmark, delete).

Fix: try both formats — with .000Z first, then without.
2026-03-21 16:45:58 +01:00
Ricardo
35ed4a333e feat: enrich embedded account stats in timeline responses
Phanpy never calls /accounts/:id for timeline authors — it trusts the
embedded account object in each status. These showed 0 counts because
timeline author data doesn't include follower stats.

Fix: after serializing statuses, batch-resolve unique authors that have
0 counts via Fedify AP collection fetch (5 concurrent). Results are
cached (1h TTL) so subsequent page loads are instant.

Applied to all three timeline endpoints (home, public, hashtag).
2026-03-21 16:05:32 +01:00
svemagie
f029c3128e Merge upstream feat/mastodon-client-api (v3.0.0–v3.6.8) into svemagie/main
Incorporates the full Mastodon Client API compatibility layer from upstream
rmdes/indiekit-endpoint-activitypub@feat/mastodon-client-api into our fork,
which retains our custom additions (likePost, /api/ap-url, async jf2ToAS2Activity,
direct-message support, resolveAuthor, PeerTube view short-circuit, OG images).

Upstream additions:
- lib/mastodon/ — 27-file Mastodon API implementation (entities, helpers,
  middleware, routes, router, backfill-timeline)
- locales/ — 13 additional language files (es, fr, de, hi, id, it, nl, pl, pt,
  pt-BR, sr, sv, zh-Hans-CN)
- index.js — Mastodon router wiring (createMastodonRouter, setLocalIdentity,
  backfillTimeline import)
- package.json — version bump to 3.6.8, add @indiekit/endpoint-micropub peer dep
- federation-setup.js — signatureTimeWindow and allowPrivateAddress now built-in
  (previously applied only via blog repo postinstall patches)

Auto-merged cleanly; no conflicts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 15:25:54 +01:00
Ricardo
3787be4c69 feat: cache remote account stats for embedded status accounts
Phanpy never calls /accounts/:id for timeline authors — it uses the
embedded account object from the status response. These had 0 counts
because the timeline author data doesn't include follower stats.

Fix: in-memory LRU cache (500 entries, 1h TTL) stores account stats
from remote resolutions. serializeAccount() reads from cache when
the actor has 0 counts, enriching embedded accounts with real data.

Cache is populated by resolveRemoteAccount() (lookup, search, and
/accounts/:id calls). Once a profile has been viewed once, all
subsequent status embeds for that author show real counts.
2026-03-21 12:38:27 +01:00
Ricardo
f9b8baec42 fix: route ordering + remote resolution for account profiles
Two bugs causing profile counts to show 0 in Phanpy:

1. Route ordering: /accounts/relationships and /accounts/familiar_followers
   were defined AFTER /accounts/:id. Express matched "relationships" as
   the :id parameter, returning 404. Moved them before the :id catch-all.

2. /accounts/:id only used local data (followers/following/timeline) which
   has no follower counts. Now tries remote actor resolution via Fedify
   to get real counts from AP collection totalItems.
2026-03-21 12:18:38 +01:00
Ricardo
bc72bf1e02 feat: populate remote profile counts, fields, and join date
Extract followers/following/statuses counts from AP collection
totalItems, profile fields from actor attachments, and published
date from the actor document. Previously showed 0/0/0 and today's
date for all remote profiles.
2026-03-21 12:06:49 +01:00