diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 39b271f..9252511 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -106,10 +106,18 @@ jobs: - name: Publish GitHub release env: GH_TOKEN: ${{ github.token }} - run: >- - gh release create "$GITHUB_REF_NAME" - "sky_phone-${GITHUB_REF_NAME}.zip" - "sky_phone-${GITHUB_REF_NAME}.zip.sha256" - --verify-tag - --generate-notes - --title "Sky Phone $GITHUB_REF_NAME" + shell: bash + run: | + set -euo pipefail + archive="sky_phone-${GITHUB_REF_NAME}.zip" + + if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then + gh release upload "$GITHUB_REF_NAME" "$archive" "${archive}.sha256" --clobber + else + gh release create "$GITHUB_REF_NAME" \ + "$archive" \ + "${archive}.sha256" \ + --verify-tag \ + --generate-notes \ + --title "Sky Phone $GITHUB_REF_NAME" + fi diff --git a/README.md b/README.md index d97573c..c1976ac 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ Sky Phone is built to be the **free FiveM phone you can choose without accepting | --- | --- | | **Frameworks** | ESX Legacy, QBCore, Qbox | | **Inventories** | ox_inventory, qb-inventory, lj-inventory, qs-inventory, codem-inventory, core_inventory, mf-inventory, smx-inventory, hex_4_inventory, and native ESX inventory | -| **Calls** | PMA Voice, SaltyChat | +| **Calls** | YACA, PMA Voice, SaltyChat | | **Radio** | YACA, PMA Voice, SaltyChat | | **Housing** | ESX Property, qbx_properties | | **Garages** | Built-in/custom data and a broad set of popular garage providers configured through the bridge | @@ -140,6 +140,7 @@ Sky Phone is built to be the **free FiveM phone you can choose without accepting Phone calls support: +- YACA - PMA Voice - SaltyChat @@ -433,10 +434,11 @@ Config.Calls.VoiceProvider = "pma" Supported values: +- `yaca` or `yaca-voice` - `pma` or `pma-voice` - `saltychat` or `salty` -SaltyChat supports the provider-backed call speaker feature. PMA Voice keeps the speaker option unavailable. +YACA supports calls, payphone calls, provider-backed speaker mode, and real microphone mute. SaltyChat supports provider-backed speaker mode. PMA Voice keeps speaker and mute controls unavailable. ### Radio diff --git a/frontend/src/stores/calls.test.ts b/frontend/src/stores/calls.test.ts index f47b1dc..8090f71 100644 --- a/frontend/src/stores/calls.test.ts +++ b/frontend/src/stores/calls.test.ts @@ -156,6 +156,54 @@ describe('calls store', () => { expect(calls.activeCall?.speakerEnabled).toBe(false) }) + it('applies the provider-authoritative mute state for a Yaca call', async () => { + vi.mocked(nuiCall).mockResolvedValueOnce({ + success: true, + data: { muted: true }, + }) + const calls = useCallsStore() + calls.applyCallState({ + direction: 'outgoing', + id: 'call-yaca-mute', + muted: false, + muteSupported: true, + otherNumber: '5551110025', + startedAt: 1, + state: 'connected', + }) + + const response = await calls.setMuted(true) + + expect(response.success).toBe(true) + expect(nuiCall).toHaveBeenCalledWith('calls:set-muted', { + enabled: true, + id: 'call-yaca-mute', + }) + expect(calls.activeCall?.muted).toBe(true) + }) + + it('does not simulate mute state for unsupported voice providers', async () => { + const calls = useCallsStore() + calls.applyCallState({ + direction: 'outgoing', + id: 'call-pma-mute', + muted: false, + muteSupported: false, + otherNumber: '5551110025', + startedAt: 1, + state: 'connected', + }) + + const response = await calls.setMuted(true) + + expect(response).toEqual({ + error: 'mute_unavailable', + success: false, + }) + expect(nuiCall).not.toHaveBeenCalled() + expect(calls.activeCall?.muted).toBe(false) + }) + it('updates a contact favorite and refreshes the contact list', async () => { vi.mocked(nuiCall) .mockResolvedValueOnce({ diff --git a/frontend/src/stores/calls.ts b/frontend/src/stores/calls.ts index e6ab071..dc875c1 100644 --- a/frontend/src/stores/calls.ts +++ b/frontend/src/stores/calls.ts @@ -130,6 +130,30 @@ export const useCallsStore = defineStore('calls', () => { return response } + async function setMuted( + enabled: boolean, + ): Promise> { + const call = activeCall.value + if (!call || call.state !== 'connected') { + return { success: false, error: 'call_not_connected' } + } + if (!call.muteSupported) { + return { success: false, error: 'mute_unavailable' } + } + + const response = await nuiCall<{ muted: boolean }>('calls:set-muted', { + enabled, + id: call.id, + }) + if (response.success && response.data && activeCall.value?.id === call.id) { + activeCall.value = { + ...activeCall.value, + muted: response.data.muted === true, + } + } + return response + } + async function blockNumber(phoneNumber: string): Promise { const response = await nuiCall('calls:block', { phoneNumber }) if (response.success && activeCall.value?.otherNumber === phoneNumber) { @@ -179,6 +203,7 @@ export const useCallsStore = defineStore('calls', () => { recents, saveContact, setContactFavorite, + setMuted, setSpeaker, } }) diff --git a/frontend/src/stores/phone.ts b/frontend/src/stores/phone.ts index 1338a20..6d76e74 100644 --- a/frontend/src/stores/phone.ts +++ b/frontend/src/stores/phone.ts @@ -2658,11 +2658,16 @@ const defaultLocales: LocaleTree = { readonly_contact: 'Official company contacts cannot be changed.', rate_limited: 'Too many calls. Try again in a minute.', voice_unavailable: 'The configured phone voice service is unavailable.', - call_not_connected: 'Connect the call before enabling speaker mode.', + call_not_connected: + 'Connect the call before changing its audio controls.', speaker_unavailable: 'Speaker mode is not available for the configured phone voice service.', speaker_unsupported: 'The configured phone voice service does not support speaker mode.', + mute_unavailable: + 'Mute is not available for the configured phone voice service.', + mute_unsupported: + 'The configured phone voice service does not support mute.', inventory_full: 'There is no room for the ejected SIM card.', operation_in_progress: 'Another phone operation is already in progress.', diff --git a/frontend/src/types/phone.ts b/frontend/src/types/phone.ts index ba2dd88..77a2ac3 100644 --- a/frontend/src/types/phone.ts +++ b/frontend/src/types/phone.ts @@ -48,6 +48,8 @@ export type PhoneCall = { device?: { imei: string; name: string } direction: CallDirection id: string + muted?: boolean + muteSupported?: boolean otherNumber: string speakerEnabled?: boolean speakerSupported?: boolean diff --git a/frontend/src/views/apps/PhoneApp.vue b/frontend/src/views/apps/PhoneApp.vue index d74f2d4..d6816b3 100644 --- a/frontend/src/views/apps/PhoneApp.vue +++ b/frontend/src/views/apps/PhoneApp.vue @@ -100,7 +100,7 @@ const inCallKeypad = ref('') const blockDialogOpened = ref(false) const blockTargetNumber = ref('') const callSpeakerPending = ref(false) -const callMuted = ref(false) +const callMutePending = ref(false) const callElapsedSeconds = ref(0) let callClock: number | null = null const tabs = [ @@ -464,6 +464,26 @@ async function toggleCallSpeaker(): Promise { } } +async function toggleCallMute(): Promise { + const call = calls.activeCall + if ( + !call || + call.state !== 'connected' || + !call.muteSupported || + callMutePending.value + ) { + return + } + + error.value = '' + callMutePending.value = true + const response = await calls.setMuted(!call.muted) + callMutePending.value = false + if (!response.success) { + error.value = phone.t(`Apps.phone.errors.${response.error ?? 'default'}`) + } +} + function updateCallElapsed(): void { const call = calls.activeCall if (!call || call.state !== 'connected') { @@ -853,8 +873,20 @@ onBeforeUnmount(() => { {{ phone.t('Apps.phone.mute') }} diff --git a/frontend/src/voiceProviders.contract.test.ts b/frontend/src/voiceProviders.contract.test.ts index f1cc388..db9a1e4 100644 --- a/frontend/src/voiceProviders.contract.test.ts +++ b/frontend/src/voiceProviders.contract.test.ts @@ -22,6 +22,10 @@ const clientMain = readFileSync( new URL('../../sky_phone/source/client/main.lua', import.meta.url), 'utf8', ) +const phoneApp = readFileSync( + new URL('./views/apps/PhoneApp.vue', import.meta.url), + 'utf8', +) const clientRadio = readFileSync( new URL('../../sky_phone/source/bridge/client/radio.lua', import.meta.url), 'utf8', @@ -76,13 +80,13 @@ describe('voice provider contracts', () => { expect(config).toMatch(/Config\.Speaker\s*=\s*\{\s*Enabled\s*=\s*true,/) expect(sharedBridge).toContain('function Bridge.Speaker.IsEnabled()') expect(clientCalls).toContain( - 'Bridge.Speaker.IsEnabled() and resolve_provider() == "saltychat"', + 'Bridge.Speaker.IsEnabled() and (selected == "yaca" or selected == "saltychat")', ) expect(clientRadio).toContain( 'Bridge.Speaker.IsEnabled() and resolve_provider() == "saltychat"', ) expect(serverVoice).toContain( - 'Bridge.Speaker.IsEnabled() and resolve_call_provider() == "saltychat"', + 'Bridge.Speaker.IsEnabled() and (selected == "yaca" or selected == "saltychat")', ) expect(serverVoice).toContain( 'Bridge.Speaker.IsEnabled() and resolve_radio_provider() == "saltychat"', @@ -90,6 +94,37 @@ describe('voice provider contracts', () => { expect(serverCalls).toContain('if not Bridge.Speaker.IsEnabled() then') }) + it('integrates Yaca calls, speaker mode and provider-backed mute end to end', () => { + expect(config).toContain('yaca (alias: yaca-voice)') + expect(clientCalls).toContain('yaca = "yaca-voice"') + expect(clientCalls).toContain( + 'Yaca and SaltyChat call membership is owned by the server bridge.', + ) + expect(serverVoice).toContain( + 'exports["yaca-voice"]:callPlayer(caller_source, target_source, true)', + ) + expect(serverVoice).toContain( + 'exports["yaca-voice"]:callPlayer(caller_source, target_source, false)', + ) + expect(serverVoice).toContain('exports["yaca-voice"]:enablePhoneSpeaker(') + expect(serverVoice).toContain('exports["yaca-voice"]:muteOnPhone(') + expect(serverCalls).toContain( + 'Bridge.Callbacks.Register("sky_phone:calls:set-muted"', + ) + expect(clientMain).toContain('"calls:set-muted"') + expect(phoneApp).toContain('@click="toggleCallMute"') + expect(phoneApp).not.toContain('callMuted = !callMuted') + }) + + it('passes Yaca radio volume arguments in the documented order', () => { + expect(clientRadio).toContain( + 'changeRadioChannelVolumeRaw(volume / 100, 1)', + ) + expect(clientRadio).toContain( + 'changeRadioChannelVolumeRaw(volume / 100, 2)', + ) + }) + it('provides safe shared defaults for the optional server radio speaker adapter', () => { expect(sharedBridge).toContain('function Bridge.Radio.SupportsSpeaker()') expect(sharedBridge).toMatch( diff --git a/sky_phone/README.md b/sky_phone/README.md index 20bc70e..cd06bd2 100644 --- a/sky_phone/README.md +++ b/sky_phone/README.md @@ -89,7 +89,7 @@ Sky Phone is built to be the **free FiveM phone you can choose without accepting | --- | --- | | **Frameworks** | ESX Legacy, QBCore, Qbox | | **Inventories** | ox_inventory, qb-inventory, lj-inventory, qs-inventory, codem-inventory, core_inventory, mf-inventory, smx-inventory, hex_4_inventory, and native ESX inventory | -| **Calls** | PMA Voice, SaltyChat | +| **Calls** | YACA, PMA Voice, SaltyChat | | **Radio** | YACA, PMA Voice, SaltyChat | | **Housing** | ESX Property, qbx_properties | | **Garages** | Built-in/custom data and a broad set of popular garage providers configured through the bridge | @@ -140,6 +140,7 @@ Sky Phone is built to be the **free FiveM phone you can choose without accepting Phone calls support: +- YACA - PMA Voice - SaltyChat @@ -433,10 +434,11 @@ Config.Calls.VoiceProvider = "pma" Supported values: +- `yaca` or `yaca-voice` - `pma` or `pma-voice` - `saltychat` or `salty` -SaltyChat supports the provider-backed call speaker feature. PMA Voice keeps the speaker option unavailable. +YACA supports calls, payphone calls, provider-backed speaker mode, and real microphone mute. SaltyChat supports provider-backed speaker mode. PMA Voice keeps speaker and mute controls unavailable. ### Radio diff --git a/sky_phone/config/config.lua b/sky_phone/config/config.lua index 96a0dcc..8c79661 100644 --- a/sky_phone/config/config.lua +++ b/sky_phone/config/config.lua @@ -82,7 +82,7 @@ Config.Speaker = { } Config.Calls = { - VoiceProvider = "pma", -- pma (alias: pma-voice), saltychat (alias: salty) + VoiceProvider = "pma", -- yaca (alias: yaca-voice), pma (alias: pma-voice), saltychat (alias: salty) RingSeconds = 30, ContactNameMaxLength = 80, ContactNotesMaxLength = 500, diff --git a/sky_phone/config/locales/de.lua b/sky_phone/config/locales/de.lua index 1e6d088..497a497 100644 --- a/sky_phone/config/locales/de.lua +++ b/sky_phone/config/locales/de.lua @@ -1008,8 +1008,9 @@ Locales["de"] = { message_unavailable = "Das Gespräch konnte nicht eröffnet werden.", contact_remove_failed = "Der Kontakt konnte nicht entfernt werden.", contact_favorite_failed = "Der Favorit konnte nicht aktualisiert werden.", blocked = "Diese Nummer ist blockiert.", recipient_not_found = "Diese Nummer ist nicht bekannt.", rate_limited = "Zu viele Anrufe. Versuch es in einer Minute erneut.", voice_unavailable = "Der konfigurierte Sprachdienst ist nicht verfügbar.", - call_not_connected = "Schließ den Anruf an, bevor du den Lautsprechermodus ermöglichst.", speaker_unavailable = "Der Lautsprechermodus ist für den konfigurierten Handy-Sprachdienst nicht verfügbar.", + call_not_connected = "Nimm den Anruf an, bevor du die Audiosteuerung änderst.", speaker_unavailable = "Der Lautsprechermodus ist für den konfigurierten Handy-Sprachdienst nicht verfügbar.", speaker_unsupported = "Der konfigurierte Handy-Sprachdienst unterstützt den Lautsprechermodus nicht.", + mute_unavailable = "Die Stummschaltung ist für den konfigurierten Handy-Sprachdienst nicht verfügbar.", mute_unsupported = "Der konfigurierte Handy-Sprachdienst unterstützt keine Stummschaltung.", inventory_full = "Es gibt keinen Platz für die ausgeworfene SIM-Karte.", request_failed = "Die Telefonanfrage ist fehlgeschlagen.", operation_in_progress = "Eine andere Handy-Aktion wird bereits ausgeführt.", sim_request_expired = "Die SIM-Auswahl ist abgelaufen. Verwende die SIM-Karte erneut.", sim_not_owned = "Diese SIM Karte ist nicht mehr in deinem Inventar.", phone_not_owned = "Das Handy ist nicht mehr in deinem Inventar.", diff --git a/sky_phone/config/locales/en.lua b/sky_phone/config/locales/en.lua index cca7783..2adda4e 100644 --- a/sky_phone/config/locales/en.lua +++ b/sky_phone/config/locales/en.lua @@ -1008,8 +1008,9 @@ Locales["en"] = { message_unavailable = "The conversation could not be opened.", contact_remove_failed = "The contact could not be removed.", contact_favorite_failed = "The favorite could not be updated.", blocked = "This number is blocked.", recipient_not_found = "This number is not known.", rate_limited = "Too many calls. Try again in a minute.", voice_unavailable = "The configured phone voice service is unavailable.", - call_not_connected = "Connect the call before enabling speaker mode.", speaker_unavailable = "Speaker mode is not available for the configured phone voice service.", + call_not_connected = "Connect the call before changing its audio controls.", speaker_unavailable = "Speaker mode is not available for the configured phone voice service.", speaker_unsupported = "The configured phone voice service does not support speaker mode.", + mute_unavailable = "Mute is not available for the configured phone voice service.", mute_unsupported = "The configured phone voice service does not support mute.", inventory_full = "There is no room for the ejected SIM card.", request_failed = "The phone request failed.", operation_in_progress = "Another phone operation is already in progress.", sim_request_expired = "The SIM selection expired. Use the SIM card again.", sim_not_owned = "That SIM card is no longer in your inventory.", phone_not_owned = "That phone is no longer in your inventory.", diff --git a/sky_phone/source/bridge/client/calls.lua b/sky_phone/source/bridge/client/calls.lua index 65ed89d..e81b9d4 100644 --- a/sky_phone/source/bridge/client/calls.lua +++ b/sky_phone/source/bridge/client/calls.lua @@ -1,8 +1,10 @@ local provider_resources = { + yaca = "yaca-voice", pma = "pma-voice", saltychat = "saltychat", } local provider_aliases = { + ["yaca-voice"] = "yaca", ["pma-voice"] = "pma", salty = "saltychat", } @@ -22,7 +24,12 @@ function Bridge.Calls.GetProvider() end function Bridge.Calls.SupportsSpeaker() - return Bridge.Speaker.IsEnabled() and resolve_provider() == "saltychat" + local selected = resolve_provider() + return Bridge.Speaker.IsEnabled() and (selected == "yaca" or selected == "saltychat") +end + +function Bridge.Calls.SupportsMute() + return resolve_provider() == "yaca" end function Bridge.Calls.Join(channel) @@ -37,8 +44,8 @@ function Bridge.Calls.Join(channel) return true end - if selected == "saltychat" then - -- SaltyChat call membership is owned by the server bridge. + if selected == "yaca" or selected == "saltychat" then + -- Yaca and SaltyChat call membership is owned by the server bridge. return true end diff --git a/sky_phone/source/bridge/client/radio.lua b/sky_phone/source/bridge/client/radio.lua index 074707f..e6ac91f 100644 --- a/sky_phone/source/bridge/client/radio.lua +++ b/sky_phone/source/bridge/client/radio.lua @@ -101,6 +101,10 @@ function Bridge.Radio.Join(primary, secondary) local selected = resolve_provider() if selected == "yaca" then local voice = exports["yaca-voice"] + if not voice:isEnabled() then + Bridge.Debug("error", "[sky_phone] Yaca is started but its voice system is disabled.") + return false + end if not voice:isRadioEnabled() then voice:enableRadio(true) Wait(100) @@ -150,9 +154,9 @@ end function Bridge.Radio.SetVolume(volume) local selected = resolve_provider() if selected == "yaca" then - exports["yaca-voice"]:changeRadioChannelVolumeRaw(1, volume / 100) + exports["yaca-voice"]:changeRadioChannelVolumeRaw(volume / 100, 1) if Bridge.Radio.SupportsSecondary() then - exports["yaca-voice"]:changeRadioChannelVolumeRaw(2, volume / 100) + exports["yaca-voice"]:changeRadioChannelVolumeRaw(volume / 100, 2) end elseif selected == "pma" then exports["pma-voice"]:setRadioVolume(volume) diff --git a/sky_phone/source/bridge/server/voice.lua b/sky_phone/source/bridge/server/voice.lua index c1672fa..6aee0cb 100644 --- a/sky_phone/source/bridge/server/voice.lua +++ b/sky_phone/source/bridge/server/voice.lua @@ -1,8 +1,10 @@ local call_provider_resources = { + yaca = "yaca-voice", pma = "pma-voice", saltychat = "saltychat", } local call_provider_aliases = { + ["yaca-voice"] = "yaca", ["pma-voice"] = "pma", salty = "saltychat", } @@ -17,6 +19,26 @@ local radio_provider_aliases = { salty = "saltychat", } +local function yaca_is_enabled() + if GetResourceState("yaca-voice") ~= "started" then + return false + end + + local success, enabled = pcall(function() + return exports["yaca-voice"]:isEnabled() + end) + if not success then + Bridge.Debug( + "error", + "[sky_phone] Yaca could not report its availability: %s", + tostring(enabled), + { always = true } + ) + return false + end + return enabled == true +end + local function resolve_call_provider() local configured = tostring(Config.Calls.VoiceProvider or "") local selected = call_provider_aliases[configured] or configured @@ -51,11 +73,17 @@ function Bridge.Calls.GetProvider() end function Bridge.Calls.IsAvailable() - return resolve_call_provider() ~= nil + local selected = resolve_call_provider() + return selected ~= nil and (selected ~= "yaca" or yaca_is_enabled()) end function Bridge.Calls.SupportsSpeaker() - return Bridge.Speaker.IsEnabled() and resolve_call_provider() == "saltychat" + local selected = resolve_call_provider() + return Bridge.Speaker.IsEnabled() and (selected == "yaca" or selected == "saltychat") +end + +function Bridge.Calls.SupportsMute() + return resolve_call_provider() == "yaca" end function Bridge.Calls.Start(identifier, player_handles) @@ -63,6 +91,37 @@ function Bridge.Calls.Start(identifier, player_handles) if selected == "pma" then return true, selected end + if selected == "yaca" then + if not yaca_is_enabled() then + return false, selected + end + local caller_source = tonumber(player_handles[1]) + local target_source = tonumber(player_handles[2]) + if not caller_source or not target_source then + Bridge.Debug( + "error", + "[sky_phone] Yaca refused call %s because a player source was invalid.", + tostring(identifier), + { always = true } + ) + return false, selected + end + + local success, error_message = pcall(function() + exports["yaca-voice"]:callPlayer(caller_source, target_source, true) + end) + if not success then + Bridge.Debug( + "error", + "[sky_phone] Yaca could not start call %s: %s", + tostring(identifier), + tostring(error_message), + { always = true } + ) + return false, selected + end + return true, selected + end if selected ~= "saltychat" then return false, nil end @@ -85,6 +144,36 @@ end function Bridge.Calls.Stop(identifier, player_handles, provider) local selected = provider or resolve_call_provider() + if selected == "yaca" then + if GetResourceState("yaca-voice") ~= "started" then + return + end + local caller_source = tonumber(player_handles[1]) + local target_source = tonumber(player_handles[2]) + if not caller_source or not target_source then + Bridge.Debug( + "error", + "[sky_phone] Yaca could not stop call %s because a player source was invalid.", + tostring(identifier), + { always = true } + ) + return + end + + local success, error_message = pcall(function() + exports["yaca-voice"]:callPlayer(caller_source, target_source, false) + end) + if not success then + Bridge.Debug( + "error", + "[sky_phone] Yaca could not stop call %s: %s", + tostring(identifier), + tostring(error_message), + { always = true } + ) + end + return + end if selected ~= "saltychat" or GetResourceState("saltychat") ~= "started" then return end @@ -108,17 +197,47 @@ function Bridge.Calls.SetSpeaker(player_source, enabled, provider) return false end local selected = provider or resolve_call_provider() - if selected ~= "saltychat" or GetResourceState("saltychat") ~= "started" then + local resource_name = call_provider_resources[selected] + if (selected ~= "yaca" and selected ~= "saltychat") + or GetResourceState(resource_name) ~= "started" + then return false end local success, error_message = pcall(function() - exports.saltychat:SetPhoneSpeaker(tonumber(player_source), enabled == true) + if selected == "yaca" then + exports["yaca-voice"]:enablePhoneSpeaker(tonumber(player_source), enabled == true) + else + exports.saltychat:SetPhoneSpeaker(tonumber(player_source), enabled == true) + end end) if not success then Bridge.Debug( "error", - "[sky_phone] SaltyChat could not update the phone speaker for source %s: %s", + "[sky_phone] %s could not update the phone speaker for source %s: %s", + selected == "yaca" and "Yaca" or "SaltyChat", + tostring(player_source), + tostring(error_message), + { always = true } + ) + return false + end + return true +end + +function Bridge.Calls.SetMuted(player_source, enabled, provider) + local selected = provider or resolve_call_provider() + if selected ~= "yaca" or GetResourceState("yaca-voice") ~= "started" then + return false + end + + local success, error_message = pcall(function() + exports["yaca-voice"]:muteOnPhone(tonumber(player_source), enabled == true) + end) + if not success then + Bridge.Debug( + "error", + "[sky_phone] Yaca could not update the phone mute state for source %s: %s", tostring(player_source), tostring(error_message), { always = true } diff --git a/sky_phone/source/bridge/shared.lua b/sky_phone/source/bridge/shared.lua index 7db3630..04d375b 100644 --- a/sky_phone/source/bridge/shared.lua +++ b/sky_phone/source/bridge/shared.lua @@ -11,6 +11,14 @@ function Bridge.Speaker.IsEnabled() return not Config.Speaker or Config.Speaker.Enabled ~= false end +function Bridge.Calls.SupportsMute() + return false +end + +function Bridge.Calls.SetMuted() + return false +end + function Bridge.Radio.SupportsSpeaker() return false end diff --git a/sky_phone/source/client/main.lua b/sky_phone/source/client/main.lua index 9f128aa..72824fc 100644 --- a/sky_phone/source/client/main.lua +++ b/sky_phone/source/client/main.lua @@ -245,6 +245,7 @@ local server_callbacks = { "calls:dial", "calls:answer", "calls:set-speaker", + "calls:set-muted", "calls:decline", "calls:hangup", "calls:block", diff --git a/sky_phone/source/server/calls.lua b/sky_phone/source/server/calls.lua index 5417624..c6d8b64 100644 --- a/sky_phone/source/server/calls.lua +++ b/sky_phone/source/server/calls.lua @@ -113,9 +113,12 @@ end local function send_state(call, source, state, channel) local outgoing = source == call.caller_source local speaker_supported = Bridge.Speaker.IsEnabled() and ( - call.voice_provider == "saltychat" + call.voice_provider == "yaca" + or call.voice_provider == "saltychat" or (not call.voice_provider and Bridge.Calls.SupportsSpeaker()) ) + local mute_supported = call.voice_provider == "yaca" + or (not call.voice_provider and Bridge.Calls.SupportsMute()) local payload = { id = call.id, state = state, @@ -126,6 +129,8 @@ local function send_state(call, source, state, channel) channel = channel, speakerEnabled = call.speakers and call.speakers[source] == true or false, speakerSupported = speaker_supported, + muted = call.muted and call.muted[source] == true or false, + muteSupported = mute_supported, } if call.payphone and outgoing then payload.elapsedSeconds = call.payphone.elapsed_seconds or 0 @@ -252,6 +257,7 @@ local function finish_call(call, status) end Bridge.Calls.Stop(call.id, player_handles, call.voice_provider) call.speakers = {} + call.muted = {} call.voice_started = false end local ended_at = os.time() @@ -1247,6 +1253,7 @@ Bridge.Callbacks.Register("sky_phone:calls:answer", function(source, data) call.voice_provider = voice_provider call.voice_started = true call.speakers = {} + call.muted = {} call.answered_at = os.time() call.channel = next_voice_channel next_voice_channel = next_voice_channel + 1 @@ -1281,7 +1288,7 @@ Bridge.Callbacks.Register("sky_phone:calls:set-speaker", function(source, data) if not call or call.id ~= data.id or not call.answered_at or call.ended or not call.voice_started then return { success = false, error = "call_not_found" } end - if call.voice_provider ~= "saltychat" then + if call.voice_provider ~= "yaca" and call.voice_provider ~= "saltychat" then return { success = false, error = "speaker_unsupported" } end if not Bridge.Speaker.IsEnabled() then @@ -1302,6 +1309,37 @@ Bridge.Callbacks.Register("sky_phone:calls:set-speaker", function(source, data) } end) +Bridge.Callbacks.Register("sky_phone:calls:set-muted", function(source, data) + if type(data) ~= "table" or type(data.id) ~= "string" or type(data.enabled) ~= "boolean" then + return { success = false, error = "invalid_request" } + end + if not SkyPhone.AllowOperation(source, "call_mute", 30, 60) then + return { success = false, error = "rate_limited" } + end + + local call_id = active_by_source[source] + local call = call_id and calls[call_id] or nil + if not call or call.id ~= data.id or not call.answered_at or call.ended or not call.voice_started then + return { success = false, error = "call_not_found" } + end + if call.voice_provider ~= "yaca" then + return { success = false, error = "mute_unsupported" } + end + if not Bridge.Calls.SetMuted(source, data.enabled, call.voice_provider) then + return { success = false, error = "voice_unavailable" } + end + + call.muted[source] = data.enabled + send_state(call, source, "connected", call.channel) + return { + success = true, + data = { + muted = data.enabled, + muteSupported = true, + }, + } +end) + Bridge.Callbacks.Register("sky_phone:calls:decline", function(source, data) local call = type(data) == "table" and calls[data.id] or nil if not call or call.callee_source ~= source or call.answered_at or call.rerouting then