Commit Graph
208 Commits
Author SHA1 Message Date
Jannis Braun f072568b8a feat(federation): relay membership mutations from dm.ts to federation outbox
Hook POST /api/dm/:id/members and DELETE /api/dm/:id/members to the
federation outbox so membership changes propagate to peer instances:

- Assign federatedId when a group DM first gains remote members
- Relay member_add events with group metadata and participant-aware routing
- Relay member_remove (leave) events to peer instances
- Relay ownership_transfer when the departing user was the group owner,
  updating the federated owner columns accordingly
2026-03-26 20:14:52 +01:00
Jannis Braun 0742c37ca1 refactor(federation): rename canonicalDmPairId to computeFederatedId 2026-03-26 20:08:37 +01:00
Jannis Braun 742d851aae fix(federation): branching message lookup in reaction relay processors
Uses messageHomeInstance to determine whether the target message
originated locally (lookup by ID) or was replicated from another
instance (lookup by sourceInstance + sourceMessageId). Supports
N-instance federation and falls back to relay-sender lookup for
backward compatibility with in-flight outbox entries.
2026-03-26 18:49:28 +01:00
Jannis Braun 10d4ca0110 chore: remove unused broadcastDmMessage import from federation.ts 2026-03-26 17:58:24 +01:00
Jannis Braun 6bf2a2621c fix(federation): skip broadcasting relay messages to source-instance users
When Instance 2 receives a relay from Instance 1, it no longer
broadcasts dm_message_created to members whose home instance is Instance
1. Those users already have the original message via their home WS
connection. This prevents duplicate messages from appearing in the
sender's chat.
2026-03-26 17:57:07 +01:00
Jannis Braun 62f1d637ac feat(federation): surface sourceMessageId/sourceInstance in DM message payloads
Both REST and WebSocket DM message payloads now include the relay
identity fields. This enables client-side deduplication of relay copies.
2026-03-26 17:54:24 +01:00
Jannis Braun a21243256e fix(federation): fix reaction relay using wrong ID for message lookup
Reaction outbox events used reactionId (add) or a composite dedup key
(remove) as the event messageId. The receiver looked up dm_messages by
sourceMessageId = event.messageId, which never matched because it was
searching for a reactionId, not the actual message ID.

Fix: include the actual DM messageId in the reaction payload JSON.
The receiver now uses event.reaction.messageId for the lookup, with
fallback to event.messageId for backward compatibility.
2026-03-26 14:27:48 +01:00
Jannis Braun 4ed9b30ce9 fix(federation): don't copy source thumbnail filename to relay attachments
The source instance's thumbnail file doesn't exist locally, so copying
its thumbnailFilename causes a 404 in the chat view. Set to null so
AttachmentRenderer falls back to the full image URL.
2026-03-26 06:37:26 +01:00
Jannis Braun 50f7a398dc fix(federation): create attachment rows immediately in processCreateEvent
Insert attachment rows with sourceUrl as interim filename so the initial
WebSocket broadcast includes working remote URLs. AttachmentRenderer
already handles http:// filenames as direct URLs. The file worker will
UPDATE the filename to the local path after download completes.

This fixes the invisible bubble bug where attachment-only messages
(content: null, attachments: []) appeared as empty messages.
2026-03-26 06:21:01 +01:00
Jannis Braun b49be65dcd refactor(federation): replace outbox hooks in dm.ts with queueDmRelay() 2026-03-26 06:15:13 +01:00
Jannis Braun cf9fcb78ed fix(federation): add participants array to relay events and fix recipient resolution
The relay was failing because processCreateEvent relied on the friends
table to discover the DM recipient, but friendships aren't federated
across instances. Also, resolveLocalUser matched deleted replicated
users before active ones.

- Add participants[] to FederationRelayEvent with homeUserId/homeInstance
  for all DM channel members
- Add getDmParticipants() helper to look up member identities
- Include participants in outbox payloads (create/update) and sync events
- Rewrite processCreateEvent to resolve participants directly, compute
  canonicalDmPairId, and findOrCreateDmChannel — removing the entire
  friends-list fallback (60+ lines)
- Fix resolveLocalUser to filter out deleted users (is_deleted = 0)
  and prefer the replicated user match when multiple candidates exist
