Commit Graph
1305 Commits
Author SHA1 Message Date
Jannis Braun 70a421864c feat: DM unreachable member indicators and admin auto-accept peering toggle 2026-04-09 13:58:45 +02:00
Jannis Braun 4285e44d2d feat: client ensurePeered API, connection flow swap, WS event handlers for peer rejection 2026-04-09 13:56:18 +02:00
Jannis Braun 5f700c1a51 feat: expose autoAcceptPeering in admin settings and add rejectedPeerOrigins to ready payload 2026-04-09 13:53:26 +02:00
Jannis Braun 83771b863c feat: outbox worker resolves pending peers and pushes rejection events 2026-04-09 13:51:37 +02:00
Jannis Braun d5a6d470f9 feat: queueOutboxEvent creates pending peer placeholders, skips rejected/revoked 2026-04-09 13:49:04 +02:00
Jannis Braun 63a7f0c922 fix: use proper HTTP status codes on peer/ensure (400 for validation, 429 for rate limit) 2026-04-09 13:47:30 +02:00
Jannis Braun 051646763a feat: add autoAcceptPeering gate on peer/accept and POST /api/federation/peer/ensure endpoint 2026-04-09 13:46:15 +02:00
Jannis Braun f802528688 feat: add ensurePeered() core function with race deduplication 2026-04-09 13:40:49 +02:00
Jannis Braun abd12223b1 feat: add federation_peer_rejected/active WS types and autoAcceptPeering setting 2026-04-09 13:38:41 +02:00
Jannis Braun b5dffba7ef feat: add autoAcceptPeering instance setting with drizzle migration
Adds `auto_accept_peering` integer column (default 1/true) to the
`instance_settings` singleton table, controlling whether this instance
auto-accepts incoming peering requests. Includes generated migration
`0001_clear_earthquake.sql` applied automatically on server boot.
2026-04-09 13:37:49 +02:00
Jannis Braun ce0b2d0e15 feat: allow any group DM member to add friends, not just owner
Remove the owner-only gate on POST /api/dm/:id/members. The S2S relay
already accepts member_add from any HMAC-verified peer, and the UI
already shows the add button to all group DM members. Only the
server-side check was blocking non-owners.
2026-04-09 02:15:44 +02:00
Jannis Braun 016ca2c59b fix: use canonical identity for federated friend/request dedup
The socialStore WS-driven handlers (addFriendFromAccepted,
addIncomingRequest, removeFriendLocally, removeRequestById,
updateFriendPresence) used instance-local id:origin composite keys
for deduplication. When the client is connected to multiple instances,
both fire WS events for the same federated user with different local
IDs, bypassing the dedup and creating duplicate entries.

Switch all handlers to use homeUserId??id (canonical identity),
matching the pattern loadFriends/loadRequests already use. Also
replace the loadRequests() re-fetch in updateFriendRequest with
optimistic canonical removal to avoid racing S2S relay propagation.
2026-04-09 01:23:49 +02:00
Jannis Braun 3789ece0ca chore: add db:generate script for drizzle-kit migrations 2026-04-09 00:37:10 +02:00
Jannis Braun e467a878d0 chore: remove completed backfill calls (thumbnails, media dimensions)
Both backfills are flagged complete on all instances.
The upload pipeline generates thumbnails at upload time.
2026-04-09 00:33:15 +02:00
Jannis Braun fec0502a2f refactor: replace createTables() with Drizzle migrate()
Delete ~270 lines of hand-written CREATE TABLE SQL.
Schema is now managed entirely by drizzle-kit migrations.
initDatabase() flow: baseline check -> migrate() -> ensureDefaults().
2026-04-09 00:32:01 +02:00
Jannis Braun 3acaea2e45 refactor: replace 2100-line migration system with ensureDefaults + baseline
Remove all manual migrations — Drizzle-kit now manages schema DDL.
Data-fix migrations have all completed on both instances.
Startup initialization (settings row, worker ID, first admin)
moves to idempotent ensureDefaults().
2026-04-09 00:30:00 +02:00
Jannis Braun 450d96407f feat: add drizzle-kit config and generate initial migration 2026-04-09 00:26:53 +02:00
Jannis Braun be2c16efa4 fix: use bare column names in partial index WHERE clauses
SQLite rejects table-qualified column names in partial index
WHERE clauses. Use literal column names instead of Drizzle
column interpolation.
2026-04-09 00:25:36 +02:00
Jannis Braun ac03151b61 feat: add database index definitions to Drizzle schema 2026-04-09 00:19:10 +02:00
Jannis Braun 213d05a810 feat: fix cross-instance friends list for federated users
Friends fan-out (loadFriends/loadRequests) now waits for all remote
connections to establish before querying, fixing the empty friends list
when logged into a remote instance as a federated user.

