Commit Graph

66 Commits

Author SHA1 Message Date
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
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
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
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
2ca491f28b fix: wire content-warning property for CW text
Updated jf2-to-as2 and compose controller to use the renamed
"content-warning" property instead of overloading "summary" for
CW text. This pairs with the endpoint-posts fix that renamed the
CW form input to prevent collision with the summary field.

Confab-Link: http://localhost:8080/sessions/1dcdf030-8015-4d23-89da-b43fd69c7138
2026-03-18 00:24:35 +01:00
Ricardo
c8aa0383b9 feat: wire reply intelligence to frontend — timeline filtering, thread reconstruction, visibility badges
- Filter isContext items and private/direct posts from main timeline, new post count, and unread count
- Post detail: query local replies from ap_timeline before remote fetch, deduplicate, sort chronologically
- Add visibility badge (unlisted/private/direct) on item cards next to timestamp

Confab-Link: http://localhost:8080/sessions/af5f8b45-6b8d-442d-8f25-78c326190709
2026-03-17 13:13:51 +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
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
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
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
b9fc98f40c feat: content enhancements — URL shortening, hashtag collapse, bot badge, edit indicator (Release 7)
Shorten long URLs in post content (30 char display limit with tooltip).
Collapse hashtag-heavy paragraphs into expandable <details> toggle.
Show BOT badge for Service/Application actors. Show pencil icon for
edited posts with hover tooltip showing edit timestamp.

Confab-Link: http://localhost:8080/sessions/e9d666ac-3c90-4298-9e92-9ac9d142bc06
2026-03-03 16:40:01 +01:00
Ricardo
2d2dcaec7d feat: interaction counts on timeline cards (Release 5)
Extract reply/boost/like counts from AP Collections (getReplies,
getLikes, getShares) and Mastodon API (replies_count, reblogs_count,
favourites_count). Display counts next to interaction buttons with
optimistic updates on like/boost actions.

Confab-Link: http://localhost:8080/sessions/e9d666ac-3c90-4298-9e92-9ac9d142bc06
2026-03-03 14:30:40 +01:00
Ricardo
c243b70629 feat: enriched media model with ALT badges (Release 3+4)
Change photo storage from bare URL strings to objects with url, alt,
width, height (AP) plus blurhash and focus (Mastodon API). Templates
handle both old string and new object format for backward compat.

Add ALT text badges on gallery images — click to expand the full
alt text in an overlay. Renders in both reader and explore views.

Also pass alt text through to lightbox and quote embed photos.

Bump version to 2.5.3.

Confab-Link: http://localhost:8080/sessions/e9d666ac-3c90-4298-9e92-9ac9d142bc06
2026-03-03 13:46:58 +01:00
Ricardo
02d449d03c feat: render custom emoji in reader (Release 1)
Extract custom emoji from ActivityPub objects (Fedify Emoji tags) and
Mastodon API (status.emojis, account.emojis). Replace :shortcode:
patterns with <img> tags in the unified processing pipeline.

Emoji rendering applies to post content, author display names, boost
attribution, and quote embed authors. Uses the shared postProcessItems()
pipeline so both reader and explore views get emoji automatically.

Bump version to 2.5.1.

Confab-Link: http://localhost:8080/sessions/e9d666ac-3c90-4298-9e92-9ac9d142bc06
2026-03-03 13:13:28 +01:00
Ricardo
af2f899073 refactor: unify reader and explore processing pipeline (Release 0)
Extract shared item-processing.js module with postProcessItems(),
applyModerationFilters(), buildInteractionMap(), applyTabFilter(),
renderItemCards(), and loadModerationData(). All controllers (reader,
api-timeline, explore, hashtag-explore, tag-timeline) now flow through
the same pipeline.

Unify Alpine.js infinite scroll into single parameterized
apInfiniteScroll component configured via data attributes, replacing
the separate apExploreScroll component.

Also adds fetchAndStoreQuote() for quote enrichment and on-demand
quote fetching in post-detail controller.

Bump version to 2.5.0.

Confab-Link: http://localhost:8080/sessions/e9d666ac-3c90-4298-9e92-9ac9d142bc06
2026-03-03 12:48:40 +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
120f2ee00e feat: render quoted posts as embedded cards in reader
Extract quoteUrl from Fedify Note objects (supports Mastodon, Misskey,
Fedibird quote formats). Fetch quoted post data asynchronously on inbox
receive and on-demand in post detail view. Render as rich embed card
with author avatar, handle, content, and timestamp.

