package connector import ( "context" "fmt" "slices" "strings" "time" "github.com/purpshell/meowcaller" "github.com/rs/zerolog" "go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types/events" "maunium.net/go/mautrix" "maunium.net/go/mautrix/bridgev2" mxbridge "maunium.net/go/mautrix/bridgev2/matrix" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" "go.mau.fi/mautrix-whatsapp/pkg/connector/voip" "go.mau.fi/mautrix-whatsapp/pkg/connector/wadb" "go.mau.fi/mautrix-whatsapp/pkg/waid" ) const ( matrixRTCRingLifetime = 90 * time.Second matrixRTCMembershipLifetime = 4 * time.Hour matrixRTCStickyDuration = time.Hour ) type incomingCallGroup struct { JID types.JID ExpiresAt time.Time } func (wa *WhatsAppClient) trackIncomingCallEvent(rawEvt any) { switch evt := rawEvt.(type) { case *events.CallOffer: wa.trackIncomingCallGroup(evt.CallID, evt.GroupJID) case *events.CallOfferNotice: wa.trackIncomingCallGroup(evt.CallID, evt.GroupJID) case *events.CallTerminate: wa.clearIncomingCallGroup(evt.CallID) } } func (wa *WhatsAppClient) handleIncomingVOIPCall(call *meowcaller.Call) { if call == nil { return } ctx := wa.UserLogin.Log.WithContext(withoutCancelOrBackground(wa.Main.Bridge.BackgroundCtx)) err := wa.announceIncomingMatrixRTCCall(ctx, call) if err != nil { wa.UserLogin.Log.Warn(). Err(err). Str("call_id", call.ID()). Stringer("peer_jid", call.Peer()). Msg("Failed to announce incoming WhatsApp call over MatrixRTC") } } func (wa *WhatsAppClient) handleVOIPCallEnded(callID, reason string) { wa.clearIncomingCallGroup(callID) wa.clearWhatsAppRemoteHandRaises(callID) ctx := wa.UserLogin.Log.WithContext(withoutCancelOrBackground(wa.Main.Bridge.BackgroundCtx)) log := wa.UserLogin.Log.With().Str("call_id", callID).Str("reason", reason).Logger() call, err := wa.Main.DB.MatrixRTCCall.Get(ctx, wa.UserLogin.ID, callID) if err != nil { log.Err(err).Msg("Failed to look up MatrixRTC call record after WhatsApp call ended") return } else if call == nil { return } if err = wa.clearMatrixRTCMembership(ctx, call); err != nil { log.Err(err).Msg("Failed to clear MatrixRTC membership after WhatsApp call ended") } endReason, lastError := matrixRTCFinalEndReason(call, reason) if err = wa.Main.DB.MatrixRTCCall.MarkEnded(ctx, wa.UserLogin.ID, callID, "ended", endReason, lastError, time.Now()); err != nil { log.Err(err).Msg("Failed to mark MatrixRTC call ended") } } func (wa *WhatsAppClient) cleanupStaleMatrixRTCCalls(ctx context.Context) { if wa == nil || wa.Main == nil || wa.Main.DB == nil || wa.UserLogin == nil { return } calls, err := wa.Main.DB.MatrixRTCCall.GetActiveForLogin(ctx, wa.UserLogin.ID) if err != nil { wa.UserLogin.Log.Err(err).Msg("Failed to query stale MatrixRTC calls during login load") return } for _, call := range calls { if call == nil { continue } lastError := "" if clearErr := wa.clearMatrixRTCMembership(ctx, call); clearErr != nil { lastError = clearErr.Error() wa.UserLogin.Log.Warn(). Err(clearErr). Str("call_id", call.WACallID). Stringer("room_id", call.RoomID). Msg("Failed to clear stale MatrixRTC membership during login load") } if markErr := wa.Main.DB.MatrixRTCCall.MarkEnded( ctx, wa.UserLogin.ID, call.WACallID, "ended", "bridge_restart", lastError, time.Now(), ); markErr != nil { wa.UserLogin.Log.Err(markErr). Str("call_id", call.WACallID). Msg("Failed to mark stale MatrixRTC call ended during login load") } } if len(calls) > 0 { wa.UserLogin.Log.Info(). Int("call_count", len(calls)). Msg("Cleaned up stale MatrixRTC calls during login load") } } func (wa *WhatsAppClient) handleWhatsAppWaitingRoom( ctx context.Context, callID string, state meowcaller.WaitingRoomState, ) { call, err := wa.Main.DB.MatrixRTCCall.Get(ctx, wa.UserLogin.ID, callID) if err != nil { wa.UserLogin.Log.Err(err). Str("call_id", callID). Msg("Failed to load MatrixRTC call for WhatsApp waiting-room update") return } if call == nil || !call.EndedTS.IsZero() || call.RoomID == "" { return } intent := wa.matrixRTCIntentForMXID(ctx, call.MatrixParticipantMXID) _, err = intent.SendMessage(ctx, call.RoomID, event.EventMessage, &event.Content{ Parsed: &event.MessageEventContent{ MsgType: event.MsgNotice, Body: formatWaitingRoomNotice(state), }, }, nil) if err != nil { wa.UserLogin.Log.Warn(). Err(err). Str("call_id", callID). Msg("Failed to send WhatsApp waiting-room update to Matrix") } } func formatWaitingRoomNotice(state meowcaller.WaitingRoomState) string { approval := "disabled" if state.Enabled { approval = "enabled" } if state.InWaitingRoom { return fmt.Sprintf("Waiting for approval to join the WhatsApp call link (approval %s).", approval) } if len(state.Users) == 0 { return fmt.Sprintf("WhatsApp call-link waiting room is empty (approval %s).", approval) } participants := make([]string, 0, len(state.Users)) for _, user := range state.Users { identity := user.JID if !user.PN.IsEmpty() { identity = user.PN } participants = append(participants, identity.String()) } slices.Sort(participants) return fmt.Sprintf( "WhatsApp call-link waiting room has %d participant(s) pending (approval %s): %s", len(participants), approval, strings.Join(participants, ", "), ) } func matrixRTCFinalEndReason(call *wadb.MatrixRTCCall, reason string) (string, string) { if call != nil && !call.EndedTS.IsZero() && (call.EndReason != "" || call.LastError != "") { if call.EndReason != "" { reason = call.EndReason } return reason, call.LastError } return reason, "" } func (wa *WhatsAppClient) announceIncomingMatrixRTCCall(ctx context.Context, call *meowcaller.Call) error { wa.voipCallStartLock.Lock() defer wa.voipCallStartLock.Unlock() log := zerolog.Ctx(ctx).With(). Str("call_id", call.ID()). Stringer("peer_jid", call.Peer()). Logger() peer := wa.matrixRTCAnnouncementPeer(ctx, call.Peer()) portalPeer := wa.incomingCallPortalPeer(call.ID(), peer) portal, err := wa.Main.Bridge.GetPortalByKey(ctx, wa.makeWAPortalKey(portalPeer)) if err != nil { return err } if portal == nil || portal.MXID == "" { log.Debug().Msg("No existing Matrix portal room for incoming MatrixRTC call announcement") return nil } activeRoomCalls, err := wa.Main.DB.MatrixRTCCall.GetActiveInRoom(ctx, portal.MXID) if err != nil { return err } if len(activeRoomCalls) > 0 { log.Warn(). Stringer("room_id", portal.MXID). Int("active_call_count", len(activeRoomCalls)). Msg("Rejecting incoming WhatsApp call because the Matrix room already has an active call") return call.Reject() } if wa.Main.Config.VOIP.MaxActiveCallsPerLogin > 0 { activeCalls, err := wa.Main.DB.MatrixRTCCall.GetActiveForLogin(ctx, wa.UserLogin.ID) if err != nil { return err } if len(activeCalls) >= wa.Main.Config.VOIP.MaxActiveCallsPerLogin { log.Warn(). Int("active_call_count", len(activeCalls)). Int("max_active_calls", wa.Main.Config.VOIP.MaxActiveCallsPerLogin). Msg("Rejecting incoming WhatsApp call because the MatrixRTC active call limit was reached") return call.Reject() } } return wa.announceMatrixRTCCallInPortal(ctx, call, portal, peer, "incoming") } func (wa *WhatsAppClient) trackIncomingCallGroup(callID string, group types.JID) { if wa == nil || callID == "" || group.Server != types.GroupServer || group.User == "" { return } wa.incomingCallGroupLock.Lock() if wa.incomingCallGroups == nil { wa.incomingCallGroups = make(map[string]incomingCallGroup) } now := time.Now() for trackedCallID, tracked := range wa.incomingCallGroups { if !tracked.ExpiresAt.After(now) { delete(wa.incomingCallGroups, trackedCallID) } } wa.incomingCallGroups[callID] = incomingCallGroup{ JID: group.ToNonAD(), ExpiresAt: now.Add(callEventMaxAge), } wa.incomingCallGroupLock.Unlock() } func (wa *WhatsAppClient) incomingCallPortalPeer(callID string, fallback types.JID) types.JID { if wa == nil || callID == "" { return fallback } wa.incomingCallGroupLock.Lock() tracked := wa.incomingCallGroups[callID] delete(wa.incomingCallGroups, callID) wa.incomingCallGroupLock.Unlock() if tracked.JID.IsEmpty() || !tracked.ExpiresAt.After(time.Now()) { return fallback } return tracked.JID } func (wa *WhatsAppClient) clearIncomingCallGroup(callID string) { if wa == nil || callID == "" { return } wa.incomingCallGroupLock.Lock() delete(wa.incomingCallGroups, callID) wa.incomingCallGroupLock.Unlock() } func (wa *WhatsAppClient) joinMatrixRTCCallLink( ctx context.Context, portal *bridgev2.Portal, tokenOrURL string, video bool, ) (*meowcaller.Call, error) { wa.voipCallStartLock.Lock() defer wa.voipCallStartLock.Unlock() if portal == nil || portal.MXID == "" { return nil, fmt.Errorf("call links must be joined from an existing portal room") } if video && !wa.Main.Config.VOIP.Video.Enabled { return nil, fmt.Errorf("WhatsApp call-link video requires voip.video.enabled") } activeRoomCalls, err := wa.Main.DB.MatrixRTCCall.GetActiveInRoom(ctx, portal.MXID) if err != nil { return nil, err } if len(activeRoomCalls) > 0 { return nil, fmt.Errorf("the Matrix room already has an active call") } if wa.Main.Config.VOIP.MaxActiveCallsPerLogin > 0 { activeCalls, err := wa.Main.DB.MatrixRTCCall.GetActiveForLogin(ctx, wa.UserLogin.ID) if err != nil { return nil, err } if len(activeCalls) >= wa.Main.Config.VOIP.MaxActiveCallsPerLogin { return nil, fmt.Errorf("active MatrixRTC call limit reached for login %s", wa.UserLogin.ID) } } call, err := wa.VOIP.JoinCallLink(ctx, tokenOrURL, video) if err != nil { return nil, err } peer := wa.matrixRTCAnnouncementPeer(ctx, call.Peer()) if err = wa.announceMatrixRTCCallInPortal(ctx, call, portal, peer, "call_link"); err != nil { _ = call.Hangup() return nil, err } return call, nil } func (wa *WhatsAppClient) announceMatrixRTCCallInPortal( ctx context.Context, call *meowcaller.Call, portal *bridgev2.Portal, peer types.JID, direction string, ) error { if call == nil { return fmt.Errorf("WhatsApp call is nil") } if portal == nil || portal.MXID == "" { return fmt.Errorf("Matrix portal room is not available") } focus, err := voip.DiscoverLiveKitFocus(ctx, nil, wa.Main.Bridge.Matrix.ServerName(), wa.Main.Config.VOIP.MatrixRTC.LiveKitServiceURL) if err != nil { return err } intent, err := wa.matrixRTCParticipantIntent(ctx, peer) if err != nil { return err } now := time.Now() deviceID := voip.MatrixRTCDeviceID(string(wa.UserLogin.ID), call.ID()) session := voip.MatrixRTCSession{ UserID: intent.GetMXID(), DeviceID: deviceID, MemberID: voip.MatrixRTCMemberID(intent.GetMXID(), deviceID), Intent: matrixRTCCallIntent(call), Focus: *focus, Created: now, Expires: matrixRTCMembershipLifetime, StickyKey: voip.MatrixRTCMemberID(intent.GetMXID(), deviceID), } record := &wadb.MatrixRTCCall{ UserLoginID: wa.UserLogin.ID, WACallID: call.ID(), RoomID: portal.MXID, PortalKey: portal.PortalKey, PeerJID: peer, Direction: direction, MediaKind: session.Intent, FocusType: focus.Type, LiveKitServiceURL: focus.LiveKitServiceURL, LiveKitRoom: portal.MXID.String(), MatrixParticipantMXID: intent.GetMXID(), MatrixSessionID: deviceID, AudioPolicy: wa.Main.Config.VOIP.LiveKit.AudioUplinkPolicy, State: "ringing", CreatedTS: now, } if err = wa.Main.DB.MatrixRTCCall.Put(ctx, record); err != nil { return err } if err = wa.sendMatrixRTCRing(ctx, intent, portal.MXID, call.ID(), &session); err != nil { _ = wa.Main.DB.MatrixRTCCall.MarkEnded(ctx, wa.UserLogin.ID, call.ID(), "ended", "matrixrtc_announce_failed", err.Error(), time.Now()) return err } record.BridgeMembershipEventID = session.MembershipEventID if err = wa.Main.DB.MatrixRTCCall.Put(ctx, record); err != nil { return err } zerolog.Ctx(ctx).Info(). Str("call_id", call.ID()). Str("direction", direction). Stringer("room_id", portal.MXID). Stringer("participant_mxid", intent.GetMXID()). Str("device_id", deviceID). Msg("Announced WhatsApp call over MatrixRTC") return nil } func (wa *WhatsAppClient) matrixRTCAnnouncementPeer(ctx context.Context, peer types.JID) types.JID { peer = peer.ToNonAD() if peer.Server != types.HiddenUserServer { return peer } pn, err := wa.GetStore().LIDs.GetPNForLID(ctx, peer) if err != nil { zerolog.Ctx(ctx).Err(err). Stringer("lid", peer). Msg("Failed to get phone number for LID incoming MatrixRTC call") return peer } else if pn.IsEmpty() { return peer } pn = pn.ToNonAD() zerolog.Ctx(ctx).Debug(). Stringer("lid", peer). Stringer("pn", pn). Msg("Using phone number portal for incoming MatrixRTC call from LID") return pn } func (wa *WhatsAppConnector) startOutboundMatrixRTCCall(ctx context.Context, portal *bridgev2.Portal, trigger voip.MatrixRTCEvent) error { if portal == nil { return nil } if portal.Receiver == "" { return fmt.Errorf("portal has no receiver login for outbound MatrixRTC call") } login, err := wa.Bridge.GetExistingUserLoginByID(ctx, portal.Receiver) if err != nil { return err } else if login == nil || login.Client == nil { return fmt.Errorf("receiver login %s not found for outbound MatrixRTC call", portal.Receiver) } else if !login.Client.IsLoggedIn() { return bridgev2.ErrNotLoggedIn } client, ok := login.Client.(*WhatsAppClient) if !ok || client == nil || client.VOIP == nil { return fmt.Errorf("receiver login %s has no WhatsApp VOIP manager", portal.Receiver) } return client.startOutboundMatrixRTCCall(ctx, portal, trigger) } func (wa *WhatsAppClient) startOutboundMatrixRTCCall(ctx context.Context, portal *bridgev2.Portal, trigger voip.MatrixRTCEvent) error { wa.voipCallStartLock.Lock() defer wa.voipCallStartLock.Unlock() if wa.VOIP == nil || !wa.VOIP.Enabled() { return voip.ErrNotEnabled } peer, err := waid.ParsePortalID(portal.ID) if err != nil { return err } if !matrixRTCPortalSupportsWhatsAppCalls(peer) { return fmt.Errorf("MatrixRTC WhatsApp calls are not supported in %s portals", peer.Server) } mediaKind, downgradedMedia := matrixRTCOutboundMediaKind(trigger) if mediaKind == "" { return fmt.Errorf("outbound WhatsApp MatrixRTC calls only support audio/video, not %q", trigger.Intent) } if downgradedMedia { wa.UserLogin.Log.Warn(). Stringer("room_id", trigger.RoomID). Str("requested_media_kind", trigger.Intent). Str("media_kind", mediaKind). Msg("Downgrading outbound MatrixRTC call media kind") } if mediaKind == "video" && !wa.Main.Config.VOIP.Video.Enabled { return fmt.Errorf("outbound WhatsApp MatrixRTC video calls require voip.video.enabled") } if wa.Main.Config.VOIP.MaxActiveCallsPerLogin > 0 { activeCalls, err := wa.Main.DB.MatrixRTCCall.GetActiveForLogin(ctx, wa.UserLogin.ID) if err != nil { return err } if len(activeCalls) >= wa.Main.Config.VOIP.MaxActiveCallsPerLogin { return fmt.Errorf("active MatrixRTC call limit reached for login %s", wa.UserLogin.ID) } } focus, err := wa.matrixRTCLiveKitFocusForTrigger(ctx, trigger) if err != nil { return err } intent, err := wa.matrixRTCParticipantIntent(ctx, peer) if err != nil { return err } var call *meowcaller.Call if peer.Server == types.GroupServer { call, err = wa.VOIP.DialGroupByID(ctx, peer.ToNonAD().String(), mediaKind == "video") } else { call, err = wa.VOIP.Dial(ctx, peer.ToNonAD().String(), mediaKind == "video") } if err != nil { return err } now := time.Now() deviceID := voip.MatrixRTCDeviceID(string(wa.UserLogin.ID), call.ID()) record := &wadb.MatrixRTCCall{ UserLoginID: wa.UserLogin.ID, WACallID: call.ID(), RoomID: trigger.RoomID, PortalKey: portal.PortalKey, PeerJID: peer, Direction: "outgoing", MediaKind: mediaKind, FocusType: focus.Type, LiveKitServiceURL: focus.LiveKitServiceURL, LiveKitRoom: trigger.RoomID.String(), MatrixParticipantMXID: intent.GetMXID(), MatrixSessionID: deviceID, SelectedPublisherID: matrixRTCTriggerParticipantID(trigger), SelectedMembershipEventID: trigger.EventID, AudioPolicy: wa.Main.Config.VOIP.LiveKit.AudioUplinkPolicy, State: "joining_livekit", CreatedTS: now, } if err = wa.Main.DB.MatrixRTCCall.Put(ctx, record); err != nil { _ = call.Hangup() return err } session := &voip.MatrixRTCSession{ UserID: intent.GetMXID(), DeviceID: deviceID, MemberID: voip.MatrixRTCMemberID(intent.GetMXID(), deviceID), Intent: mediaKind, Focus: *focus, Created: now, Expires: matrixRTCMembershipLifetime, StickyKey: voip.MatrixRTCMemberID(intent.GetMXID(), deviceID), } if err = wa.sendMatrixRTCMembership(ctx, intent, trigger.RoomID, session); err != nil { return wa.failMatrixRTCActivation(ctx, record, "matrixrtc_membership_failed", err) } record.BridgeMembershipEventID = session.MembershipEventID if err = wa.connectOutboundMatrixRTCCall(ctx, record, trigger); err != nil { wa.UserLogin.Log.Warn(). Err(err). Str("call_id", call.ID()). Stringer("room_id", trigger.RoomID). Stringer("peer_jid", peer). Msg("Failed to connect outbound MatrixRTC call to LiveKit") return err } wa.UserLogin.Log.Info(). Str("call_id", call.ID()). Stringer("room_id", trigger.RoomID). Stringer("peer_jid", peer). Stringer("matrix_participant_mxid", trigger.Sender). Msg("Started outbound WhatsApp call from MatrixRTC") return nil } func (wa *WhatsAppClient) sendMatrixRTCRing(ctx context.Context, intent bridgev2.MatrixAPI, roomID id.RoomID, waCallID string, session *voip.MatrixRTCSession) error { now := time.Now() notificationMode := wa.Main.Config.VOIP.MatrixRTC.NotificationEventCompat if matrixRTCCompatAllowsModern(notificationMode) { resp, err := sendMatrixRTCMessage(ctx, intent, roomID, voip.RTCNotificationEventType(), voip.BuildRTCNotificationContent(now, matrixRTCRingLifetime, session.Intent), 0) if err != nil { return err } if resp != nil { session.NotificationEventID = resp.EventID } } if matrixRTCCompatAllowsLegacy(notificationMode) { _, err := sendMatrixRTCMessage(ctx, intent, roomID, voip.LegacyCallNotifyEventType(), voip.BuildLegacyCallNotifyContent(waCallID, session.Intent), 0) if err != nil { return err } } return wa.sendMatrixRTCMembership(ctx, intent, roomID, session) } func (wa *WhatsAppClient) sendMatrixRTCMembership(ctx context.Context, intent bridgev2.MatrixAPI, roomID id.RoomID, session *voip.MatrixRTCSession) error { now := time.Now() membershipMode := wa.Main.Config.VOIP.MatrixRTC.MembershipEventCompat modernMessageSent := false if matrixRTCCompatAllowsModern(membershipMode) { content := voip.BuildRTCMembershipContent(*session) resp, err := sendMatrixRTCMessage(ctx, intent, roomID, voip.RTCMembershipEventType(event.MessageEventType), content, matrixRTCStickyDuration) if err != nil { return err } if resp != nil { session.MembershipEventID = resp.EventID } modernMessageSent = true stateKey := voip.MatrixRTCStateKey(session.UserID, session.DeviceID) stateResp, err := intent.SendState(ctx, roomID, voip.RTCMembershipEventType(event.StateEventType), stateKey, &event.Content{Raw: content}, now) if err != nil { wa.UserLogin.Log.Warn(). Err(err). Stringer("room_id", roomID). Str("state_key", stateKey). Msg("Failed to send MatrixRTC membership state event after sticky message membership") } else if session.MembershipEventID == "" && stateResp != nil { session.MembershipEventID = stateResp.EventID } } if matrixRTCCompatAllowsLegacy(membershipMode) { resp, err := intent.SendState(ctx, roomID, voip.GroupCallMemberEventType(), "", &event.Content{Raw: voip.BuildLegacyCallMemberContent(*session)}, now) if err != nil { if modernMessageSent { wa.UserLogin.Log.Warn(). Err(err). Stringer("room_id", roomID). Msg("Failed to send legacy MatrixRTC membership state event after modern membership") return nil } return err } if session.MembershipEventID == "" && resp != nil { session.MembershipEventID = resp.EventID } } return nil } func (wa *WhatsAppClient) clearMatrixRTCMembership(ctx context.Context, call *wadb.MatrixRTCCall) error { if call.RoomID == "" || call.MatrixParticipantMXID == "" { return nil } intent := wa.matrixRTCIntentForMXID(ctx, call.MatrixParticipantMXID) if intent == nil { intent = wa.Main.Bridge.Bot } stickyKey := voip.MatrixRTCMemberID(call.MatrixParticipantMXID, call.MatrixSessionID) emptyContent := voip.EmptyMatrixRTCContent(stickyKey) now := time.Now() membershipMode := wa.Main.Config.VOIP.MatrixRTC.MembershipEventCompat modernMessageSent := false if matrixRTCCompatAllowsModern(membershipMode) { if _, err := sendMatrixRTCMessage(ctx, intent, call.RoomID, voip.RTCMembershipEventType(event.MessageEventType), emptyContent, matrixRTCStickyDuration); err != nil { return err } modernMessageSent = true stateKey := voip.MatrixRTCStateKey(call.MatrixParticipantMXID, call.MatrixSessionID) if _, err := intent.SendState(ctx, call.RoomID, voip.RTCMembershipEventType(event.StateEventType), stateKey, &event.Content{Raw: map[string]any{}}, now); err != nil { wa.UserLogin.Log.Warn(). Err(err). Stringer("room_id", call.RoomID). Str("state_key", stateKey). Msg("Failed to clear MatrixRTC membership state event after sticky message cleanup") } } if matrixRTCCompatAllowsLegacy(membershipMode) { if _, err := intent.SendState(ctx, call.RoomID, voip.GroupCallMemberEventType(), "", &event.Content{Raw: map[string]any{}}, now); err != nil { if modernMessageSent { wa.UserLogin.Log.Warn(). Err(err). Stringer("room_id", call.RoomID). Msg("Failed to clear legacy MatrixRTC membership state event after modern cleanup") return nil } return err } } return nil } func (wa *WhatsAppConnector) cleanupFailedOutboundMatrixRTCStart(ctx context.Context, trigger voip.MatrixRTCEvent) error { if trigger.RoomID == "" || wa.Bridge == nil || wa.Bridge.Bot == nil { return nil } intent := wa.Bridge.Bot now := time.Now() membershipMode := wa.Config.VOIP.MatrixRTC.MembershipEventCompat if trigger.Kind == voip.MatrixRTCEventKindRTCMembership && matrixRTCCompatAllowsModern(membershipMode) { emptyContent := voip.EmptyMatrixRTCContent(matrixRTCTriggerStickyKey(trigger)) if _, err := sendMatrixRTCMessage(ctx, intent, trigger.RoomID, voip.RTCMembershipEventType(event.MessageEventType), emptyContent, matrixRTCStickyDuration); err != nil { return err } stateKey := matrixRTCTriggerStateKey(trigger) if stateKey != "" { if _, err := intent.SendState(ctx, trigger.RoomID, voip.RTCMembershipEventType(event.StateEventType), stateKey, &event.Content{Raw: map[string]any{}}, now); err != nil { return err } } } if trigger.Kind == voip.MatrixRTCEventKindGroupCallMember && matrixRTCCompatAllowsLegacy(membershipMode) { if _, err := intent.SendState(ctx, trigger.RoomID, voip.GroupCallMemberEventType(), trigger.StateKey, &event.Content{Raw: map[string]any{}}, now); err != nil { return err } } return nil } func (wa *WhatsAppClient) activateMatrixRTCCall(ctx context.Context, call *wadb.MatrixRTCCall, trigger voip.MatrixRTCEvent) error { if call == nil { return nil } call.State = "joining_livekit" call.LastError = "" call.SelectedPublisherID = matrixRTCTriggerParticipantID(trigger) call.SelectedMembershipEventID = trigger.EventID if err := wa.Main.DB.MatrixRTCCall.Put(ctx, call); err != nil { return err } authResp, err := wa.requestMatrixRTCLiveKitAuth(ctx, call, trigger) if err != nil { return err } if err = wa.VOIP.BridgeCallToLiveKit(ctx, call.WACallID, authResp, call.SelectedPublisherID); err != nil { return wa.failMatrixRTCActivation(ctx, call, "livekit_bridge_failed", err) } now := time.Now() call.State = "active" call.JoinedTS = now call.AnsweredTS = now if authResp.RoomName != "" { call.LiveKitRoom = authResp.RoomName } if err = wa.Main.DB.MatrixRTCCall.Put(ctx, call); err != nil { return err } wa.UserLogin.Log.Info(). Str("call_id", call.WACallID). Stringer("room_id", call.RoomID). Stringer("trigger_sender", trigger.Sender). Msg("Activated MatrixRTC LiveKit bridge for WhatsApp call") return nil } func (wa *WhatsAppClient) connectOutboundMatrixRTCCall(ctx context.Context, call *wadb.MatrixRTCCall, trigger voip.MatrixRTCEvent) error { authResp, err := wa.requestMatrixRTCLiveKitAuth(ctx, call, trigger) if err != nil { return err } if err = wa.VOIP.BridgeCallToLiveKit(ctx, call.WACallID, authResp, call.SelectedPublisherID); err != nil { return wa.failMatrixRTCActivation(ctx, call, "livekit_bridge_failed", err) } now := time.Now() call.State = "active" call.JoinedTS = now if authResp.RoomName != "" { call.LiveKitRoom = authResp.RoomName } return wa.Main.DB.MatrixRTCCall.Put(ctx, call) } func (wa *WhatsAppClient) requestMatrixRTCLiveKitAuth(ctx context.Context, call *wadb.MatrixRTCCall, trigger voip.MatrixRTCEvent) (*voip.LiveKitAuthResponse, error) { intent := wa.matrixRTCIntentForMXID(ctx, call.MatrixParticipantMXID) openIDToken, err := requestMatrixOpenIDToken(ctx, intent) if err != nil { return nil, wa.failMatrixRTCActivation(ctx, call, "matrix_openid_failed", err) } if matrixRTCCompatAllowsLegacy(wa.Main.Config.VOIP.MatrixRTC.MembershipEventCompat) { authResp, err := voip.RequestLegacyLiveKitAuth(ctx, nil, call.LiveKitServiceURL, matrixRTCLegacyLiveKitAuthRequest(call, openIDToken)) if err != nil { return nil, wa.failMatrixRTCActivation(ctx, call, "livekit_auth_failed", err) } return authResp, nil } authResp, err := voip.RequestLiveKitAuth(ctx, nil, call.LiveKitServiceURL, matrixRTCLiveKitAuthRequest(call, openIDToken)) if err != nil { return nil, wa.failMatrixRTCActivation(ctx, call, "livekit_auth_failed", err) } return authResp, nil } func (wa *WhatsAppClient) failMatrixRTCActivation(ctx context.Context, call *wadb.MatrixRTCCall, reason string, err error) error { _ = wa.Main.DB.MatrixRTCCall.MarkEnded(ctx, call.UserLoginID, call.WACallID, "ended", reason, err.Error(), time.Now()) if wa.VOIP != nil { wa.VOIP.HandleMatrixRTCCallEvent(ctx, voip.MatrixRTCEvent{ Kind: voip.MatrixRTCEventKindRTCDecline, RoomID: call.RoomID, }, call.WACallID) } return err } func matrixRTCLiveKitAuthRequest(call *wadb.MatrixRTCCall, openIDToken voip.MatrixOpenIDToken) voip.LiveKitAuthRequest { if call == nil { return voip.LiveKitAuthRequest{OpenIDToken: openIDToken} } return voip.LiveKitAuthRequest{ RoomID: call.RoomID.String(), SlotID: voip.MatrixRTCDefaultSlotID, OpenIDToken: openIDToken, Member: matrixRTCLiveKitAuthMember(call), } } func matrixRTCLegacyLiveKitAuthRequest(call *wadb.MatrixRTCCall, openIDToken voip.MatrixOpenIDToken) voip.LegacyLiveKitAuthRequest { if call == nil { return voip.LegacyLiveKitAuthRequest{OpenIDToken: openIDToken} } return voip.LegacyLiveKitAuthRequest{ Room: call.RoomID.String(), OpenIDToken: openIDToken, DeviceID: call.MatrixSessionID, } } func matrixRTCLiveKitAuthMember(call *wadb.MatrixRTCCall) *voip.LiveKitAuthMember { if call == nil { return nil } return &voip.LiveKitAuthMember{ ID: voip.MatrixRTCMemberID(call.MatrixParticipantMXID, call.MatrixSessionID), ClaimedDeviceID: call.MatrixSessionID, ClaimedUserID: call.MatrixParticipantMXID.String(), } } func (wa *WhatsAppClient) matrixRTCLiveKitFocusForTrigger(ctx context.Context, trigger voip.MatrixRTCEvent) (*voip.Focus, error) { for _, focus := range trigger.FociPreferred { if focus.Type == "livekit" && focus.LiveKitServiceURL != "" { focusCopy := focus return &focusCopy, nil } } return voip.DiscoverLiveKitFocus(ctx, nil, wa.Main.Bridge.Matrix.ServerName(), wa.Main.Config.VOIP.MatrixRTC.LiveKitServiceURL) } func (wa *WhatsAppClient) matrixRTCParticipantIntent(ctx context.Context, peer types.JID) (bridgev2.MatrixAPI, error) { mode := strings.ToLower(wa.Main.Config.VOIP.MatrixRTC.ParticipantMode) if mode == "" || mode == "whatsapp_ghost" { if ghostID := waid.MakeUserID(peer); ghostID != "" { ghost, err := wa.Main.Bridge.GetGhostByID(ctx, ghostID) if err != nil { return nil, err } if ghost != nil && ghost.Intent != nil { return ghost.Intent, nil } } } return wa.Main.Bridge.Bot, nil } func (wa *WhatsAppClient) matrixRTCIntentForMXID(ctx context.Context, mxid id.UserID) bridgev2.MatrixAPI { if mxid == "" || mxid == wa.Main.Bridge.Bot.GetMXID() { return wa.Main.Bridge.Bot } if ghost, err := wa.Main.Bridge.GetGhostByMXID(ctx, mxid); err == nil && ghost != nil && ghost.Intent != nil { return ghost.Intent } return wa.Main.Bridge.Bot } func matrixRTCPortalSupportsWhatsAppCalls(peer types.JID) bool { switch peer.Server { case types.DefaultUserServer, types.HiddenUserServer, types.GroupServer: return true default: return false } } func matrixRTCCallIntent(call *meowcaller.Call) string { if call != nil && call.IsVideo() { return "video" } return "audio" } func matrixRTCOutboundMediaKind(trigger voip.MatrixRTCEvent) (mediaKind string, downgraded bool) { switch trigger.Intent { case "", "audio": return "audio", false case "video": return "video", false default: return "", false } } func matrixRTCTriggerParticipantID(trigger voip.MatrixRTCEvent) string { if trigger.Sender == "" { return "" } deviceID := trigger.SessionID if deviceID == "" { deviceID = trigger.DeviceID } return voip.MatrixRTCMemberID(trigger.Sender, deviceID) } func matrixRTCTriggerStateKey(trigger voip.MatrixRTCEvent) string { if trigger.StateKey != "" { return trigger.StateKey } deviceID := trigger.SessionID if deviceID == "" { deviceID = trigger.DeviceID } if trigger.Sender == "" { return deviceID } return voip.MatrixRTCStateKey(trigger.Sender, deviceID) } func matrixRTCTriggerStickyKey(trigger voip.MatrixRTCEvent) string { if stickyKey, ok := trigger.Raw["sticky_key"].(string); ok && stickyKey != "" { return stickyKey } if stickyKey, ok := trigger.Raw["msc4354_sticky_key"].(string); ok && stickyKey != "" { return stickyKey } return matrixRTCTriggerParticipantID(trigger) } func sendMatrixRTCMessage(ctx context.Context, intent bridgev2.MatrixAPI, roomID id.RoomID, eventType event.Type, raw map[string]any, sticky time.Duration) (*mautrix.RespSendEvent, error) { if asIntent, ok := intent.(*mxbridge.ASIntent); ok { return asIntent.Matrix.SendMessageEvent(ctx, roomID, eventType, &event.Content{Raw: raw}, mautrix.ReqSendEvent{ UnstableStickyDuration: sticky, DontEncrypt: true, }) } return intent.SendMessage(ctx, roomID, eventType, &event.Content{Raw: raw}, nil) } func requestMatrixOpenIDToken(ctx context.Context, intent bridgev2.MatrixAPI) (voip.MatrixOpenIDToken, error) { asIntent, ok := intent.(*mxbridge.ASIntent) if !ok { return voip.MatrixOpenIDToken{}, fmt.Errorf("matrix intent %T does not support OpenID token requests", intent) } if asIntent.Matrix == nil || asIntent.Matrix.Client == nil { return voip.MatrixOpenIDToken{}, fmt.Errorf("matrix intent %T has no Matrix client", intent) } resp, err := asIntent.Matrix.Client.RequestOpenIDToken(ctx) if err != nil { return voip.MatrixOpenIDToken{}, err } return voip.MatrixOpenIDToken{ AccessToken: resp.AccessToken, TokenType: resp.TokenType, MatrixServerName: resp.MatrixServerName, ExpiresIn: resp.ExpiresIn, }, nil } func matrixRTCCompatAllowsModern(mode string) bool { switch strings.ToLower(mode) { case "legacy", "legacy_only", "msc3401", "org.matrix.msc3401.call.member": return false default: return true } } func matrixRTCCompatAllowsLegacy(mode string) bool { switch strings.ToLower(mode) { case "modern", "modern_only", "msc4143", "org.matrix.msc4143.rtc.member", "none", "off", "false", "disabled": return false default: return true } }