- Add _autoConnectDone wait guard to loadFriends, loadRequests, and
  loadFederatedMutuals (same pattern as discoverStore)
- Add concurrency guards to prevent thundering herd from multiple
  ready events firing simultaneous fan-outs
- Fix deduplication to use canonical identity (homeUserId ?? id)
  instead of id:origin, preventing duplicate entries for the same
  user across instances
- Auto-connect to home instance when logged in as a federated user,
  with registry entry so it appears in Connections UI
- Allow re-adding error/disconnected instances in probeInstance
2026-04-08 18:41:13 +02:00
Jannis Braun d58464fe95 docs: document profile image file replication in federation spec 2026-04-08 16:59:55 +02:00
Jannis Braun 0db5e4e453 feat: download profile images locally in processProfileUpdateEvent
When a profile_update relay arrives with avatar/banner URLs, download
the files to local storage instead of storing remote absolute URLs.
Falls back to absolute URL on any download failure. Cleans up old
local files when replaced.
2026-04-08 16:57:06 +02:00
Jannis Braun fd2254c9c4 feat: add downloadProfileAsset helper for profile image replication 2026-04-08 16:54:07 +02:00
Jannis Braun adc2a1cb01 docs: update voice.md with client-side federated call implementation details 2026-04-08 15:49:20 +02:00
Jannis Braun a1a7c2ff18 fix: null roomRef before destroyRoom to suppress teardown sounds
During room.disconnect(), LiveKit fires ParticipantDisconnected for
each remote participant BEFORE the final Disconnected event. Because
roomRef was still set, guardedUpdate() called updateParticipants(),
which updated the voiceStore while isLiveKitConnected was still true.
SoundController played user_leave for each departing participant
alongside the disconnect sound.

Fix: set roomRef.current = null before calling destroyRoom(). This
causes guardedUpdate() to return early for all teardown events.
The disconnect function handles cleanup after destroyRoom resolves.
2026-04-08 15:39:21 +02:00
Jannis Braun b03df992dc fix: SoundController isSelf must check BOTH local ID and homeUserId
The identity flip during disconnect: updateParticipants resolves
homeUserId → localSnowflake when activeDmCall is set, but reverts
to raw homeUserId when activeDmCall is cleared (before LiveKit
disconnect completes). SoundController sees the snowflake "leave"
and the homeUserId "join" — two phantom events for the same person.

Previous fix only checked homeUserId OR id. Now checks BOTH via
isSelf(id) which matches against a Set of {id, homeUserId}. This
recognizes the user as "self" regardless of which identity format
the participant currently has.
2026-04-08 15:33:51 +02:00
Jannis Braun 5ab1f7b89d fix: SoundController identity mismatch for federated users
Root cause: SoundController compared LiveKit participant p.userId
(which is homeUserId from the home instance) against currentUser.id
(local snowflake on the current instance). For federated users these
are different IDs, so the controller thought the user's own presence
was a stranger — playing user_join/user_leave for self.

Fix: use homeUserId || id for the self-check. This matches the
LiveKit identity format used in federated calls.
2026-04-08 15:25:15 +02:00
Jannis Braun 1468b68199 fix: skip participant leave sounds during self-disconnect
Added justDisconnected guard to the participant sound loop. When
the user hangs up, isLiveKitConnected transitions to false — but
in a separate or same subscription tick, the participants list
also empties. Without the guard, SoundController plays user_leave
for every departed participant AND the disconnect sound simultaneously.

