mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-28 17:01:18 +00:00
Merge branch 'dev' into feat/inter-font
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -130,6 +130,30 @@ export const useCallsStore = defineStore('calls', () => {
|
||||
return response
|
||||
}
|
||||
|
||||
async function setMuted(
|
||||
enabled: boolean,
|
||||
): Promise<NuiResponse<{ muted: boolean }>> {
|
||||
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<NuiResponse> {
|
||||
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,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleCallMute(): Promise<void> {
|
||||
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(() => {
|
||||
<sky-button
|
||||
rounded
|
||||
class="phone-call-action"
|
||||
:class="{ 'is-active': callMuted }"
|
||||
@click="callMuted = !callMuted"
|
||||
:class="{
|
||||
'is-active': calls.activeCall.muted,
|
||||
'is-disabled':
|
||||
calls.activeCall.state !== 'connected' ||
|
||||
!calls.activeCall.muteSupported,
|
||||
}"
|
||||
:disabled="
|
||||
calls.activeCall.state !== 'connected' ||
|
||||
!calls.activeCall.muteSupported ||
|
||||
callMutePending
|
||||
"
|
||||
:aria-busy="callMutePending || undefined"
|
||||
:aria-pressed="calls.activeCall.muted === true"
|
||||
@click="toggleCallMute"
|
||||
>
|
||||
<MicOff />
|
||||
<span>{{ phone.t('Apps.phone.mute') }}</span>
|
||||
|
||||
@@ -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(
|
||||
|
||||
+4
-2
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -245,6 +245,7 @@ local server_callbacks = {
|
||||
"calls:dial",
|
||||
"calls:answer",
|
||||
"calls:set-speaker",
|
||||
"calls:set-muted",
|
||||
"calls:decline",
|
||||
"calls:hangup",
|
||||
"calls:block",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user