2026-03-26 05:36:41 +01:00
Jannis Braun cb9a70d7a6 fix(federation): use canonical_pair_id for sync scope instead of home_instance matching
The old query missed channels where both members appear as 'local'
(e.g., native Nova + native youruser on orbit). Using
canonical_pair_id is simpler and correct — any channel with a pair
ID is a federated DM that should be synced.
2026-03-26 04:41:00 +01:00
Jannis Braun 49f88c2552 fix(federation): actually remove source_instance IS NULL filter from sync SQL queries 2026-03-26 04:37:47 +01:00
Jannis Braun 277ff0e50c fix(federation): sync ALL locally-created messages, not just local-home-user messages
Messages by replicated users (e.g., Jannis browsing orbit) have
source_instance IS NULL because they were created on that instance.
The sync endpoint was incorrectly filtering these out, causing most
DM history to be missing from the sync response.
2026-03-26 04:34:13 +01:00
Jannis Braun 44de9b6f41 feat(federation): add client peering integration, transparency indicator, and admin federation settings
Task 13: Hook server-to-server peering into connectToRemote (non-fatal)
and add federation API namespace to the client (initiate, peers, revoke).

Task 14: Show a transparency notice in the DM welcome header when the
other user is on a remote instance, informing that messages are stored
on both home instances and are not end-to-end encrypted.

Task 15: Add Federation section to the instance settings General panel
with DM relay toggle, TTL config, and a live peer list with revoke
buttons. Also extends InstanceAdminSettings type and the server settings
route to support federationRelayEnabled / federationRelayTtlDays.
2026-03-25 21:47:10 +01:00
Jannis Braun 7f20a4517c feat(federation): implement checkpoint sync endpoint using mutation log
Add POST /api/federation/sync for peer instances to catch up on missed
DM mutations after downtime. Uses HMAC-SHA256 authentication (same as
relay), determines shared channels between peers via raw SQL join on
dm_members/users, queries the federation_mutation_log with timestamp
pagination, and returns FederationRelayEvent payloads for each mutation
type (create, update, delete, reaction_add, reaction_remove).
2026-03-25 21:33:47 +01:00
Jannis Braun 5b9272e22b feat(federation): implement relay receive endpoint with dedup, channel mapping, SSRF validation 2026-03-25 21:27:28 +01:00
Jannis Braun fbdec82172 feat(federation): add peering handshake and admin peer management endpoints
Add federation routes module with four endpoints:
- POST /api/federation/peer/initiate — admin-only, starts peering handshake
  with a remote instance (validates origin, generates HMAC secret, sends
  server-to-server accept request with 10s timeout)
- POST /api/federation/peer/accept — server-to-server first contact,
  rate-limited to 10 req/min per IP, creates/activates peer record
- GET /api/federation/peers — admin-only, lists all peers (hmacSecret excluded)
- DELETE /api/federation/peers/:id — admin-only, revokes peer and cleans outbox
2026-03-25 21:19:47 +01:00
Jannis Braun 32a0c2e618 feat(federation): hook outbox and mutation log into DM message and reaction handlers
Wire appendMutationLog + queueOutboxEvent + buildRelayPayload into all
DM mutation paths so federation peers receive relay events:

- REST: POST /api/dm/:id/messages, PATCH /api/dm/messages/:id,
  DELETE /api/dm/messages/:id
- WebSocket: dm_message_create, dm_message_edit, dm_message_delete,
  reaction_add (DM path), reaction_remove (DM path)
