Commit Graph
1136 Commits
Author SHA1 Message Date
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 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
Jannis Braun f51e427493 fix: pass readState and dmCloseReopen through outbox worker relay reconstruction
The outbox worker's event reconstruction whitelist was missing these two
fields, causing read_state_update and dm_close/dm_reopen relay events to
arrive at remote instances with empty payloads and get rejected.
2026-04-08 02:01:41 +02:00
Jannis Braun a1c47e071e fix: prevent connecting to own instance as a remote
Add shared isSelfOrigin() helper that normalizes origins before
comparing to window.location.origin. Fixes auto-connect treating
self-referencing replicatedInstances entries as remote connections,
causing duplicate friends/DMs/data. Also hides self-referencing
entries from the Connections panel UI.
2026-04-08 01:28:20 +02:00
Jannis Braun fa5dbbfd4f fix: DMs vanishing after WebSocket reconnection
populateFromReady() built the federatedId dedup set from ALL existing
DMs, including those belonging to the reconnecting origin. Incoming DMs
then matched their own stale entries and were skipped as "duplicates."
The subsequent origin-removal step deleted the old copies, leaving no
DMs from that origin in state.

Scope the dedup set to DMs from OTHER origins only, so reconnecting
origins replace their DMs cleanly while cross-instance dedup still works.
2026-04-08 01:02:15 +02:00
Jannis Braun a496dc01bd fix: federation DM identity corruption — sync federatedId, guard backfill, remove bad merge criterion
Three bugs that combined to corrupt DM identities during initial sync:

1. Sync endpoint omitted federatedId for group DMs, causing the receiver
   to treat them as 1-on-1 DMs and compute wrong pair hashes — creating
   phantom channels that collide with real ones.

2. backfillHomeUserId unconditionally overwrote existing homeUserIds,
   so a single wrong match would permanently corrupt a user's identity
   and cascade to all subsequent lookups.

3. Migration duplicate-stub Criterion 1 ("shared 1-on-1 DM membership")
   incorrectly merged different users from the same domain who were
   simply having a conversation, destroying one user's identity.
2026-04-08 00:49:14 +02:00
Jannis Braun 2d32d9ae41 fix: deliver relayed DM messages to federated users on receiving instance
The relay broadcast loop skipped members whose homeInstance matched the
source instance, assuming they already received the message on their home
server. This broke delivery for federated users (e.g. youruser@nova on
orbit) who are actively connected to the remote instance.

Client-side dedup in addRealtimeMessage already handles double delivery
via sourceMessageId cross-matching, making the server-side skip both
unnecessary and harmful.
2026-04-07 23:50:35 +02:00
Jannis Braun f85bb4cca3 fix: relay handler reopen + dm_close/dm_reopen handlers
Add closed-state reopen logic to relay broadcast loop (bug fix).
Add dm_close and dm_reopen relay event handlers.
Extract buildDmChannelPayload helper for DM channel payload construction.
2026-04-07 22:31:43 +02:00
Jannis Braun 213325edaa feat: relay dm_close and dm_reopen to federated peers
Queue dm_close relay in DELETE /api/dm/:id after broadcasting locally,
and queue dm_reopen relay inside the myDm.closed === 1 branch of POST
/api/dm so peers are notified only on actual reopen, not new DM creation.
2026-04-07 22:26:14 +02:00
Jannis Braun 053f5efb4b feat: add queueDmCloseRelay utility for S2S close/reopen relay 2026-04-07 22:23:25 +02:00
Jannis Braun ba0df6305e feat: add dm_close and dm_reopen federation relay event types 2026-04-07 22:23:01 +02:00
Jannis Braun 23f2863bdc fix: seed DM read states for federated users on first access
When a federated user first accesses DMs on a remote instance,
all channels appear unread because no read_states rows exist.
Seed missing read states to the latest message during the ready
payload build. The S2S relay keeps things in sync going forward.
2026-04-07 21:38:11 +02:00
Jannis Braun 1ada46baff fix: relay mark_unread to peers, fix docs and timestamp consistency
- Add queueReadStateRelay call in handleMarkUnread (skip '0' sentinel)
- Fix double Date.now() in queueReadStateRelay (use single const)
- Fix federation.md: read state relay uses outbox (not fire-and-forget),
  correct payload schema to match implementation
