Commit Graph

72 Commits

Author SHA1 Message Date
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
svemagie
6089df0c27 Merge remote-tracking branch 'upstream/main' 2026-03-22 15:51:46 +01:00
Ricardo
cb6c1b5bbc fix: serve AP JSON for actor URLs without explicit text/html Accept header
Fedify's acceptsJsonLd() returns false for Accept: */* or no Accept header
because it only checks for explicit application/activity+json in the list.
Remote servers fetching actor URLs for HTTP Signature verification (e.g.
tags.pub) often omit Accept or use */*, getting HTML back instead of the
actor JSON and causing "public key not found" failures.

Add middleware to upgrade ambiguous Accept headers to application/activity+json
for GET requests to /users/:id paths. Explicit text/html requests (browsers)
are unaffected.

Also fix followActor() storing inbox: "" for actors where Fedify uses
remoteActor.inboxId?.href (not remoteActor.inbox?.id?.href). The inbox URL
is stored correctly now for all actor types.
2026-03-22 15:31:29 +01:00
Ricardo
9a0d6d208e fix: serve AP JSON for actor URLs without explicit text/html Accept header
Fedify's acceptsJsonLd() returns false for Accept: */* or no Accept header
because it only checks for explicit application/activity+json in the list.
Remote servers fetching actor URLs for HTTP Signature verification (e.g.
tags.pub) often omit Accept or use */*, getting HTML back instead of the
actor JSON and causing "public key not found" failures.

Add middleware to upgrade ambiguous Accept headers to application/activity+json
for GET requests to /users/:id paths. Explicit text/html requests (browsers)
are unaffected.

Also fix followActor() storing inbox: "" for actors where Fedify uses
remoteActor.inboxId?.href (not remoteActor.inbox?.id?.href). The inbox URL
is stored correctly now for all actor types.
2026-03-22 13:22:27 +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
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
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
cad9829cd7 fix: fallback to unsigned lookup when authenticated fetch fails in followActor
Some servers (e.g., tags.pub relay) reject or mishandle HTTP-signed GET
requests during actor resolution. The authenticated document loader is
tried first (required by Authorized Fetch servers like hachyderm.io),
then falls back to unsigned fetch if it returns null.

Same pattern should apply to unfollowActor.
2026-03-21 18:06:14 +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
svemagie
ce30dfea3b feat(activitypub): AP protocol compliance — Like id, Like dispatcher, repost commentary, ap-url API
Five improvements to strict ActivityPub protocol compliance and
real-world Mastodon interoperability:

1. allowPrivateAddress: true in createFederation (federation-setup.js)
   Fixes Fedify's SSRF guard rejecting own-site URLs that resolve to
   private IPs on the local LAN (e.g. home-network deployments where
   the blog hostname maps to 10.x.x.x internally).

2. Canonical id on Like activities (jf2-to-as2.js)
   Per AP §6.2.1, activities SHOULD have an id URI so remote servers
   can dereference them. Derives mount path from actor URL and constructs
   {publicationUrl}{mount}/activities/like/{post-path}.

3. Like activity object dispatcher (federation-setup.js)
   Per AP §3.1, objects with an id MUST be dereferenceable at that URI.
   Registers federation.setObjectDispatcher(Like, .../activities/like/{+id})
   so fetching the canonical Like URL returns the activity as AP JSON.
   Adds Like to @fedify/fedify/vocab imports.

4. Repost commentary in AP output (jf2-to-as2.js)
   - jf2ToAS2Activity: only sends Announce for pure reposts (no content);
     reposts with commentary fall through to Create(Note) with content
     formatted as "{commentary}<br><br>🔁 <url>" so followers see the text.
   - jf2ToActivityStreams: prepends commentary to the repost Note content
     for correct display in content-negotiation / search responses.

5. GET /api/ap-url public endpoint (index.js)
   Resolves a blog post URL → its Fedify-served AP object URL for use by
   "Also on Fediverse" widgets. Prevents nginx from intercepting
   authorize_interaction requests that need AP JSON.
   Special case: AP-likes return { apUrl: likeOf } so authorize_interaction
   opens the original remote post rather than the blog's like post.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 09:12:21 +01:00
Ricardo
0cde298b46 fix: detect own posts in Mastodon API status serialization
Own posts in ap_timeline have author.url set to the publication URL
(site root like "https://rmendes.net/") with no /@handle or /users/handle
pattern. extractUsername("/") returns "" which falls back to "unknown".

Fix: set module-level local identity (publicationUrl + handle) at plugin
init via setLocalIdentity(). serializeStatus() compares item.author.url
against the publication URL and passes isLocal:true + handle to
serializeAccount() when they match.

This is zero-cost for callers — no signature changes needed at the 20+
serializeStatus() call sites.
2026-03-20 14:00:44 +01:00
Ricardo
5fc4d3a6f5 fix: add sparse:true to accessToken index (duplicate key on OAuth authorize)
The accessToken_1 unique index on ap_oauth_tokens lacked sparse:true.
During OAuth2 authorization, POST /oauth/authorize inserts a document
with accessToken:null (auth code phase — token not yet issued). MongoDB
unique indexes include null values by default, so only one such document
could exist. Every subsequent authorization attempt failed with E11000
duplicate key error.

Adding sparse:true skips null values in the index, allowing multiple
auth code documents to coexist while still enforcing uniqueness among
actual access tokens. This matches the code index pattern (line 1423)
which already uses sparse:true.

Note: existing deployments must drop the stale index before restart:
  mongosh $MONGODB_URL --eval 'db.ap_oauth_tokens.dropIndex("accessToken_1")'
  mongosh $MONGODB_URL --eval 'db.ap_oauth_tokens.deleteMany({accessToken:null})'

Confab-Link: http://localhost:8080/sessions/0b241cd6-aff2-4fec-853c-2b5a61e61946
2026-03-20 13:29:25 +01:00
svemagie
842fc5af2a feat(like): send Like activity for AP objects, bookmark for regular URLs
When the `like-of` URL serves ActivityPub content (detected via content
negotiation with Accept: application/activity+json), deliver a proper
`Like { actor, object, to: Public }` activity to followers.

For likes of regular (non-AP) URLs, fall through to the existing
bookmark-style `Create(Note)` behaviour (🔖 content with #bookmark tag).

- Add `isApUrl()` async helper (3 s timeout, fails silently)
- Make `jf2ToAS2Activity` async; add Like detection before repost block
- Update all four call sites in federation-setup.js and index.js

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 08:50:00 +01:00
svemagie
b99f5fb73e Merge upstream rmdes:main — v2.13.0–v2.15.4 into svemagie/main
New upstream features:
- v2.13.0: FEP-8fcf/fe34 compliance, custom emoji, manual follow approval
- v2.14.0: Server blocking, Redis caching, key refresh, async inbox queue
- v2.15.0: Outbox failure handling (strike system), reply chain forwarding
- v2.15.1: Reply intelligence in reader (visibility badges, thread reconstruction)
- v2.15.2: Strip invalid as:Endpoints type from actor serialization
- v2.15.3: Exclude soft-deleted posts from outbox/content negotiation
- v2.15.4: Wire content-warning property for CW text

Conflict resolution:
- federation-setup.js: merged our draft/unlisted/visibility filters with
  upstream's soft-delete filter
- compose.js: kept our DM compose path, adopted upstream's
  lookupWithSecurity for remote object resolution
- notifications.js: kept our separate reply/mention tabs, added upstream's
  follow_request grouping
- inbox-listeners.js: took upstream's thin-shim rewrite (handlers moved to
  inbox-handlers.js which already has DM detection)
- notification-card.njk: merged DM badge with follow_request support

Preserved from our fork:
- Like/Announce to:Public cc:followers addressing
- Nested tag normalization (cat.split("/").at(-1))
- DM compose/reply path in compose controller

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 00:42:31 +01:00
Ricardo
2c0cfffd54 feat: add Mastodon Client API layer for Phanpy/Elk compatibility
Implement the Mastodon Client REST API (/api/v1/*, /api/v2/*) and OAuth2
server within the ActivityPub plugin, enabling Mastodon-compatible clients
to connect to the Fedify-based server.

Core features:
- OAuth2 with PKCE (S256) — app registration, authorization, token exchange
- Instance info + nodeinfo for client discovery
- Account lookup, verification, relationships, follow/unfollow/mute/block
- Home/public/hashtag timelines with cursor-based pagination
- Status viewing, creation, deletion, thread context
- Favourite, boost, bookmark interactions with AP federation
- Notifications with type filtering and pagination
- Search across accounts, statuses, and hashtags
- Markers for read position tracking
- Bookmarks and favourites collection lists
- 25+ stub endpoints preventing client errors on unimplemented features

Architecture:
- 24 new files under lib/mastodon/ (entities, helpers, middleware, routes)
- Virtual endpoint at "/" via Indiekit.addEndpoint() for domain-root access
- CORS + JSON error handling for browser-based clients
- Six-layer mute/block filtering reusing existing moderation infrastructure

BREAKING CHANGE: bumps to v3.0.0 — adds new MongoDB collections
(ap_oauth_apps, ap_oauth_tokens, ap_markers) and new route registrations

Confab-Link: http://localhost:8080/sessions/5360e3f5-b3cc-4bf3-8c31-5448e2b23947
2026-03-18 12:50:52 +01:00
Ricardo
26c81a6a76 fix: exclude soft-deleted posts from outbox and content negotiation
Deleted posts (with properties.deleted timestamp) were still served
via the outbox dispatcher and content negotiation catch-all. Now:
- Outbox find() and countDocuments() filter out deleted posts
- Object dispatcher returns null for deleted posts (Fedify 404)
- Content negotiation falls through to Express for deleted posts

Confab-Link: http://localhost:8080/sessions/af5f8b45-6b8d-442d-8f25-78c326190709
2026-03-17 17:12:30 +01:00
Ricardo
1567b7c4e5 feat: operational resilience hardening — server blocking, caching, key refresh, async inbox (v2.14.0)
- Server-level blocking: O(1) Redis SISMEMBER check in all inbox listeners,
  admin UI for blocking/unblocking servers by hostname, MongoDB fallback
- Redis caching for collection dispatchers: 300s TTL on followers/following/liked
  counters and paginated pages, one-shot followers recipients cache
- Proactive key refresh: daily cron re-fetches actor documents for followers
  with 7+ day stale keys using lookupWithSecurity()
- Async inbox processing: MongoDB-backed queue with 3s polling, retry (3 attempts),
  24h TTL auto-prune. Follow keeps synchronous Accept, Block keeps synchronous
  follower removal. All other activity types fully deferred to background processor.

Inspired by wafrn's battle-tested multi-user AP implementation.

Confab-Link: http://localhost:8080/sessions/af5f8b45-6b8d-442d-8f25-78c326190709
2026-03-17 09:16:05 +01:00
Ricardo
9a61145d97 feat: FEP-8fcf/fe34 compliance, custom emoji, manual follow approval (v2.13.0)
- FEP-8fcf: add syncCollection to Undo(Announce) sendActivity
- FEP-fe34: centralized lookupWithSecurity() helper with crossOrigin: "ignore" on all 23 lookupObject call sites
- Custom emoji: replaceCustomEmoji() renders :shortcode: as inline <img> in content and actor display names
- Manual follow approval: profile toggle, ap_pending_follows collection, approve/reject controllers with federation, pending tab on followers page, follow_request notification type
- Coverage audit updated to v2.12.x (overall ~70% → ~82%)

Confab-Link: http://localhost:8080/sessions/1f1e729b-0087-499e-a991-f36f46211fe4
2026-03-17 08:21:36 +01:00
svemagie
d0cb9a76aa Merge upstream rmdes:main — v2.11.0, v2.12.0, v2.12.1 into svemagie/main (v2.12.2)
Integrates upstream features (visibility/CW compose controls, @mention
support, federation management page, layout fix) while preserving
svemagie DM support. Visibility and syndication controls are hidden
for direct messages.
2026-03-15 19:25:54 +01:00
Ricardo
19aa83ab8d feat: federation management page with collection stats, post actions, object lookup (v2.12.0)
Confab-Link: http://localhost:8080/sessions/c2335791-4b8c-44a6-b1b7-8d0fa8d7f647
2026-03-15 16:32:14 +01:00
Ricardo
6238e7d4e5 feat: visibility/CW compose controls, @mention support (v2.11.0)
Add visibility and content warning controls to the reply compose form.
Add @user@domain mention parsing, WebFinger resolution, Mention tags,
inbox delivery, and content linkification for outbound posts.

Confab-Link: http://localhost:8080/sessions/cc343b15-8d10-43cd-a48f-ca912eb79b83
2026-03-14 21:28:24 +01:00
Sven Giersig
eefa46f0c1 Merge upstream rmdes:main — v2.10.0 (Delete, visibility, CW, polls, Flag) into svemagie/main (v2.10.1)
Upstream v2.10.0 adds: outbound Delete, visibility addressing (unlisted/
followers-only), Content Warning (sensitive flag + summary), inbound poll
rendering, Flag/report handler, DM support files.

Conflict resolution — all four conflicts were additive (no code removed):

  lib/controllers/reader.js: union of validTabs — fork added "mention",
    upstream added "dm" and "report"; result keeps all five additions.

  lib/storage/notifications.js: union of count keys — fork added mention:0,
    upstream added dm:0 and report:0; result keeps the fork's mention split
    logic alongside the new upstream keys.

  views/partials/ap-notification-card.njk: fork kept isDirect 🔒 badge for
    direct mentions; upstream added ✉ for dm and ⚑ for report; result keeps
    the isDirect branch and appends the two new type badges.

  package.json: upstream bumped to 2.10.0; we bump to 2.10.1 to reflect our
    own Alpine.js and publication-aware docloader bug fixes on top.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 13:00:58 +01:00
Ricardo
1dc42ad5e5 feat: outbound Delete, visibility addressing, CW/sensitive, polls, Flag reports (v2.10.0)
- Outbound Delete: broadcastDelete() + POST /admin/federation/delete route
- Visibility: unlisted + followers-only addressing via defaultVisibility config
- Content Warning: outbound sensitive flag + summary as CW text
- Polls: inbound Question/poll parsing with progress bar rendering
- Flag: inbound report handler with ap_reports collection + Reports tab
- Includes DM support files from v2.9.x (messages controller, storage, templates)
- Includes coverage audit and high-impact gaps implementation plan

Confab-Link: http://localhost:8080/sessions/cc343b15-8d10-43cd-a48f-ca912eb79b83
2026-03-14 08:51:44 +01:00
svemagie
39d45ec04e feat: integrate docloader loglevel, unlisted guards, alias-clear, likePost/boostPost
federation-setup.js:
- Suppress fedify docloader logs below fatal level to reduce noise from
  deleted remote actors (404/410)
- Add visibility:unlisted guard to outbox dispatcher, counter, and
  resolvePost object dispatcher

controllers/migrate.js:
- Allow clearing alsoKnownAs by detecting submitted empty aliasUrl field
  via hasOwnProperty check (previously only set when non-empty)

index.js:
- Add resolveAuthor import
- Skip federation for unlisted posts in syndicate()
- Add likePost(postUrl, collections) — sends AP Like activity to author
- Add boostPost(postUrl, collections) — sends AP Announce to followers
  and directly to the post author's inbox

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 08:08:23 +01:00
Ricardo
7611dba40f feat: remove quick reply, streamline blog reply (v2.7.0)
Remove the quick-reply code path entirely — all replies now go through
Micropub as blog posts. Quick replies created orphan URLs that served
raw JSON-LD to browsers and caused unreadable links in conversations.

- Delete quick-reply controller (note-object.js) and route
- Remove ap_notes collection registration
- Simplify compose form: no mode toggle, no character counter
- Remove quick-reply CSS and locale strings

Confab-Link: http://localhost:8080/sessions/d116ad5b-ef8a-424e-9ebe-76c06bef1df6
2026-03-04 17:33:02 +01:00
Ricardo
9fa3412875 fix: chain dropIndex before createIndex on ap_muted to prevent race condition
The non-async init() fired dropIndex and createIndex concurrently,
causing MongoDB to abort the index build (IndexBuildAborted error 276).
Chain createIndex via .then() so it runs after the drop completes.

Confab-Link: http://localhost:8080/sessions/e9d666ac-3c90-4298-9e92-9ac9d142bc06
2026-03-02 11:40:43 +01:00
Ricardo
508ac75363 feat: new posts banner, mark-as-read on scroll, unread filter
- Poll every 30s for new items, show sticky "N new posts — Load" banner
- IntersectionObserver marks cards as read at 50% visibility, batches to
  server every 5s
- Read cards fade to 70% opacity, full opacity on hover
- "Unread" toggle in tab bar filters to unread-only items
- New API: GET /api/timeline/count-new, POST /api/timeline/mark-read

Confab-Link: http://localhost:8080/sessions/e9d666ac-3c90-4298-9e92-9ac9d142bc06
2026-03-02 10:54:11 +01:00
Ricardo
abf1b94bd6 feat: migrate Fedify KV store and plugin cache from MongoDB to Redis
Replace unbounded ap_kv MongoDB collection (169K docs, 49MB) with Redis:
- Fedify KV store uses @fedify/redis RedisKvStore (native TTL support)
- Plugin cache (fedidb, batch-refollow state, migration flags) uses new
  redis-cache.js utility with indiekit: key prefix
- All controllers updated to remove kvCollection parameter passing
- Addresses OOM kills caused by ap_kv growing ~14K entries/day
2026-03-01 16:26:17 +01:00
Ricardo
55baa7cef5 feat: replace explore deck layout with full-width tabbed design
Replace the cramped deck/column layout on the explore page with a
tabbed interface. Three tab types: Search (always first), Instance
(pinned with local/federated badge), and Hashtag (aggregated across
all pinned instances).

- New ap_explore_tabs collection replaces ap_decks (clean start)
- Tab CRUD API: add, remove, reorder with CSRF/SSRF validation
- Per-tab infinite scroll with IntersectionObserver + AbortController
- Hashtag tabs query up to 10 instances in parallel, merge by date,
  deduplicate by URL
- WAI-ARIA tabs pattern with arrow key navigation
- LRU cache (5 tabs) for tab content
- Extract shared explore-utils.js (validators + status mapping)
- Remove all old deck code (JS, CSS, controllers, locale strings)
2026-02-28 16:30:48 +01:00
Ricardo
145e329d2f feat: add TweetDeck-style deck layout for explore view
Users can favorite instances (with local or federated scope) as persistent
columns in a multi-column deck view. Each column streams its own public
timeline with independent infinite scroll. Includes two-tab explore UI
(Search + Decks), deck CRUD API with CSRF/SSRF protection, 8-deck limit,
responsive CSS Grid layout, and scope badges.
2026-02-27 11:24:53 +01:00
Ricardo
525abcbf84 feat: expose signed avatar resolver for cross-plugin use
Register resolveActorAvatar() on Indiekit.config.application during
init(). Uses Fedify's authenticated document loader to fetch actor
profiles from servers with Authorized Fetch enabled (e.g., hachyderm.io,
indieweb.social). Called by the conversations plugin's avatar backfill.
2026-02-27 11:01:56 +01:00
Ricardo
cee0050be8 feat: add FediDB-powered autocomplete for explore and reader lookup
- Add FediDB API client (lib/fedidb.js) with MongoDB caching (24h TTL)
  for instance search, timeline support checks, and popular accounts
- Explore page: instance input now shows autocomplete suggestions from
  FediDB with software type, MAU count, and timeline support indicator
  (checkmark/cross) via background pre-check
- Reader page: @handle lookup input now shows popular fediverse accounts
  from FediDB with avatar, name, handle, and follower count
- Three new API endpoints: /api/instances, /api/instance-check,
  /api/popular-accounts
- Alpine.js components for both autocomplete UIs with keyboard navigation
2026-02-27 09:26:45 +01:00
Ricardo
a4f72a588d feat: enhance ActivityPub reader with mentions, hashtags, infinite scroll, explore, and tag following
- Fix mentions/hashtags bug: separate Fedify Mention and Hashtag types into
  distinct mentions[] and category[] arrays with proper @ and # rendering
- Add hashtag timeline filtering at /admin/reader/tag with regex-safe queries
- Replace prev/next pagination with AlpineJS infinite scroll (IntersectionObserver)
  with no-JS fallback pagination preserved
- Add public instance timeline explorer at /admin/reader/explore with SSRF
  prevention and XSS sanitization via Mastodon-compatible API
- Add hashtag following with ap_followed_tags collection, inbox listener
  integration for non-followed accounts, and followed tags sidebar display
- Include one-time migration script for legacy timeline data
2026-02-26 18:15:21 +01:00
Ricardo
17b0f582d1 feat: add remote follow / authorize interaction support
Add OStatus subscribe template to WebFinger responses so remote servers
(WordPress AP, Misskey, etc.) can discover and redirect users to complete
follow interactions. Unauthenticated users are sent to login first, then
redirected to the existing reader profile page with follow/unfollow UI.
2026-02-24 15:00:40 +01:00
Ricardo
046c776195 fix: guard index creation against undefined collections on startup
MongoDB collections may not be available yet when init() runs if the
database connection hasn't completed. Wrap all createIndex calls in
try-catch so the plugin doesn't crash on startup. Indexes already exist
from previous runs; this is non-fatal.
2026-02-24 10:52:11 +01:00
Ricardo
a95d68c98f fix: remove await in non-async init() causing SyntaxError on startup
dropIndex() was called with await inside the non-async init() method,
causing "Unexpected reserved word" and preventing Indiekit from starting.
Use promise .catch() instead since the result isn't needed.
2026-02-23 23:31:11 +01:00
Ricardo
23fc8f4614 feat: rewrite moderation UI with filter mode, fix sparse index bug
Moderation page rewritten as single Alpine.js component with inline DOM
updates instead of location.reload(). Added hide/warn filter mode toggle
— warn mode shows muted items behind content warning instead of hiding.

Expanded keyword matching to check content, titles, and summaries.
Fixed MongoDB E11000 duplicate key error by dropping non-sparse indexes
on startup and recreating with sparse:true. Storage layer no longer
stores null url/keyword fields.
2026-02-23 23:11:28 +01:00
Ricardo
9805cb9eff fix: my-profile replies tab queries posts collection instead of ap_activities
The replies tab was empty because it queried ap_activities for outbound
Create activities with a non-null targetUrl, but targetUrl was always null
(remote actor resolution often fails). Now queries posts collection for
post-type "reply" which reliably has in-reply-to URLs.

Also fixes activity log to store in-reply-to URL as targetUrl instead of
the resolved actor URL.
2026-02-23 16:29:32 +01:00
Ricardo
743cb6b85b feat: notification tabs, my-profile page, clickable timestamps, quick-reply
- Notification view: tab navigation (Replies, Likes, Boosts, Follows, All)
  with count badges; defaults to Replies tab; type filter in storage layer
  with compound index for efficient queries
- My Profile admin page: profile header with avatar/stats/bio, tabbed
  activity view (Posts, Replies, Likes, Boosts) pulling from posts,
  ap_activities, and ap_interactions collections
- Reader: default tab changed from All to Notes
- Timeline cards: timestamps now link to post detail view
- Notification cards: Reply and View Thread buttons on reply/mention types
2026-02-23 15:55:44 +01:00
Ricardo
e5c0fa1191 fix: store and serve quick reply Notes for remote dereferencing
Remote servers (Mastodon, Bonfire) dereference Note IDs to verify
Create activities. Quick reply Notes had no public route — servers
got 302 to login and rejected the activity.

- Store quick reply Note data in ap_notes collection
- Add public GET /quick-replies/:id serving JSON-LD
- Use shared resolveAuthor() in compose.js for quick replies
2026-02-22 21:49:04 +01:00
Ricardo
dd9bba711f feat: migrate to Fedify 2.0 with debug dashboard and modular imports
- Upgrade @fedify/fedify, @fedify/redis to ^2.0.0
- Add @fedify/debugger ^2.0.0 for live federation traffic dashboard
- Move all vocab type imports to @fedify/fedify/vocab (13 files)
- Move crypto imports (exportJwk, importJwk, generateCryptoKeyPair) to @fedify/fedify/sig
- Replace removed importSpki() with local Web Crypto API helper
- Add KvStore.list() async generator required by Fedify 2.0
- Add setOutboxPermanentFailureHandler for delivery failure logging
- Add debugDashboard/debugPassword config options
- Skip manual LogTape configure when debugger auto-configures it
- Fix Express-Fedify bridge to reconstruct body from req.body when
  Express body parser has already consumed the stream (fixes debug
  dashboard login TypeError)
- Add response.bodyUsed safety check in sendFedifyResponse
- Remove @fedify/express dependency (custom bridge handles sub-path mounting)
2026-02-22 14:28:31 +01:00
Ricardo
5c5e53bf3d feat: public profile page for actor URL
Replace the browser redirect on /activitypub/users/:handle with a
standalone HTML profile page showing avatar, bio, profile fields,
stats (posts/following/followers/joined), follow-me prompt with
copy button, pinned posts, and recent posts. Supports light/dark
mode via prefers-color-scheme. ActivityPub clients still get JSON-LD
from Fedify before this route is reached.
2026-02-22 12:36:07 +01:00
Ricardo
7587d99013 feat: batch broadcast delivery and redirect browsers on actor URL
broadcastActorUpdate() now fetches followers from MongoDB, deduplicates
by shared inbox, and delivers in batches of 25 with 5s delays to prevent
thundering herd (hundreds of 499s from simultaneous re-fetches).

Browser GET on /users/:handle now redirects to homepage instead of 404.
2026-02-22 12:13:58 +01:00
Ricardo
c648606525 fix: broadcastActorUpdate was silently failing due to Fedify API mismatch
ctx.getActor() only exists on RequestContext (inside HTTP handlers), not
on the base Context returned by createContext(). Extracted actor-building
logic into shared buildPersonActor() helper used by both the dispatcher
and broadcastActorUpdate(). Profile link attachments now propagate to
remote instances via Update(Person) activity.
2026-02-22 11:27:35 +01:00
Ricardo
0fa446ceb2 feat: make Fedify log level configurable via logLevel option
Default changed from "info" to "warning" so production logs are quiet.
Set logLevel to "info" or "debug" in config to troubleshoot federation.
2026-02-21 22:51:07 +01:00
Ricardo
cf284e8633 feat: add fediverse URL/handle lookup input to reader
Adds a search box at the top of the reader page where users can paste
any fediverse URL or @user@domain handle. Uses Fedify's lookupObject()
which natively resolves URLs, handles, and acct: URIs, then redirects
to the internal post detail or remote profile view.
2026-02-21 21:33:08 +01:00
Ricardo
d20dea2dc8 feat: notification management — clear, mark read, dismiss, TTL retention
- Add "Mark all read" and "Clear all" toolbar buttons on notifications page
- Add per-notification dismiss (×) button
- Remove auto-mark-all-as-read on page load (explicit action only)
- Add 30-day TTL index on createdAt for automatic notification cleanup
- New config option: notificationRetentionDays (default 30)
2026-02-21 20:00:05 +01:00
Ricardo
5ff3197493 feat: add internal AP link resolution and OpenGraph card unfurling (v1.1.14)
Reader now resolves ActivityPub links internally instead of navigating
to external instances. Actor links open the profile view, post links
open a new post detail view with thread context (parent chain + replies).

External links in post content get rich preview cards (title, description,
image, favicon) fetched via unfurl.js at ingest time with fire-and-forget
async processing and concurrency limiting.

New files: post-detail controller, og-unfurl module, lookup-cache,
link preview template/CSS, client-side link interception JS.
Includes SSRF protection for OG fetching and GoToSocial URL support.
2026-02-21 18:32:12 +01:00
Ricardo
313d5d414c fix: reader UI fixes and correct Fedify API usage (v1.1.8→1.1.12)
- Fix Unknown authors by adding multi-strategy fallback chain in
  extractObjectData (getAttributedTo → actorFallback → attributionIds)
- Fix empty boosts from Lemmy/PieFed by checking content before storing
- Fix @mention/hashtag styling to stay inline instead of breaking layout
- Fix compose reply to show sanitized HTML blockquote instead of raw text
- Add default-checked syndication targets for AP and Bluesky
- Use authenticated document loader for all lookupObject calls
  (fixes 401 errors on servers requiring Authorized Fetch)
- Fix like handler 404 by using canonical AP uid for interactions
  instead of display URLs; add data-item-uid to card template
- Fix profile bio showing Nunjucks macro source code by renaming
  summary→bio to avoid collision with Indiekit's summary macro
- Fix Fedify API misuse in timeline-store.js: use instanceof Article
  (not string comparison), replyTargetId (not inReplyTo), getTags()
  and getAttachments() async methods (not sync property access)
- Fix inbox-listeners.js: use replyTargetId instead of non-existent
  getInReplyTo(), use instanceof Article for Update handler
- Add error logging to interaction catch blocks
2026-02-21 17:08:28 +01:00
Ricardo
3ad86ffb39 fix: reader UI — navigation, Alpine.js loading, avatar fallback, Temporal dates
- Return multiple navigation items (ActivityPub, Reader, Notifications, Moderation)
  so all AP sub-pages are accessible from the sidebar
- Fix Alpine.js not loading: `{% block head %}` was silently discarded because
  the parent template chain has no such block — moved script/css into content block
- Pin Alpine.js to exact version 3.14.9 to prevent CDN resolution issues
- Add fallback avatar (first letter) when author photo is missing
- Guard empty author URLs to prevent broken links
- Fix Temporal.Instant TypeError: use String() instead of new Date() for
  Fedify published timestamps in inbox-listeners and timeline-store
- Link author names to remote profile view instead of raw AP URLs
- Bump to 1.1.3
2026-02-21 13:31:52 +01:00