- Fix buildRelayPayload parameter types to accept optional replyToId
  and editedAt (matching DmMessageWithUser's optional fields)
2026-03-25 21:14:34 +01:00
Jannis Braun b4582a53f1 feat: wire allowCustomBitrate through rowToLimits and PATCH handler in settings route 2026-03-25 04:05:48 +01:00
Jannis Braun 7240aac2e0 fix(social): allow single-char search and prefer native federated profiles
Lower minimum query length from 2 to 1 character so single-letter
searches return results.

Fix dedup to prefer native profiles (homeUserId=null) over replicated
ones. Previously the first-seen result won, which was usually the
local replicated profile (no instance badge, namespaced username).
Now when a native profile is found on the remote instance, it replaces
the replicated copy — showing the clean username with the instance badge.
2026-03-25 02:26:33 +01:00
Jannis Braun 58f6fb1baa fix: raise upload rate limit from 10 to 30 per minute 2026-03-23 02:55:00 +01:00
Jannis Braun fdc8732ba7 fix: raise upload size cap from 500 MB to 5 GB
The 500 MB cap was arbitrary. Self-hosted admins should decide based
on their own disk space. Global multipart ceiling raised to 5 GB,
validation cap to 5120 MB.
2026-03-23 02:36:22 +01:00
Jannis Braun 4807bfeeb1 feat: read/write maxUploadSizeMb in instance settings API 2026-03-23 02:22:22 +01:00
Jannis Braun 5be2e73c8a fix: use per-request upload limit from DB and fix silent truncation
Reads max_upload_size_bytes from instance_settings per-request and
passes it to request.file() so Fastify kills the stream at the
admin's configured limit. Checks file.truncated to properly reject
files that exceed the limit instead of saving corrupted data.
2026-03-23 02:22:15 +01:00
Jannis Braun ee11408060 feat: add age-based media cleanup endpoint
Adds cleanupOldMedia() to the storage janitor and a new endpoint
POST /api/admin/storage/cleanup-media { maxAgeDays, dryRun }.
Deletes chat attachments older than the specified threshold while
preserving profile images.
2026-03-23 02:20:28 +01:00
Jannis Braun 1bd684a6b0 feat: self-healing login for federated users with stale password hashes
When a federated user's local password hash is stale (e.g. they changed
their password on the home instance and sync failed), the login handler
now falls back to verifying credentials against the home instance. If the
home instance accepts the password, the local hash is silently updated
without touching passwordChangedAt, so existing valid JWTs remain valid.
2026-03-23 00:48:32 +01:00
Jannis Braun 41caf5f90c fix: skip currentPassword check for federated users on change-password 2026-03-23 00:48:02 +01:00
Jannis Braun fd4c73c3cb feat: add filter and sort params to admin users endpoint
Add homeInstance, role, joinedAfter, joinedBefore, and sort query
params to GET /api/admin/users. Add GET /api/admin/users/instances
endpoint returning distinct home instance domains.
2026-03-22 04:22:22 +01:00
Jannis Braun 19cdfe58a4 feat: add bitrateMatrixOverrides validation, raise bitrate caps to 1 Gbps
- Import BITRATE_MATRIX_KBPS from shared constants
- Parse and return bitrateMatrixOverrides in rowToLimits
- Widen updateData type to accept null values
- Raise maxBitrateKbps and minBitrateKbps caps from 50 000 to 1 000 000 kbps
- Add full validation for bitrateMatrixOverrides (null to clear, key/value checks)
2026-03-22 00:44:24 +01:00
Jannis Braun b9f11489fe feat: update server validation for expanded resolutions and framerates
Replace hardcoded VALID_RESOLUTIONS/VALID_FRAMERATES with shared STANDARD_RESOLUTIONS/STANDARD_FRAMERATES constants. Update rowToLimits to handle the 'native' resolution value in DB parsing, and update all PATCH validation blocks to accept the expanded resolution/framerate sets and the 'native' option.
2026-03-21 23:23:25 +01:00
Jannis Braun ec71dd2c50 feat: widen InstanceStreamingLimits.allowedResolutions to support 'native' 2026-03-21 23:22:46 +01:00
Jannis Braun 05fe1df5f0 feat: enforce case-insensitive unique role names per space 2026-03-21 23:20:40 +01:00
Jannis Braun 7fb87de09a feat: default new roles to @everyone permissions instead of null 2026-03-21 23:17:16 +01:00
Jannis Braun 637e083e9a feat: add category override API endpoints, fix channel categoryId reassignment, include isPrivate on categories
- Add GET/PUT/DELETE /api/categories/:id/overrides endpoints with
  privilege escalation guard matching existing channel override pattern
- Add isCategoryPrivate() and broadcastCategoryOverrideChange() helpers
  that cascade visibility changes to all channels in a category
- Fix PATCH /api/channels/:id to recompute permissions via
  broadcastOverrideChange when categoryId changes (different category
  overrides may apply)
- Include isPrivate flag on categories in GET /api/spaces/:id payload
  using batch-fetched @everyone overrides
- Include isPrivate on categories in broadcastChannelLayout and
  PATCH /api/categories/:id broadcasts
