mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-30 17:59:02 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c7c5d0175d | |||
| 64a7bc78c6 | |||
| 7985ca08d9 |
@@ -87,7 +87,7 @@ Sky Phone is built to be the **free FiveM phone you can choose without accepting
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| **Frameworks** | ESX Legacy, QBCore, Qbox |
|
| **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 |
|
| **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 |
|
| **Radio** | YACA, PMA Voice, SaltyChat |
|
||||||
| **Housing** | ESX Property, qbx_properties |
|
| **Housing** | ESX Property, qbx_properties |
|
||||||
| **Garages** | Built-in/custom data and a broad set of popular garage providers configured through the bridge |
|
| **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:
|
Phone calls support:
|
||||||
|
|
||||||
|
- YACA
|
||||||
- PMA Voice
|
- PMA Voice
|
||||||
- SaltyChat
|
- SaltyChat
|
||||||
|
|
||||||
@@ -433,10 +434,11 @@ Config.Calls.VoiceProvider = "pma"
|
|||||||
|
|
||||||
Supported values:
|
Supported values:
|
||||||
|
|
||||||
|
- `yaca` or `yaca-voice`
|
||||||
- `pma` or `pma-voice`
|
- `pma` or `pma-voice`
|
||||||
- `saltychat` or `salty`
|
- `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
|
### Radio
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ describe('PhoneSetupAssistant contract', () => {
|
|||||||
expect(source).toContain('WALLPAPER_IDS')
|
expect(source).toContain('WALLPAPER_IDS')
|
||||||
expect(source).toContain('setAllAppNotifications')
|
expect(source).toContain('setAllAppNotifications')
|
||||||
expect(source).toContain('appStore.claimApp')
|
expect(source).toContain('appStore.claimApp')
|
||||||
expect(source).toContain('phone.completeSetup()')
|
expect(source).toContain('await phone.completeSetup()')
|
||||||
|
expect(source).toContain(':disabled="setupCompleteBusy"')
|
||||||
|
expect(source).toContain("phone.t('Setup.ready.saveFailed')")
|
||||||
})
|
})
|
||||||
|
|
||||||
it('persists progress and supports resuming or moving backward', () => {
|
it('persists progress and supports resuming or moving backward', () => {
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ const passcodeLength = ref<4 | 6>(phone.security.length === 4 ? 4 : 6)
|
|||||||
const notificationsEnabled = ref(true)
|
const notificationsEnabled = ref(true)
|
||||||
const notificationSounds = ref(true)
|
const notificationSounds = ref(true)
|
||||||
const selectedApps = ref<BuiltinPhoneAppId[]>(['banking', 'garage', 'skyride'])
|
const selectedApps = ref<BuiltinPhoneAppId[]>(['banking', 'garage', 'skyride'])
|
||||||
|
const setupCompleteBusy = ref(false)
|
||||||
|
const setupCompleteError = ref('')
|
||||||
|
|
||||||
const setupApps = (
|
const setupApps = (
|
||||||
['banking', 'garage', 'skyride', 'citymarkt', 'picstagram', 'snake'] as const
|
['banking', 'garage', 'skyride', 'citymarkt', 'picstagram', 'snake'] as const
|
||||||
@@ -204,8 +206,16 @@ function choosePasscodeLength(length: 4 | 6): void {
|
|||||||
passcodeResetKey.value += 1
|
passcodeResetKey.value += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
function finish(): void {
|
async function finish(): Promise<void> {
|
||||||
phone.completeSetup()
|
if (setupCompleteBusy.value) return
|
||||||
|
setupCompleteBusy.value = true
|
||||||
|
setupCompleteError.value = ''
|
||||||
|
const completed = await phone.completeSetup()
|
||||||
|
setupCompleteBusy.value = false
|
||||||
|
if (!completed) {
|
||||||
|
setupCompleteError.value = phone.t('Setup.ready.saveFailed')
|
||||||
|
return
|
||||||
|
}
|
||||||
emit('complete')
|
emit('complete')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -794,12 +804,29 @@ function skipSetupForDevelopment(): void {
|
|||||||
}}</b></span
|
}}</b></span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<SkyButton class="setup-assistant__primary" @click="finish">{{
|
<SkyButton
|
||||||
phone.t('Setup.ready.enter')
|
class="setup-assistant__primary"
|
||||||
}}</SkyButton>
|
:disabled="setupCompleteBusy"
|
||||||
|
@click="finish"
|
||||||
|
>
|
||||||
|
<SkySpinner
|
||||||
|
v-if="setupCompleteBusy"
|
||||||
|
:label="phone.t('Setup.ready.saving')"
|
||||||
|
:size="18"
|
||||||
|
/>
|
||||||
|
<span v-else>{{ phone.t('Setup.ready.enter') }}</span>
|
||||||
|
</SkyButton>
|
||||||
|
<p
|
||||||
|
v-if="setupCompleteError"
|
||||||
|
class="setup-assistant__error"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
{{ setupCompleteError }}
|
||||||
|
</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="setup-assistant__later"
|
class="setup-assistant__later"
|
||||||
|
:disabled="setupCompleteBusy"
|
||||||
@click="moveTo(0)"
|
@click="moveTo(0)"
|
||||||
>
|
>
|
||||||
{{ phone.t('Setup.ready.review') }}
|
{{ phone.t('Setup.ready.review') }}
|
||||||
|
|||||||
@@ -85,4 +85,16 @@ describe('phone inventory contracts', () => {
|
|||||||
)
|
)
|
||||||
expect(phoneServer).toContain('preferred_device_imeis[source] = imei')
|
expect(phoneServer).toContain('preferred_device_imeis[source] = imei')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps non-unique phones bound to one persistent device per character', () => {
|
||||||
|
const phoneServer = readResourceFile('source/server/phone.lua')
|
||||||
|
const migration = readResourceFile('source/server/db_migrate.lua')
|
||||||
|
|
||||||
|
expect(phoneServer).toContain('if not unique_phones then')
|
||||||
|
expect(phoneServer).toContain('return map_character_device(source, slot)')
|
||||||
|
expect(phoneServer).toContain('FROM `sky_phone_character_devices`')
|
||||||
|
expect(phoneServer).toContain('WHERE `owner_identifier` = ?')
|
||||||
|
expect(migration).toContain('name = "sky_phone_character_devices"')
|
||||||
|
expect(migration).toContain('primaryKey = "owner_identifier"')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -156,6 +156,54 @@ describe('calls store', () => {
|
|||||||
expect(calls.activeCall?.speakerEnabled).toBe(false)
|
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 () => {
|
it('updates a contact favorite and refreshes the contact list', async () => {
|
||||||
vi.mocked(nuiCall)
|
vi.mocked(nuiCall)
|
||||||
.mockResolvedValueOnce({
|
.mockResolvedValueOnce({
|
||||||
|
|||||||
@@ -130,6 +130,30 @@ export const useCallsStore = defineStore('calls', () => {
|
|||||||
return response
|
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> {
|
async function blockNumber(phoneNumber: string): Promise<NuiResponse> {
|
||||||
const response = await nuiCall('calls:block', { phoneNumber })
|
const response = await nuiCall('calls:block', { phoneNumber })
|
||||||
if (response.success && activeCall.value?.otherNumber === phoneNumber) {
|
if (response.success && activeCall.value?.otherNumber === phoneNumber) {
|
||||||
@@ -179,6 +203,7 @@ export const useCallsStore = defineStore('calls', () => {
|
|||||||
recents,
|
recents,
|
||||||
saveContact,
|
saveContact,
|
||||||
setContactFavorite,
|
setContactFavorite,
|
||||||
|
setMuted,
|
||||||
setSpeaker,
|
setSpeaker,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -167,4 +167,66 @@ describe('phone device persistence scope', () => {
|
|||||||
expect(mockNuiCall).toHaveBeenCalledTimes(2)
|
expect(mockNuiCall).toHaveBeenCalledTimes(2)
|
||||||
expect(phone.deviceRevisions.widgets).toBe(1)
|
expect(phone.deviceRevisions.widgets).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('marks setup complete only after the settings save is acknowledged', async () => {
|
||||||
|
const completion = deferredResponse<{ revision: number }>()
|
||||||
|
mockNuiCall.mockReturnValueOnce(completion.promise)
|
||||||
|
const phone = usePhoneStore()
|
||||||
|
phone.open({
|
||||||
|
device: {
|
||||||
|
data: {
|
||||||
|
settings: {
|
||||||
|
payload: {
|
||||||
|
settings: { setupCompleted: false, setupStep: 9 },
|
||||||
|
version: 1,
|
||||||
|
},
|
||||||
|
revision: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
imei: '111',
|
||||||
|
name: 'Phone 111',
|
||||||
|
sim: null,
|
||||||
|
},
|
||||||
|
token: 'session-a',
|
||||||
|
})
|
||||||
|
|
||||||
|
const completed = phone.completeSetup()
|
||||||
|
await Promise.resolve()
|
||||||
|
|
||||||
|
expect(phone.preferences.settings.setupCompleted).toBe(false)
|
||||||
|
completion.resolve({ data: { revision: 4 }, success: true })
|
||||||
|
|
||||||
|
await expect(completed).resolves.toBe(true)
|
||||||
|
expect(phone.preferences.settings.setupCompleted).toBe(true)
|
||||||
|
expect(phone.deviceRevisions.settings).toBe(4)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps setup open when the completion save is rejected', async () => {
|
||||||
|
mockNuiCall.mockResolvedValueOnce({
|
||||||
|
error: 'request_failed',
|
||||||
|
success: false,
|
||||||
|
})
|
||||||
|
const phone = usePhoneStore()
|
||||||
|
phone.open({
|
||||||
|
device: {
|
||||||
|
data: {
|
||||||
|
settings: {
|
||||||
|
payload: {
|
||||||
|
settings: { setupCompleted: false, setupStep: 9 },
|
||||||
|
version: 1,
|
||||||
|
},
|
||||||
|
revision: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
imei: '111',
|
||||||
|
name: 'Phone 111',
|
||||||
|
sim: null,
|
||||||
|
},
|
||||||
|
token: 'session-a',
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(phone.completeSetup()).resolves.toBe(false)
|
||||||
|
expect(phone.preferences.settings.setupCompleted).toBe(false)
|
||||||
|
expect(phone.deviceRevisions.settings).toBe(3)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export type PhoneOpenPayload = {
|
|||||||
token?: string
|
token?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const namespaceQueues = new Map<string, Promise<void>>()
|
const namespaceQueues = new Map<string, Promise<boolean>>()
|
||||||
let nextPersistenceSession = 0
|
let nextPersistenceSession = 0
|
||||||
|
|
||||||
const companiesFallbackLocales = {
|
const companiesFallbackLocales = {
|
||||||
@@ -2658,11 +2658,16 @@ const defaultLocales: LocaleTree = {
|
|||||||
readonly_contact: 'Official company contacts cannot be changed.',
|
readonly_contact: 'Official company contacts cannot be changed.',
|
||||||
rate_limited: 'Too many calls. Try again in a minute.',
|
rate_limited: 'Too many calls. Try again in a minute.',
|
||||||
voice_unavailable: 'The configured phone voice service is unavailable.',
|
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_unavailable:
|
||||||
'Speaker mode is not available for the configured phone voice service.',
|
'Speaker mode is not available for the configured phone voice service.',
|
||||||
speaker_unsupported:
|
speaker_unsupported:
|
||||||
'The configured phone voice service does not support speaker mode.',
|
'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.',
|
inventory_full: 'There is no room for the ejected SIM card.',
|
||||||
operation_in_progress:
|
operation_in_progress:
|
||||||
'Another phone operation is already in progress.',
|
'Another phone operation is already in progress.',
|
||||||
@@ -4944,6 +4949,8 @@ const defaultLocales: LocaleTree = {
|
|||||||
localOnly: 'Stored on this phone',
|
localOnly: 'Stored on this phone',
|
||||||
enter: 'Enter Sky Phone',
|
enter: 'Enter Sky Phone',
|
||||||
review: 'Review Setup',
|
review: 'Review Setup',
|
||||||
|
saveFailed: 'Setup could not be saved. Try again.',
|
||||||
|
saving: 'Saving setup',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Common: {
|
Common: {
|
||||||
@@ -5218,13 +5225,13 @@ export const usePhoneStore = defineStore('phone', {
|
|||||||
JSON.stringify(device.data.settings?.payload ?? null),
|
JSON.stringify(device.data.settings?.payload ?? null),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
saveDeviceNamespace(namespace: string, payload: unknown): void {
|
saveDeviceNamespace(namespace: string, payload: unknown): Promise<boolean> {
|
||||||
const imei = this.device?.imei
|
const imei = this.device?.imei
|
||||||
if (!imei) {
|
if (!imei) {
|
||||||
console.error(
|
console.error(
|
||||||
`[Phone persistence] Could not save ${namespace} without an active device.`,
|
`[Phone persistence] Could not save ${namespace} without an active device.`,
|
||||||
)
|
)
|
||||||
return
|
return Promise.resolve(false)
|
||||||
}
|
}
|
||||||
const generation = this.persistenceGeneration
|
const generation = this.persistenceGeneration
|
||||||
const session = this.persistenceSession
|
const session = this.persistenceSession
|
||||||
@@ -5238,7 +5245,7 @@ export const usePhoneStore = defineStore('phone', {
|
|||||||
this.deviceSessionToken === token
|
this.deviceSessionToken === token
|
||||||
const previous = namespaceQueues.get(queueKey) ?? Promise.resolve()
|
const previous = namespaceQueues.get(queueKey) ?? Promise.resolve()
|
||||||
const queued = previous.then(async () => {
|
const queued = previous.then(async () => {
|
||||||
if (!isCurrentScope()) return
|
if (!isCurrentScope()) return false
|
||||||
const response = await nuiCall<{ revision: number }>('device:save', {
|
const response = await nuiCall<{ revision: number }>('device:save', {
|
||||||
imei,
|
imei,
|
||||||
namespace,
|
namespace,
|
||||||
@@ -5253,13 +5260,21 @@ export const usePhoneStore = defineStore('phone', {
|
|||||||
Number(response.data?.revision) >= 0
|
Number(response.data?.revision) >= 0
|
||||||
) {
|
) {
|
||||||
this.deviceRevisions[namespace] = Number(response.data?.revision)
|
this.deviceRevisions[namespace] = Number(response.data?.revision)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
if (isCurrentScope()) {
|
||||||
|
console.error(
|
||||||
|
`[Phone persistence] Could not save ${namespace}: ${response.error ?? 'request_failed'}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return false
|
||||||
})
|
})
|
||||||
const tracked = queued.finally(() => {
|
const tracked = queued.finally(() => {
|
||||||
if (namespaceQueues.get(queueKey) === tracked)
|
if (namespaceQueues.get(queueKey) === tracked)
|
||||||
namespaceQueues.delete(queueKey)
|
namespaceQueues.delete(queueKey)
|
||||||
})
|
})
|
||||||
namespaceQueues.set(queueKey, tracked)
|
namespaceQueues.set(queueKey, tracked)
|
||||||
|
return tracked
|
||||||
},
|
},
|
||||||
async flushDevicePersistence(): Promise<void> {
|
async flushDevicePersistence(): Promise<void> {
|
||||||
const imei = this.device?.imei
|
const imei = this.device?.imei
|
||||||
@@ -5324,10 +5339,16 @@ export const usePhoneStore = defineStore('phone', {
|
|||||||
)
|
)
|
||||||
this.saveDeviceNamespace('settings', this.preferences)
|
this.saveDeviceNamespace('settings', this.preferences)
|
||||||
},
|
},
|
||||||
completeSetup(): void {
|
async completeSetup(): Promise<boolean> {
|
||||||
this.preferences.settings.setupCompleted = true
|
const completedPreferences = cloneJsonData(this.preferences)
|
||||||
this.preferences.settings.setupStep = PHONE_SETUP_LAST_STEP
|
completedPreferences.settings.setupCompleted = true
|
||||||
this.saveDeviceNamespace('settings', this.preferences)
|
completedPreferences.settings.setupStep = PHONE_SETUP_LAST_STEP
|
||||||
|
const saved = await this.saveDeviceNamespace(
|
||||||
|
'settings',
|
||||||
|
completedPreferences,
|
||||||
|
)
|
||||||
|
if (saved) this.preferences = completedPreferences
|
||||||
|
return saved
|
||||||
},
|
},
|
||||||
resetAfterFactoryReset(): void {
|
resetAfterFactoryReset(): void {
|
||||||
this.persistenceGeneration += 1
|
this.persistenceGeneration += 1
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
const readResourceFile = (path: string) =>
|
||||||
|
readFileSync(new URL(`../../sky_phone/${path}`, import.meta.url), 'utf8')
|
||||||
|
|
||||||
|
const config = readResourceFile('config/config.lua')
|
||||||
|
const testData = readResourceFile('source/server/testdata.lua')
|
||||||
|
|
||||||
|
describe('test data seeding contracts', () => {
|
||||||
|
it('keeps test data disabled while retaining the development phone command', () => {
|
||||||
|
expect(config).toContain('DevelopmentCommand = true')
|
||||||
|
expect(config).toContain(
|
||||||
|
'Enabled = false, -- development/test servers only',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns before registering the test-data command when disabled', () => {
|
||||||
|
expect(
|
||||||
|
testData.indexOf('if not Config.TestData.Enabled then'),
|
||||||
|
).toBeLessThan(testData.indexOf('RegisterCommand(Config.TestData.Command'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('moves an existing player SIM before attaching it to the selected phone', () => {
|
||||||
|
expect(testData).toContain('local function move_sim_to_device')
|
||||||
|
expect(testData).toContain(
|
||||||
|
'SET `sim_id` = NULL WHERE `sim_id` = ? AND `imei` <> ?',
|
||||||
|
)
|
||||||
|
expect(testData).toContain(
|
||||||
|
'SET `sim_id` = ? WHERE `imei` = ? AND `sim_id` IS NULL',
|
||||||
|
)
|
||||||
|
expect(testData).toContain(
|
||||||
|
'local previous_imei = move_sim_to_device(sim.id, imei)',
|
||||||
|
)
|
||||||
|
expect(testData).toContain(
|
||||||
|
'restore_sim_attachment(sim.id, imei, previous_imei)',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('derives distinct DarkChat identifiers from each account', () => {
|
||||||
|
expect(testData).toContain(
|
||||||
|
'local function darkchat_identifiers(account_id)',
|
||||||
|
)
|
||||||
|
expect(testData).toContain(
|
||||||
|
'local user_dark_id, user_invite_code = darkchat_identifiers(account_id)',
|
||||||
|
)
|
||||||
|
expect(testData).toContain(
|
||||||
|
'local bot_dark_id, bot_invite_code = darkchat_identifiers(bot_id)',
|
||||||
|
)
|
||||||
|
expect(testData).not.toContain("'DARK0000000001'")
|
||||||
|
expect(testData).not.toContain("'INV00000001'")
|
||||||
|
})
|
||||||
|
|
||||||
|
it('loads the persisted Flare match before inserting its test message', () => {
|
||||||
|
const matchLookup = testData.indexOf(
|
||||||
|
'SELECT `id` FROM `sky_phone_flare_matches`',
|
||||||
|
)
|
||||||
|
const messageInsert = testData.indexOf(
|
||||||
|
'INSERT INTO `sky_phone_flare_messages`',
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(matchLookup).toBeGreaterThan(-1)
|
||||||
|
expect(messageInsert).toBeGreaterThan(matchLookup)
|
||||||
|
expect(testData).toContain(
|
||||||
|
'stable_uuid("sky_phone:testdata:flare:message:" .. match_id)',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -48,6 +48,8 @@ export type PhoneCall = {
|
|||||||
device?: { imei: string; name: string }
|
device?: { imei: string; name: string }
|
||||||
direction: CallDirection
|
direction: CallDirection
|
||||||
id: string
|
id: string
|
||||||
|
muted?: boolean
|
||||||
|
muteSupported?: boolean
|
||||||
otherNumber: string
|
otherNumber: string
|
||||||
speakerEnabled?: boolean
|
speakerEnabled?: boolean
|
||||||
speakerSupported?: boolean
|
speakerSupported?: boolean
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ const inCallKeypad = ref('')
|
|||||||
const blockDialogOpened = ref(false)
|
const blockDialogOpened = ref(false)
|
||||||
const blockTargetNumber = ref('')
|
const blockTargetNumber = ref('')
|
||||||
const callSpeakerPending = ref(false)
|
const callSpeakerPending = ref(false)
|
||||||
const callMuted = ref(false)
|
const callMutePending = ref(false)
|
||||||
const callElapsedSeconds = ref(0)
|
const callElapsedSeconds = ref(0)
|
||||||
let callClock: number | null = null
|
let callClock: number | null = null
|
||||||
const tabs = [
|
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 {
|
function updateCallElapsed(): void {
|
||||||
const call = calls.activeCall
|
const call = calls.activeCall
|
||||||
if (!call || call.state !== 'connected') {
|
if (!call || call.state !== 'connected') {
|
||||||
@@ -853,8 +873,20 @@ onBeforeUnmount(() => {
|
|||||||
<sky-button
|
<sky-button
|
||||||
rounded
|
rounded
|
||||||
class="phone-call-action"
|
class="phone-call-action"
|
||||||
:class="{ 'is-active': callMuted }"
|
:class="{
|
||||||
@click="callMuted = !callMuted"
|
'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 />
|
<MicOff />
|
||||||
<span>{{ phone.t('Apps.phone.mute') }}</span>
|
<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),
|
new URL('../../sky_phone/source/client/main.lua', import.meta.url),
|
||||||
'utf8',
|
'utf8',
|
||||||
)
|
)
|
||||||
|
const phoneApp = readFileSync(
|
||||||
|
new URL('./views/apps/PhoneApp.vue', import.meta.url),
|
||||||
|
'utf8',
|
||||||
|
)
|
||||||
const clientRadio = readFileSync(
|
const clientRadio = readFileSync(
|
||||||
new URL('../../sky_phone/source/bridge/client/radio.lua', import.meta.url),
|
new URL('../../sky_phone/source/bridge/client/radio.lua', import.meta.url),
|
||||||
'utf8',
|
'utf8',
|
||||||
@@ -76,13 +80,13 @@ describe('voice provider contracts', () => {
|
|||||||
expect(config).toMatch(/Config\.Speaker\s*=\s*\{\s*Enabled\s*=\s*true,/)
|
expect(config).toMatch(/Config\.Speaker\s*=\s*\{\s*Enabled\s*=\s*true,/)
|
||||||
expect(sharedBridge).toContain('function Bridge.Speaker.IsEnabled()')
|
expect(sharedBridge).toContain('function Bridge.Speaker.IsEnabled()')
|
||||||
expect(clientCalls).toContain(
|
expect(clientCalls).toContain(
|
||||||
'Bridge.Speaker.IsEnabled() and resolve_provider() == "saltychat"',
|
'Bridge.Speaker.IsEnabled() and (selected == "yaca" or selected == "saltychat")',
|
||||||
)
|
)
|
||||||
expect(clientRadio).toContain(
|
expect(clientRadio).toContain(
|
||||||
'Bridge.Speaker.IsEnabled() and resolve_provider() == "saltychat"',
|
'Bridge.Speaker.IsEnabled() and resolve_provider() == "saltychat"',
|
||||||
)
|
)
|
||||||
expect(serverVoice).toContain(
|
expect(serverVoice).toContain(
|
||||||
'Bridge.Speaker.IsEnabled() and resolve_call_provider() == "saltychat"',
|
'Bridge.Speaker.IsEnabled() and (selected == "yaca" or selected == "saltychat")',
|
||||||
)
|
)
|
||||||
expect(serverVoice).toContain(
|
expect(serverVoice).toContain(
|
||||||
'Bridge.Speaker.IsEnabled() and resolve_radio_provider() == "saltychat"',
|
'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')
|
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', () => {
|
it('provides safe shared defaults for the optional server radio speaker adapter', () => {
|
||||||
expect(sharedBridge).toContain('function Bridge.Radio.SupportsSpeaker()')
|
expect(sharedBridge).toContain('function Bridge.Radio.SupportsSpeaker()')
|
||||||
expect(sharedBridge).toMatch(
|
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 |
|
| **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 |
|
| **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 |
|
| **Radio** | YACA, PMA Voice, SaltyChat |
|
||||||
| **Housing** | ESX Property, qbx_properties |
|
| **Housing** | ESX Property, qbx_properties |
|
||||||
| **Garages** | Built-in/custom data and a broad set of popular garage providers configured through the bridge |
|
| **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:
|
Phone calls support:
|
||||||
|
|
||||||
|
- YACA
|
||||||
- PMA Voice
|
- PMA Voice
|
||||||
- SaltyChat
|
- SaltyChat
|
||||||
|
|
||||||
@@ -433,10 +434,11 @@ Config.Calls.VoiceProvider = "pma"
|
|||||||
|
|
||||||
Supported values:
|
Supported values:
|
||||||
|
|
||||||
|
- `yaca` or `yaca-voice`
|
||||||
- `pma` or `pma-voice`
|
- `pma` or `pma-voice`
|
||||||
- `saltychat` or `salty`
|
- `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
|
### Radio
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ Config.Phone = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Config.TestData = {
|
Config.TestData = {
|
||||||
Enabled = true,
|
Enabled = false, -- development/test servers only; keep disabled in production
|
||||||
Command = "phonetestdata",
|
Command = "phonetestdata",
|
||||||
AdminOnly = false, -- enable only on development servers; every run is scoped to the executing player's phone
|
AdminOnly = false, -- enable only on development servers; every run is scoped to the executing player's phone
|
||||||
AdminGroups = { "admin", "superadmin" },
|
AdminGroups = { "admin", "superadmin" },
|
||||||
@@ -82,7 +82,7 @@ Config.Speaker = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Config.Calls = {
|
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,
|
RingSeconds = 30,
|
||||||
ContactNameMaxLength = 80,
|
ContactNameMaxLength = 80,
|
||||||
ContactNotesMaxLength = 500,
|
ContactNotesMaxLength = 500,
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ Locales["de"] = {
|
|||||||
ready = {
|
ready = {
|
||||||
eyebrow = "Einrichtung abgeschlossen", title = "Willkommen, {name}", body = "Dein Sky Phone ist eingerichtet und bereit. Du kannst alles später in den Einstellungen anpassen.",
|
eyebrow = "Einrichtung abgeschlossen", title = "Willkommen, {name}", body = "Dein Sky Phone ist eingerichtet und bereit. Du kannst alles später in den Einstellungen anpassen.",
|
||||||
localOnly = "Auf diesem Handy gespeichert", enter = "Sky Phone öffnen", review = "Einrichtung prüfen",
|
localOnly = "Auf diesem Handy gespeichert", enter = "Sky Phone öffnen", review = "Einrichtung prüfen",
|
||||||
|
saving = "Einrichtung wird gespeichert", saveFailed = "Die Einrichtung konnte nicht gespeichert werden. Versuche es erneut.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Common = {
|
Common = {
|
||||||
@@ -1008,8 +1009,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.",
|
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.",
|
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.",
|
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.",
|
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.",
|
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.",
|
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.",
|
sim_not_owned = "Diese SIM Karte ist nicht mehr in deinem Inventar.", phone_not_owned = "Das Handy ist nicht mehr in deinem Inventar.",
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ Locales["en"] = {
|
|||||||
ready = {
|
ready = {
|
||||||
eyebrow = "Setup Complete", title = "Welcome, {name}", body = "Your Sky Phone is configured and ready. Your choices can always be refined in Settings.",
|
eyebrow = "Setup Complete", title = "Welcome, {name}", body = "Your Sky Phone is configured and ready. Your choices can always be refined in Settings.",
|
||||||
localOnly = "Stored on this phone", enter = "Enter Sky Phone", review = "Review Setup",
|
localOnly = "Stored on this phone", enter = "Enter Sky Phone", review = "Review Setup",
|
||||||
|
saving = "Saving setup", saveFailed = "Setup could not be saved. Try again.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Common = {
|
Common = {
|
||||||
@@ -1008,8 +1009,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.",
|
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.",
|
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.",
|
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.",
|
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.",
|
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.",
|
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.",
|
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 = {
|
local provider_resources = {
|
||||||
|
yaca = "yaca-voice",
|
||||||
pma = "pma-voice",
|
pma = "pma-voice",
|
||||||
saltychat = "saltychat",
|
saltychat = "saltychat",
|
||||||
}
|
}
|
||||||
local provider_aliases = {
|
local provider_aliases = {
|
||||||
|
["yaca-voice"] = "yaca",
|
||||||
["pma-voice"] = "pma",
|
["pma-voice"] = "pma",
|
||||||
salty = "saltychat",
|
salty = "saltychat",
|
||||||
}
|
}
|
||||||
@@ -22,7 +24,12 @@ function Bridge.Calls.GetProvider()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Bridge.Calls.SupportsSpeaker()
|
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
|
end
|
||||||
|
|
||||||
function Bridge.Calls.Join(channel)
|
function Bridge.Calls.Join(channel)
|
||||||
@@ -37,8 +44,8 @@ function Bridge.Calls.Join(channel)
|
|||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
if selected == "saltychat" then
|
if selected == "yaca" or selected == "saltychat" then
|
||||||
-- SaltyChat call membership is owned by the server bridge.
|
-- Yaca and SaltyChat call membership is owned by the server bridge.
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -101,6 +101,10 @@ function Bridge.Radio.Join(primary, secondary)
|
|||||||
local selected = resolve_provider()
|
local selected = resolve_provider()
|
||||||
if selected == "yaca" then
|
if selected == "yaca" then
|
||||||
local voice = exports["yaca-voice"]
|
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
|
if not voice:isRadioEnabled() then
|
||||||
voice:enableRadio(true)
|
voice:enableRadio(true)
|
||||||
Wait(100)
|
Wait(100)
|
||||||
@@ -150,9 +154,9 @@ end
|
|||||||
function Bridge.Radio.SetVolume(volume)
|
function Bridge.Radio.SetVolume(volume)
|
||||||
local selected = resolve_provider()
|
local selected = resolve_provider()
|
||||||
if selected == "yaca" then
|
if selected == "yaca" then
|
||||||
exports["yaca-voice"]:changeRadioChannelVolumeRaw(1, volume / 100)
|
exports["yaca-voice"]:changeRadioChannelVolumeRaw(volume / 100, 1)
|
||||||
if Bridge.Radio.SupportsSecondary() then
|
if Bridge.Radio.SupportsSecondary() then
|
||||||
exports["yaca-voice"]:changeRadioChannelVolumeRaw(2, volume / 100)
|
exports["yaca-voice"]:changeRadioChannelVolumeRaw(volume / 100, 2)
|
||||||
end
|
end
|
||||||
elseif selected == "pma" then
|
elseif selected == "pma" then
|
||||||
exports["pma-voice"]:setRadioVolume(volume)
|
exports["pma-voice"]:setRadioVolume(volume)
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
local call_provider_resources = {
|
local call_provider_resources = {
|
||||||
|
yaca = "yaca-voice",
|
||||||
pma = "pma-voice",
|
pma = "pma-voice",
|
||||||
saltychat = "saltychat",
|
saltychat = "saltychat",
|
||||||
}
|
}
|
||||||
local call_provider_aliases = {
|
local call_provider_aliases = {
|
||||||
|
["yaca-voice"] = "yaca",
|
||||||
["pma-voice"] = "pma",
|
["pma-voice"] = "pma",
|
||||||
salty = "saltychat",
|
salty = "saltychat",
|
||||||
}
|
}
|
||||||
@@ -17,6 +19,26 @@ local radio_provider_aliases = {
|
|||||||
salty = "saltychat",
|
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 function resolve_call_provider()
|
||||||
local configured = tostring(Config.Calls.VoiceProvider or "")
|
local configured = tostring(Config.Calls.VoiceProvider or "")
|
||||||
local selected = call_provider_aliases[configured] or configured
|
local selected = call_provider_aliases[configured] or configured
|
||||||
@@ -51,11 +73,17 @@ function Bridge.Calls.GetProvider()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Bridge.Calls.IsAvailable()
|
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
|
end
|
||||||
|
|
||||||
function Bridge.Calls.SupportsSpeaker()
|
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
|
end
|
||||||
|
|
||||||
function Bridge.Calls.Start(identifier, player_handles)
|
function Bridge.Calls.Start(identifier, player_handles)
|
||||||
@@ -63,6 +91,37 @@ function Bridge.Calls.Start(identifier, player_handles)
|
|||||||
if selected == "pma" then
|
if selected == "pma" then
|
||||||
return true, selected
|
return true, selected
|
||||||
end
|
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
|
if selected ~= "saltychat" then
|
||||||
return false, nil
|
return false, nil
|
||||||
end
|
end
|
||||||
@@ -85,6 +144,36 @@ end
|
|||||||
|
|
||||||
function Bridge.Calls.Stop(identifier, player_handles, provider)
|
function Bridge.Calls.Stop(identifier, player_handles, provider)
|
||||||
local selected = provider or resolve_call_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
|
if selected ~= "saltychat" or GetResourceState("saltychat") ~= "started" then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -108,17 +197,47 @@ function Bridge.Calls.SetSpeaker(player_source, enabled, provider)
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
local selected = provider or resolve_call_provider()
|
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
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
local success, error_message = pcall(function()
|
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)
|
end)
|
||||||
if not success then
|
if not success then
|
||||||
Bridge.Debug(
|
Bridge.Debug(
|
||||||
"error",
|
"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(player_source),
|
||||||
tostring(error_message),
|
tostring(error_message),
|
||||||
{ always = true }
|
{ always = true }
|
||||||
|
|||||||
@@ -11,6 +11,14 @@ function Bridge.Speaker.IsEnabled()
|
|||||||
return not Config.Speaker or Config.Speaker.Enabled ~= false
|
return not Config.Speaker or Config.Speaker.Enabled ~= false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Bridge.Calls.SupportsMute()
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function Bridge.Calls.SetMuted()
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
function Bridge.Radio.SupportsSpeaker()
|
function Bridge.Radio.SupportsSpeaker()
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -245,6 +245,7 @@ local server_callbacks = {
|
|||||||
"calls:dial",
|
"calls:dial",
|
||||||
"calls:answer",
|
"calls:answer",
|
||||||
"calls:set-speaker",
|
"calls:set-speaker",
|
||||||
|
"calls:set-muted",
|
||||||
"calls:decline",
|
"calls:decline",
|
||||||
"calls:hangup",
|
"calls:hangup",
|
||||||
"calls:block",
|
"calls:block",
|
||||||
|
|||||||
@@ -113,9 +113,12 @@ end
|
|||||||
local function send_state(call, source, state, channel)
|
local function send_state(call, source, state, channel)
|
||||||
local outgoing = source == call.caller_source
|
local outgoing = source == call.caller_source
|
||||||
local speaker_supported = Bridge.Speaker.IsEnabled() and (
|
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())
|
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 = {
|
local payload = {
|
||||||
id = call.id,
|
id = call.id,
|
||||||
state = state,
|
state = state,
|
||||||
@@ -126,6 +129,8 @@ local function send_state(call, source, state, channel)
|
|||||||
channel = channel,
|
channel = channel,
|
||||||
speakerEnabled = call.speakers and call.speakers[source] == true or false,
|
speakerEnabled = call.speakers and call.speakers[source] == true or false,
|
||||||
speakerSupported = speaker_supported,
|
speakerSupported = speaker_supported,
|
||||||
|
muted = call.muted and call.muted[source] == true or false,
|
||||||
|
muteSupported = mute_supported,
|
||||||
}
|
}
|
||||||
if call.payphone and outgoing then
|
if call.payphone and outgoing then
|
||||||
payload.elapsedSeconds = call.payphone.elapsed_seconds or 0
|
payload.elapsedSeconds = call.payphone.elapsed_seconds or 0
|
||||||
@@ -252,6 +257,7 @@ local function finish_call(call, status)
|
|||||||
end
|
end
|
||||||
Bridge.Calls.Stop(call.id, player_handles, call.voice_provider)
|
Bridge.Calls.Stop(call.id, player_handles, call.voice_provider)
|
||||||
call.speakers = {}
|
call.speakers = {}
|
||||||
|
call.muted = {}
|
||||||
call.voice_started = false
|
call.voice_started = false
|
||||||
end
|
end
|
||||||
local ended_at = os.time()
|
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_provider = voice_provider
|
||||||
call.voice_started = true
|
call.voice_started = true
|
||||||
call.speakers = {}
|
call.speakers = {}
|
||||||
|
call.muted = {}
|
||||||
call.answered_at = os.time()
|
call.answered_at = os.time()
|
||||||
call.channel = next_voice_channel
|
call.channel = next_voice_channel
|
||||||
next_voice_channel = next_voice_channel + 1
|
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
|
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" }
|
return { success = false, error = "call_not_found" }
|
||||||
end
|
end
|
||||||
if call.voice_provider ~= "saltychat" then
|
if call.voice_provider ~= "yaca" and call.voice_provider ~= "saltychat" then
|
||||||
return { success = false, error = "speaker_unsupported" }
|
return { success = false, error = "speaker_unsupported" }
|
||||||
end
|
end
|
||||||
if not Bridge.Speaker.IsEnabled() then
|
if not Bridge.Speaker.IsEnabled() then
|
||||||
@@ -1302,6 +1309,37 @@ Bridge.Callbacks.Register("sky_phone:calls:set-speaker", function(source, data)
|
|||||||
}
|
}
|
||||||
end)
|
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)
|
Bridge.Callbacks.Register("sky_phone:calls:decline", function(source, data)
|
||||||
local call = type(data) == "table" and calls[data.id] or nil
|
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
|
if not call or call.callee_source ~= source or call.answered_at or call.rerouting then
|
||||||
|
|||||||
@@ -43,6 +43,13 @@ local function seed_hash(seed)
|
|||||||
return value
|
return value
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function darkchat_identifiers(account_id)
|
||||||
|
local account_key = tostring(account_id)
|
||||||
|
local dark_id = "DC" .. seed_hash("darkchat:id:" .. account_key):upper()
|
||||||
|
local invite_code = "I" .. seed_hash("darkchat:invite:" .. account_key):sub(1, 10):upper()
|
||||||
|
return dark_id, invite_code
|
||||||
|
end
|
||||||
|
|
||||||
local function database_uuid()
|
local function database_uuid()
|
||||||
local rows = Bridge.Database.Query("SELECT UUID() AS `id`", {})
|
local rows = Bridge.Database.Query("SELECT UUID() AS `id`", {})
|
||||||
local id = rows[1] and rows[1].id
|
local id = rows[1] and rows[1].id
|
||||||
@@ -127,6 +134,71 @@ local function reserve_sim(owner_identifier, firstname, lastname)
|
|||||||
return { id = sim_id, phone_number = number, sim_type = "registered" }
|
return { id = sim_id, phone_number = number, sim_type = "registered" }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function restore_sim_attachment(sim_id, current_imei, previous_imei)
|
||||||
|
local statements = {
|
||||||
|
{
|
||||||
|
query = "UPDATE `sky_phone_devices` SET `sim_id` = NULL WHERE `imei` = ? AND `sim_id` = ?",
|
||||||
|
params = { current_imei, sim_id },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if previous_imei then
|
||||||
|
statements[#statements + 1] = {
|
||||||
|
query = "UPDATE `sky_phone_devices` SET `sim_id` = ? WHERE `imei` = ? AND `sim_id` IS NULL",
|
||||||
|
params = { sim_id, previous_imei },
|
||||||
|
}
|
||||||
|
end
|
||||||
|
if not Bridge.Database.Transaction(statements) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local rows = Bridge.Database.Query(
|
||||||
|
"SELECT `imei` FROM `sky_phone_devices` WHERE `sim_id` = ? LIMIT 1",
|
||||||
|
{ sim_id }
|
||||||
|
)
|
||||||
|
if previous_imei then
|
||||||
|
return rows[1] and rows[1].imei == previous_imei
|
||||||
|
end
|
||||||
|
return rows[1] == nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function move_sim_to_device(sim_id, imei)
|
||||||
|
local rows = Bridge.Database.Query(
|
||||||
|
"SELECT `imei` FROM `sky_phone_devices` WHERE `sim_id` = ? LIMIT 1",
|
||||||
|
{ sim_id }
|
||||||
|
)
|
||||||
|
local previous_imei = rows[1] and rows[1].imei or nil
|
||||||
|
if previous_imei == imei then
|
||||||
|
return previous_imei
|
||||||
|
end
|
||||||
|
|
||||||
|
local moved = Bridge.Database.Transaction({
|
||||||
|
{
|
||||||
|
query = "UPDATE `sky_phone_devices` SET `sim_id` = NULL WHERE `sim_id` = ? AND `imei` <> ?",
|
||||||
|
params = { sim_id, imei },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query = "UPDATE `sky_phone_devices` SET `sim_id` = ? WHERE `imei` = ? AND `sim_id` IS NULL",
|
||||||
|
params = { sim_id, imei },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if not moved then
|
||||||
|
error("[sky_phone] Test data could not move the player's SIM to the selected phone.")
|
||||||
|
end
|
||||||
|
|
||||||
|
rows = Bridge.Database.Query(
|
||||||
|
"SELECT `sim_id` FROM `sky_phone_devices` WHERE `imei` = ? LIMIT 1",
|
||||||
|
{ imei }
|
||||||
|
)
|
||||||
|
if not rows[1] or rows[1].sim_id ~= sim_id then
|
||||||
|
if not restore_sim_attachment(sim_id, imei, previous_imei) then
|
||||||
|
error("[sky_phone] Test data could not verify the SIM move or restore its previous device.")
|
||||||
|
end
|
||||||
|
error("[sky_phone] Test data could not verify the SIM move.")
|
||||||
|
end
|
||||||
|
|
||||||
|
return previous_imei
|
||||||
|
end
|
||||||
|
|
||||||
local function ensure_bot(label, email_local, imei, firstname, lastname)
|
local function ensure_bot(label, email_local, imei, firstname, lastname)
|
||||||
local account = ensure_account(email_local .. "@" .. Config.Mail.Domain)
|
local account = ensure_account(email_local .. "@" .. Config.Mail.Domain)
|
||||||
local sim = reserve_sim("sky_phone:testbot:" .. label, firstname, lastname)
|
local sim = reserve_sim("sky_phone:testbot:" .. label, firstname, lastname)
|
||||||
@@ -667,36 +739,57 @@ local function seed_social_apps(context)
|
|||||||
INSERT IGNORE INTO `sky_phone_flare_profile_photos` (`profile_id`, `media_id`, `sort_order`)
|
INSERT IGNORE INTO `sky_phone_flare_profile_photos` (`profile_id`, `media_id`, `sort_order`)
|
||||||
VALUES (?, ?, 1), (?, ?, 1)
|
VALUES (?, ?, 1), (?, ?, 1)
|
||||||
]], { flare_user, context.media.user_portrait, flare_bot, context.media.bot_two_portrait })
|
]], { flare_user, context.media.user_portrait, flare_bot, context.media.bot_two_portrait })
|
||||||
local match_id = stable_uuid(context.key .. ":flare:match")
|
|
||||||
local account_a = math.min(account_id, bot_two_id)
|
local account_a = math.min(account_id, bot_two_id)
|
||||||
local account_b = math.max(account_id, bot_two_id)
|
local account_b = math.max(account_id, bot_two_id)
|
||||||
|
local proposed_match_id = stable_uuid(
|
||||||
|
("sky_phone:testdata:flare:match:%s:%s"):format(account_a, account_b)
|
||||||
|
)
|
||||||
Bridge.Database.Query([[
|
Bridge.Database.Query([[
|
||||||
INSERT INTO `sky_phone_flare_matches` (`id`, `account_a_id`, `account_b_id`)
|
INSERT INTO `sky_phone_flare_matches` (`id`, `account_a_id`, `account_b_id`)
|
||||||
VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE `created_at` = `created_at`
|
VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE `created_at` = `created_at`
|
||||||
]], { match_id, account_a, account_b })
|
]], { proposed_match_id, account_a, account_b })
|
||||||
local flare_message = stable_uuid(context.key .. ":flare:message")
|
local match_rows = Bridge.Database.Query([[
|
||||||
|
SELECT `id` FROM `sky_phone_flare_matches`
|
||||||
|
WHERE `account_a_id` = ? AND `account_b_id` = ? LIMIT 1
|
||||||
|
]], { account_a, account_b })
|
||||||
|
local match_id = match_rows[1] and match_rows[1].id or nil
|
||||||
|
if type(match_id) ~= "string" then
|
||||||
|
error("[sky_phone] Test data Flare match could not be loaded.")
|
||||||
|
end
|
||||||
|
local flare_body = "Hey! Bereit für einen vollständigen App-Test?"
|
||||||
|
local message_rows = Bridge.Database.Query([[
|
||||||
|
SELECT `id` FROM `sky_phone_flare_messages`
|
||||||
|
WHERE `match_id` = ? AND `sender_account_id` = ? AND `body` = ?
|
||||||
|
ORDER BY `created_at`, `id` LIMIT 1
|
||||||
|
]], { match_id, bot_two_id, flare_body })
|
||||||
|
local flare_message = message_rows[1] and message_rows[1].id
|
||||||
|
or stable_uuid("sky_phone:testdata:flare:message:" .. match_id)
|
||||||
Bridge.Database.Query([[
|
Bridge.Database.Query([[
|
||||||
INSERT INTO `sky_phone_flare_messages` (`id`, `match_id`, `sender_account_id`, `body`, `read_at`)
|
INSERT INTO `sky_phone_flare_messages` (`id`, `match_id`, `sender_account_id`, `body`, `read_at`)
|
||||||
VALUES (?, ?, ?, 'Hey! Bereit für einen vollständigen App-Test?', NULL)
|
VALUES (?, ?, ?, ?, NULL)
|
||||||
ON DUPLICATE KEY UPDATE `body` = VALUES(`body`), `read_at` = NULL
|
ON DUPLICATE KEY UPDATE `body` = VALUES(`body`), `read_at` = NULL
|
||||||
]], { flare_message, match_id, bot_two_id })
|
]], { flare_message, match_id, bot_two_id, flare_body })
|
||||||
end
|
end
|
||||||
|
|
||||||
local function seed_private_and_services(context)
|
local function seed_private_and_services(context)
|
||||||
local account_id = context.account.id
|
local account_id = context.account.id
|
||||||
local bot_id = context.bot_one.account.id
|
local bot_id = context.bot_one.account.id
|
||||||
|
local user_dark_id, user_invite_code = darkchat_identifiers(account_id)
|
||||||
|
local bot_dark_id, bot_invite_code = darkchat_identifiers(bot_id)
|
||||||
Bridge.Database.Query([[
|
Bridge.Database.Query([[
|
||||||
INSERT INTO `sky_phone_darkchat_profiles`
|
INSERT INTO `sky_phone_darkchat_profiles`
|
||||||
(`account_id`, `dark_id`, `invite_code`, `alias`, `avatar_seed`, `notification_mode`, `activity_visible`)
|
(`account_id`, `dark_id`, `invite_code`, `alias`, `avatar_seed`, `notification_mode`, `activity_visible`)
|
||||||
VALUES (?, ?, ?, 'NightTester', 42, 'private', 1)
|
VALUES (?, ?, ?, 'NightTester', 42, 'private', 1)
|
||||||
ON DUPLICATE KEY UPDATE `alias` = VALUES(`alias`), `notification_mode` = VALUES(`notification_mode`)
|
ON DUPLICATE KEY UPDATE `dark_id` = VALUES(`dark_id`), `invite_code` = VALUES(`invite_code`),
|
||||||
]], { account_id, ("DARK%010d"):format(account_id % 10000000000), ("INV%08d"):format(account_id % 100000000) })
|
`alias` = VALUES(`alias`), `notification_mode` = VALUES(`notification_mode`)
|
||||||
|
]], { account_id, user_dark_id, user_invite_code })
|
||||||
Bridge.Database.Query([[
|
Bridge.Database.Query([[
|
||||||
INSERT INTO `sky_phone_darkchat_profiles`
|
INSERT INTO `sky_phone_darkchat_profiles`
|
||||||
(`account_id`, `dark_id`, `invite_code`, `alias`, `avatar_seed`, `notification_mode`, `activity_visible`)
|
(`account_id`, `dark_id`, `invite_code`, `alias`, `avatar_seed`, `notification_mode`, `activity_visible`)
|
||||||
VALUES (?, 'DARK0000000001', 'INV00000001', 'GhostAlex', 17, 'full', 1)
|
VALUES (?, ?, ?, 'GhostAlex', 17, 'full', 1)
|
||||||
ON DUPLICATE KEY UPDATE `alias` = VALUES(`alias`)
|
ON DUPLICATE KEY UPDATE `dark_id` = VALUES(`dark_id`), `invite_code` = VALUES(`invite_code`),
|
||||||
]], { bot_id })
|
`alias` = VALUES(`alias`)
|
||||||
|
]], { bot_id, bot_dark_id, bot_invite_code })
|
||||||
local dark_user = ensure_numeric_profile("sky_phone_darkchat_profiles", account_id)
|
local dark_user = ensure_numeric_profile("sky_phone_darkchat_profiles", account_id)
|
||||||
local dark_bot = ensure_numeric_profile("sky_phone_darkchat_profiles", bot_id)
|
local dark_bot = ensure_numeric_profile("sky_phone_darkchat_profiles", bot_id)
|
||||||
Bridge.Database.Query([[
|
Bridge.Database.Query([[
|
||||||
@@ -897,7 +990,7 @@ local function seed_for_source(source)
|
|||||||
sim = rows[1]
|
sim = rows[1]
|
||||||
else
|
else
|
||||||
sim = reserve_sim(identifier, Bridge.Framework.GetFirstname(source), Bridge.Framework.GetLastname(source))
|
sim = reserve_sim(identifier, Bridge.Framework.GetFirstname(source), Bridge.Framework.GetLastname(source))
|
||||||
Bridge.Database.Query("UPDATE `sky_phone_devices` SET `sim_id` = ? WHERE `imei` = ?", { sim.id, imei })
|
local previous_imei = move_sim_to_device(sim.id, imei)
|
||||||
local metadata = phone_slot.metadata or {}
|
local metadata = phone_slot.metadata or {}
|
||||||
metadata.sim_id = sim.id
|
metadata.sim_id = sim.id
|
||||||
metadata.phone_number = sim.phone_number
|
metadata.phone_number = sim.phone_number
|
||||||
@@ -908,6 +1001,11 @@ local function seed_for_source(source)
|
|||||||
Config.Sim.NumberPrefix
|
Config.Sim.NumberPrefix
|
||||||
)
|
)
|
||||||
if not Bridge.Inventory.SetSlotMetadata(source, phone_slot.slot, metadata) then
|
if not Bridge.Inventory.SetSlotMetadata(source, phone_slot.slot, metadata) then
|
||||||
|
if not restore_sim_attachment(sim.id, imei, previous_imei) then
|
||||||
|
error(
|
||||||
|
"[sky_phone] Test data could not update the phone item's SIM metadata or restore its previous device."
|
||||||
|
)
|
||||||
|
end
|
||||||
error("[sky_phone] Test data could not update the phone item's SIM metadata.")
|
error("[sky_phone] Test data could not update the phone item's SIM metadata.")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user