2026-04-07 20:08:31 +02:00
Jannis Braun 5504a34dd8 feat: include federatedId in federation dm_channel_created events
Ensures DM channels bootstrapped via S2S relay include federatedId
for client-side dedup.
2026-04-07 19:57:23 +02:00
Jannis Braun 658eba556e feat: include federatedId in dm_channel_created WS events
Ensures the client receives federatedId for dedup when DM channels
are created, reopened, or bootstrapped via REST handlers.
2026-04-07 19:56:23 +02:00
Jannis Braun 95f97c1526 feat: process DM WS events from all origins
Remove home-only restriction on DM events. Add federatedId
dedup check to dm_channel_created to prevent duplicate sidebar
entries for cross-instance DMs.
2026-04-07 19:55:08 +02:00
Jannis Braun b7c4a020db feat: accept DMs from all origins with federatedId dedup
Replace home-only DM filter with unified DM list. DMs from all
connected instances are merged, deduplicated by federatedId.
First-loaded copy wins; both copies are in sync via S2S relay.
2026-04-07 19:54:16 +02:00
Jannis Braun ad172b34e1 feat: relax group DM authority check for trusted peers
Remove sourceInstance === ownerHomeInstance check for incremental
member_add. Allows federated users to create group DMs on non-home
instances. HMAC trust boundary + attribution check remain.
2026-04-07 19:53:15 +02:00
Jannis Braun dc57a050b0 feat: inbound S2S read state processor
Receive read_state_update events from peers, translate message
coordinates to local IDs via sourceInstance/sourceMessageId
mapping, update read_states with timestamp-only LWW.
2026-04-07 19:51:55 +02:00
Jannis Braun 32ccd9c410 feat: outbound S2S read state relay
Queue read_state_update events when users ack DM messages on
channels with a federatedId. Translates local message IDs to
federation coordinates using sourceInstance/sourceMessageId.
2026-04-07 19:49:52 +02:00
Jannis Braun 662143bf08 feat: lift DM gates for federated users
Remove requireLocalUser from DM routes, include DMs in federated
ready payload, replace blanket dm_* WS gate with call-only blocklist.
DM calls remain gated (separate scope).
2026-04-07 19:48:07 +02:00
Jannis Braun 9b8330fcdb feat: add federatedId to DmChannel type, read_state_update to relay events 2026-04-07 19:45:20 +02:00
Jannis Braun 43900576b2 fix: update contextType casts to include 'profile', remove stale profileSync comments 2026-04-07 14:05:19 +02:00
Jannis Braun a7cd6a1bf3 feat: remove client-driven profile sync — replaced by S2S relay 2026-04-07 13:59:16 +02:00
Jannis Braun ec6cb236a2 fix: ensure profile_update payload is included in outbox event reconstruction 2026-04-07 13:58:11 +02:00
Jannis Braun 58c0da03c1 feat: S2S profile sync — write-protection, relay queueing, bootstrap trigger, broadcast refactor
- Add write-protection guard: replicated users can't update durable profile fields (403)
- Remove LWW guard (replaced by S2S version check)
- Add change detection + relay queueing for profile_update events
- Add bootstrap trigger when new origins appear in replicatedInstances
- Refactor broadcast to use collectProfileBroadcastTargetIds shared helper
2026-04-07 13:57:21 +02:00
Jannis Braun c229b32771 feat: add processProfileUpdateEvent S2S relay processor 2026-04-07 13:54:43 +02:00
Jannis Braun 34902fe320 refactor: extract collectProfileBroadcastTargetIds shared helper from userDeletion 2026-04-07 13:52:21 +02:00
Jannis Braun 3a2dfde4a8 feat: add profile_update federation relay event type and payload interface 2026-04-07 13:51:53 +02:00