2026-03-21 18:29:25 +01:00
Jannis Braun 561c9bc4f3 feat: include width, height, duration in all attachment serialization 2026-03-21 17:20:04 +01:00
Jannis Braun 7c767b580a feat: extract media dimensions and video thumbnails on upload 2026-03-21 17:18:59 +01:00
Jannis Braun 9403ded8da feat: add showActivity toggle to PATCH @me with activity broadcast clear
- Accept showActivity boolean in PATCH /api/users/@me body
- Validate and persist as integer in DB
- Update ConnectionManager cache on toggle
- Broadcast empty activities via presence_update when toggled off
2026-03-21 01:49:41 +01:00
Jannis Braun bc408235c9 feat: add Activity type system, DB migration, and self-only showActivity in sanitizeUser
- Add Activity, ActivityType, ActivityTimestamps, ActivityAssets types to shared types
- Add activity_update client event and activities field on presence_update server event
- Add userActivities to ready payload and showActivity to User/UpdateUserRequest
- Create shared activities.ts with ACTIVITY_LIMITS, ACTIVITY_PRIORITY, getPrimaryActivity
- Add show_activity column to users table (schema + migration)
- Update sanitizeUser with isSelf parameter; only include showActivity for self
- Fix .map(sanitizeUser) calls to use arrow wrapper to prevent index-as-boolean bug
- Mark auth routes (register/login) as isSelf=true since they return own user data
2026-03-21 01:38:36 +01:00
Jannis Braun c0133397e3 fix: resolve 4 media embed bugs from testing
1. Video aspect ratio: remove container border/overflow-hidden, use
   preload="metadata" so browser knows dimensions before play
2. YouTube Error 153: remove sandbox attr (too restrictive), add full
   allow permissions (encrypted-media, accelerometer, gyroscope, etc.)
3. Google Images not displaying: detect image Content-Type from HTTP
   response in metadataFetcher, override classifier to create image
   embed for URLs that serve image/* content
4. Audio seeking broken: add HTTP Range request support in uploads
   route (Accept-Ranges, Content-Range, 206 Partial Content)
2026-03-21 00:19:04 +01:00
Jannis Braun ff2decded5 fix: address code review issues for embeds implementation
- Fix ?? to || in metadataFetcher.ts to handle empty strings from Cheerio
- Fix stale embeds on message edit: delete old embeds synchronously before
  broadcast, then resolve new ones async (all 4 edit paths: REST+WS, msg+DM)
- Revert unrelated MessageList.tsx scroll threshold change (5000 not 150)
- Remove duplicate embed indexes from migrateAddIndexes (kept standalone ones)
2026-03-20 23:58:06 +01:00
Jannis Braun 01ee7ab67a feat: wire embeds into all message query and creation paths
Integrate embed infrastructure into the complete message flow:
- messages.ts: batch-fetch embeds in GET, resolve on POST, re-resolve on PATCH
- dm.ts: same pattern for DM messages with isDm=true
- search.ts: include embeds in all 4 search/around endpoints
- events.ts: embed resolution in WS message create/edit for both space and DM
- Fix embedClassifier.ts type errors (regex match undefined → null)
- Add embeds: [] to all inline MessageWithUser/DmMessageWithUser constructions
- Add embeds: [] to chatStore optimistic message
2026-03-20 23:41:53 +01:00
Jannis Braun de1c0b4ed8 refactor: extract metadata fetcher into reusable utility 2026-03-20 23:30:01 +01:00
Jannis Braun cbef0fe0f8 fix: resolve federated avatar double-path URL that broke cross-instance profile pictures
profileSync stored avatar/banner paths with /api/uploads/ prefix on remote
instances, causing resolveAssetUrl to produce double-path URLs like
https://remote/api/uploads//api/uploads/file.jpg that 404'd. Store bare
filenames instead, strip prefix defensively in resolveAssetUrl and server-side
for existing data self-healing.
2026-03-17 00:36:45 +01:00
Jannis Braun e9c43f21f1 fix: eliminate phantom notifications across the entire read-state pipeline
Root cause: own messages echoed by the server marked channels unread when
the user had already navigated away. Seven related bugs compounded the
problem — stale read states, missing cleanup on space/DM removal, REST
broadcast ignoring VIEW_CHANNEL, and no validation on channel_ack writes.

Frontend:
- Skip markChannelUnread for the user's own messages (federation-aware)
- Walk backward past temp_ IDs in ackChannel instead of bailing
- Re-fire ack timer when temp message is replaced by server-confirmed ID
- Add removeChannelStates to clean up unread/read/message caches
- Clean up chatStore on removeSpace, removeDmChannel, removeInstanceSpaces

Server:
- Use sendToChannel instead of sendToSpace for REST message creation
- Clean up read_states on space deletion, member kick/leave, and ban
- Validate channel membership before accepting channel_ack writes
- Clean up read_states on DM leave and DM channel deletion
2026-03-16 20:20:25 +01:00
Jannis Braun 2e6fa3cdc6 feat: optimize profile image sizes, silent PWA updates, storage cleanup fixes
- Resize avatars/icons to 256px and banners to 1280px (client crop + server safety net)
- Add server-side resizeProfileImage() for federation/API uploads without crop modal
- Fix unconstrained crop on RegisterPage and CreateSpace (was missing maxOutputDimension)
- PWA: switch to autoUpdate with skipWaiting/clientsClaim for seamless deploys
- Storage janitor: exclude profile images from unlinked cleanup, delete stale thumbnails
- Add deleteAttachmentByFilename() to clean orphaned attachment records for profile images
- Migration: one-time cleanup of stale profile image attachment records
- GeneralPanel: wrap in <form> to prevent implicit submission
2026-03-15 19:16:48 +01:00
Jannis Braun 4d230711fc feat: launch readiness — PWA, API hardening, memory leak fixes, sticker removal
- Add PWA infrastructure: vite-plugin-pwa, manifest, service worker,
  SW update prompt component, placeholder icons, Apple meta tags
- Harden API client: 401 auto-logout, AbortController timeouts
  (30s standard, 120s uploads), onUnauthorized callback
- Fix memory leaks: clear voice user status on leave, clean up all
  Maps (channelToSpaceMap, permissions, etc.) on removeSpace
- Upgrade error boundary to Aether Drift design with Try Again button,
  collapsible stack trace, and componentDidCatch logging
- Configure desktop icon paths in electron-builder.yml
- Remove sticker feature (server routes, schema, types, UI components)
- Fix Docker build: use **/node_modules in .dockerignore to prevent
  COPY from clobbering pnpm-installed workspace dependencies
