mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-29 01:01:31 +00:00
ENH - merge dev into Companies feature
This commit is contained in:
@@ -15,6 +15,7 @@ Config.Command = "phone"
|
||||
|
||||
Config.Phone = {
|
||||
Item = "phone",
|
||||
Unique = true, -- true: data follows each phone item; false: one persistent phone per character
|
||||
DevelopmentCommand = true,
|
||||
DeviceName = "iFruit Phone",
|
||||
}
|
||||
@@ -44,6 +45,7 @@ Config.Security = {
|
||||
}
|
||||
|
||||
Config.Sim = {
|
||||
Enabled = true, -- false: devices without a SIM receive a persistent random number automatically
|
||||
RegisteredItem = "sky_phone_sim_registered",
|
||||
AnonymousItem = "sky_phone_sim_anonymous",
|
||||
NumberLength = 10,
|
||||
@@ -55,6 +57,7 @@ Config.Calls = {
|
||||
VoiceProvider = "pma",
|
||||
RingSeconds = 30,
|
||||
ContactNameMaxLength = 80,
|
||||
ContactNotesMaxLength = 500,
|
||||
RecentPageSize = 100,
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ Locales["en"] = {
|
||||
phone_stacked = "Phones cannot be stacked.",
|
||||
invalid_imei = "This phone has invalid IMEI metadata.",
|
||||
metadata_unsupported = "The configured inventory cannot store unique phone metadata.",
|
||||
player_unavailable = "Your character identity is not available.",
|
||||
sim_slot_missing = "The used SIM card could not be identified.",
|
||||
sim_stacked = "SIM cards cannot be stacked.",
|
||||
invalid_sim = "This SIM card has invalid metadata.",
|
||||
@@ -685,9 +686,17 @@ Locales["en"] = {
|
||||
phone = {
|
||||
name = "Phone", recents = "Recents", contacts = "Contacts", keypad = "Keypad",
|
||||
noSim = "No SIM", noSimBody = "Insert a SIM card in Settings to make calls.",
|
||||
noRecents = "No Recent Calls", noContacts = "No Contacts", searchContacts = "Search Contacts",
|
||||
addContact = "New Contact", editContact = "Edit Contact", contactName = "Name", phoneNumber = "Phone Number",
|
||||
noRecents = "No Recent Calls", noContacts = "No Contacts", searchContacts = "Search Contacts", favorites = "Favorites",
|
||||
addContact = "New Contact", newContact = "New", editContact = "Edit Contact", contactName = "Name", firstName = "First Name", lastName = "Last Name", companyOrGroup = "Company or Group", phoneNumber = "Phone Number",
|
||||
officialContact = "Official company contact",
|
||||
choosePhoto = "Choose Contact Photo", chooseGallery = "Gallery", takePhoto = "Camera", removePhoto = "Remove Photo",
|
||||
message = "Message", video = "Video", mail = "Mail", defaultLabel = "Default", privateLabel = "Private",
|
||||
contactCard = "Contact Card", removeContact = "Remove Contact", returnToCall = "Return to Call", mobile = "Mobile", messagesProfile = "Messages", notes = "Notes",
|
||||
sendMessage = "Send Message", shareContact = "Share Contact", addFavorite = "Add to Favorites", removeFavorite = "Remove from Favorites",
|
||||
addEmergency = "Add to Emergency Contacts", blockContact = "Block Contact",
|
||||
block = "Block", blockCaller = "Block Caller", blockCallerTitle = "Block this caller?",
|
||||
blockCallerBody = "{number} will no longer be able to call this SIM.",
|
||||
faceTime = "FaceTime", hideKeypad = "Hide Keypad", more = "More", mute = "Mute", speaker = "Speaker", viewContact = "View Contact",
|
||||
call = "Call", calling = "calling...", incoming = "Incoming Call", incomingDirection = "Incoming", outgoingDirection = "Outgoing", connected = "Connected",
|
||||
missed = "Missed", declined = "Declined", busy = "Busy", unavailable = "Unavailable",
|
||||
noAnswer = "No Answer", cancelled = "Cancelled", disconnected = "Disconnected", sim_removed = "SIM Removed", completed = "Call Ended",
|
||||
@@ -702,6 +711,8 @@ Locales["en"] = {
|
||||
self_call = "You cannot call your own number.", busy = "The line is busy.",
|
||||
company_unavailable = "This company service line is unavailable.",
|
||||
readonly_contact = "Official company contacts cannot be changed.",
|
||||
message_unavailable = "The conversation could not be opened.", contact_remove_failed = "The contact could not be removed.", contact_favorite_failed = "The favorite could not be updated.",
|
||||
blocked = "This number is blocked.", recipient_not_found = "This number is not known.",
|
||||
rate_limited = "Too many calls. Try again in a minute.", voice_unavailable = "The configured phone voice service is unavailable.",
|
||||
inventory_full = "There is no room for the ejected SIM card.", request_failed = "The phone request failed.",
|
||||
operation_in_progress = "Another phone operation is already in progress.", sim_request_expired = "The SIM selection expired. Use the SIM card again.",
|
||||
|
||||
@@ -9,11 +9,26 @@ end
|
||||
local ESX = exports["es_extended"]:getSharedObject()
|
||||
local metadata_key = "sky_phone_inventory"
|
||||
|
||||
ESX.AddItems({
|
||||
local inventory_items = {
|
||||
{ name = Config.Phone.Item, label = Config.Phone.DeviceName, weight = 100, rare = false, canRemove = true },
|
||||
{ name = Config.Sim.RegisteredItem, label = "Registered SIM", weight = 5, rare = false, canRemove = true },
|
||||
{ name = Config.Sim.AnonymousItem, label = "Anonymous SIM", weight = 5, rare = false, canRemove = true },
|
||||
})
|
||||
}
|
||||
if Config.Sim.Enabled ~= false then
|
||||
inventory_items[#inventory_items + 1] = {
|
||||
name = Config.Sim.RegisteredItem,
|
||||
label = "Registered SIM",
|
||||
weight = 5,
|
||||
rare = false,
|
||||
canRemove = true,
|
||||
}
|
||||
inventory_items[#inventory_items + 1] = {
|
||||
name = Config.Sim.AnonymousItem,
|
||||
label = "Anonymous SIM",
|
||||
weight = 5,
|
||||
rare = false,
|
||||
canRemove = true,
|
||||
}
|
||||
end
|
||||
ESX.AddItems(inventory_items)
|
||||
|
||||
local function get_player(source)
|
||||
return ESX.GetPlayerFromId(source)
|
||||
|
||||
@@ -194,11 +194,13 @@ local server_callbacks = {
|
||||
"contacts:list",
|
||||
"contacts:save",
|
||||
"contacts:delete",
|
||||
"contacts:favorite",
|
||||
"calls:recents",
|
||||
"calls:dial",
|
||||
"calls:answer",
|
||||
"calls:decline",
|
||||
"calls:hangup",
|
||||
"calls:block",
|
||||
"banking:overview",
|
||||
"banking:transfer",
|
||||
"billing:overview",
|
||||
|
||||
@@ -333,8 +333,8 @@ function SkyPhoneCalls.CopyCloudToDevice(account_id, 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`
|
||||
(`id`, `contact_id`, `device_imei`, `name`, `notes`, `organization`, `phone_number`, `avatar_media_id`, `favorite`, `created_at`, `updated_at`)
|
||||
SELECT UUID(), `contact_id`, ?, `name`, `notes`, `organization`, `phone_number`, NULL, `favorite`, `created_at`, `updated_at`
|
||||
FROM `sky_phone_contacts` WHERE `account_id` = ?
|
||||
]],
|
||||
params = { imei, account_id },
|
||||
@@ -358,7 +358,9 @@ Bridge.Callbacks.Register("sky_phone:contacts:list", function(source)
|
||||
end
|
||||
local condition, params = scope_condition(scope)
|
||||
local rows = Bridge.Database.Query(([[
|
||||
SELECT `contact_id` AS `id`, `name`, `phone_number`, `created_at`, `updated_at`
|
||||
SELECT `contact_id` AS `id`, `name`, `notes`, `organization`, `phone_number`, `avatar_media_id`, `favorite`,
|
||||
(SELECT media.`url` FROM `sky_phone_media` media WHERE media.`id` = `avatar_media_id`) AS `avatar_url`,
|
||||
`created_at`, `updated_at`
|
||||
FROM `sky_phone_contacts` WHERE %s ORDER BY LOWER(`name`), `phone_number`
|
||||
]]):format(condition), params)
|
||||
local system_contacts = SkyPhoneCompanies.GetSystemContacts()
|
||||
@@ -393,8 +395,11 @@ Bridge.Callbacks.Register("sky_phone:contacts:save", function(source, data)
|
||||
return error_response
|
||||
end
|
||||
local name = trim(data.name)
|
||||
local notes = trim(data.notes) or ""
|
||||
local organization = trim(data.organization) or ""
|
||||
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
|
||||
local avatar_media_id = tonumber(data.avatarMediaId) or 0
|
||||
if not name or name == "" or #name > Config.Calls.ContactNameMaxLength or #notes > Config.Calls.ContactNotesMaxLength or #organization > Config.Calls.ContactNameMaxLength or not number then
|
||||
return { success = false, error = "invalid_contact" }
|
||||
end
|
||||
if (type(data.id) == "string" and data.id:sub(1, 8) == "company:")
|
||||
@@ -407,6 +412,17 @@ Bridge.Callbacks.Register("sky_phone:contacts:save", function(source, data)
|
||||
)
|
||||
return { success = false, error = "readonly_contact" }
|
||||
end
|
||||
if avatar_media_id < 0 or avatar_media_id ~= math.floor(avatar_media_id) then
|
||||
return { success = false, error = "invalid_contact" }
|
||||
end
|
||||
local avatar_url
|
||||
if avatar_media_id > 0 then
|
||||
local media_error
|
||||
avatar_url, media_error = SkyPhoneMedia.ResolveOwnedMedia(source, tostring(avatar_media_id), "photo")
|
||||
if not avatar_url then
|
||||
return { success = false, error = media_error }
|
||||
end
|
||||
end
|
||||
local condition, condition_params = scope_condition(scope)
|
||||
local id = type(data.id) == "string" and data.id or uuid()
|
||||
if data.id then
|
||||
@@ -418,24 +434,63 @@ Bridge.Callbacks.Register("sky_phone:contacts:save", function(source, data)
|
||||
if not owned[1] then
|
||||
return { success = false, error = "contact_not_found" }
|
||||
end
|
||||
local params = { name, number, id }
|
||||
local params = { name, notes, organization, number, avatar_media_id, id }
|
||||
for _, value in ipairs(condition_params) do
|
||||
params[#params + 1] = value
|
||||
end
|
||||
Bridge.Database.Query(([[
|
||||
UPDATE `sky_phone_contacts` SET `name` = ?, `phone_number` = ?
|
||||
UPDATE `sky_phone_contacts` SET `name` = ?, `notes` = NULLIF(?, ''), `organization` = NULLIF(?, ''), `phone_number` = ?, `avatar_media_id` = NULLIF(?, 0)
|
||||
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 })
|
||||
INSERT INTO `sky_phone_contacts` (`id`, `contact_id`, `account_id`, `device_imei`, `name`, `notes`, `organization`, `phone_number`, `avatar_media_id`)
|
||||
VALUES (?, ?, ?, ?, ?, NULLIF(?, ''), NULLIF(?, ''), ?, NULLIF(?, 0))
|
||||
]], { uuid(), id, scope.account_id, scope.device_imei, name, notes, organization, number, avatar_media_id })
|
||||
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 } }
|
||||
return {
|
||||
success = true,
|
||||
data = {
|
||||
id = id,
|
||||
name = name,
|
||||
notes = notes ~= "" and notes or nil,
|
||||
organization = organization ~= "" and organization or nil,
|
||||
phone_number = number,
|
||||
avatar_media_id = avatar_media_id > 0 and avatar_media_id or nil,
|
||||
avatar_url = avatar_url,
|
||||
},
|
||||
}
|
||||
end)
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:contacts:favorite", function(source, data)
|
||||
if not SkyPhone.AllowOperation(source, "contact_favorite", 60, 60) or type(data) ~= "table" or type(data.id) ~= "string" or type(data.favorite) ~= "boolean" 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, condition_params = scope_condition(scope)
|
||||
local owned_params = { data.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 = { data.favorite and 1 or 0, data.id }
|
||||
for _, value in ipairs(condition_params) do
|
||||
params[#params + 1] = value
|
||||
end
|
||||
Bridge.Database.Query(("UPDATE `sky_phone_contacts` SET `favorite` = ? 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, data = { id = data.id, favorite = data.favorite } }
|
||||
end)
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:contacts:delete", function(source, data)
|
||||
@@ -728,10 +783,19 @@ handle_no_answer = function(call)
|
||||
end
|
||||
end
|
||||
|
||||
local function lock_direct_target(caller_source, target)
|
||||
local function lock_direct_target(caller_source, target, caller_sim_id)
|
||||
if not target or not target.imei then
|
||||
return nil, "unavailable"
|
||||
end
|
||||
if caller_sim_id then
|
||||
local blocks = Bridge.Database.Query([[
|
||||
SELECT 1 FROM `sky_phone_call_blocks`
|
||||
WHERE `blocker_sim_id` = ? AND `blocked_sim_id` = ? LIMIT 1
|
||||
]], { target.id, caller_sim_id })
|
||||
if blocks[1] then
|
||||
return nil, "declined"
|
||||
end
|
||||
end
|
||||
local callee_source = find_device_holder(target.imei)
|
||||
if not callee_source or callee_source == caller_source then
|
||||
return nil, "unavailable"
|
||||
@@ -805,7 +869,7 @@ function SkyPhoneCalls.StartCompanyCall(source, company_id, customer_number)
|
||||
WHERE s.`phone_number` = ? LIMIT 1
|
||||
]], { number })
|
||||
local target = targets[1]
|
||||
local callee_source, target_status = lock_direct_target(source, target)
|
||||
local callee_source, target_status = lock_direct_target(source, target, scope.device.sim_id)
|
||||
if not callee_source then
|
||||
local terminal = create_terminal_call(scope, number, target, target_status, service_line.number)
|
||||
dialing_by_sim[scope.device.sim_id] = nil
|
||||
@@ -928,7 +992,12 @@ Bridge.Callbacks.Register("sky_phone:calls:dial", function(source, data)
|
||||
WHERE s.`phone_number` = ? LIMIT 1
|
||||
]], { number })
|
||||
local target = targets[1]
|
||||
local callee_source, target_status = lock_direct_target(source, target)
|
||||
if not target then
|
||||
dialing_by_sim[scope.device.sim_id] = nil
|
||||
dial_locks[source] = nil
|
||||
return { success = false, error = "recipient_not_found" }
|
||||
end
|
||||
local callee_source, target_status = lock_direct_target(source, target, scope.device.sim_id)
|
||||
if not callee_source then
|
||||
local terminal = create_terminal_call(scope, number, target, target_status)
|
||||
dialing_by_sim[scope.device.sim_id] = nil
|
||||
@@ -1169,6 +1238,51 @@ Bridge.Callbacks.Register("sky_phone:calls:hangup", function(source, data)
|
||||
return { success = true }
|
||||
end)
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:calls:block", function(source, data)
|
||||
if not SkyPhone.AllowOperation(source, "call_block", 10, 60) then
|
||||
return { success = false, error = "rate_limited" }
|
||||
end
|
||||
local scope, error_response = current_scope(source)
|
||||
if not scope then
|
||||
return error_response
|
||||
end
|
||||
if not scope.device.sim_id then
|
||||
return { success = false, error = "no_sim" }
|
||||
end
|
||||
local number = type(data) == "table" and SkyPhoneSimNumber.Normalize(
|
||||
data.phoneNumber,
|
||||
Config.Sim.NumberLength,
|
||||
Config.Sim.NumberPrefix
|
||||
) or nil
|
||||
if not number then
|
||||
return { success = false, error = "invalid_number" }
|
||||
end
|
||||
if number == scope.device.phone_number then
|
||||
return { success = false, error = "self_call" }
|
||||
end
|
||||
local rows = Bridge.Database.Query(
|
||||
"SELECT `id` FROM `sky_phone_sims` WHERE `phone_number` = ? LIMIT 1",
|
||||
{ number }
|
||||
)
|
||||
if not rows[1] then
|
||||
return { success = false, error = "recipient_not_found" }
|
||||
end
|
||||
Bridge.Database.Query([[
|
||||
INSERT IGNORE INTO `sky_phone_call_blocks` (`blocker_sim_id`, `blocked_sim_id`)
|
||||
VALUES (?, ?)
|
||||
]], { scope.device.sim_id, rows[1].id })
|
||||
|
||||
local call_id = active_by_source[source]
|
||||
local call = call_id and calls[call_id] or nil
|
||||
if call then
|
||||
local other_number = source == call.caller_source and call.callee_number or call.caller_number
|
||||
if other_number == number then
|
||||
finish_call(call, call.answered_at and "completed" or "declined")
|
||||
end
|
||||
end
|
||||
return { success = true }
|
||||
end)
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:payphone:hangup", function(source, data)
|
||||
local call_id = active_by_source[source]
|
||||
local call = call_id and calls[call_id] or nil
|
||||
|
||||
@@ -122,6 +122,7 @@ local schema = {
|
||||
{ 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 = "is_virtual", type = "TINYINT(1) NOT NULL DEFAULT 0" },
|
||||
{ name = "owner_identifier", type = "VARCHAR(80) NULL" },
|
||||
{ name = "owner_firstname", type = "VARCHAR(80) NULL" },
|
||||
{ name = "owner_lastname", type = "VARCHAR(80) NULL" },
|
||||
@@ -173,6 +174,39 @@ local schema = {
|
||||
},
|
||||
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
},
|
||||
{
|
||||
name = "sky_phone_character_devices",
|
||||
columns = {
|
||||
{
|
||||
name = "owner_identifier",
|
||||
type = "VARCHAR(80) NOT NULL",
|
||||
characterSet = "ascii",
|
||||
collation = "ascii_bin",
|
||||
},
|
||||
{
|
||||
name = "device_imei",
|
||||
type = "CHAR(15) 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 = "owner_identifier",
|
||||
uniqueKeys = {
|
||||
{ name = "uniq_sky_phone_character_devices_device", columns = "(`device_imei`)" },
|
||||
},
|
||||
foreignKeys = {
|
||||
{
|
||||
column = "device_imei",
|
||||
references = "`sky_phone_devices` (`imei`) ON DELETE CASCADE",
|
||||
},
|
||||
},
|
||||
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
},
|
||||
{
|
||||
name = "sky_phone_device_data",
|
||||
columns = {
|
||||
@@ -370,7 +404,11 @@ local schema = {
|
||||
{ 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 = "notes", type = "VARCHAR(500) NULL" },
|
||||
{ name = "organization", type = "VARCHAR(80) NULL" },
|
||||
{ name = "phone_number", type = "VARCHAR(24) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
|
||||
{ name = "avatar_media_id", type = "BIGINT UNSIGNED NULL" },
|
||||
{ name = "favorite", type = "TINYINT(1) NOT NULL DEFAULT 0" },
|
||||
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
|
||||
{ name = "updated_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" },
|
||||
},
|
||||
@@ -378,10 +416,12 @@ local schema = {
|
||||
indexes = {
|
||||
{ name = "idx_sky_phone_contacts_account", columns = "(`account_id`, `name`)" },
|
||||
{ name = "idx_sky_phone_contacts_device", columns = "(`device_imei`, `name`)" },
|
||||
{ name = "idx_sky_phone_contacts_avatar", columns = "(`avatar_media_id`)" },
|
||||
},
|
||||
foreignKeys = {
|
||||
{ column = "account_id", references = "`sky_phone_accounts` (`id`) ON DELETE CASCADE" },
|
||||
{ column = "device_imei", references = "`sky_phone_devices` (`imei`) ON DELETE CASCADE" },
|
||||
{ column = "avatar_media_id", references = "`sky_phone_media` (`id`) ON DELETE SET NULL" },
|
||||
},
|
||||
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
},
|
||||
|
||||
@@ -433,10 +433,14 @@ Bridge.Callbacks.Register("sky_phone:messages:send", function(source, data)
|
||||
else
|
||||
return { success = false, error = "invalid_request" }
|
||||
end
|
||||
local recipients = Bridge.Database.Query(
|
||||
"SELECT `id`, `phone_number` FROM `sky_phone_sims` WHERE `phone_number` = ? LIMIT 1",
|
||||
{ number }
|
||||
)
|
||||
local recipients = Bridge.Database.Query([[
|
||||
SELECT s.`id`, s.`phone_number`
|
||||
FROM `sky_phone_sims` s
|
||||
LEFT JOIN `sky_phone_devices` d ON d.`sim_id` = s.`id`
|
||||
WHERE s.`phone_number` = ?
|
||||
AND (s.`is_virtual` = 0 OR d.`imei` IS NOT NULL)
|
||||
LIMIT 1
|
||||
]], { number })
|
||||
local recipient = recipients[1]
|
||||
if not recipient then
|
||||
return { success = false, error = "recipient_not_found" }
|
||||
|
||||
@@ -3,9 +3,12 @@ Bridge.Debug("debug", "[sky_phone] Server initialization started after database
|
||||
|
||||
SkyPhone = {}
|
||||
|
||||
local unique_phones = Config.Phone.Unique ~= false
|
||||
local sim_cards_enabled = Config.Sim.Enabled ~= false
|
||||
local sessions = {}
|
||||
local auth_attempts = {}
|
||||
local operation_attempts = {}
|
||||
local character_device_cache = {}
|
||||
local max_device_data_bytes = 100000
|
||||
local passcode_pepper = GetConvar(Config.Security.PasscodePepperConvar, "")
|
||||
if passcode_pepper == "" then
|
||||
@@ -64,9 +67,130 @@ local function reserve_imei()
|
||||
return imei
|
||||
end
|
||||
|
||||
local function character_identifier(source)
|
||||
local identifier = Bridge.Framework.GetIdentifier(source)
|
||||
if identifier == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
identifier = tostring(identifier)
|
||||
if identifier == "" or #identifier > 80 then
|
||||
Bridge.Debug(
|
||||
"warn",
|
||||
"[sky_phone] Could not resolve a valid character identifier for source %s.",
|
||||
tostring(source)
|
||||
)
|
||||
return nil
|
||||
end
|
||||
return identifier
|
||||
end
|
||||
|
||||
local function load_character_device(source, identifier)
|
||||
identifier = identifier or character_identifier(source)
|
||||
if not identifier then
|
||||
return nil, "player_unavailable"
|
||||
end
|
||||
|
||||
local cached = character_device_cache[source]
|
||||
if cached and cached.identifier == identifier then
|
||||
return cached.imei, nil, identifier
|
||||
end
|
||||
|
||||
local rows = Bridge.Database.Query([[
|
||||
SELECT `device_imei`
|
||||
FROM `sky_phone_character_devices`
|
||||
WHERE `owner_identifier` = ?
|
||||
LIMIT 1
|
||||
]], { identifier })
|
||||
local imei = rows[1] and rows[1].device_imei or nil
|
||||
if imei then
|
||||
character_device_cache[source] = { identifier = identifier, imei = imei }
|
||||
else
|
||||
character_device_cache[source] = nil
|
||||
end
|
||||
return imei, nil, identifier
|
||||
end
|
||||
|
||||
local function cache_character_device(source, identifier, imei)
|
||||
character_device_cache[source] = { identifier = identifier, imei = imei }
|
||||
return imei
|
||||
end
|
||||
|
||||
local function map_character_device(source, slot)
|
||||
local mapped_imei, error_code, identifier = load_character_device(source)
|
||||
if error_code or mapped_imei then
|
||||
return mapped_imei, error_code
|
||||
end
|
||||
|
||||
-- On the first switch from unique to shared phones, adopt the currently used
|
||||
-- legacy device when it has not already been claimed by another character.
|
||||
local legacy_imei = slot.metadata and slot.metadata.imei or nil
|
||||
if SkyPhoneImei.IsValid(legacy_imei) then
|
||||
Bridge.Database.Query([[
|
||||
INSERT IGNORE INTO `sky_phone_devices` (`imei`, `device_name`)
|
||||
VALUES (?, ?)
|
||||
]], { legacy_imei, Config.Phone.DeviceName })
|
||||
local result = Bridge.Database.Query([[
|
||||
INSERT IGNORE INTO `sky_phone_character_devices` (`owner_identifier`, `device_imei`)
|
||||
VALUES (?, ?)
|
||||
]], { identifier, legacy_imei })
|
||||
if affected_rows(result) == 1 then
|
||||
return cache_character_device(source, identifier, legacy_imei)
|
||||
end
|
||||
|
||||
mapped_imei = load_character_device(source, identifier)
|
||||
if mapped_imei then
|
||||
return mapped_imei
|
||||
end
|
||||
end
|
||||
|
||||
local imei = reserve_imei()
|
||||
local result = Bridge.Database.Query([[
|
||||
INSERT IGNORE INTO `sky_phone_character_devices` (`owner_identifier`, `device_imei`)
|
||||
VALUES (?, ?)
|
||||
]], { identifier, imei })
|
||||
if affected_rows(result) == 1 then
|
||||
return cache_character_device(source, identifier, imei)
|
||||
end
|
||||
|
||||
-- Another simultaneous open may have created the mapping first. Keep its
|
||||
-- winner and remove only the fresh, unmapped device reserved by this call.
|
||||
mapped_imei = load_character_device(source, identifier)
|
||||
Bridge.Database.Query([[
|
||||
DELETE d FROM `sky_phone_devices` d
|
||||
LEFT JOIN `sky_phone_character_devices` c ON c.`device_imei` = d.`imei`
|
||||
WHERE d.`imei` = ? AND c.`device_imei` IS NULL
|
||||
]], { imei })
|
||||
if mapped_imei then
|
||||
return mapped_imei
|
||||
end
|
||||
|
||||
Bridge.Debug(
|
||||
"error",
|
||||
"[sky_phone] Could not create a shared device mapping for source %s.",
|
||||
tostring(source)
|
||||
)
|
||||
return nil, "request_failed"
|
||||
end
|
||||
|
||||
local function find_device_slots(source, imei)
|
||||
local matches = {}
|
||||
for _, item in ipairs(Bridge.Inventory.GetSlotsWithItem(source, Config.Phone.Item)) do
|
||||
local slots = Bridge.Inventory.GetSlotsWithItem(source, Config.Phone.Item)
|
||||
if not unique_phones then
|
||||
if not slots[1] then
|
||||
return matches
|
||||
end
|
||||
local mapped_imei = load_character_device(source)
|
||||
if mapped_imei ~= imei then
|
||||
return matches
|
||||
end
|
||||
for _, item in ipairs(slots) do
|
||||
matches[#matches + 1] = item
|
||||
end
|
||||
return matches
|
||||
end
|
||||
|
||||
for _, item in ipairs(slots) do
|
||||
if item.metadata and item.metadata.imei == imei then
|
||||
matches[#matches + 1] = item
|
||||
end
|
||||
@@ -134,7 +258,7 @@ local function resolve_used_slot(source, used_item)
|
||||
{ always = true }
|
||||
)
|
||||
end
|
||||
if #slots == 1 then
|
||||
if #slots == 1 or (not unique_phones and #slots > 0) then
|
||||
return slots[1]
|
||||
end
|
||||
|
||||
@@ -159,6 +283,13 @@ local function ensure_device(source, slot)
|
||||
tostring(Bridge.Inventory.GetResourceName()),
|
||||
{ always = true }
|
||||
)
|
||||
if not unique_phones then
|
||||
if amount < 1 then
|
||||
return nil, "phone_slot_missing"
|
||||
end
|
||||
return map_character_device(source, slot)
|
||||
end
|
||||
|
||||
if amount ~= 1 then
|
||||
Bridge.Debug("warn", "[sky_phone] Phone item in slot %s is stacked for source %s.", tostring(slot.slot), tostring(source))
|
||||
return nil, "phone_stacked"
|
||||
@@ -217,7 +348,7 @@ end
|
||||
local function load_device(imei)
|
||||
local rows = Bridge.Database.Query([[
|
||||
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`
|
||||
a.`email`, s.`phone_number`, s.`sim_type`, s.`registered_at`, s.`is_virtual` AS `sim_is_virtual`
|
||||
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`
|
||||
@@ -370,6 +501,7 @@ local function bootstrap(source, security, security_loaded)
|
||||
sim = device.sim_id and {
|
||||
id = device.sim_id,
|
||||
number = device.phone_number,
|
||||
removable = sim_cards_enabled and tonumber(device.sim_is_virtual) ~= 1,
|
||||
type = device.sim_type,
|
||||
registered = device.registered_at ~= nil,
|
||||
} or nil,
|
||||
@@ -648,24 +780,38 @@ function SkyPhone.NotifyAccountDevices(account_id, event_name, data)
|
||||
devices[row.imei] = row
|
||||
end
|
||||
|
||||
local function notify_device(source, device)
|
||||
local payload = {}
|
||||
for key, value in pairs(data) do
|
||||
payload[key] = value
|
||||
end
|
||||
payload.device = {
|
||||
imei = device.imei,
|
||||
name = device.device_name,
|
||||
settings = device.settings,
|
||||
}
|
||||
TriggerClientEvent(event_name, source, payload)
|
||||
end
|
||||
|
||||
for _, player_source in ipairs(Bridge.Framework.GetPlayers()) do
|
||||
local source = tonumber(player_source) or player_source
|
||||
local notified_devices = {}
|
||||
for _, item in ipairs(Bridge.Inventory.GetSlotsWithItem(source, Config.Phone.Item)) do
|
||||
local imei = item.metadata and item.metadata.imei
|
||||
local device = imei and devices[imei]
|
||||
if device and not notified_devices[imei] then
|
||||
local payload = {}
|
||||
for key, value in pairs(data) do
|
||||
payload[key] = value
|
||||
if not unique_phones then
|
||||
if Bridge.Inventory.GetSlotsWithItem(source, Config.Phone.Item)[1] then
|
||||
local imei = load_character_device(source)
|
||||
local device = imei and devices[imei] or nil
|
||||
if device then
|
||||
notify_device(source, device)
|
||||
end
|
||||
end
|
||||
else
|
||||
local notified_devices = {}
|
||||
for _, item in ipairs(Bridge.Inventory.GetSlotsWithItem(source, Config.Phone.Item)) do
|
||||
local imei = item.metadata and item.metadata.imei
|
||||
local device = imei and devices[imei]
|
||||
if device and not notified_devices[imei] then
|
||||
notified_devices[imei] = true
|
||||
notify_device(source, device)
|
||||
end
|
||||
payload.device = {
|
||||
imei = device.imei,
|
||||
name = device.device_name,
|
||||
settings = device.settings,
|
||||
}
|
||||
notified_devices[imei] = true
|
||||
TriggerClientEvent(event_name, source, payload)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -721,6 +867,18 @@ local function open_phone(source, used_item)
|
||||
TriggerClientEvent("sky_phone:device:error", source, error_code)
|
||||
return false
|
||||
end
|
||||
local prepared, prepare_error = SkyPhoneSim.PrepareDevice(source, slot, imei)
|
||||
if not prepared then
|
||||
Bridge.Debug(
|
||||
"error",
|
||||
"[sky_phone] Phone preparation failed for source %s IMEI %s: %s.",
|
||||
tostring(source),
|
||||
imei,
|
||||
tostring(prepare_error)
|
||||
)
|
||||
TriggerClientEvent("sky_phone:device:error", source, prepare_error or "request_failed")
|
||||
return false
|
||||
end
|
||||
|
||||
local security = load_device_security(imei)
|
||||
if sessions[source] and sessions[source].imei ~= imei then
|
||||
@@ -1157,6 +1315,7 @@ AddEventHandler("playerDropped", function()
|
||||
sessions[source] = nil
|
||||
auth_attempts[source] = nil
|
||||
operation_attempts[source] = nil
|
||||
character_device_cache[source] = nil
|
||||
end)
|
||||
|
||||
AddEventHandler("onResourceStop", function(resource_name)
|
||||
@@ -1164,6 +1323,7 @@ AddEventHandler("onResourceStop", function(resource_name)
|
||||
sessions = {}
|
||||
auth_attempts = {}
|
||||
operation_attempts = {}
|
||||
character_device_cache = {}
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
+160
-29
@@ -2,6 +2,8 @@ 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 = {
|
||||
@@ -24,7 +26,7 @@ local function uuid()
|
||||
return rows[1].id
|
||||
end
|
||||
|
||||
local function reserve_sim(sim_type)
|
||||
local function reserve_sim(sim_type, is_virtual)
|
||||
local sim_id
|
||||
local number = SkyPhoneSimNumber.Reserve(uuid, function(candidate)
|
||||
if SkyPhoneCompanies.IsServiceNumber(candidate) then
|
||||
@@ -32,9 +34,9 @@ local function reserve_sim(sim_type)
|
||||
end
|
||||
sim_id = uuid()
|
||||
local result = Bridge.Database.Query([[
|
||||
INSERT IGNORE INTO `sky_phone_sims` (`id`, `phone_number`, `sim_type`)
|
||||
VALUES (?, ?, ?)
|
||||
]], { sim_id, candidate, sim_type })
|
||||
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
|
||||
@@ -65,6 +67,91 @@ local function sim_metadata(sim)
|
||||
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
|
||||
@@ -85,7 +172,12 @@ local function ensure_sim(source, slot, sim_type)
|
||||
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
|
||||
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
|
||||
@@ -109,30 +201,46 @@ 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 = 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,
|
||||
}
|
||||
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
|
||||
return choices, choice_error
|
||||
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
|
||||
Bridge.Inventory.SetSlotMetadata(source, phone_slot.slot, metadata)
|
||||
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
|
||||
@@ -161,6 +269,10 @@ local function insert_sim(source, phone_imei, confirmed)
|
||||
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
|
||||
@@ -168,11 +280,7 @@ local function insert_sim(source, phone_imei, confirmed)
|
||||
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 Bridge.Inventory.SetSlotMetadata(source, phone_slot.slot, phone_metadata) then
|
||||
if not set_phone_sim_metadata(source, phone_slot, pending.sim) then
|
||||
operation_locks[source] = nil
|
||||
return { success = false, error = "metadata_unsupported" }
|
||||
end
|
||||
@@ -234,6 +342,9 @@ local function insert_sim(source, phone_imei, confirmed)
|
||||
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
|
||||
@@ -257,10 +368,10 @@ local function use_sim(source, used_item)
|
||||
TriggerClientEvent("sky_phone:device:error", source, error_code)
|
||||
return false
|
||||
end
|
||||
local choices = list_phone_choices(source)
|
||||
local choices, choice_error = list_phone_choices(source)
|
||||
if #choices == 0 then
|
||||
operation_locks[source] = nil
|
||||
TriggerClientEvent("sky_phone:device:error", source, "phone_required")
|
||||
TriggerClientEvent("sky_phone:device:error", source, choice_error or "phone_required")
|
||||
return false
|
||||
end
|
||||
pending_insertions[source] = {
|
||||
@@ -280,10 +391,15 @@ local function use_sim(source, used_item)
|
||||
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
|
||||
@@ -291,6 +407,9 @@ Bridge.Callbacks.Register("sky_phone:sim:insert", function(source, data)
|
||||
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
|
||||
@@ -300,7 +419,7 @@ Bridge.Callbacks.Register("sky_phone:sim:eject", function(source)
|
||||
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
|
||||
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)
|
||||
@@ -311,7 +430,10 @@ Bridge.Callbacks.Register("sky_phone:sim:eject", function(source)
|
||||
end
|
||||
|
||||
operation_locks[source] = true
|
||||
rollback_phone_metadata(source, phone_slot, nil)
|
||||
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
|
||||
@@ -337,4 +459,13 @@ 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)
|
||||
|
||||
@@ -55,6 +55,7 @@ CREATE TABLE IF NOT EXISTS `sky_phone_sims` (
|
||||
`contact_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NULL,
|
||||
`phone_number` VARCHAR(24) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
`sim_type` ENUM('registered', 'anonymous') NOT NULL,
|
||||
`is_virtual` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`owner_identifier` VARCHAR(80) NULL,
|
||||
`owner_firstname` VARCHAR(80) NULL,
|
||||
`owner_lastname` VARCHAR(80) NULL,
|
||||
@@ -80,6 +81,16 @@ CREATE TABLE IF NOT EXISTS `sky_phone_devices` (
|
||||
FOREIGN KEY (`sim_id`) REFERENCES `sky_phone_sims` (`id`) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `sky_phone_character_devices` (
|
||||
`owner_identifier` VARCHAR(80) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
`device_imei` CHAR(15) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`owner_identifier`),
|
||||
UNIQUE KEY `uniq_sky_phone_character_devices_device` (`device_imei`),
|
||||
FOREIGN KEY (`device_imei`) REFERENCES `sky_phone_devices` (`imei`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `sky_phone_device_data` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`device_imei` CHAR(15) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
@@ -156,7 +167,11 @@ CREATE TABLE IF NOT EXISTS `sky_phone_contacts` (
|
||||
`account_id` BIGINT UNSIGNED NULL,
|
||||
`device_imei` CHAR(15) CHARACTER SET ascii COLLATE ascii_bin NULL,
|
||||
`name` VARCHAR(80) NOT NULL,
|
||||
`notes` VARCHAR(500) NULL,
|
||||
`organization` VARCHAR(80) NULL,
|
||||
`phone_number` VARCHAR(24) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
`avatar_media_id` BIGINT UNSIGNED NULL,
|
||||
`favorite` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
@@ -164,8 +179,10 @@ CREATE TABLE IF NOT EXISTS `sky_phone_contacts` (
|
||||
UNIQUE KEY `uniq_sky_phone_contacts_device_contact` (`device_imei`, `contact_id`),
|
||||
KEY `idx_sky_phone_contacts_account` (`account_id`, `name`),
|
||||
KEY `idx_sky_phone_contacts_device` (`device_imei`, `name`),
|
||||
KEY `idx_sky_phone_contacts_avatar` (`avatar_media_id`),
|
||||
FOREIGN KEY (`account_id`) REFERENCES `sky_phone_accounts` (`id`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`device_imei`) REFERENCES `sky_phone_devices` (`imei`) ON DELETE CASCADE
|
||||
FOREIGN KEY (`device_imei`) REFERENCES `sky_phone_devices` (`imei`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`avatar_media_id`) REFERENCES `sky_phone_media` (`id`) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `sky_phone_calls` (
|
||||
@@ -186,6 +203,16 @@ CREATE TABLE IF NOT EXISTS `sky_phone_calls` (
|
||||
FOREIGN KEY (`callee_sim_id`) REFERENCES `sky_phone_sims` (`id`) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `sky_phone_call_blocks` (
|
||||
`blocker_sim_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
`blocked_sim_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`blocker_sim_id`, `blocked_sim_id`),
|
||||
KEY `idx_sky_phone_call_blocks_blocked` (`blocked_sim_id`),
|
||||
FOREIGN KEY (`blocker_sim_id`) REFERENCES `sky_phone_sims` (`id`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`blocked_sim_id`) REFERENCES `sky_phone_sims` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `sky_phone_call_entries` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`call_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
|
||||
Reference in New Issue
Block a user