Files
2026-08-12 18:59:05 +02:00

480 lines
18 KiB
Lua

Bridge.Database.AfterMigration("sky_phone", function()
SkyPhoneSim = {}
local unique_phones = Config.Phone.Unique ~= false
local sim_cards_enabled = Config.Sim.Enabled ~= false
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 = 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 SIM UUID.")
end
return rows[1].id
end
local function reserve_sim(sim_type, is_virtual)
local sim_id
local number = SkyPhoneSimNumber.Reserve(uuid, function(candidate)
if SkyPhoneCompanies.IsServiceNumber(candidate) then
return false
end
sim_id = uuid()
local result = Bridge.Database.Query([[
INSERT IGNORE INTO `sky_phone_sims` (`id`, `phone_number`, `sim_type`, `is_virtual`)
VALUES (?, ?, ?, ?)
]], { sim_id, candidate, sim_type, is_virtual and 1 or 0 })
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 = Bridge.Database.Query("SELECT * FROM `sky_phone_sims` WHERE `id` = ? LIMIT 1", { sim_id })
return rows[1]
end
local function sim_metadata(sim)
local metadata = {
sim_metadata_version = 1,
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
metadata.registered_at = sim.registered_at
end
return metadata
end
local function set_phone_sim_metadata(source, phone_slot, sim)
if not unique_phones then
return true
end
if not phone_slot then
return false
end
local metadata = phone_slot.metadata or {}
metadata.sim_id = sim and sim.id or nil
metadata.phone_number = sim and sim.phone_number or nil
metadata.formatted_number = sim and SkyPhoneSimNumber.Format(
sim.phone_number,
Config.Sim.NumberGroups,
Config.Sim.NumberLength,
Config.Sim.NumberPrefix
) or nil
return Bridge.Inventory.SetSlotMetadata(source, phone_slot.slot, metadata)
end
local function prepare_device(source, phone_slot, imei)
local device = SkyPhone.LoadDevice(imei)
if not device then
return false, "request_failed"
end
local sim = device.sim_id and load_sim(device.sim_id) or nil
if sim_cards_enabled then
if sim and tonumber(sim.is_virtual) == 1 then
local result = Bridge.Database.Query([[
UPDATE `sky_phone_devices`
SET `sim_id` = NULL
WHERE `imei` = ? AND `sim_id` = ?
]], { imei, sim.id })
if affected_rows(result) ~= 1 then
return false, "request_failed"
end
sim = nil
end
if not set_phone_sim_metadata(source, phone_slot, sim) then
return false, "metadata_unsupported"
end
return true
end
if sim then
if not set_phone_sim_metadata(source, phone_slot, sim) then
return false, "metadata_unsupported"
end
return true
end
local automatic_sim = reserve_sim("anonymous", true)
local result = Bridge.Database.Query([[
UPDATE `sky_phone_devices`
SET `sim_id` = ?
WHERE `imei` = ? AND `sim_id` IS NULL
]], { automatic_sim.id, imei })
if affected_rows(result) ~= 1 then
Bridge.Database.Query("DELETE FROM `sky_phone_sims` WHERE `id` = ?", { automatic_sim.id })
device = SkyPhone.LoadDevice(imei)
sim = device and device.sim_id and load_sim(device.sim_id) or nil
if not sim then
return false, "request_failed"
end
if not set_phone_sim_metadata(source, phone_slot, sim) then
return false, "metadata_unsupported"
end
return true
end
if not set_phone_sim_metadata(source, phone_slot, automatic_sim) then
Bridge.Database.Query(
"UPDATE `sky_phone_devices` SET `sim_id` = NULL WHERE `imei` = ? AND `sim_id` = ?",
{ imei, automatic_sim.id }
)
Bridge.Database.Query("DELETE FROM `sky_phone_sims` WHERE `id` = ?", { automatic_sim.id })
return false, "metadata_unsupported"
end
return true
end
SkyPhoneSim.PrepareDevice = prepare_device
local function resolve_used_sim(source, used_item, item_name)
local slot_id = used_item and (used_item.slot or used_item.id)
local slot = slot_id and Bridge.Inventory.GetSlot(source, slot_id) or nil
if slot and slot.name == item_name then
return slot
end
local slots = Bridge.Inventory.GetSlotsWithItem(source, item_name)
if #slots == 1 then
return slots[1]
end
Bridge.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 tonumber(sim.is_virtual) == 1
or sim.sim_type ~= sim_type
or sim.phone_number ~= metadata.phone_number
) then
return nil, "invalid_sim"
end
if sim and SkyPhoneCompanies.IsServiceNumber(sim.phone_number) then
Bridge.Debug(
"error",
"[sky_phone] SIM %s uses reserved company service number %s.",
tostring(sim.id),
tostring(sim.phone_number)
)
return nil, "invalid_sim"
end
if not sim then
sim = reserve_sim(sim_type)
if not Bridge.Inventory.SetSlotMetadata(source, slot.slot, sim_metadata(sim)) then
Bridge.Database.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 = {}
local seen = {}
local choice_error
for _, slot in ipairs(Bridge.Inventory.GetSlotsWithItem(source, Config.Phone.Item)) do
local imei, error_code = SkyPhone.EnsureDevice(source, slot)
if imei and not seen[imei] then
local prepared, prepare_error = prepare_device(source, slot, imei)
if prepared then
local device = SkyPhone.LoadDevice(imei)
choices[#choices + 1] = {
imei = imei,
name = device.device_name,
occupied = device.sim_id ~= nil,
number = device.phone_number,
}
seen[imei] = true
else
Bridge.Debug(
"error",
"[sky_phone] Could not prepare SIM target %s for source %s: %s.",
imei,
tostring(source),
tostring(prepare_error)
)
choice_error = choice_error or prepare_error
end
elseif not imei then
choice_error = choice_error or error_code
end
end
return choices, choice_error
end
local function rollback_phone_metadata(source, phone_slot, old_sim)
set_phone_sim_metadata(source, phone_slot, old_sim)
end
local function insert_sim(source, phone_imei, confirmed)
if not sim_cards_enabled then
return { success = false, error = "disabled" }
end
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
if SkyPhoneCompanies.IsServiceNumber(pending.sim.phone_number) then
pending_insertions[source] = nil
Bridge.Debug(
"error",
"[sky_phone] Refused to insert SIM %s with reserved company service number %s.",
tostring(pending.sim.id),
tostring(pending.sim.phone_number)
)
return { success = false, error = "invalid_sim" }
end
local sim_slot = Bridge.Inventory.GetSlot(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 prepared, prepare_error = prepare_device(source, phone_slot, phone_imei)
if not prepared then
return { success = false, error = prepare_error }
end
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
if not set_phone_sim_metadata(source, phone_slot, pending.sim) then
operation_locks[source] = nil
return { success = false, error = "metadata_unsupported" }
end
if Bridge.Inventory.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 Bridge.Inventory.AddItem(source, old_item_name, 1, pending.slot, sim_metadata(old_sim)) then
Bridge.Inventory.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 = {
Bridge.Framework.GetIdentifier(source),
Bridge.Framework.GetFirstname(source),
Bridge.Framework.GetLastname(source),
Bridge.Framework.GetBirthdate(source),
pending.sim.id,
},
}
end
if not Bridge.Database.Transaction(transaction) then
if old_sim then
Bridge.Inventory.RemoveItem(source, old_sim.sim_type == "registered" and Config.Sim.RegisteredItem or Config.Sim.AnonymousItem, 1, pending.slot)
end
Bridge.Inventory.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
SkyPhoneCompanies.ClearCallAvailability(source)
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)
if not sim_cards_enabled then
return false
end
local item_name = used_item and used_item.name
local sim_type = item_name and sim_types[item_name]
if not sim_type then
Bridge.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, choice_error = list_phone_choices(source)
if #choices == 0 then
operation_locks[source] = nil
TriggerClientEvent("sky_phone:device:error", source, choice_error or "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
-- Register the guarded callbacks in both modes so a resource-only restart can
-- replace registrations left behind in framework-owned usable-item tables.
Bridge.Inventory.RegisterUsableItem(Config.Sim.RegisteredItem, use_sim)
Bridge.Inventory.RegisterUsableItem(Config.Sim.AnonymousItem, use_sim)
Bridge.Callbacks.Register("sky_phone:sim:insert", function(source, data)
if not sim_cards_enabled then
return { success = false, error = "disabled" }
end
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)
Bridge.Callbacks.Register("sky_phone:sim:picker-close", function(source)
if operation_locks[source] then
return { success = false, error = "operation_in_progress" }
end
pending_insertions[source] = nil
return { success = true }
end)
Bridge.Callbacks.Register("sky_phone:sim:eject", function(source)
if not sim_cards_enabled then
return { success = false, error = "disabled" }
end
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 or tonumber(sim.is_virtual) == 1 then
return { success = false, error = "no_sim" }
end
local phone_slot = Bridge.Inventory.GetSlot(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 Bridge.Inventory.CanCarryItem(source, item_name, 1, metadata) then
return { success = false, error = "inventory_full" }
end
operation_locks[source] = true
if not set_phone_sim_metadata(source, phone_slot, nil) then
operation_locks[source] = nil
return { success = false, error = "metadata_unsupported" }
end
if not Bridge.Inventory.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(Bridge.Database.Query("UPDATE `sky_phone_devices` SET `sim_id` = NULL WHERE `imei` = ? AND `sim_id` = ?", {
session.imei,
sim.id,
})) ~= 1 then
Bridge.Inventory.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
SkyPhoneCompanies.ClearCallAvailability(source)
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)
if sim_cards_enabled then
Bridge.Database.Query([[
UPDATE `sky_phone_devices` d
INNER JOIN `sky_phone_sims` s ON s.`id` = d.`sim_id`
SET d.`sim_id` = NULL
WHERE s.`is_virtual` = 1
]], {})
end
end)