mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-28 21:01:30 +00:00
30559048fd
* FIX - preserve LB custom app lifecycle * FIX - reset LB export aliases before startup * FIX - report competing custom app providers * FIX - identify LB app frames as live NUI * FIX - complete LB custom app compatibility * FIX - provide LB PicChat runtime exports * FIX - route LB PicChat through Sky Phone * FIX - return cached LB equipped phone number * FIX - harden PicChat compatibility migration * ENH - complete modular phone provider and Creator APIs * DOC - document the Sky Phone Creator API --------- Co-authored-by: DerEchteAlec <bycraky@gmail.com> Co-authored-by: DerEchteAlec <alec.schitzkat@luwan.io>
289 lines
10 KiB
Lua
289 lines
10 KiB
Lua
local registered_callbacks = {}
|
|
local migration_callback = nil
|
|
local client_events = {}
|
|
local transactions = {}
|
|
local stopped_calls = {}
|
|
local speaker_enabled = true
|
|
|
|
Bridge = {
|
|
Callbacks = {
|
|
Register = function(name, callback)
|
|
assert(type(name) == "string" and type(callback) == "function")
|
|
registered_callbacks[name] = callback
|
|
end,
|
|
},
|
|
Calls = {
|
|
IsAvailable = function()
|
|
return true
|
|
end,
|
|
SetMuted = function()
|
|
return true
|
|
end,
|
|
SetSpeaker = function()
|
|
return true
|
|
end,
|
|
Start = function()
|
|
return true, "yaca"
|
|
end,
|
|
Stop = function(call_id, sources, provider)
|
|
stopped_calls[#stopped_calls + 1] = {
|
|
callId = call_id,
|
|
provider = provider,
|
|
sources = sources,
|
|
}
|
|
end,
|
|
SupportsMute = function()
|
|
return false
|
|
end,
|
|
SupportsSpeaker = function()
|
|
return false
|
|
end,
|
|
},
|
|
Database = {
|
|
AfterMigration = function(name, callback)
|
|
assert(name == "sky_phone")
|
|
migration_callback = callback
|
|
end,
|
|
Query = function()
|
|
return {}
|
|
end,
|
|
Transaction = function(operations)
|
|
transactions[#transactions + 1] = operations
|
|
return true
|
|
end,
|
|
},
|
|
Debug = function()
|
|
end,
|
|
Framework = {
|
|
GetPlayers = function()
|
|
return {}
|
|
end,
|
|
},
|
|
Speaker = {
|
|
IsEnabled = function()
|
|
return speaker_enabled
|
|
end,
|
|
},
|
|
}
|
|
|
|
Config = {
|
|
Calls = {
|
|
RecentPageSize = 50,
|
|
RingSeconds = 30,
|
|
},
|
|
Companies = {
|
|
CallRouting = {
|
|
MaxAttempts = 1,
|
|
RingSeconds = 30,
|
|
},
|
|
},
|
|
Mail = {
|
|
Domain = "test.local",
|
|
LocalPartMaxLength = 32,
|
|
LocalPartMinLength = 3,
|
|
},
|
|
Payphones = {
|
|
Animation = {
|
|
HangupDurationMs = 2000,
|
|
},
|
|
},
|
|
Sim = {
|
|
NumberLength = 7,
|
|
NumberPrefix = "",
|
|
},
|
|
}
|
|
|
|
SkyPhone = {}
|
|
SkyPhoneCompanies = {}
|
|
SkyPhoneSimNumber = {}
|
|
|
|
json = {
|
|
decode = function()
|
|
return {}
|
|
end,
|
|
}
|
|
|
|
function AddEventHandler()
|
|
end
|
|
|
|
function CreateThread()
|
|
end
|
|
|
|
function GetCurrentResourceName()
|
|
return "sky_phone"
|
|
end
|
|
|
|
function SetTimeout()
|
|
end
|
|
|
|
function TriggerClientEvent(name, target, payload)
|
|
client_events[#client_events + 1] = {
|
|
name = name,
|
|
payload = payload,
|
|
target = target,
|
|
}
|
|
end
|
|
|
|
assert(loadfile("sky_phone/source/server/calls.lua"))()
|
|
assert(type(migration_callback) == "function", "calls module must wait for its migration")
|
|
migration_callback()
|
|
|
|
local function find_upvalue(callback, expected_name)
|
|
for index = 1, 100 do
|
|
local name, value = debug.getupvalue(callback, index)
|
|
if not name then
|
|
break
|
|
end
|
|
if name == expected_name then
|
|
return value
|
|
end
|
|
end
|
|
error("missing upvalue: " .. expected_name)
|
|
end
|
|
|
|
local active_lookup = find_upvalue(SkyPhoneCalls.GetForSource, "active_call_for_source")
|
|
local active_by_source = find_upvalue(active_lookup, "active_by_source")
|
|
local calls = find_upvalue(active_lookup, "calls")
|
|
|
|
local function seed_call(call)
|
|
calls[call.id] = call
|
|
active_by_source[call.caller_source] = call.id
|
|
active_by_source[call.callee_source] = call.id
|
|
end
|
|
|
|
local call = {
|
|
id = "550e8400-e29b-41d4-a716-446655440000",
|
|
caller_number = "5550101",
|
|
caller_source = 10,
|
|
callee_number = "5550102",
|
|
callee_source = 20,
|
|
muted = {},
|
|
speakers = {},
|
|
started_at = 1000,
|
|
}
|
|
seed_call(call)
|
|
|
|
assert(SkyPhoneCalls.IsActiveForSource(10), "caller must be active while ringing")
|
|
assert(SkyPhoneCalls.IsActiveForSource(20), "callee must be active while ringing")
|
|
assert(not SkyPhoneCalls.IsActiveForSource("10"), "numeric strings must not be coerced into player sources")
|
|
assert(not SkyPhoneCalls.IsActiveForSource(10.5), "fractional player sources must be rejected")
|
|
|
|
local invalid_call, invalid_error = SkyPhoneCalls.GetForSource("10")
|
|
assert(invalid_call == nil and invalid_error == "invalid_source", "invalid source lookups must be explicit")
|
|
|
|
local caller_state = assert(SkyPhoneCalls.GetForSource(10))
|
|
assert(type(caller_state.id) == "string", "Sky call UUIDs must stay strings")
|
|
assert(caller_state.id == call.id and caller_state.state == "ringing", "ringing call state changed")
|
|
assert(caller_state.direction == "outgoing" and caller_state.otherNumber == "5550102", "caller view changed")
|
|
assert(caller_state.channel == nil, "ringing calls must not expose a voice channel")
|
|
assert(caller_state.caller.source == 10 and caller_state.caller.number == "5550101", "caller identity changed")
|
|
assert(caller_state.callee.source == 20 and caller_state.callee.number == "5550102", "callee identity changed")
|
|
assert(not caller_state.anonymous and not caller_state.video, "unsupported call modes must remain explicit")
|
|
assert(not caller_state.payphone and caller_state.companyId == nil, "ordinary call classification changed")
|
|
|
|
caller_state.state = "ended"
|
|
assert(SkyPhoneCalls.GetForSource(10).state == "ringing", "call lookups must return isolated snapshots")
|
|
|
|
local callee_state = assert(SkyPhoneCalls.GetForSource(20))
|
|
assert(callee_state.direction == "incoming" and callee_state.otherNumber == "5550101", "callee view changed")
|
|
|
|
local by_id_state = assert(SkyPhoneCalls.GetById(call.id))
|
|
assert(by_id_state.id == call.id and by_id_state.direction == "outgoing", "ID lookup must use a caller view")
|
|
by_id_state.caller.number = "changed"
|
|
assert(SkyPhoneCalls.GetById(call.id).caller.number == "5550101", "ID lookups must be isolated")
|
|
local invalid_id_call, invalid_id_error = SkyPhoneCalls.GetById("not-a-uuid")
|
|
assert(invalid_id_call == nil and invalid_id_error == "invalid_call_id", "invalid call IDs must be rejected")
|
|
|
|
call.answered_at = 1010
|
|
call.channel = 42
|
|
call.muted[10] = true
|
|
call.speakers[10] = true
|
|
call.voice_provider = "yaca"
|
|
call.voice_started = true
|
|
|
|
caller_state = assert(SkyPhoneCalls.GetForSource(10))
|
|
assert(caller_state.state == "connected" and caller_state.channel == 42, "connected call state changed")
|
|
assert(caller_state.muted and caller_state.muteSupported, "muted state must be projected for the caller")
|
|
assert(caller_state.speakerEnabled and caller_state.speakerSupported, "speaker state must be projected for the caller")
|
|
|
|
local ended, end_error = SkyPhoneCalls.EndForSource(0)
|
|
assert(not ended and end_error == "invalid_source", "invalid termination sources must be rejected")
|
|
assert(SkyPhoneCalls.EndForSource(10), "authoritative source termination must end the call")
|
|
assert(not SkyPhoneCalls.IsActiveForSource(10) and not SkyPhoneCalls.IsActiveForSource(20), "termination must clear participants")
|
|
assert(calls[call.id] == nil, "termination must remove the authoritative call")
|
|
local ended_id_call, ended_id_error = SkyPhoneCalls.GetById(call.id)
|
|
assert(ended_id_call == nil and ended_id_error == "call_not_found", "ended calls must not remain addressable")
|
|
assert(#stopped_calls == 1 and stopped_calls[1].callId == call.id, "termination must stop the voice backend")
|
|
assert(#transactions == 1, "termination must persist the completed call lifecycle")
|
|
assert(transactions[1][1].params[1] == "completed", "answered calls must remain completed")
|
|
assert(#client_events == 2, "both participants must receive the terminal state")
|
|
assert(client_events[1].payload.channel == nil and client_events[2].payload.channel == nil, "terminal states must drop the voice channel")
|
|
|
|
ended, end_error = SkyPhoneCalls.EndForSource(10)
|
|
assert(not ended and end_error == "call_not_found", "finished calls must not be ended twice")
|
|
|
|
local callback_call = {
|
|
id = "550e8400-e29b-41d4-a716-446655440001",
|
|
caller_number = "5550103",
|
|
caller_source = 30,
|
|
callee_number = "5550104",
|
|
callee_source = 40,
|
|
muted = {},
|
|
speakers = {},
|
|
started_at = 1100,
|
|
}
|
|
seed_call(callback_call)
|
|
|
|
local hangup_response = registered_callbacks["sky_phone:calls:hangup"](30, { id = callback_call.id })
|
|
assert(hangup_response.success, "the existing NUI hangup callback must retain its behavior")
|
|
assert(transactions[2][1].params[1] == "cancelled", "unanswered caller hangups must remain cancelled")
|
|
assert(transactions[2][3].params[1] == "missed", "unanswered callees must retain their missed status")
|
|
|
|
local company_call = {
|
|
id = "550e8400-e29b-41d4-a716-446655440002",
|
|
caller_number = "5550105",
|
|
caller_source = 50,
|
|
callee_number = "911",
|
|
callee_source = 60,
|
|
company_service_call = true,
|
|
muted = {},
|
|
speakers = {},
|
|
started_at = 1200,
|
|
}
|
|
seed_call(company_call)
|
|
assert(SkyPhoneCalls.TerminateForSource(60), "provider termination must end a routed company call")
|
|
assert(calls[company_call.id] == nil, "forced company-call termination must remove the call")
|
|
assert(not SkyPhoneCalls.IsActiveForSource(50) and not SkyPhoneCalls.IsActiveForSource(60),
|
|
"forced company-call termination must clear both participants")
|
|
assert(transactions[3][1].params[1] == "cancelled",
|
|
"forced unanswered company calls must remain cancelled")
|
|
|
|
local provider_call = {
|
|
id = "550e8400-e29b-41d4-a716-446655440003",
|
|
caller_number = "5550106",
|
|
caller_source = 70,
|
|
callee_number = "911",
|
|
callee_source = 80,
|
|
company_service_call = true,
|
|
muted = {},
|
|
speakers = {},
|
|
started_at = 1300,
|
|
}
|
|
seed_call(provider_call)
|
|
local terminate_response = registered_callbacks["sky_phone:calls:terminate"](
|
|
80,
|
|
{ id = provider_call.id }
|
|
)
|
|
assert(terminate_response.success, "provider termination callback must end the participant's call")
|
|
assert(calls[provider_call.id] == nil, "provider termination callback must bypass company rerouting")
|
|
assert(not SkyPhoneCalls.IsActiveForSource(70) and not SkyPhoneCalls.IsActiveForSource(80),
|
|
"provider termination callback must clear both participants")
|
|
local spoofed_terminate = registered_callbacks["sky_phone:calls:terminate"](
|
|
70,
|
|
{ id = "550e8400-e29b-41d4-a716-446655440099" }
|
|
)
|
|
assert(not spoofed_terminate.success and spoofed_terminate.error == "call_not_found",
|
|
"provider termination must revalidate the participant and call ID")
|
|
|
|
print("Server call seam tests passed")
|