Confab-Link: http://localhost:8080/sessions/e9d666ac-3c90-4298-9e92-9ac9d142bc06
2026-03-02 10:33: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
5c2fd09f8f fix: normalize category to array in my-profile controller
When a post has a single category, Indiekit stores it as a string
(e.g. "Fraude") rather than an array. Nunjucks iterates strings
character by character, producing hashtag pills like #F #r #a #u #d #e.
2026-02-28 18:21:32 +01:00
Ricardo
ecba2b5748 fix: pass CSRF token to API-rendered item cards
The explore and hashtag API controllers rendered ap-item-card.njk with
csrfToken: "" causing Like/Boost/Save buttons in tab panels to fail
with 403 Invalid CSRF token. Now generates a proper token from the
session via getToken().
2026-02-28 17:31:54 +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
cfc5af8092 fix: rename tag template variable to hashtag to avoid Nunjucks macro collision
The base layout default.njk imports a `tag` component macro which shadows
the controller's `tag` variable in function/filter argument contexts.
Renaming to `hashtag` eliminates the collision entirely.
2026-02-27 14:05:44 +01:00
Ricardo
25513c7ea5 feat: add breadcrumb navigation across all ActivityPub UI pages
Document.njk pages (followers, following, activities, featured, tags,
profile, migrate) get parent breadcrumbs via the upstream heading
component. Reader pages (explore, notifications, compose, moderation,
tag timeline, post detail, remote profile, my profile) get a new
breadcrumb nav bar in ap-reader.njk layout.
2026-02-27 12:10:31 +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
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
4052eebc9d fix: re-fetch media from Fedify when stored timeline item has none
Post-detail view now re-fetches from the origin server when the stored
timeline item has empty photo/video/audio arrays (from before the async
iteration fix). On successful extraction, updates MongoDB so future
views don't need to re-fetch.
2026-02-24 10:58:13 +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
bd07edefbb fix: robust author resolution for like/boost with URL pattern fallback
When lookupObject fails (Authorized Fetch, network issues) and the post
isn't in ap_timeline, likes returned 404 "Could not resolve post author".

Adds shared resolveAuthor() with 3 strategies:
1. lookupObject on post URL → getAttributedTo
2. Timeline + notifications DB lookup
3. Extract author from URL pattern (/users/NAME/, /@NAME/)

Refactors like, unlike, boost controllers to use the shared helper.
2026-02-22 21:33:45 +01:00
Ricardo
77aad65947 fix: include reply author in cc and log delivery failures
Quick replies only sent to followers, never directly to the
replied-to author's server. The author was also missing from
the Note's cc field, so Mastodon couldn't thread or notify.

Now resolves the author before constructing the Note, includes
them in ccs, sends directly to their inbox, and logs failures
instead of silently swallowing them.
2026-02-22 21:21:55 +01:00
Ricardo
4a553da94e fix: use HTTPS activity IDs and add addressing for boost/like
Boost (Announce) was missing to/cc addressing so Mastodon silently
discarded it. Both boost and like used urn:uuid: IDs which are not
dereferenceable. Changed to HTTPS URLs and added Public/followers
addressing on Announce.
2026-02-22 20:51:18 +01:00
Ricardo
eab440bceb fix: use Fedify 2.0 replyTarget for reply threading
Fedify 2.0 renamed the Note/Article constructor parameter from
inReplyTo to replyTarget. The old name was silently ignored,
causing replies to appear as standalone posts on the fediverse.
2026-02-22 20:28:40 +01:00
Ricardo
cffe094222 fix(compose): use HTTPS Note ID and add to/cc on Create activity
Mastodon silently discards activities with urn:uuid: IDs since they
can't be dereferenced. Use an HTTPS URL under our domain instead.

Also add to/cc (Public + followers) on the Create wrapper activity,
not just the Note object — Mastodon requires addressing on both.
2026-02-22 20:13:38 +01:00
Ricardo
e0a606c8c2 fix(compose): add to/cc addressing to quick reply Notes
Remote servers (Mastodon, etc.) require explicit audience addressing
to display a post. Without to/cc, the Note was silently discarded.

- to: as:Public (visible to everyone)
- cc: followers collection
2026-02-22 19:31:45 +01:00
Ricardo
b19a33df2f fix(compose): add diagnostic logging for syndication debugging
Log syndicateTo values, micropubBody, and micropubUrl before the
Micropub request to trace why compose replies aren't syndicating
to Mastodon/Bluesky.
2026-02-22 19:18:11 +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
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
0cf49e037c fix: remove duplicate page headings across all AP templates
document.njk already renders title as h1 via the heading macro.
All 14 AP templates were also calling heading() with level 1 inside
their content block, producing two h1 elements per page. Removed
the redundant calls and moved dynamic count prefixes into the title
variable in followers/following controllers.
2026-02-21 21:32:56 +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
348a183e46 fix: profile links lost on save due to qs body parser key mismatch
express.urlencoded({ extended: true }) uses qs which strips [] from
field names, so link_name[] arrives as request.body.link_name — not
request.body["link_name[]"]. The old lookup always got undefined,
producing an empty attachments array that overwrote existing links.
2026-02-21 15:04:36 +01:00