- Add vite-env.d.ts declarations for noise suppressor wasm imports
- Exclude test files from tsc build via tsconfig
2026-03-15 15:41:22 +01:00
Jannis Braun 3de6e4a668 feat: GIF search (Klipy), stickers, emoji picker, and bug fixes
- Add GIF search powered by Klipy API with correct response mapping
  (file.sm/hd tiers, not flat files structure)
- Add sticker system: packs, upload with auto-downscale, send in messages
- Add tabbed InputPopover with emoji, GIF, and sticker pickers
- Fix GIF API key migration race condition (column-add loop vs rename)
- Fix masked API key corruption on settings save (server + client guards)
- Fix sticker packs 403 (reversed isMember parameter order)
- Fix emoji picker not filling popover width (perLine 8→9, CSS 100%)
- Add error logging for Klipy API failures
2026-03-15 02:04:37 +01:00
Jannis Braun 3a266e07ed fix: allow bare filenames in avatar/banner validation, fix password min length
isValidAssetUrl() was rejecting bare filenames (e.g. "1234567890.webp") which
is the established convention the frontend sends. Now accepts bare filenames
while still blocking path traversal and unsafe schemes.

Also updates client-side password validation to match server's 8-char minimum.
2026-03-15 00:11:10 +01:00
Jannis Braun 7c544c1ff4 feat: security hardening, DB indexes, token revocation, and input validation
- SSRF protection: DNS resolution + private IP blocking on metadata fetcher
- Upload security: CSP/X-Frame-Options headers, SVG forced download, nosniff
- Auth hardening: JWT secret min length, password min 8 chars, token revocation via password_changed_at
- Attachment ownership verification before linking to messages
- Message length limit (4000 chars) enforced on client and server
- Asset URL validation on avatar/banner updates
- Federation instance validation (domain regex, origin scheme, length limits)
- DB indexes on all FK columns for query performance
- Migrations: nullable moderator columns, dm_messages reply_to FK constraint
- File cleanup on avatar/banner replacement and space deletion
- Fastify trustProxy, AbortController on fetches, typing map size cap
2026-03-15 00:06:15 +01:00