Commit Graph
131 Commits
Author SHA1 Message Date
Jannis Braun ceb0c9812d fix(federation): resolve code review issues in upload size mismatch
- Fix critical: outbox worker now copies file_rejected payload fields
  (attachmentId, sourceFilename, rejectionReason, rejectionLimit,
  affectedUserIds) so the reverse relay actually delivers them
- Fix: add sourceFilename to file_rejected event for reliable
  multi-attachment matching on the sender side
- Fix: change text-accent-warning to text-accent-amber (valid class)
- Add Array.isArray guard on federationMeta parse
2026-03-27 04:51:30 +01:00
Jannis Braun bfb358c530 feat(federation): include federation status columns in attachment queries
Both buildDmMessageWithUser and buildMessageWithUser manually map
attachment fields — add federationStatus and federationMeta so they
reach the frontend.
2026-03-27 04:40:48 +01:00
Jannis Braun b99cbea268 feat(federation): process file_rejected relay events on sender instance 2026-03-27 04:39:48 +01:00
Jannis Braun 0d6bb9dbd3 fix(federation): auto-create and hydrate user profiles during DM relay
Federated 1-on-1 DMs showed the raw snowflake ID as the display name
and no avatar when the remote user had no pre-existing local record.
processCreateEvent used resolveLocalUser (find-only) instead of
resolveOrCreateReplicatedUser, and relay events carried no profile data
for participants.

- Add profile snapshot (displayName, avatar, avatarColor) to
  FederationRelayParticipant and populate it in getDmParticipants
- Change processCreateEvent to auto-create replicated user stubs and
  hydrate them with profile data from the relay event
- Fix hydrateReplicatedUserProfile URL resolution for homeInstance
  values without protocol prefix
- Fix WelcomeHeader: return null while DM data is loading (eliminates
  "unknown" flash on reload), use displayName for @mention text
2026-03-27 03:43:45 +01:00
Jannis Braun becd5c8ac8 fix: exempt existing DM members from friend check in createGroup
When converting a 1-on-1 DM to a group, the existing DM partner was
incorrectly required to be your friend. DMs don't require friendship,
so this check was over-strict. Added fromDmChannelId parameter to
createGroup — existing members of the source DM are exempt from the
friendship validation.
2026-03-27 02:48:26 +01:00
Jannis Braun 843a2ce727 fix(federation): store absolute URLs for replicated user avatars
Bare filenames stored on replicated user stubs can't be resolved by
the home WS (normalizeUserAssets only runs for remote origins).
Now resolves avatar/banner to absolute URLs pointing to the user's
home instance so they render correctly without page refresh.
Also overwrites stale bare filenames from the prior deploy.
2026-03-27 01:36:23 +01:00
Jannis Braun 699c5a4365 fix(federation): include profile snapshots in friend relay events
Replicated user stubs created by resolveOrCreateReplicatedUser had
null avatar/displayName, causing blank profiles in the UI until
page refresh. Friend relay events now carry profile snapshots
(displayName, avatar, avatarColor, banner, bio) so the receiving
instance can hydrate stubs with real data.
2026-03-27 01:26:46 +01:00
Jannis Braun 97f83b8d1e chore: remove unused imports from federation.ts 2026-03-27 01:07:09 +01:00
Jannis Braun 376a17847b feat(federation): relay friend requests and friendships to peer instances 2026-03-27 00:57:37 +01:00
Jannis Braun 8918c43597 feat(federation): update sync endpoint for friend event context type 2026-03-27 00:55:07 +01:00
Jannis Braun a485bf3098 feat(federation): add friend event processors for relay receiving side 2026-03-27 00:51:35 +01:00
Jannis Braun 4150e32b84 feat(federation): resolve federated identities in POST /api/dm/group
Rewrite the group DM creation endpoint to accept identity objects
(GroupDmUserIdentity) instead of raw user ID strings. Each identity
is resolved to a local database user via resolveOrCreateReplicatedUser
for federated users or direct ID lookup with resolveLocalUser fallback
for local users. Dedup and caller-exclusion checks now operate on
resolved local IDs rather than input IDs.
2026-03-26 23:16:53 +01:00
Jannis Braun 4b181b925d feat(federation): export resolveLocalUser and resolveOrCreateReplicatedUser
Make both identity resolution helpers module-level exports so the group DM
endpoint can import and use them when resolving federated user identities
during group DM creation.
2026-03-26 23:13:17 +01:00
Jannis Braun 4422d97ba7 fix(dm): deduplicate callerUser fetch and align federation guard pattern
- Hoist callerUser DB query above the federation block so it's fetched
  once and reused for response building, federation ID assignment, and
  relay payload construction (was fetched 3 times).