Now: if justDisconnected is true, the entire participant loop is
skipped. Only the disconnect sound plays.
2026-04-08 15:05:49 +02:00
Jannis Braun 22aa1e3f66 fix: batch participants + isLiveKitConnected on disconnect to prevent double sound
RoomEvent.Disconnected handler set participants=[] and
isLiveKitConnected=false in separate setState calls. SoundController
subscription fired between them — saw empty participants while still
"connected" → played user_leave, then saw disconnected → played
disconnect. Both sounds played simultaneously.

Batching into one setState ensures SoundController sees the final
state atomically: participants gone AND disconnected in one update.
2026-04-08 14:59:08 +02:00
Jannis Braun 5b1c847e57 fix: exclude acting user from federated call end/reject broadcast
sendToFederatedCallUsers sent dm_call_ended/rejected back to the user
who initiated the action. They already disconnected in their click
handler — the redundant event triggered disconnectFn() again, causing
connect and disconnect sounds to play simultaneously.

Added excludeUserId parameter to sendToFederatedCallUsers, used in
handleDmCallEnd and handleDmCallReject Path 2.
2026-04-08 14:49:29 +02:00
Jannis Braun 9e7e068df7 chore: remove federated call diagnostic logging 2026-04-08 14:44:27 +02:00
Jannis Braun 782f5d280d debug: log federated call relay params 2026-04-08 14:38:05 +02:00
Jannis Braun 27514e1596 fix: use wss:// LiveKit URL in federated call relay, not https://
sendFederatedCallStart was sending `https://${domain}/livekit` as the
LiveKit URL. The LiveKit SDK requires `wss://` for WebSocket connections.
The caller (local) worked because it gets the URL from config.livekit.url
(wss://). The federated acceptor failed because it used the relay URL
(https://) — the SDK can't connect over HTTPS.

Now uses config.livekit.url directly, falling back to wss:// if unset.
2026-04-08 14:30:48 +02:00
Jannis Braun c037803c5a fix: acceptor sets activeDmCall in click handler, not server response
Bug A: handleAccept relied on dm_call_accepted server response to set
activeDmCall. But connectFn's async AudioContext resume yields to the
event loop, dm_call_accepted arrives during the yield, finds
isLiveKitConnected=false (connectFn just reset it), and skips
setActiveDmCall. The acceptor connects to LiveKit but the UI never
shows the call. Fix: set activeDmCall and clear incomingCall
directly in the click handler.

Bug B: ready handler no longer sets activeDmCall for active calls.
On refresh/restart the client has no LiveKit connection — showing
"Connecting..." with no connection is broken. The call exists on
the server but this client session is disconnected.
2026-04-08 14:23:41 +02:00
Jannis Braun aae0b1a74e fix: comprehensive client-side session management for federated DM calls
Four fixes addressing the full state management problem:

1. Passive ready handler: no longer auto-connects to LiveKit on
   page refresh. Prevents identity conflicts when the same user
   has multiple sessions fighting for one LiveKit identity slot.
   The user must re-accept to join; state is shown but not acted on.

2. SoundController sync guard: incomingCallLoading/outgoingCallLoading
   refs prevent multiple playSound calls during async audio load.
   If call is cancelled while sound loads, stops it immediately on
   completion. Eliminates the "5 ringtones at once" bug.

3. Host dm_call_accepted broadcasts now include federatedCallId so
   all clients (including remote instances) can match the event.

4. Removed all diagnostic console.log statements.
2026-04-08 14:12:45 +02:00
Jannis Braun 86fe713a7c fix: cross-instance event contamination in federated DM calls
Root cause: sendToFederatedCallUsers used sendToDmMembers when dmChannelId
was set, which broadcast to ALL DM members including the caller's replicated
stub. The caller's multi-instance WS received dm_call_accepted with the
REMOTE instance's dmChannelId, causing token request for a non-existent
channel (403) and preventing the caller from connecting.

Fix 1: sendToFederatedCallUsers always uses ringedUserIds (exact recipients)
instead of sendToDmMembers (all members including caller stub).

Fix 2: dm_call_accepted handler only sets activeDmCall if the client is the
caller (wasOutgoingCall) or already connected to LiveKit. Other instances of
the same user just clear ringing without entering stuck "Connecting..." state.
2026-04-08 13:50:25 +02:00
Jannis Braun 94461f8967 debug: add token endpoint logging 2026-04-08 13:39:09 +02:00
Jannis Braun f6af6bf579 debug: more diagnostic logging in processDmCallAcceptEvent 2026-04-08 13:32:41 +02:00
Jannis Braun 70dbe04e6d debug: add diagnostic logging to DM call accept flow 2026-04-08 13:29:45 +02:00
Jannis Braun df2701b17f fix: route call accept/reject through delivering WS, not host URL
callOrigin was set to event.callOrigin (the HOST instance URL), which
routed accept/reject through the multi-instance WS connection. On
mobile hotspot or when the multi-instance WS drops, the accept is
silently lost — the host never knows, the call stays ringing forever.

Now callOrigin = origin (the WS that delivered dm_call_incoming).
This is always connected. The server on that instance finds the
FederatedCallEntry and relays to the host via S2S HTTP, which is
reliable and independent of client WS state.
2026-04-08 13:15:38 +02:00
Jannis Braun 5265117d29 fix: three root causes for federated DM call failures
1. Accept/reject/end from remote instance now resolves federatedId
   to local dmChannelId via DB lookup, so the host can find its
   VoiceRoom when the event arrives with only a federatedCallId.
   Previously silently failed with "No active call" error.

2. Batch all dm_call_incoming state updates into a single
   useVoiceStore.setState() call. Prevents SoundController from
   starting multiple ringtone instances (async playSound guard
   race when 4 separate set() calls each triggered the subscription).

3. Always overwrite callOrigin/federatedCallId (with null if absent)
   on dm_call_incoming. Prevents stale values from a previous
   federated call routing local accepts to the wrong instance.
2026-04-08 12:53:58 +02:00
Jannis Braun 9ad240495f fix: prevent auto-connect on dm_call_accepted for non-caller instances
Three fixes for multi-instance call state consistency:

1. Client dm_call_accepted handler only auto-connects to LiveKit if
   the user was the caller (outgoingCall was set). Other instances of
   the same user just clear ringing state without connecting.

2. Server processDmCallAcceptEvent remote path skips duplicate
   broadcast when FederatedCallEntry is already active (prevents
   state conflicts from host fan-out arriving after local accept).

3. Ready payload handler clears stuck incomingCall when restoring
   an already-active call after page refresh.
2026-04-08 12:21:27 +02:00
Jannis Braun f40ea03cfb docs: update system specs for federated DM calls v2 2026-04-08 03:28:54 +02:00
Jannis Braun ba7b975bc8 feat: add federatedCallId and callOrigin to client call routing
Enable federated DM calls to route accept/reject/end through the correct
WebSocket connection using callOrigin, and include federatedCallId in all
dm_call payloads for server-side FederatedCallEntry lookup.
2026-04-08 03:26:48 +02:00
Jannis Braun 0c57f9491f feat: late-bind dmChannelId on FederatedCallEntry when DM created mid-call
When findOrCreateDmChannel creates or finds a local channel for a
federatedId that has an active FederatedCallEntry with null dmChannelId,
update the entry. Prevents stale null references for Path B calls.
2026-04-08 03:20:01 +02:00
Jannis Braun 07edb25d12 feat: update DM call handlers and processors for federatedCallId lookup 2026-04-08 03:18:25 +02:00
Jannis Braun 1690295db5 feat: universal call relay — broadcast to all active federation peers 2026-04-08 03:11:56 +02:00
Jannis Braun 2c7eefc7b1 feat: fix caller exclusion (Bug 1) and add Path B receiver processing 2026-04-08 03:09:16 +02:00
Jannis Braun 2538bb5e4c fix: remove federated user gate for DM call events
Federated users can now send dm_call_start/accept/reject/end.
Handlers already branch correctly for local vs federated call state.
2026-04-08 03:04:51 +02:00
Jannis Braun 365c64794d refactor: re-key FederatedCallEntry by federatedId, add ringedUserIds 2026-04-08 03:03:44 +02:00