mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-04 16:23:22 +00:00
FIX - complete LB custom app compatibility (#11)
* 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>
This commit is contained in:
co-authored by
DerEchteAlec
DerEchteAlec
parent
9aec3eefef
commit
30559048fd
@@ -0,0 +1,288 @@
|
||||
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")
|
||||
Reference in New Issue
Block a user