- Add homeInstance !== domainOrigin guard to finalTargets augmentation,
  matching the existing pattern in POST /api/dm/:id/members.
2026-03-26 22:30:07 +01:00
Jannis Braun 342eb8cca3 feat(dm): add POST /api/dm/group endpoint for creating group DMs
Adds a dedicated endpoint for creating group DMs with 3-10 members.
Validates friendship, deduplication, and member caps. Includes federation
relay support for remote instance members.
2026-03-26 22:24:05 +01:00
Jannis Braun bec4c2446f fix(dm): restore ownerId=NULL semantics for 1-on-1 DM channels
- Add migrateFixOneOnOneOwnerIds migration to NULL-out ownerId on all
  existing 1-on-1 DMs (those with exactly 2 members)
- Fix POST /api/dm to create 1-on-1 channels with ownerId=null instead
  of the creator's ID
- Guard POST /api/dm/:id/members: reject with 400 if channel has no
  owner (i.e. is a 1-on-1), directing callers to POST /api/dm/group
- Guard DELETE /api/dm/:id/members: replace member-count check with
  ownerId check; remove now-duplicate dmChannel query in that handler
- Add CreateGroupDmRequest type to shared types
2026-03-26 22:16:55 +01:00
Jannis Braun d8563ec505 fix(federation): validate source authority on membership mutation receivers 2026-03-26 20:48:39 +01:00
Jannis Braun 31729f360e fix(federation): create replicated users during group DM bootstrap for cross-instance members 2026-03-26 20:46:53 +01:00
Jannis Braun 4efa35f311 feat(federation): add soft-delete GC for empty group DMs with 24h grace period
Replace the hard-delete in the leave handler with a soft-delete (sets
deleted_at timestamp) when the last member leaves a group DM. A new
janitor sweep in the federation worker runs hourly and purges channels
whose grace period has expired, cascading through reactions, embeds,
attachments, messages, members, outbox/mutation-log/file-queue entries,
and finally the channel itself.

All client-facing dm_channels queries now filter on deleted_at IS NULL
to hide soft-deleted channels from the REST API and WebSocket ready
payload.
2026-03-26 20:35:55 +01:00
Jannis Braun 62a16e884a feat(federation): extend catch-up sync to include group DMs and membership mutations 2026-03-26 20:28:19 +01:00
Jannis Braun f7ba391678 feat(federation): handle group DM messages via federatedId lookup in relay processor
In processCreateEvent, branch on event.federatedId: group DM messages now
look up the pre-bootstrapped local channel by federatedId instead of
computing a pair hash from two participants. In queueDmRelay, fetch the
channel's federatedId and ownerId and include federatedId in the outgoing
relay payload for group DMs so receiving instances can route correctly.
2026-03-26 20:26:23 +01:00
Jannis Braun 242004e8ee feat(federation): add membership mutation receiver processors
Add three new event processors to the relay endpoint for handling
inbound group DM membership changes from peer instances:

- processMemberAddEvent: handles member additions with channel bootstrap
  (creates the local channel from group metadata on first contact)
- processMemberRemoveEvent: handles member removals with read state
  cleanup and soft-delete GC when no local members remain
- processOwnershipTransferEvent: updates local channel owner tracking

All three are wired into the relay endpoint's event dispatch switch.
2026-03-26 20:23:40 +01:00
Jannis Braun 6a83b6c6a5 fix(federation): compute relay targets before member deletion in leave handler
getGroupDmTargetOrigins() was called after the leaving user's dm_members row
was deleted, causing their instance to be excluded from member_remove and
ownership_transfer relay fan-out if they were the only member from that peer.
Move the target origins and leaving user lookups to before the deletion.
2026-03-26 20:19:55 +01:00
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