Bridge.Database.AfterMigration("sky_phone", function() SkyPhoneCalls = {} local calls = {} local active_by_source = {} local active_by_sim = {} local dial_locks = {} local dialing_by_sim = {} local next_voice_channel = 10000 local function uuid() local rows = Bridge.Database.Query("SELECT UUID() AS `id`", {}) if not rows[1] or type(rows[1].id) ~= "string" then error("[sky_phone] Database did not generate a call UUID.") end return rows[1].id end local function trim(value) if type(value) ~= "string" then return nil end return value:match("^%s*(.-)%s*$") end local function scope_for_device(device) if device.account_id then return tonumber(device.account_id), nil end return nil, device.imei end local function current_scope(source) local session, error_response = SkyPhone.RequireSession(source) if not session then return nil, error_response end local device = SkyPhone.LoadDevice(session.imei) if not device then return nil, { success = false, error = "device_not_found" } end local account_id, device_imei = scope_for_device(device) return { account_id = account_id, device = device, device_imei = device_imei, session = session, } end local function scope_condition(scope, alias) local prefix = alias and (alias .. ".") or "" if scope.account_id then return prefix .. "`account_id` = ?", { scope.account_id } end return prefix .. "`device_imei` = ?", { scope.device_imei } end local function find_device_holder(imei) for _, player_source in ipairs(Bridge.Framework.GetPlayers()) do local source = tonumber(player_source) or player_source if SkyPhone.FindDeviceSlots(source, imei)[1] then return source end end return nil end local function airplane_mode(imei) local rows = Bridge.Database.Query([[ SELECT `payload` FROM `sky_phone_device_data` WHERE `device_imei` = ? AND `namespace` = 'settings' LIMIT 1 ]], { imei }) if not rows[1] then return false end local payload = json.decode(rows[1].payload) return payload and payload.settings and payload.settings.airplaneMode == true end local function add_call_entry(call_id, device, direction, status, other_number) local account_id, device_imei = scope_for_device(device) Bridge.Database.Query([[ INSERT INTO `sky_phone_call_entries` (`call_id`, `account_id`, `device_imei`, `direction`, `status`, `other_number`) VALUES (?, ?, ?, ?, ?, ?) ]], { call_id, account_id, device_imei, direction, status, other_number }) end local function send_state(call, source, state, channel) local outgoing = source == call.caller_source TriggerClientEvent("sky_phone:call:state", source, { id = call.id, state = state, direction = outgoing and "outgoing" or "incoming", otherNumber = outgoing and call.callee_number or call.caller_number, startedAt = call.started_at, answeredAt = call.answered_at, channel = channel, }) end local function notify_recents(device, source) if device.account_id then SkyPhone.NotifyAccount(device.account_id, "sky_phone:calls:changed", {}) elseif source then TriggerClientEvent("sky_phone:calls:changed", source) end end local function finish_call(call, status) if not call or call.ended then return end call.ended = true local ended_at = os.time() local duration = call.answered_at and math.max(0, ended_at - call.answered_at) or 0 local callee_status = status if status == "no_answer" or status == "cancelled" then callee_status = "missed" end Bridge.Database.Transaction({ { query = [[ UPDATE `sky_phone_calls` SET `status` = ?, `ended_at` = CURRENT_TIMESTAMP, `duration_seconds` = ? WHERE `id` = ? ]], params = { status, duration, call.id }, }, { query = "UPDATE `sky_phone_call_entries` SET `status` = ? WHERE `call_id` = ? AND `direction` = 'outgoing'", params = { status, call.id }, }, { query = "UPDATE `sky_phone_call_entries` SET `status` = ? WHERE `call_id` = ? AND `direction` = 'incoming'", params = { callee_status, call.id }, }, }) active_by_source[call.caller_source] = nil active_by_sim[call.caller_sim_id] = nil if call.callee_source then active_by_source[call.callee_source] = nil end if call.callee_sim_id then active_by_sim[call.callee_sim_id] = nil end send_state(call, call.caller_source, status) if call.callee_source then send_state(call, call.callee_source, callee_status) end notify_recents(call.caller_device, call.caller_source) if call.callee_device then notify_recents(call.callee_device, call.callee_source) end calls[call.id] = nil end function SkyPhoneCalls.EndForSim(sim_id, reason) local call_id = active_by_sim[sim_id] if call_id then finish_call(calls[call_id], reason or "ended") end end function SkyPhoneCalls.LinkAccountData(account_id, imei) local counts = Bridge.Database.Query([[ SELECT (SELECT COUNT(*) FROM `sky_phone_contacts` WHERE `account_id` = ?) AS `contacts`, (SELECT COUNT(*) FROM `sky_phone_call_entries` WHERE `account_id` = ?) AS `recents` ]], { account_id, account_id }) local has_cloud_data = counts[1] and ((tonumber(counts[1].contacts) or 0) > 0 or (tonumber(counts[1].recents) or 0) > 0) if has_cloud_data then return Bridge.Database.Transaction({ { query = "DELETE FROM `sky_phone_contacts` WHERE `device_imei` = ? AND `account_id` IS NULL", params = { imei } }, { query = "DELETE FROM `sky_phone_call_entries` WHERE `device_imei` = ? AND `account_id` IS NULL", params = { imei } }, }) end return Bridge.Database.Transaction({ { query = "UPDATE `sky_phone_contacts` SET `account_id` = ?, `device_imei` = NULL WHERE `device_imei` = ? AND `account_id` IS NULL", params = { account_id, imei }, }, { query = "UPDATE `sky_phone_call_entries` SET `account_id` = ?, `device_imei` = NULL WHERE `device_imei` = ? AND `account_id` IS NULL", params = { account_id, imei }, }, }) end function SkyPhoneCalls.CopyCloudToDevice(account_id, imei) return Bridge.Database.Transaction({ { query = "DELETE FROM `sky_phone_contacts` WHERE `device_imei` = ? AND `account_id` IS NULL", params = { imei } }, { query = "DELETE FROM `sky_phone_call_entries` WHERE `device_imei` = ? AND `account_id` IS NULL", params = { imei } }, { query = [[ INSERT INTO `sky_phone_contacts` (`id`, `contact_id`, `device_imei`, `name`, `phone_number`, `created_at`, `updated_at`) SELECT UUID(), `contact_id`, ?, `name`, `phone_number`, `created_at`, `updated_at` FROM `sky_phone_contacts` WHERE `account_id` = ? ]], params = { imei, account_id }, }, { query = [[ INSERT INTO `sky_phone_call_entries` (`call_id`, `device_imei`, `direction`, `status`, `other_number`, `created_at`) SELECT `call_id`, ?, `direction`, `status`, `other_number`, `created_at` FROM `sky_phone_call_entries` WHERE `account_id` = ? ]], params = { imei, account_id }, }, }) end Bridge.Callbacks.Register("sky_phone:contacts:list", function(source) local scope, error_response = current_scope(source) if not scope then return error_response end local condition, params = scope_condition(scope) local rows = Bridge.Database.Query(([[ SELECT `contact_id` AS `id`, `name`, `phone_number`, `created_at`, `updated_at` FROM `sky_phone_contacts` WHERE %s ORDER BY LOWER(`name`), `phone_number` ]]):format(condition), params) return { success = true, data = rows } end) Bridge.Callbacks.Register("sky_phone:contacts:save", function(source, data) if not SkyPhone.AllowOperation(source, "contact_save", 30, 60) or type(data) ~= "table" then return { success = false, error = "invalid_request" } end local scope, error_response = current_scope(source) if not scope then return error_response end local name = trim(data.name) local number = SkyPhoneSimNumber.Normalize(data.phoneNumber, Config.Sim.NumberLength, Config.Sim.NumberPrefix) if not name or name == "" or #name > Config.Calls.ContactNameMaxLength or not number then return { success = false, error = "invalid_contact" } end local condition, condition_params = scope_condition(scope) local id = type(data.id) == "string" and data.id or uuid() if data.id then local owned_params = { id } for _, value in ipairs(condition_params) do owned_params[#owned_params + 1] = value end local owned = Bridge.Database.Query(("SELECT `id` FROM `sky_phone_contacts` WHERE `contact_id` = ? AND %s LIMIT 1"):format(condition), owned_params) if not owned[1] then return { success = false, error = "contact_not_found" } end local params = { name, number, id } for _, value in ipairs(condition_params) do params[#params + 1] = value end Bridge.Database.Query(([[ UPDATE `sky_phone_contacts` SET `name` = ?, `phone_number` = ? WHERE `contact_id` = ? AND %s ]]):format(condition), params) else Bridge.Database.Query([[ INSERT INTO `sky_phone_contacts` (`id`, `contact_id`, `account_id`, `device_imei`, `name`, `phone_number`) VALUES (?, ?, ?, ?, ?, ?) ]], { uuid(), id, scope.account_id, scope.device_imei, name, number }) end if scope.account_id then SkyPhone.NotifyAccount(scope.account_id, "sky_phone:contacts:changed", {}) end return { success = true, data = { id = id, name = name, phone_number = number } } end) Bridge.Callbacks.Register("sky_phone:contacts:delete", function(source, data) if type(data) ~= "table" or type(data.id) ~= "string" then return { success = false, error = "invalid_request" } end local scope, error_response = current_scope(source) if not scope then return error_response end local condition, values = scope_condition(scope) local params = { data.id } for _, value in ipairs(values) do params[#params + 1] = value end Bridge.Database.Query(("DELETE FROM `sky_phone_contacts` WHERE `contact_id` = ? AND %s"):format(condition), params) if scope.account_id then SkyPhone.NotifyAccount(scope.account_id, "sky_phone:contacts:changed", {}) end return { success = true } end) Bridge.Callbacks.Register("sky_phone:calls:recents", function(source) local scope, error_response = current_scope(source) if not scope then return error_response end local condition, params = scope_condition(scope, "e") params[#params + 1] = Config.Calls.RecentPageSize local rows = Bridge.Database.Query(([[ SELECT e.`id`, e.`call_id`, e.`direction`, e.`status`, e.`other_number`, e.`created_at`, c.`duration_seconds` FROM `sky_phone_call_entries` e JOIN `sky_phone_calls` c ON c.`id` = e.`call_id` WHERE %s ORDER BY e.`created_at` DESC, e.`id` DESC LIMIT ? ]]):format(condition), params) return { success = true, data = rows } end) local function create_terminal_call(scope, number, target_sim, status) local id = uuid() Bridge.Database.Query([[ INSERT INTO `sky_phone_calls` (`id`, `caller_sim_id`, `callee_sim_id`, `caller_number`, `callee_number`, `status`, `ended_at`) VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) ]], { id, scope.device.sim_id, target_sim and target_sim.id or nil, scope.device.phone_number, number, status }) add_call_entry(id, scope.device, "outgoing", status, number) notify_recents(scope.device, nil) return { id = id, state = status, direction = "outgoing", otherNumber = number, startedAt = os.time(), } end Bridge.Callbacks.Register("sky_phone:calls:dial", function(source, data) if not SkyPhone.AllowOperation(source, "call_dial", 15, 60) then return { success = false, error = "rate_limited" } end if type(data) ~= "table" then return { success = false, error = "invalid_request" } end if dial_locks[source] then return { success = false, error = "busy" } end dial_locks[source] = true local scope, error_response = current_scope(source) if not scope then dial_locks[source] = nil return error_response end if not scope.device.sim_id then dial_locks[source] = nil return { success = false, error = "no_sim" } end if airplane_mode(scope.device.imei) then dial_locks[source] = nil return { success = false, error = "airplane_mode" } end local number = SkyPhoneSimNumber.Normalize(data.phoneNumber, Config.Sim.NumberLength, Config.Sim.NumberPrefix) if not number then dial_locks[source] = nil return { success = false, error = "invalid_number" } end if number == scope.device.phone_number then dial_locks[source] = nil return { success = false, error = "self_call" } end if active_by_source[source] or active_by_sim[scope.device.sim_id] or dialing_by_sim[scope.device.sim_id] then dial_locks[source] = nil return { success = false, error = "busy" } end dialing_by_sim[scope.device.sim_id] = true local targets = Bridge.Database.Query([[ SELECT s.`id`, s.`phone_number`, d.`imei`, d.`account_id`, d.`device_name` FROM `sky_phone_sims` s LEFT JOIN `sky_phone_devices` d ON d.`sim_id` = s.`id` WHERE s.`phone_number` = ? LIMIT 1 ]], { number }) local target = targets[1] if not target or not target.imei then local terminal = create_terminal_call(scope, number, target, "unavailable") dialing_by_sim[scope.device.sim_id] = nil dial_locks[source] = nil return { success = true, data = terminal } end local callee_source = find_device_holder(target.imei) if not callee_source or airplane_mode(target.imei) then local terminal = create_terminal_call(scope, number, target, "unavailable") dialing_by_sim[scope.device.sim_id] = nil dial_locks[source] = nil return { success = true, data = terminal } end if active_by_source[callee_source] or active_by_sim[target.id] or dialing_by_sim[target.id] then local terminal = create_terminal_call(scope, number, target, "busy") dialing_by_sim[scope.device.sim_id] = nil dial_locks[source] = nil return { success = true, data = terminal } end dialing_by_sim[target.id] = true local id = uuid() local call = { id = id, caller_source = source, caller_sim_id = scope.device.sim_id, caller_number = scope.device.phone_number, caller_device = scope.device, callee_source = callee_source, callee_sim_id = target.id, callee_number = number, callee_device = target, started_at = os.time(), } Bridge.Database.Query([[ INSERT INTO `sky_phone_calls` (`id`, `caller_sim_id`, `callee_sim_id`, `caller_number`, `callee_number`, `status`) VALUES (?, ?, ?, ?, ?, 'ringing') ]], { id, call.caller_sim_id, call.callee_sim_id, call.caller_number, call.callee_number }) add_call_entry(id, scope.device, "outgoing", "ringing", number) add_call_entry(id, target, "incoming", "ringing", call.caller_number) calls[id] = call active_by_source[source] = id active_by_source[callee_source] = id active_by_sim[call.caller_sim_id] = id active_by_sim[call.callee_sim_id] = id dialing_by_sim[call.caller_sim_id] = nil dialing_by_sim[call.callee_sim_id] = nil dial_locks[source] = nil send_state(call, source, "ringing") SkyPhone.OpenDeviceForCall(callee_source, target.imei) TriggerClientEvent("sky_phone:call:incoming", callee_source, { id = id, state = "ringing", direction = "incoming", otherNumber = call.caller_number, startedAt = call.started_at, device = { imei = target.imei, name = target.device_name, }, }) SetTimeout(Config.Calls.RingSeconds * 1000, function() if calls[id] and not calls[id].answered_at then finish_call(calls[id], "no_answer") end end) return { success = true, data = { id = id, state = "ringing", direction = "outgoing", otherNumber = number, startedAt = call.started_at } } end) Bridge.Callbacks.Register("sky_phone:calls:answer", 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 then return { success = false, error = "call_not_found" } end if not SkyPhone.FindDeviceSlots(source, call.callee_device.imei)[1] then finish_call(call, "unavailable") return { success = false, error = "phone_not_owned" } end if Config.Calls.VoiceProvider ~= "pma" or GetResourceState("pma-voice") ~= "started" then return { success = false, error = "voice_unavailable" } end call.answered_at = os.time() call.channel = next_voice_channel next_voice_channel = next_voice_channel + 1 Bridge.Database.Query("UPDATE `sky_phone_calls` SET `status` = 'connected', `answered_at` = CURRENT_TIMESTAMP WHERE `id` = ?", { call.id }) Bridge.Database.Query("UPDATE `sky_phone_call_entries` SET `status` = 'connected' WHERE `call_id` = ?", { call.id }) send_state(call, call.caller_source, "connected", call.channel) send_state(call, call.callee_source, "connected", call.channel) return { success = 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 then return { success = false, error = "call_not_found" } end finish_call(call, "declined") return { success = true } end) Bridge.Callbacks.Register("sky_phone:calls:hangup", function(source, data) local call_id = active_by_source[source] local call = call_id and calls[call_id] or nil if not call or (type(data) == "table" and data.id and data.id ~= call.id) then return { success = false, error = "call_not_found" } end finish_call(call, call.answered_at and "completed" or "cancelled") return { success = true } end) CreateThread(function() while true do Wait(2000) local invalid_calls = {} for call_id, call in pairs(calls) do if not SkyPhone.FindDeviceSlots(call.caller_source, call.caller_device.imei)[1] or (call.callee_source and not SkyPhone.FindDeviceSlots(call.callee_source, call.callee_device.imei)[1]) then invalid_calls[#invalid_calls + 1] = call_id end end for _, call_id in ipairs(invalid_calls) do finish_call(calls[call_id], "disconnected") end end end) AddEventHandler("playerDropped", function() local call_id = active_by_source[source] if call_id then finish_call(calls[call_id], "disconnected") end end) AddEventHandler("onResourceStop", function(resource_name) if resource_name ~= GetCurrentResourceName() then return end local call_ids = {} for call_id in pairs(calls) do call_ids[#call_ids + 1] = call_id end for _, call_id in ipairs(call_ids) do finish_call(calls[call_id], "disconnected") end end) end)