mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-04 16:23:22 +00:00
ADD - calls app and sim cards
This commit is contained in:
@@ -0,0 +1,521 @@
|
||||
if not Sky.DB.AwaitMigrations("sky_phone") then
|
||||
error("[sky_phone] Calling database migrations did not complete.")
|
||||
end
|
||||
|
||||
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 = Sky.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(Sky.FW.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 = Sky.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)
|
||||
Sky.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
|
||||
Sky.DB.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 = Sky.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 Sky.DB.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 Sky.DB.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 Sky.DB.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
|
||||
|
||||
Sky.Cb.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 = Sky.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)
|
||||
|
||||
Sky.Cb.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 = Sky.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
|
||||
Sky.Query(([[
|
||||
UPDATE `sky_phone_contacts` SET `name` = ?, `phone_number` = ?
|
||||
WHERE `contact_id` = ? AND %s
|
||||
]]):format(condition), params)
|
||||
else
|
||||
Sky.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)
|
||||
|
||||
Sky.Cb.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
|
||||
Sky.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)
|
||||
|
||||
Sky.Cb.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 = Sky.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()
|
||||
Sky.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
|
||||
|
||||
Sky.Cb.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 = Sky.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(),
|
||||
}
|
||||
Sky.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)
|
||||
|
||||
Sky.Cb.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
|
||||
Sky.Query("UPDATE `sky_phone_calls` SET `status` = 'connected', `answered_at` = CURRENT_TIMESTAMP WHERE `id` = ?", { call.id })
|
||||
Sky.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)
|
||||
|
||||
Sky.Cb.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)
|
||||
|
||||
Sky.Cb.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)
|
||||
@@ -115,6 +115,27 @@ local schema = {
|
||||
},
|
||||
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
},
|
||||
{
|
||||
name = "sky_phone_sims",
|
||||
columns = {
|
||||
{ name = "id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
|
||||
{ name = "contact_id", type = "CHAR(36) NULL", characterSet = "ascii", collation = "ascii_bin" },
|
||||
{ name = "phone_number", type = "VARCHAR(24) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
|
||||
{ name = "sim_type", type = "ENUM('registered', 'anonymous') NOT NULL" },
|
||||
{ name = "owner_identifier", type = "VARCHAR(80) NULL" },
|
||||
{ name = "owner_firstname", type = "VARCHAR(80) NULL" },
|
||||
{ name = "owner_lastname", type = "VARCHAR(80) NULL" },
|
||||
{ name = "owner_birthdate", type = "VARCHAR(32) NULL" },
|
||||
{ name = "registered_at", type = "DATETIME NULL" },
|
||||
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
|
||||
{ name = "updated_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" },
|
||||
},
|
||||
primaryKey = "id",
|
||||
uniqueKeys = {
|
||||
{ name = "uniq_sky_phone_sim_number", columns = "(`phone_number`)" },
|
||||
},
|
||||
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
},
|
||||
{
|
||||
name = "sky_phone_devices",
|
||||
columns = {
|
||||
@@ -125,6 +146,7 @@ local schema = {
|
||||
collation = "ascii_bin",
|
||||
},
|
||||
{ name = "account_id", type = "BIGINT UNSIGNED NULL" },
|
||||
{ name = "sim_id", type = "CHAR(36) NULL", characterSet = "ascii", collation = "ascii_bin" },
|
||||
{ name = "device_name", type = "VARCHAR(64) NOT NULL DEFAULT 'iFruit Phone'" },
|
||||
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
|
||||
{
|
||||
@@ -133,6 +155,9 @@ local schema = {
|
||||
},
|
||||
},
|
||||
primaryKey = "imei",
|
||||
uniqueKeys = {
|
||||
{ name = "uniq_sky_phone_devices_sim", columns = "(`sim_id`)" },
|
||||
},
|
||||
indexes = {
|
||||
{ name = "idx_sky_phone_devices_account", columns = "(`account_id`, `updated_at`)" },
|
||||
},
|
||||
@@ -141,6 +166,10 @@ local schema = {
|
||||
column = "account_id",
|
||||
references = "`sky_phone_accounts` (`id`) ON DELETE SET NULL",
|
||||
},
|
||||
{
|
||||
column = "sim_id",
|
||||
references = "`sky_phone_sims` (`id`) ON DELETE SET NULL",
|
||||
},
|
||||
},
|
||||
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
},
|
||||
@@ -217,6 +246,81 @@ local schema = {
|
||||
},
|
||||
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
},
|
||||
{
|
||||
name = "sky_phone_contacts",
|
||||
columns = {
|
||||
{ name = "id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
|
||||
{ name = "account_id", type = "BIGINT UNSIGNED NULL" },
|
||||
{ name = "device_imei", type = "CHAR(15) NULL", characterSet = "ascii", collation = "ascii_bin" },
|
||||
{ name = "name", type = "VARCHAR(80) NOT NULL" },
|
||||
{ name = "phone_number", type = "VARCHAR(24) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
|
||||
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
|
||||
{ name = "updated_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" },
|
||||
},
|
||||
primaryKey = "id",
|
||||
indexes = {
|
||||
{ name = "idx_sky_phone_contacts_account", columns = "(`account_id`, `name`)" },
|
||||
{ name = "idx_sky_phone_contacts_device", columns = "(`device_imei`, `name`)" },
|
||||
},
|
||||
foreignKeys = {
|
||||
{ column = "account_id", references = "`sky_phone_accounts` (`id`) ON DELETE CASCADE" },
|
||||
{ column = "device_imei", references = "`sky_phone_devices` (`imei`) ON DELETE CASCADE" },
|
||||
},
|
||||
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
},
|
||||
{
|
||||
name = "sky_phone_calls",
|
||||
columns = {
|
||||
{ name = "id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
|
||||
{ name = "caller_sim_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
|
||||
{ name = "callee_sim_id", type = "CHAR(36) NULL", characterSet = "ascii", collation = "ascii_bin" },
|
||||
{ name = "caller_number", type = "VARCHAR(24) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
|
||||
{ name = "callee_number", type = "VARCHAR(24) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
|
||||
{ name = "status", type = "VARCHAR(24) NOT NULL" },
|
||||
{ name = "started_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
|
||||
{ name = "answered_at", type = "DATETIME NULL" },
|
||||
{ name = "ended_at", type = "DATETIME NULL" },
|
||||
{ name = "duration_seconds", type = "INT UNSIGNED NOT NULL DEFAULT 0" },
|
||||
},
|
||||
primaryKey = "id",
|
||||
indexes = {
|
||||
{ name = "idx_sky_phone_calls_caller", columns = "(`caller_sim_id`, `started_at`)" },
|
||||
{ name = "idx_sky_phone_calls_callee", columns = "(`callee_sim_id`, `started_at`)" },
|
||||
},
|
||||
foreignKeys = {
|
||||
{ column = "caller_sim_id", references = "`sky_phone_sims` (`id`) ON DELETE CASCADE" },
|
||||
{ column = "callee_sim_id", references = "`sky_phone_sims` (`id`) ON DELETE SET NULL" },
|
||||
},
|
||||
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
},
|
||||
{
|
||||
name = "sky_phone_call_entries",
|
||||
columns = {
|
||||
{ name = "id", type = "BIGINT UNSIGNED NOT NULL AUTO_INCREMENT" },
|
||||
{ name = "call_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
|
||||
{ name = "account_id", type = "BIGINT UNSIGNED NULL" },
|
||||
{ name = "device_imei", type = "CHAR(15) NULL", characterSet = "ascii", collation = "ascii_bin" },
|
||||
{ name = "direction", type = "ENUM('incoming', 'outgoing') NOT NULL" },
|
||||
{ name = "status", type = "VARCHAR(24) NOT NULL" },
|
||||
{ name = "other_number", type = "VARCHAR(24) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
|
||||
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
|
||||
},
|
||||
primaryKey = "id",
|
||||
indexes = {
|
||||
{ name = "idx_sky_phone_call_entries_account", columns = "(`account_id`, `created_at`)" },
|
||||
{ name = "idx_sky_phone_call_entries_device", columns = "(`device_imei`, `created_at`)" },
|
||||
},
|
||||
foreignKeys = {
|
||||
{ column = "call_id", references = "`sky_phone_calls` (`id`) ON DELETE CASCADE" },
|
||||
{ column = "account_id", references = "`sky_phone_accounts` (`id`) ON DELETE CASCADE" },
|
||||
{ column = "device_imei", references = "`sky_phone_devices` (`imei`) ON DELETE CASCADE" },
|
||||
},
|
||||
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
},
|
||||
}
|
||||
|
||||
Sky.DB.Migrate("sky_phone", schema)
|
||||
Sky.DB.EnsureIndex("sky_phone_devices", "uniq_sky_phone_devices_sim", "(`sim_id`)", { unique = true })
|
||||
Sky.Query("UPDATE `sky_phone_contacts` SET `contact_id` = `id` WHERE `contact_id` IS NULL", {})
|
||||
Sky.DB.EnsureIndex("sky_phone_contacts", "uniq_sky_phone_contacts_account_contact", "(`account_id`, `contact_id`)", { unique = true })
|
||||
Sky.DB.EnsureIndex("sky_phone_contacts", "uniq_sky_phone_contacts_device_contact", "(`device_imei`, `contact_id`)", { unique = true })
|
||||
|
||||
@@ -216,9 +216,11 @@ end
|
||||
|
||||
local function load_device(imei)
|
||||
local rows = Sky.Query([[
|
||||
SELECT d.`imei`, d.`device_name`, d.`account_id`, d.`created_at`, d.`updated_at`, a.`email`
|
||||
SELECT d.`imei`, d.`device_name`, d.`account_id`, d.`sim_id`, d.`created_at`, d.`updated_at`,
|
||||
a.`email`, s.`phone_number`, s.`sim_type`, s.`registered_at`
|
||||
FROM `sky_phone_devices` d
|
||||
LEFT JOIN `sky_phone_accounts` a ON a.`id` = d.`account_id`
|
||||
LEFT JOIN `sky_phone_sims` s ON s.`id` = d.`sim_id`
|
||||
WHERE d.`imei` = ?
|
||||
LIMIT 1
|
||||
]], { imei })
|
||||
@@ -270,6 +272,12 @@ local function bootstrap(source)
|
||||
device = {
|
||||
imei = device.imei,
|
||||
name = device.device_name,
|
||||
sim = device.sim_id and {
|
||||
id = device.sim_id,
|
||||
number = device.phone_number,
|
||||
type = device.sim_type,
|
||||
registered = device.registered_at ~= nil,
|
||||
} or nil,
|
||||
data = load_device_data(device.imei),
|
||||
},
|
||||
account = device.account_id and {
|
||||
@@ -288,6 +296,11 @@ local function refresh_source(source)
|
||||
end
|
||||
end
|
||||
|
||||
SkyPhone.EnsureDevice = ensure_device
|
||||
SkyPhone.FindDeviceSlots = find_device_slots
|
||||
SkyPhone.LoadDevice = load_device
|
||||
SkyPhone.RefreshSource = refresh_source
|
||||
|
||||
local function allow_auth_attempt(source)
|
||||
local now = os.time()
|
||||
local attempts = auth_attempts[source]
|
||||
@@ -338,6 +351,9 @@ local function link_account(source, account)
|
||||
return error_response
|
||||
end
|
||||
|
||||
if not SkyPhoneCalls.LinkAccountData(account.id, session.imei) then
|
||||
return { success = false, error = "request_failed" }
|
||||
end
|
||||
if not Sky.DB.Transaction({
|
||||
{
|
||||
query = "UPDATE `sky_phone_devices` SET `account_id` = ? WHERE `imei` = ?",
|
||||
@@ -569,6 +585,21 @@ local function open_phone(source, used_item)
|
||||
return true
|
||||
end
|
||||
|
||||
function SkyPhone.OpenDeviceForCall(source, imei)
|
||||
local matches = find_device_slots(source, imei)
|
||||
if not matches[1] then
|
||||
Sky.Debug("warn", "[sky_phone] Could not open ringing device %s for source %s.", tostring(imei), tostring(source))
|
||||
return false
|
||||
end
|
||||
sessions[source] = {
|
||||
imei = imei,
|
||||
slot = matches[1].slot,
|
||||
token = ("%s:%s:%s"):format(imei, tostring(source), tostring(GetGameTimer())),
|
||||
}
|
||||
TriggerClientEvent("sky_phone:device:open", source, bootstrap(source))
|
||||
return true
|
||||
end
|
||||
|
||||
Sky.Debug(
|
||||
"debug",
|
||||
"[sky_phone] Registering usable item '%s' through inventory '%s'.",
|
||||
@@ -674,6 +705,9 @@ for _, endpoint in ipairs({ "account:logout", "mail:logout" }) do
|
||||
if not account then
|
||||
return error_response
|
||||
end
|
||||
if not SkyPhoneCalls.CopyCloudToDevice(account.id, account.imei) then
|
||||
return { success = false, error = "request_failed" }
|
||||
end
|
||||
Sky.Query("UPDATE `sky_phone_devices` SET `account_id` = NULL WHERE `imei` = ?", { account.imei })
|
||||
refresh_source(source)
|
||||
return { success = true }
|
||||
@@ -738,6 +772,14 @@ Sky.Cb.Register("sky_phone:device:factory-reset", function(source)
|
||||
query = "DELETE FROM `sky_phone_notes` WHERE `device_imei` = ? AND `account_id` IS NULL",
|
||||
params = { session.imei },
|
||||
},
|
||||
{
|
||||
query = "DELETE FROM `sky_phone_contacts` WHERE `device_imei` = ? AND `account_id` IS NULL",
|
||||
params = { session.imei },
|
||||
},
|
||||
{
|
||||
query = "DELETE FROM `sky_phone_call_entries` WHERE `device_imei` = ? AND `account_id` IS NULL",
|
||||
params = { session.imei },
|
||||
},
|
||||
{
|
||||
query = "UPDATE `sky_phone_devices` SET `account_id` = NULL, `device_name` = ? WHERE `imei` = ?",
|
||||
params = { Config.Phone.DeviceName, session.imei },
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
if not Sky.DB.AwaitMigrations("sky_phone") then
|
||||
error("[sky_phone] SIM database migrations did not complete.")
|
||||
end
|
||||
|
||||
SkyPhoneSim = {}
|
||||
|
||||
local pending_insertions = {}
|
||||
local operation_locks = {}
|
||||
local sim_types = {
|
||||
[Config.Sim.RegisteredItem] = "registered",
|
||||
[Config.Sim.AnonymousItem] = "anonymous",
|
||||
}
|
||||
|
||||
local function affected_rows(result)
|
||||
if type(result) == "number" then
|
||||
return result
|
||||
end
|
||||
return type(result) == "table" and tonumber(result.affectedRows) or 0
|
||||
end
|
||||
|
||||
local function uuid()
|
||||
local rows = Sky.Query("SELECT UUID() AS `id`", {})
|
||||
if not rows[1] or type(rows[1].id) ~= "string" then
|
||||
error("[sky_phone] Database did not generate a SIM UUID.")
|
||||
end
|
||||
return rows[1].id
|
||||
end
|
||||
|
||||
local function reserve_sim(sim_type)
|
||||
local sim_id
|
||||
local number = SkyPhoneSimNumber.Reserve(uuid, function(candidate)
|
||||
sim_id = uuid()
|
||||
local result = Sky.Query([[
|
||||
INSERT IGNORE INTO `sky_phone_sims` (`id`, `phone_number`, `sim_type`)
|
||||
VALUES (?, ?, ?)
|
||||
]], { sim_id, candidate, sim_type })
|
||||
return affected_rows(result) == 1
|
||||
end, Config.Sim.NumberLength, Config.Sim.NumberPrefix)
|
||||
if not number then
|
||||
error("[sky_phone] Could not reserve a unique SIM number after 20 attempts.")
|
||||
end
|
||||
return { id = sim_id, phone_number = number, sim_type = sim_type }
|
||||
end
|
||||
|
||||
local function load_sim(sim_id)
|
||||
local rows = Sky.Query("SELECT * FROM `sky_phone_sims` WHERE `id` = ? LIMIT 1", { sim_id })
|
||||
return rows[1]
|
||||
end
|
||||
|
||||
local function sim_metadata(sim)
|
||||
local metadata = {
|
||||
sim_id = sim.id,
|
||||
phone_number = sim.phone_number,
|
||||
formatted_number = SkyPhoneSimNumber.Format(sim.phone_number, Config.Sim.NumberGroups, Config.Sim.NumberLength, Config.Sim.NumberPrefix),
|
||||
sim_type = sim.sim_type,
|
||||
}
|
||||
if sim.sim_type == "registered" and sim.owner_identifier then
|
||||
metadata.firstname = sim.owner_firstname
|
||||
metadata.lastname = sim.owner_lastname
|
||||
metadata.birthdate = sim.owner_birthdate
|
||||
end
|
||||
return metadata
|
||||
end
|
||||
|
||||
local function resolve_used_sim(source, used_item, item_name)
|
||||
local slot_id = tonumber(used_item and (used_item.slot or used_item.id))
|
||||
local slot = slot_id and Sky.FW.GetInventorySlot(source, slot_id) or nil
|
||||
if slot and slot.name == item_name then
|
||||
return slot
|
||||
end
|
||||
local slots = Sky.FW.GetInventorySlotsWithItem(source, item_name)
|
||||
if #slots == 1 then
|
||||
return slots[1]
|
||||
end
|
||||
Sky.Debug("warn", "[sky_phone] Could not resolve exact SIM slot for source %s.", tostring(source))
|
||||
return nil
|
||||
end
|
||||
|
||||
local function ensure_sim(source, slot, sim_type)
|
||||
if (tonumber(slot.amount or slot.count) or 0) ~= 1 then
|
||||
return nil, "sim_stacked"
|
||||
end
|
||||
local metadata = slot.metadata or {}
|
||||
local sim = metadata.sim_id and load_sim(metadata.sim_id) or nil
|
||||
if metadata.sim_id and (not sim or sim.sim_type ~= sim_type or sim.phone_number ~= metadata.phone_number) then
|
||||
return nil, "invalid_sim"
|
||||
end
|
||||
if not sim then
|
||||
sim = reserve_sim(sim_type)
|
||||
if not Sky.FW.SetInventorySlotMetadata(source, slot.slot, sim_metadata(sim)) then
|
||||
Sky.Query("DELETE FROM `sky_phone_sims` WHERE `id` = ?", { sim.id })
|
||||
return nil, "metadata_unsupported"
|
||||
end
|
||||
end
|
||||
return sim
|
||||
end
|
||||
|
||||
local function list_phone_choices(source)
|
||||
local choices = {}
|
||||
for _, slot in ipairs(Sky.FW.GetInventorySlotsWithItem(source, Config.Phone.Item)) do
|
||||
local imei = SkyPhone.EnsureDevice(source, slot)
|
||||
if imei then
|
||||
local device = SkyPhone.LoadDevice(imei)
|
||||
choices[#choices + 1] = {
|
||||
imei = imei,
|
||||
name = device.device_name,
|
||||
occupied = device.sim_id ~= nil,
|
||||
number = device.phone_number,
|
||||
}
|
||||
end
|
||||
end
|
||||
return choices
|
||||
end
|
||||
|
||||
local function rollback_phone_metadata(source, phone_slot, old_sim)
|
||||
local metadata = phone_slot.metadata or {}
|
||||
metadata.sim_id = old_sim and old_sim.id or nil
|
||||
metadata.phone_number = old_sim and old_sim.phone_number or nil
|
||||
metadata.formatted_number = old_sim and SkyPhoneSimNumber.Format(old_sim.phone_number, Config.Sim.NumberGroups, Config.Sim.NumberLength, Config.Sim.NumberPrefix) or nil
|
||||
Sky.FW.SetInventorySlotMetadata(source, phone_slot.slot, metadata)
|
||||
end
|
||||
|
||||
local function insert_sim(source, phone_imei, confirmed)
|
||||
if operation_locks[source] then
|
||||
return { success = false, error = "operation_in_progress" }
|
||||
end
|
||||
local pending = pending_insertions[source]
|
||||
if not pending or GetGameTimer() - pending.created_at > 60000 then
|
||||
pending_insertions[source] = nil
|
||||
return { success = false, error = "sim_request_expired" }
|
||||
end
|
||||
local sim_slot = Sky.FW.GetInventorySlot(source, pending.slot)
|
||||
if not sim_slot or sim_slot.name ~= pending.item_name or not sim_slot.metadata
|
||||
or sim_slot.metadata.sim_id ~= pending.sim.id then
|
||||
return { success = false, error = "sim_not_owned" }
|
||||
end
|
||||
local phone_matches = SkyPhone.FindDeviceSlots(source, phone_imei)
|
||||
if not phone_matches[1] then
|
||||
return { success = false, error = "phone_not_owned" }
|
||||
end
|
||||
local phone_slot = phone_matches[1]
|
||||
local device = SkyPhone.LoadDevice(phone_imei)
|
||||
local old_sim = device.sim_id and load_sim(device.sim_id) or nil
|
||||
if old_sim and not confirmed then
|
||||
return { success = false, error = "confirmation_required", data = { requiresConfirmation = true } }
|
||||
end
|
||||
|
||||
operation_locks[source] = true
|
||||
local phone_metadata = phone_slot.metadata or {}
|
||||
phone_metadata.sim_id = pending.sim.id
|
||||
phone_metadata.phone_number = pending.sim.phone_number
|
||||
phone_metadata.formatted_number = SkyPhoneSimNumber.Format(pending.sim.phone_number, Config.Sim.NumberGroups, Config.Sim.NumberLength, Config.Sim.NumberPrefix)
|
||||
if not Sky.FW.SetInventorySlotMetadata(source, phone_slot.slot, phone_metadata) then
|
||||
operation_locks[source] = nil
|
||||
return { success = false, error = "metadata_unsupported" }
|
||||
end
|
||||
if Sky.FW.RemoveItem(source, pending.item_name, 1, pending.slot) ~= 1 then
|
||||
rollback_phone_metadata(source, phone_slot, old_sim)
|
||||
operation_locks[source] = nil
|
||||
return { success = false, error = "sim_not_owned" }
|
||||
end
|
||||
local old_item_name = old_sim and (old_sim.sim_type == "registered" and Config.Sim.RegisteredItem or Config.Sim.AnonymousItem) or nil
|
||||
if old_sim and not Sky.FW.AddItem(source, old_item_name, 1, pending.slot, sim_metadata(old_sim)) then
|
||||
Sky.FW.AddItem(source, pending.item_name, 1, pending.slot, sim_metadata(pending.sim))
|
||||
rollback_phone_metadata(source, phone_slot, old_sim)
|
||||
operation_locks[source] = nil
|
||||
return { success = false, error = "inventory_full" }
|
||||
end
|
||||
|
||||
local transaction = {
|
||||
{
|
||||
query = "UPDATE `sky_phone_devices` SET `sim_id` = ? WHERE `imei` = ?",
|
||||
params = { pending.sim.id, phone_imei },
|
||||
},
|
||||
}
|
||||
if pending.sim.sim_type == "registered" and not pending.sim.owner_identifier then
|
||||
transaction[#transaction + 1] = {
|
||||
query = [[
|
||||
UPDATE `sky_phone_sims`
|
||||
SET `owner_identifier` = ?, `owner_firstname` = ?, `owner_lastname` = ?,
|
||||
`owner_birthdate` = ?, `registered_at` = CURRENT_TIMESTAMP
|
||||
WHERE `id` = ? AND `owner_identifier` IS NULL
|
||||
]],
|
||||
params = {
|
||||
Sky_Jobs.PlayerCache.GetIdentifier(source),
|
||||
Sky.FW.GetFirstname(source),
|
||||
Sky.FW.GetLastname(source),
|
||||
Sky.FW.GetBirthdate(source),
|
||||
pending.sim.id,
|
||||
},
|
||||
}
|
||||
end
|
||||
if not Sky.DB.Transaction(transaction) then
|
||||
if old_sim then
|
||||
Sky.FW.RemoveItem(source, old_sim.sim_type == "registered" and Config.Sim.RegisteredItem or Config.Sim.AnonymousItem, 1, pending.slot)
|
||||
end
|
||||
Sky.FW.AddItem(source, pending.item_name, 1, pending.slot, sim_metadata(pending.sim))
|
||||
rollback_phone_metadata(source, phone_slot, old_sim)
|
||||
operation_locks[source] = nil
|
||||
return { success = false, error = "request_failed" }
|
||||
end
|
||||
|
||||
pending_insertions[source] = nil
|
||||
operation_locks[source] = nil
|
||||
if old_sim then
|
||||
SkyPhoneCalls.EndForSim(old_sim.id, "sim_removed")
|
||||
end
|
||||
SkyPhone.RefreshDevice(phone_imei)
|
||||
TriggerClientEvent("sky_phone:sim:picker-close", source)
|
||||
return { success = true }
|
||||
end
|
||||
|
||||
local function use_sim(source, used_item)
|
||||
local item_name = used_item and used_item.name
|
||||
local sim_type = item_name and sim_types[item_name]
|
||||
if not sim_type then
|
||||
Sky.Debug("warn", "[sky_phone] Usable SIM callback received an invalid item for source %s.", tostring(source))
|
||||
return false
|
||||
end
|
||||
if operation_locks[source] then
|
||||
TriggerClientEvent("sky_phone:device:error", source, "operation_in_progress")
|
||||
return false
|
||||
end
|
||||
operation_locks[source] = true
|
||||
local slot = resolve_used_sim(source, used_item, item_name)
|
||||
if not slot then
|
||||
operation_locks[source] = nil
|
||||
TriggerClientEvent("sky_phone:device:error", source, "sim_slot_missing")
|
||||
return false
|
||||
end
|
||||
local sim, error_code = ensure_sim(source, slot, sim_type)
|
||||
if not sim then
|
||||
operation_locks[source] = nil
|
||||
TriggerClientEvent("sky_phone:device:error", source, error_code)
|
||||
return false
|
||||
end
|
||||
local choices = list_phone_choices(source)
|
||||
if #choices == 0 then
|
||||
operation_locks[source] = nil
|
||||
TriggerClientEvent("sky_phone:device:error", source, "phone_required")
|
||||
return false
|
||||
end
|
||||
pending_insertions[source] = {
|
||||
created_at = GetGameTimer(),
|
||||
item_name = item_name,
|
||||
sim = sim,
|
||||
slot = slot.slot,
|
||||
}
|
||||
operation_locks[source] = nil
|
||||
if #choices == 1 and not choices[1].occupied then
|
||||
return insert_sim(source, choices[1].imei, false).success
|
||||
end
|
||||
TriggerClientEvent("sky_phone:sim:picker", source, {
|
||||
choices = choices,
|
||||
number = sim.phone_number,
|
||||
})
|
||||
return true
|
||||
end
|
||||
|
||||
Sky.FW.RegisterUsableItem(Config.Sim.RegisteredItem, use_sim, true)
|
||||
Sky.FW.RegisterUsableItem(Config.Sim.AnonymousItem, use_sim, true)
|
||||
|
||||
Sky.Cb.Register("sky_phone:sim:insert", function(source, data)
|
||||
if type(data) ~= "table" or not SkyPhoneImei.IsValid(data.imei) then
|
||||
return { success = false, error = "invalid_request" }
|
||||
end
|
||||
return insert_sim(source, data.imei, data.confirmed == true)
|
||||
end)
|
||||
|
||||
Sky.Cb.Register("sky_phone:sim:eject", function(source)
|
||||
if operation_locks[source] then
|
||||
return { success = false, error = "operation_in_progress" }
|
||||
end
|
||||
local session, error_response = SkyPhone.RequireSession(source)
|
||||
if not session then
|
||||
return error_response
|
||||
end
|
||||
local device = SkyPhone.LoadDevice(session.imei)
|
||||
local sim = device and device.sim_id and load_sim(device.sim_id) or nil
|
||||
if not sim then
|
||||
return { success = false, error = "no_sim" }
|
||||
end
|
||||
local phone_slot = Sky.FW.GetInventorySlot(source, session.slot)
|
||||
local item_name = sim.sim_type == "registered" and Config.Sim.RegisteredItem or Config.Sim.AnonymousItem
|
||||
local metadata = sim_metadata(sim)
|
||||
if not Sky.FW.CanCarryItem(source, item_name, 1, metadata) then
|
||||
return { success = false, error = "inventory_full" }
|
||||
end
|
||||
|
||||
operation_locks[source] = true
|
||||
rollback_phone_metadata(source, phone_slot, nil)
|
||||
if not Sky.FW.AddItem(source, item_name, 1, nil, metadata) then
|
||||
rollback_phone_metadata(source, phone_slot, sim)
|
||||
operation_locks[source] = nil
|
||||
return { success = false, error = "inventory_full" }
|
||||
end
|
||||
if affected_rows(Sky.Query("UPDATE `sky_phone_devices` SET `sim_id` = NULL WHERE `imei` = ? AND `sim_id` = ?", {
|
||||
session.imei,
|
||||
sim.id,
|
||||
})) ~= 1 then
|
||||
Sky.FW.RemoveItem(source, item_name, 1, nil, metadata)
|
||||
rollback_phone_metadata(source, phone_slot, sim)
|
||||
operation_locks[source] = nil
|
||||
return { success = false, error = "request_failed" }
|
||||
end
|
||||
operation_locks[source] = nil
|
||||
SkyPhoneCalls.EndForSim(sim.id, "sim_removed")
|
||||
SkyPhone.RefreshDevice(session.imei)
|
||||
return { success = true }
|
||||
end)
|
||||
|
||||
AddEventHandler("playerDropped", function()
|
||||
pending_insertions[source] = nil
|
||||
operation_locks[source] = nil
|
||||
end)
|
||||
Reference in New Issue
Block a user