diff --git a/README.md b/README.md index 634fa99..f980839 100644 --- a/README.md +++ b/README.md @@ -6,15 +6,53 @@ An iFruit account is optional. Unlinked devices retain local settings, alarms, m ## Requirements -- `sky_base` with a slot-aware inventory adapter implementing `GetInventorySlot`, `GetInventorySlotsWithItem`, and `SetInventorySlotMetadata`. -- `sky_jobs_base` for the authoritative character identifier captured by registered SIM cards. +- ESX Legacy (`es_extended`), Qbox (`qbx_core`), or QBCore (`qb-core`). The bridge selects a running supported framework when `Config.Bridge.Framework` is set to `"auto"`. +- A supported metadata inventory: `ox_inventory`, `qb-inventory`, `lj-inventory`, `qs-inventory`, `codem-inventory`, `core_inventory`, or `mf-inventory`. The bridge auto-detects a running provider and normalizes `metadata`/`info`, slots, counts, item mutations, and usable-item callbacks. `mf-inventory` requires ESX. - A non-stackable inventory item named `sky_phone`. - Two unique, non-stackable inventory items named `sky_phone_sim_registered` and `sky_phone_sim_anonymous`. Their metadata is initialized automatically on first use, so shops and crafting recipes add plain items without supplying a number. -- MySQL/MariaDB through the database driver configured in `sky_base`. +- `oxmysql` with MySQL/MariaDB. - `pma-voice` when `Config.Calls.VoiceProvider` is set to `"pma"`. Database migrations run automatically. Existing `sky_phone_mail_accounts` installations are renamed to `sky_phone_accounts` while preserving account IDs and mail foreign keys. iFruit passwords are intentional in-character credentials and remain plaintext `VARCHAR(64)` values; registration screens warn players never to reuse a real password. +For a fresh manual database installation, import `sky_phone/sql/install.sql`. It contains the complete current table, key, index, collation, and foreign-key schema. Runtime migrations remain authoritative for upgrading an existing installation and must stay enabled. + +Framework, inventory, callback, notification, and database integrations live under `sky_phone/source/bridge`. The resource has no dependency on any other Sky resource. + +Inventory metadata has no framework-wide standard: providers differ in export names, callback payloads, slot handling, and whether metadata is called `metadata` or `info`. For that reason, `sky_phone` uses explicit provider adapters instead of guessing exports at runtime. Every supported adapter implements slot lookup, item lookup, metadata replacement, capacity handling, add/remove operations, and usable-item registration. Providers without a separate capacity export use their authoritative add operation as the final capacity gate. + +When a SIM is ejected or replaced, the returned inventory item is rebuilt from the authoritative `sky_phone_sims` row. Its metadata contains `sim_metadata_version`, `sim_id`, `phone_number`, `formatted_number`, and `sim_type`. Registered SIMs additionally contain `firstname`, `lastname`, `birthdate`, and `registered_at`. The internal framework owner identifier remains database-only. Inserting the item again resolves the SIM by `sim_id`; contacts and device/cloud data remain attached to their existing phone-owned persistence instead of being copied into inventory metadata. + +For `ox_inventory`, configure all three items with `stack = false` and `consume = 0`. Do not configure a client event or export. Ox then completes its normal server-authoritative use flow and emits `ox_inventory:usedItem`; the bridge resolves the authoritative slot again and only opens the matching device or SIM. A client export would return before Ox calls `useItem` and therefore prevent `ox_inventory:usedItem` from being emitted. + +Example `ox_inventory/data/items.lua` entries: + +```lua +["sky_phone"] = { + label = "iFruit Phone", + weight = 200, + stack = false, + close = true, + consume = 0, +}, +["sky_phone_sim_registered"] = { + label = "Registered SIM", + weight = 5, + stack = false, + close = true, + consume = 0, +}, +["sky_phone_sim_anonymous"] = { + label = "Anonymous SIM", + weight = 5, + stack = false, + close = true, + consume = 0, +}, +``` + +For QBCore-style item tables, define the same names with `unique = true`, `useable = true`, and `shouldClose = true`. The provider adapter registers the server-side usable callbacks; no `lb-phone` event or export is used. + The homescreen is an original implementation inspired by the interaction and layout concepts in [lukejacksonn/homescreen](https://github.com/lukejacksonn/homescreen), inspected at commit [`98a812f`](https://github.com/lukejacksonn/homescreen/tree/98a812f4f7c33594e791d65092f73b8f54b3c598). No source code or image assets from that project are included. ## Development diff --git a/sky_phone/config/config.lua b/sky_phone/config/config.lua index f32b0bd..5f0b0b2 100644 --- a/sky_phone/config/config.lua +++ b/sky_phone/config/config.lua @@ -1,3 +1,16 @@ +Config.Bridge = { + Framework = "auto", -- auto, esx, qbox, qb + Inventory = "auto", -- auto, ox, qb, lj, qs, codem, core, mf + Locale = "en", + CallbackTimeout = 5000, + Debug = false, + DebugLevels = { + info = true, + warn = true, + error = true, + }, +} + Config.Command = "phone" Config.Phone = { diff --git a/sky_phone/fxmanifest.lua b/sky_phone/fxmanifest.lua index 1315bae..a414025 100644 --- a/sky_phone/fxmanifest.lua +++ b/sky_phone/fxmanifest.lua @@ -1,17 +1,20 @@ fx_version 'cerulean' game 'gta5' lua54 'yes' +use_experimental_fxv2_oal 'yes' author 'Sky-Systems' description 'Sky Phone' version '0.1.0' -escrow_ignore 'config/**' +escrow_ignore { + 'config/**', + 'source/bridge/**', +} shared_scripts { - '@sky_base/source/import.lua', - '@sky_jobs_base/source/import.lua', 'config/init.lua', + 'source/bridge/shared.lua', 'source/shared/imei.lua', 'source/shared/sim_number.lua', } @@ -19,11 +22,21 @@ shared_scripts { client_scripts { 'config/config.lua', 'config/locales/*.lua', + 'source/bridge/client/framework.lua', + 'source/bridge/client/callbacks.lua', 'source/client/main.lua', } server_scripts { + '@oxmysql/lib/MySQL.lua', 'config/config.lua', + 'source/bridge/server/database.lua', + 'source/bridge/server/migrations.lua', + 'source/bridge/server/callbacks.lua', + 'source/bridge/server/framework.lua', + 'source/bridge/server/frameworks/*.lua', + 'source/bridge/server/inventory.lua', + 'source/bridge/server/inventory/*.lua', 'source/server/db_migrate.lua', 'source/server/phone.lua', 'source/server/sim.lua', @@ -40,4 +53,4 @@ files { ui_page 'source/html/index.html' -dependencies { 'sky_base', 'sky_jobs_base' } +dependency 'oxmysql' diff --git a/sky_phone/source/bridge/client/callbacks.lua b/sky_phone/source/bridge/client/callbacks.lua new file mode 100644 index 0000000..004ad13 --- /dev/null +++ b/sky_phone/source/bridge/client/callbacks.lua @@ -0,0 +1,44 @@ +local pending_requests = {} +local next_request_id = 0 + +function Bridge.Callbacks.Trigger(name, data) + next_request_id = next_request_id + 1 + local request_id = next_request_id + local request = promise.new() + pending_requests[request_id] = request + + TriggerServerEvent("sky_phone:bridge:callback:request", name, request_id, data) + + SetTimeout(Config.Bridge.CallbackTimeout, function() + if pending_requests[request_id] ~= request then + return + end + + pending_requests[request_id] = nil + Bridge.Debug("error", "[sky_phone] Server callback '%s' timed out.", tostring(name)) + request:resolve(nil) + end) + + return Citizen.Await(request) +end + +RegisterNetEvent("sky_phone:bridge:callback:response", function(request_id, result) + local request = pending_requests[request_id] + if not request then + return + end + + pending_requests[request_id] = nil + request:resolve(result) +end) + +AddEventHandler("onResourceStop", function(resource_name) + if resource_name ~= GetCurrentResourceName() then + return + end + + for request_id, request in pairs(pending_requests) do + pending_requests[request_id] = nil + request:resolve(nil) + end +end) diff --git a/sky_phone/source/bridge/client/framework.lua b/sky_phone/source/bridge/client/framework.lua new file mode 100644 index 0000000..32fd73d --- /dev/null +++ b/sky_phone/source/bridge/client/framework.lua @@ -0,0 +1,52 @@ +local framework_name +local esx +local qb + +local function resolve_framework() + local configured = Config.Bridge.Framework + if configured ~= "auto" then + return configured + end + + if GetResourceState("es_extended") == "started" then + return "esx" + end + if GetResourceState("qbx_core") == "started" then + return "qbox" + end + if GetResourceState("qb-core") == "started" then + return "qb" + end + + return nil +end + +framework_name = resolve_framework() +if not framework_name then + error("[sky_phone] No supported framework is running. Configure Config.Bridge.Framework.") +end + +function Bridge.Framework.GetName() + return framework_name +end + +function Bridge.Framework.Notify(title, message, notification_type, duration) + if framework_name == "esx" then + esx = esx or exports["es_extended"]:getSharedObject() + esx.ShowNotification(message, notification_type, duration, title) + return + end + + if framework_name == "qbox" then + exports.qbx_core:Notify(message, notification_type, duration, title) + return + end + + if framework_name == "qb" then + qb = qb or exports["qb-core"]:GetCoreObject() + qb.Functions.Notify(message, notification_type, duration) + return + end + + Bridge.Debug("error", "[sky_phone] Notification requested for unsupported framework '%s'.", tostring(framework_name)) +end diff --git a/sky_phone/source/bridge/server/callbacks.lua b/sky_phone/source/bridge/server/callbacks.lua new file mode 100644 index 0000000..75b0e75 --- /dev/null +++ b/sky_phone/source/bridge/server/callbacks.lua @@ -0,0 +1,32 @@ +local registered_callbacks = {} + +function Bridge.Callbacks.Register(name, callback) + assert(type(name) == "string", "Callback name must be a string") + assert(type(callback) == "function", "Callback handler must be a function") + assert(not registered_callbacks[name], ("Callback '%s' is already registered"):format(name)) + registered_callbacks[name] = callback +end + +RegisterNetEvent("sky_phone:bridge:callback:request", function(name, request_id, data) + local player_source = source + if type(name) ~= "string" or type(request_id) ~= "number" or type(data) ~= "table" then + Bridge.Debug("warn", "[sky_phone] Rejected malformed callback request from source %s.", tostring(player_source)) + return + end + + local callback = registered_callbacks[name] + if not callback then + Bridge.Debug("error", "[sky_phone] Server callback '%s' is not registered.", name) + TriggerClientEvent("sky_phone:bridge:callback:response", player_source, request_id, nil) + return + end + + local success, result = pcall(callback, player_source, data) + if not success then + Bridge.Debug("error", "[sky_phone] Server callback '%s' failed: %s", name, tostring(result)) + TriggerClientEvent("sky_phone:bridge:callback:response", player_source, request_id, nil) + return + end + + TriggerClientEvent("sky_phone:bridge:callback:response", player_source, request_id, result) +end) diff --git a/sky_phone/source/bridge/server/database.lua b/sky_phone/source/bridge/server/database.lua new file mode 100644 index 0000000..584043b --- /dev/null +++ b/sky_phone/source/bridge/server/database.lua @@ -0,0 +1,16 @@ +function Bridge.Database.Query(query, parameters) + return MySQL.query.await(query, parameters or {}) +end + +function Bridge.Database.Transaction(statements) + local queries = {} + for index = 1, #statements do + local statement = statements[index] + queries[index] = { + query = statement.query, + values = statement.params or statement.values or {}, + } + end + + return MySQL.transaction.await(queries) +end diff --git a/sky_phone/source/bridge/server/framework.lua b/sky_phone/source/bridge/server/framework.lua new file mode 100644 index 0000000..66ad9bd --- /dev/null +++ b/sky_phone/source/bridge/server/framework.lua @@ -0,0 +1,21 @@ +local configured_framework = Config.Bridge.Framework + +if configured_framework == "auto" then + if GetResourceState("es_extended") == "started" then + configured_framework = "esx" + elseif GetResourceState("qbx_core") == "started" then + configured_framework = "qbox" + elseif GetResourceState("qb-core") == "started" then + configured_framework = "qb" + end +end + +if configured_framework ~= "esx" and configured_framework ~= "qbox" and configured_framework ~= "qb" then + error(("[sky_phone] Unsupported or unavailable framework '%s'."):format(tostring(configured_framework))) +end + +Bridge.Framework.Name = configured_framework + +function Bridge.Framework.GetName() + return Bridge.Framework.Name +end diff --git a/sky_phone/source/bridge/server/frameworks/esx.lua b/sky_phone/source/bridge/server/frameworks/esx.lua new file mode 100644 index 0000000..cfb534a --- /dev/null +++ b/sky_phone/source/bridge/server/frameworks/esx.lua @@ -0,0 +1,42 @@ +if Bridge.Framework.Name ~= "esx" then + return +end + +local ESX = exports["es_extended"]:getSharedObject() + +local function get_player(source) + return ESX.GetPlayerFromId(source) +end + +function Bridge.Framework.GetPlayers() + local players = {} + for _, player in pairs(ESX.GetExtendedPlayers()) do + players[#players + 1] = player.source + end + return players +end + +function Bridge.Framework.GetIdentifier(source) + local player = get_player(source) + return player and player.identifier or nil +end + +function Bridge.Framework.GetFirstname(source) + local player = get_player(source) + return player and player.get("firstName") or nil +end + +function Bridge.Framework.GetLastname(source) + local player = get_player(source) + return player and player.get("lastName") or nil +end + +function Bridge.Framework.GetBirthdate(source) + local player = get_player(source) + return player and (player.get("dateofbirth") or player.get("dob")) or nil +end + +function Bridge.Framework.RegisterUsableItem(item_name, callback) + ESX.RegisterUsableItem(item_name, callback) + return true +end diff --git a/sky_phone/source/bridge/server/frameworks/qb.lua b/sky_phone/source/bridge/server/frameworks/qb.lua new file mode 100644 index 0000000..71fa1e9 --- /dev/null +++ b/sky_phone/source/bridge/server/frameworks/qb.lua @@ -0,0 +1,47 @@ +if Bridge.Framework.Name ~= "qb" then + return +end + +local QBCore = exports["qb-core"]:GetCoreObject() + +local function get_player(source) + return QBCore.Functions.GetPlayer(tonumber(source)) +end + +function Bridge.Framework.GetPlayers() + local players = {} + for player_source in pairs(QBCore.Functions.GetQBPlayers()) do + players[#players + 1] = tonumber(player_source) + end + return players +end + +function Bridge.Framework.GetIdentifier(source) + local player = get_player(source) + return player and player.PlayerData and tostring(player.PlayerData.citizenid) or nil +end + +local function get_character_info(source) + local player = get_player(source) + return player and player.PlayerData and player.PlayerData.charinfo or nil +end + +function Bridge.Framework.GetFirstname(source) + local character = get_character_info(source) + return character and character.firstname or nil +end + +function Bridge.Framework.GetLastname(source) + local character = get_character_info(source) + return character and character.lastname or nil +end + +function Bridge.Framework.GetBirthdate(source) + local character = get_character_info(source) + return character and character.birthdate or nil +end + +function Bridge.Framework.RegisterUsableItem(item_name, callback) + QBCore.Functions.CreateUseableItem(item_name, callback) + return true +end diff --git a/sky_phone/source/bridge/server/frameworks/qbox.lua b/sky_phone/source/bridge/server/frameworks/qbox.lua new file mode 100644 index 0000000..6bde02f --- /dev/null +++ b/sky_phone/source/bridge/server/frameworks/qbox.lua @@ -0,0 +1,45 @@ +if Bridge.Framework.Name ~= "qbox" then + return +end + +local function get_player(source) + return exports.qbx_core:GetPlayer(tonumber(source)) +end + +function Bridge.Framework.GetPlayers() + local players = {} + for _, player_source in ipairs(GetPlayers()) do + players[#players + 1] = tonumber(player_source) + end + return players +end + +function Bridge.Framework.GetIdentifier(source) + local player = get_player(source) + return player and player.PlayerData and tostring(player.PlayerData.citizenid) or nil +end + +local function get_character_info(source) + local player = get_player(source) + return player and player.PlayerData and player.PlayerData.charinfo or nil +end + +function Bridge.Framework.GetFirstname(source) + local character = get_character_info(source) + return character and character.firstname or nil +end + +function Bridge.Framework.GetLastname(source) + local character = get_character_info(source) + return character and character.lastname or nil +end + +function Bridge.Framework.GetBirthdate(source) + local character = get_character_info(source) + return character and character.birthdate or nil +end + +function Bridge.Framework.RegisterUsableItem(item_name, callback) + exports.qbx_core:CreateUseableItem(item_name, callback) + return true +end diff --git a/sky_phone/source/bridge/server/inventory.lua b/sky_phone/source/bridge/server/inventory.lua new file mode 100644 index 0000000..97cbb85 --- /dev/null +++ b/sky_phone/source/bridge/server/inventory.lua @@ -0,0 +1,67 @@ +local configured_inventory = Config.Bridge.Inventory + +if configured_inventory == "auto" then + if GetResourceState("ox_inventory") == "started" then + configured_inventory = "ox" + elseif GetResourceState("qb-inventory") == "started" then + configured_inventory = "qb" + elseif GetResourceState("lj-inventory") == "started" then + configured_inventory = "lj" + elseif GetResourceState("qs-inventory") == "started" then + configured_inventory = "qs" + elseif GetResourceState("codem-inventory") == "started" then + configured_inventory = "codem" + elseif GetResourceState("core_inventory") == "started" then + configured_inventory = "core" + elseif GetResourceState("mf-inventory") == "started" then + configured_inventory = "mf" + end +end + +local supported_inventories = { + ox = true, + qb = true, + lj = true, + qs = true, + codem = true, + core = true, + mf = true, +} + +if not supported_inventories[configured_inventory] then + error(("[sky_phone] Unsupported or unavailable inventory '%s'. A metadata-capable inventory adapter is required."):format(tostring(configured_inventory))) +end + +Bridge.Inventory.Name = configured_inventory + +function Bridge.Inventory.NormalizeItem(item, metadata_field) + if not item then + return nil + end + + local metadata = item[metadata_field or "metadata"] + if type(metadata) ~= "table" then + metadata = item.metadata or item.info + end + + return { + name = item.name, + slot = item.slot or item.id, + count = tonumber(item.count or item.amount) or 0, + amount = tonumber(item.amount or item.count) or 0, + metadata = type(metadata) == "table" and metadata or {}, + } +end + +function Bridge.Inventory.MetadataMatches(actual, expected) + if type(expected) ~= "table" then + return true + end + actual = type(actual) == "table" and actual or {} + for key, value in pairs(expected) do + if actual[key] ~= value then + return false + end + end + return true +end diff --git a/sky_phone/source/bridge/server/inventory/codem.lua b/sky_phone/source/bridge/server/inventory/codem.lua new file mode 100644 index 0000000..cec2690 --- /dev/null +++ b/sky_phone/source/bridge/server/inventory/codem.lua @@ -0,0 +1,72 @@ +if Bridge.Inventory.Name ~= "codem" then + return +end + +local inventory = exports["codem-inventory"] + +local function normalize(item) + return Bridge.Inventory.NormalizeItem(item, "info") +end + +local function get_inventory(source) + return inventory:GetInventory(false, source) or {} +end + +function Bridge.Inventory.GetResourceName() + return "codem-inventory" +end + +function Bridge.Inventory.GetSlot(source, slot_id) + for _, item in pairs(get_inventory(source)) do + if tostring(item.slot) == tostring(slot_id) then + return normalize(item) + end + end + return nil +end + +function Bridge.Inventory.GetSlotsWithItem(source, item_name, metadata) + local matches = {} + for _, item in pairs(get_inventory(source)) do + local normalized = normalize(item) + if normalized.name == item_name and Bridge.Inventory.MetadataMatches(normalized.metadata, metadata) then + matches[#matches + 1] = normalized + end + end + return matches +end + +function Bridge.Inventory.SetSlotMetadata(source, slot_id, metadata) + local slot = Bridge.Inventory.GetSlot(source, slot_id) + if not slot then + return false + end + inventory:SetItemMetadata(source, slot.slot, metadata or {}) + local updated = Bridge.Inventory.GetSlot(source, slot.slot) + return updated and Bridge.Inventory.MetadataMatches(updated.metadata, metadata or {}) or false +end + +function Bridge.Inventory.CanCarryItem() + return true +end + +function Bridge.Inventory.AddItem(source, item_name, count, slot, metadata) + return inventory:AddItem(source, item_name, count, slot, metadata or {}) == true +end + +function Bridge.Inventory.RemoveItem(source, item_name, count, slot, metadata) + if metadata and not slot then + local slots = Bridge.Inventory.GetSlotsWithItem(source, item_name, metadata) + slot = slots[1] and slots[1].slot or nil + if not slot then + return 0 + end + end + return inventory:RemoveItem(source, item_name, count, slot) == true and count or 0 +end + +function Bridge.Inventory.RegisterUsableItem(item_name, callback) + return Bridge.Framework.RegisterUsableItem(item_name, function(source, item) + callback(source, normalize(item)) + end) +end diff --git a/sky_phone/source/bridge/server/inventory/core.lua b/sky_phone/source/bridge/server/inventory/core.lua new file mode 100644 index 0000000..939ab5c --- /dev/null +++ b/sky_phone/source/bridge/server/inventory/core.lua @@ -0,0 +1,80 @@ +if Bridge.Inventory.Name ~= "core" then + return +end + +local inventory = exports.core_inventory + +local function normalize(item) + return Bridge.Inventory.NormalizeItem(item, "metadata") +end + +local function get_inventory(source) + return inventory:getInventory(source) or {} +end + +function Bridge.Inventory.GetResourceName() + return "core_inventory" +end + +function Bridge.Inventory.GetSlot(source, slot_id) + for _, item in pairs(get_inventory(source)) do + local item_slot = item.slot or item.id + if tostring(item_slot) == tostring(slot_id) then + return normalize(item) + end + end + return nil +end + +function Bridge.Inventory.GetSlotsWithItem(source, item_name, metadata) + local matches = {} + for _, item in pairs(get_inventory(source)) do + local normalized = normalize(item) + if normalized.name == item_name and Bridge.Inventory.MetadataMatches(normalized.metadata, metadata) then + matches[#matches + 1] = normalized + end + end + return matches +end + +function Bridge.Inventory.SetSlotMetadata(source, slot_id, metadata) + local slot = Bridge.Inventory.GetSlot(source, slot_id) + if not slot then + return false + end + inventory:updateMetadata(source, slot.slot, metadata or {}) + local updated = Bridge.Inventory.GetSlot(source, slot.slot) + return updated and Bridge.Inventory.MetadataMatches(updated.metadata, metadata or {}) or false +end + +function Bridge.Inventory.CanCarryItem() + return true +end + +function Bridge.Inventory.AddItem(source, item_name, count, _, metadata) + return inventory:addItem(source, item_name, count, metadata or {}) == true +end + +function Bridge.Inventory.RemoveItem(source, item_name, count, slot, metadata) + if metadata and not slot then + local slots = Bridge.Inventory.GetSlotsWithItem(source, item_name, metadata) + slot = slots[1] and slots[1].slot or nil + if not slot then + return 0 + end + end + local success + if slot then + success = inventory:removeItemExact(source, slot, count) + else + success = inventory:removeItem(source, item_name, count) + end + return success == true and count or 0 +end + +function Bridge.Inventory.RegisterUsableItem(item_name, callback) + return Bridge.Framework.RegisterUsableItem(item_name, function(source, item_name_or_item, item) + local used_item = Bridge.Framework.GetName() == "esx" and item or item_name_or_item + callback(source, normalize(used_item)) + end) +end diff --git a/sky_phone/source/bridge/server/inventory/mf.lua b/sky_phone/source/bridge/server/inventory/mf.lua new file mode 100644 index 0000000..c713071 --- /dev/null +++ b/sky_phone/source/bridge/server/inventory/mf.lua @@ -0,0 +1,94 @@ +if Bridge.Inventory.Name ~= "mf" then + return +end + +if Bridge.Framework.GetName() ~= "esx" then + error("[sky_phone] mf-inventory is only supported with ESX.") +end + +local ESX = exports["es_extended"]:getSharedObject() +local inventory = exports["mf-inventory"] + +local function get_identifier(source) + local player = ESX.GetPlayerFromId(source) + return player and player.identifier or nil +end + +local function get_inventory(source) + local identifier = get_identifier(source) + return identifier and inventory:getInventoryItems(identifier) or {} +end + +local function normalize(item) + return Bridge.Inventory.NormalizeItem(item, "metadata") +end + +function Bridge.Inventory.GetResourceName() + return "mf-inventory" +end + +function Bridge.Inventory.GetSlot(source, slot_id) + for _, item in pairs(get_inventory(source)) do + if tostring(item.slot) == tostring(slot_id) then + return normalize(item) + end + end + return nil +end + +function Bridge.Inventory.GetSlotsWithItem(source, item_name, metadata) + local matches = {} + for _, item in pairs(get_inventory(source)) do + local normalized = normalize(item) + if normalized.name == item_name and Bridge.Inventory.MetadataMatches(normalized.metadata, metadata) then + matches[#matches + 1] = normalized + end + end + return matches +end + +function Bridge.Inventory.SetSlotMetadata(source, slot_id, metadata) + local identifier = get_identifier(source) + local slot = identifier and Bridge.Inventory.GetSlot(source, slot_id) or nil + if not slot then + return false + end + inventory:setItemProperty(identifier, slot.slot, "metadata", metadata or {}) + local updated = Bridge.Inventory.GetSlot(source, slot.slot) + return updated and Bridge.Inventory.MetadataMatches(updated.metadata, metadata or {}) or false +end + +function Bridge.Inventory.CanCarryItem(source, item_name, count) + local identifier = get_identifier(source) + return identifier and inventory:canCarry(identifier, item_name, count) == true or false +end + +function Bridge.Inventory.AddItem(source, item_name, count, _, metadata) + local identifier = get_identifier(source) + if not identifier or not Bridge.Inventory.CanCarryItem(source, item_name, count, metadata) then + return false + end + return inventory:addInventoryItem(identifier, item_name, count, source, 100, metadata or {}) == true +end + +function Bridge.Inventory.RemoveItem(source, item_name, count, slot, metadata) + if metadata and not slot then + local slots = Bridge.Inventory.GetSlotsWithItem(source, item_name, metadata) + slot = slots[1] and slots[1].slot or nil + if not slot then + return 0 + end + end + local identifier = get_identifier(source) + if not identifier then + return 0 + end + return inventory:removeInventoryItem(identifier, item_name, count, source, slot) == true and count or 0 +end + +function Bridge.Inventory.RegisterUsableItem(item_name, callback) + ESX.RegisterUsableItem(item_name, function(source, _, _, item) + callback(source, normalize(item)) + end) + return true +end diff --git a/sky_phone/source/bridge/server/inventory/ox.lua b/sky_phone/source/bridge/server/inventory/ox.lua new file mode 100644 index 0000000..6df08bb --- /dev/null +++ b/sky_phone/source/bridge/server/inventory/ox.lua @@ -0,0 +1,85 @@ +if Bridge.Inventory.Name ~= "ox" then + return +end + +local inventory = exports.ox_inventory +local usable_items = {} + +AddEventHandler("ox_inventory:usedItem", function(source, item_name, slot_id) + local callback = usable_items[item_name] + if not callback then + return + end + + local slot = Bridge.Inventory.GetSlot(source, slot_id) + if not slot or slot.name ~= item_name then + Bridge.Debug( + "warn", + "[sky_phone] ox_inventory reported an invalid used slot %s for item '%s' and source %s.", + tostring(slot_id), + tostring(item_name), + tostring(source) + ) + return + end + + callback(source, slot) +end) + +function Bridge.Inventory.GetResourceName() + return "ox_inventory" +end + +function Bridge.Inventory.GetSlot(source, slot_id) + return inventory:GetSlot(source, tonumber(slot_id)) +end + +function Bridge.Inventory.GetSlotsWithItem(source, item_name, metadata) + return inventory:GetSlotsWithItem(source, item_name, metadata, false) or {} +end + +function Bridge.Inventory.SetSlotMetadata(source, slot_id, metadata) + local slot = Bridge.Inventory.GetSlot(source, slot_id) + if not slot then + return false + end + + inventory:SetMetadata(source, tonumber(slot_id), metadata or {}) + return true +end + +function Bridge.Inventory.CanCarryItem(source, item_name, count, metadata) + return inventory:CanCarryItem(source, item_name, count, metadata) +end + +function Bridge.Inventory.AddItem(source, item_name, count, slot, metadata) + if not Bridge.Inventory.CanCarryItem(source, item_name, count, metadata) then + return false + end + + local success = inventory:AddItem(source, item_name, count, metadata, slot) + return success == true +end + +function Bridge.Inventory.RemoveItem(source, item_name, count, slot, metadata) + if slot then + local success = inventory:RemoveItem(source, item_name, count, nil, slot) + return success and count or 0 + end + + if metadata then + local slots = Bridge.Inventory.GetSlotsWithItem(source, item_name, metadata) + slot = slots[1] and slots[1].slot or nil + if not slot then + return 0 + end + end + + local success = inventory:RemoveItem(source, item_name, count, metadata, slot, false, false) + return success and count or 0 +end + +function Bridge.Inventory.RegisterUsableItem(item_name, callback) + usable_items[item_name] = callback + return true +end diff --git a/sky_phone/source/bridge/server/inventory/qb.lua b/sky_phone/source/bridge/server/inventory/qb.lua new file mode 100644 index 0000000..acd0781 --- /dev/null +++ b/sky_phone/source/bridge/server/inventory/qb.lua @@ -0,0 +1,124 @@ +if Bridge.Inventory.Name ~= "qb" and Bridge.Inventory.Name ~= "lj" then + return +end + +local resource_name = Bridge.Inventory.Name == "lj" and "lj-inventory" or "qb-inventory" +local inventory = exports[resource_name] +local QBCore = Bridge.Inventory.Name == "lj" and exports["qb-core"]:GetCoreObject() or nil + +local function get_lj_player(source) + return QBCore and QBCore.Functions.GetPlayer(tonumber(source)) or nil +end + +local function normalize(item) + if not item then + return nil + end + + return { + name = item.name, + slot = tonumber(item.slot), + count = tonumber(item.amount) or 0, + amount = tonumber(item.amount) or 0, + metadata = type(item.info) == "table" and item.info or {}, + } +end + +local function metadata_matches(actual, expected) + if type(expected) ~= "table" then + return true + end + actual = type(actual) == "table" and actual or {} + for key, value in pairs(expected) do + if actual[key] ~= value then + return false + end + end + return true +end + +function Bridge.Inventory.GetResourceName() + return resource_name +end + +function Bridge.Inventory.GetSlot(source, slot_id) + if Bridge.Inventory.Name == "lj" then + local player = get_lj_player(source) + return normalize(player and player.PlayerData.items[tonumber(slot_id)] or nil) + end + return normalize(inventory:GetItemBySlot(source, tonumber(slot_id))) +end + +function Bridge.Inventory.GetSlotsWithItem(source, item_name, metadata) + local matches = {} + local items + if Bridge.Inventory.Name == "lj" then + local player = get_lj_player(source) + items = player and player.PlayerData.items or {} + else + items = inventory:GetItemsByName(source, item_name) or {} + end + for _, item in pairs(items) do + local normalized = normalize(item) + if normalized.name == item_name and metadata_matches(normalized.metadata, metadata) then + matches[#matches + 1] = normalized + end + end + return matches +end + +function Bridge.Inventory.SetSlotMetadata(source, slot_id, metadata) + local slot = Bridge.Inventory.GetSlot(source, slot_id) + if not slot then + return false + end + if Bridge.Inventory.Name == "lj" then + local player = get_lj_player(source) + if not player then + return false + end + player.PlayerData.items[slot.slot].info = metadata or {} + player.Functions.SetInventory(player.PlayerData.items, true) + return true + end + return inventory:SetItemData(source, slot.name, "info", metadata or {}, slot.slot) == true +end + +function Bridge.Inventory.CanCarryItem(source, item_name, count) + if Bridge.Inventory.Name == "lj" then + return get_lj_player(source) ~= nil + end + return inventory:CanAddItem(source, item_name, count) == true +end + +function Bridge.Inventory.AddItem(source, item_name, count, slot, metadata) + if not Bridge.Inventory.CanCarryItem(source, item_name, count, metadata) then + return false + end + if Bridge.Inventory.Name == "lj" then + local player = get_lj_player(source) + return player and player.Functions.AddItem(item_name, count, slot, metadata or {}) == true or false + end + return inventory:AddItem(source, item_name, count, slot, metadata or {}, "sky_phone") == true +end + +function Bridge.Inventory.RemoveItem(source, item_name, count, slot, metadata) + if metadata and not slot then + local slots = Bridge.Inventory.GetSlotsWithItem(source, item_name, metadata) + slot = slots[1] and slots[1].slot or nil + if not slot then + return 0 + end + end + if Bridge.Inventory.Name == "lj" then + local player = get_lj_player(source) + return player and player.Functions.RemoveItem(item_name, count, slot) == true and count or 0 + end + return inventory:RemoveItem(source, item_name, count, slot, "sky_phone") == true and count or 0 +end + +function Bridge.Inventory.RegisterUsableItem(item_name, callback) + return Bridge.Framework.RegisterUsableItem(item_name, function(source, item) + callback(source, normalize(item)) + end) +end diff --git a/sky_phone/source/bridge/server/inventory/qs.lua b/sky_phone/source/bridge/server/inventory/qs.lua new file mode 100644 index 0000000..ce65805 --- /dev/null +++ b/sky_phone/source/bridge/server/inventory/qs.lua @@ -0,0 +1,81 @@ +if Bridge.Inventory.Name ~= "qs" then + return +end + +local inventory = exports["qs-inventory"] + +local function normalize(item) + return Bridge.Inventory.NormalizeItem(item, "info") +end + +local function get_inventory(source) + return inventory:GetInventory(source) or {} +end + +function Bridge.Inventory.GetResourceName() + return "qs-inventory" +end + +function Bridge.Inventory.GetSlot(source, slot_id) + for _, item in pairs(get_inventory(source)) do + if tostring(item.slot) == tostring(slot_id) then + return normalize(item) + end + end + return nil +end + +function Bridge.Inventory.GetSlotsWithItem(source, item_name, metadata) + local matches = {} + for _, item in pairs(get_inventory(source)) do + local normalized = normalize(item) + if normalized.name == item_name and Bridge.Inventory.MetadataMatches(normalized.metadata, metadata) then + matches[#matches + 1] = normalized + end + end + return matches +end + +function Bridge.Inventory.SetSlotMetadata(source, slot_id, metadata) + local slot = Bridge.Inventory.GetSlot(source, slot_id) + if not slot then + return false + end + inventory:SetItemMetadata(source, slot.slot, metadata or {}) + local updated = Bridge.Inventory.GetSlot(source, slot.slot) + return updated and Bridge.Inventory.MetadataMatches(updated.metadata, metadata or {}) or false +end + +function Bridge.Inventory.CanCarryItem(source, item_name, count) + return inventory:CanCarryItem(source, item_name, count) == true +end + +function Bridge.Inventory.AddItem(source, item_name, count, slot, metadata) + if not Bridge.Inventory.CanCarryItem(source, item_name, count, metadata) then + return false + end + return inventory:AddItem(source, item_name, count, slot, metadata or {}) == true +end + +function Bridge.Inventory.RemoveItem(source, item_name, count, slot, metadata) + if metadata and not slot then + local slots = Bridge.Inventory.GetSlotsWithItem(source, item_name, metadata) + slot = slots[1] and slots[1].slot or nil + if not slot then + return 0 + end + end + return inventory:RemoveItem(source, item_name, count, slot, metadata) == true and count or 0 +end + +function Bridge.Inventory.RegisterUsableItem(item_name, callback) + if Bridge.Framework.GetName() == "esx" then + inventory:CreateUsableItem(item_name, function(source, item) + callback(source, normalize(item)) + end) + return true + end + return Bridge.Framework.RegisterUsableItem(item_name, function(source, item) + callback(source, normalize(item)) + end) +end diff --git a/sky_phone/source/bridge/server/migrations.lua b/sky_phone/source/bridge/server/migrations.lua new file mode 100644 index 0000000..21ccf81 --- /dev/null +++ b/sky_phone/source/bridge/server/migrations.lua @@ -0,0 +1,192 @@ +local completed_migrations = {} +local migration_callbacks = {} + +local function build_column_definition(column) + local data_type, attributes = column.type:match("^(%S+)%s*(.*)$") + local definition = data_type + + if column.characterSet then + definition = definition .. " CHARACTER SET " .. column.characterSet + end + if column.collation then + definition = definition .. " COLLATE " .. column.collation + end + if attributes ~= "" then + definition = definition .. " " .. attributes + end + + return definition +end + +local function build_create_query(table_definition) + local definitions = {} + for index = 1, #table_definition.columns do + local column = table_definition.columns[index] + definitions[#definitions + 1] = ("`%s` %s"):format(column.name, build_column_definition(column)) + end + + if table_definition.primaryKey then + definitions[#definitions + 1] = ("PRIMARY KEY (`%s`)"):format(table_definition.primaryKey) + end + for _, unique_key in ipairs(table_definition.uniqueKeys or {}) do + definitions[#definitions + 1] = ("UNIQUE KEY `%s` %s"):format(unique_key.name, unique_key.columns) + end + for _, index in ipairs(table_definition.indexes or {}) do + definitions[#definitions + 1] = ("INDEX `%s` %s"):format(index.name, index.columns) + end + for _, foreign_key in ipairs(table_definition.foreignKeys or {}) do + definitions[#definitions + 1] = ("FOREIGN KEY (`%s`) REFERENCES %s"):format( + foreign_key.column, + foreign_key.references + ) + end + + return ("CREATE TABLE IF NOT EXISTS `%s` (\n%s\n) %s"):format( + table_definition.name, + table.concat(definitions, ",\n"), + table_definition.tableOptions or "" + ) +end + +local function query_or_error(query, parameters, context) + local success, result = pcall(Bridge.Database.Query, query, parameters) + if not success then + error(("[sky_phone] Database migration failed while %s: %s"):format(context, tostring(result))) + end + return result +end + +function Bridge.Database.EnsureIndex(table_name, index_name, columns, options) + local table_count = Bridge.Database.Query([[ + SELECT COUNT(*) AS `count` + FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? + ]], { table_name }) + if not table_count[1] or tonumber(table_count[1].count) == 0 then + return false + end + + local indexes = Bridge.Database.Query(("SHOW INDEX FROM `%s`"):format(table_name), {}) + for _, index in ipairs(indexes) do + if index.Key_name == index_name then + return false + end + end + + local index_type = options and options.unique and "UNIQUE KEY" or "INDEX" + query_or_error( + ("ALTER TABLE `%s` ADD %s `%s` %s"):format(table_name, index_type, index_name, columns), + {}, + ("adding index '%s'"):format(index_name) + ) + Bridge.Debug("info", "[sky_phone] Added database index '%s' to '%s'.", index_name, table_name) + return true +end + +function Bridge.Database.Migrate(migration_name, schema) + local table_names = {} + local placeholders = {} + for index = 1, #schema do + table_names[index] = schema[index].name + placeholders[index] = "?" + end + + local existing_tables = {} + local existing_columns = {} + if #table_names > 0 then + local placeholder_list = table.concat(placeholders, ", ") + local tables = Bridge.Database.Query(([[ + SELECT TABLE_NAME + FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN (%s) + ]]):format(placeholder_list), table_names) + for _, row in ipairs(tables) do + existing_tables[(row.TABLE_NAME or row.table_name):lower()] = true + end + + local columns = Bridge.Database.Query(([[ + SELECT TABLE_NAME, COLUMN_NAME, CHARACTER_SET_NAME, COLLATION_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN (%s) + ]]):format(placeholder_list), table_names) + for _, row in ipairs(columns) do + local table_name = (row.TABLE_NAME or row.table_name):lower() + local column_name = (row.COLUMN_NAME or row.column_name):lower() + existing_columns[table_name] = existing_columns[table_name] or {} + existing_columns[table_name][column_name] = { + character_set = row.CHARACTER_SET_NAME or row.character_set_name, + collation = row.COLLATION_NAME or row.collation_name, + } + end + end + + for _, table_definition in ipairs(schema) do + local table_name = table_definition.name:lower() + if not existing_tables[table_name] then + query_or_error(build_create_query(table_definition), {}, ("creating table '%s'"):format(table_definition.name)) + Bridge.Debug("info", "[sky_phone] Created database table '%s'.", table_definition.name) + else + local columns = existing_columns[table_name] or {} + for _, column in ipairs(table_definition.columns) do + local current = columns[column.name:lower()] + local definition = build_column_definition(column) + if not current then + query_or_error( + ("ALTER TABLE `%s` ADD COLUMN `%s` %s"):format(table_definition.name, column.name, definition), + {}, + ("adding column '%s.%s'"):format(table_definition.name, column.name) + ) + elseif (column.characterSet and current.character_set ~= column.characterSet) + or (column.collation and current.collation ~= column.collation) then + query_or_error( + ("ALTER TABLE `%s` MODIFY COLUMN `%s` %s"):format(table_definition.name, column.name, definition), + {}, + ("updating column '%s.%s'"):format(table_definition.name, column.name) + ) + end + end + + for _, index in ipairs(table_definition.indexes or {}) do + Bridge.Database.EnsureIndex(table_definition.name, index.name, index.columns) + end + end + end + +end + +function Bridge.Database.CompleteMigration(migration_name) + if completed_migrations[migration_name] then + error(("[sky_phone] Database migration '%s' was completed more than once."):format(tostring(migration_name))) + end + + completed_migrations[migration_name] = true + local callbacks = migration_callbacks[migration_name] or {} + migration_callbacks[migration_name] = nil + + Bridge.Debug("info", "[sky_phone] Database migration '%s' completed.", migration_name) + for index = 1, #callbacks do + callbacks[index]() + end +end + +function Bridge.Database.AfterMigration(migration_name, callback) + if type(callback) ~= "function" then + error("[sky_phone] Database migration callback must be a function.") + end + if completed_migrations[migration_name] then + callback() + return + end + + migration_callbacks[migration_name] = migration_callbacks[migration_name] or {} + migration_callbacks[migration_name][#migration_callbacks[migration_name] + 1] = callback +end + +function Bridge.Database.AwaitMigration(migration_name) + if completed_migrations[migration_name] then + return true + end + + Bridge.Debug("error", "[sky_phone] Database migration '%s' has not completed.", tostring(migration_name)) + return false +end diff --git a/sky_phone/source/bridge/shared.lua b/sky_phone/source/bridge/shared.lua new file mode 100644 index 0000000..d72fdcb --- /dev/null +++ b/sky_phone/source/bridge/shared.lua @@ -0,0 +1,30 @@ +Bridge = Bridge or {} +Bridge.Callbacks = Bridge.Callbacks or {} +Bridge.Database = Bridge.Database or {} +Bridge.Framework = Bridge.Framework or {} +Bridge.Inventory = Bridge.Inventory or {} + +local level_colours = { + debug = "^5", + info = "^2", + warn = "^3", + error = "^1", +} + +function Bridge.Debug(level, message, ...) + local arguments = { ... } + local options = type(arguments[#arguments]) == "table" and arguments[#arguments] or nil + if options then + arguments[#arguments] = nil + end + + local bridge_config = Config and Config.Bridge or nil + local enabled = options and options.always + or bridge_config and (bridge_config.Debug or bridge_config.DebugLevels and bridge_config.DebugLevels[level]) + if not enabled then + return + end + + local formatted = #arguments > 0 and message:format(table.unpack(arguments)) or message + print(("%s%s^0"):format(level_colours[level] or "^7", formatted)) +end diff --git a/sky_phone/source/client/main.lua b/sky_phone/source/client/main.lua index cf78d6a..6850907 100644 --- a/sky_phone/source/client/main.lua +++ b/sky_phone/source/client/main.lua @@ -4,7 +4,7 @@ local device_payload = nil local sim_picker_open = false local call_channel = 0 -Sky.Debug("debug", "[sky_phone] Client script initialized.", { always = true }) +Bridge.Debug("debug", "[sky_phone] Client script initialized.", { always = true }) local server_callbacks = { "device:save", @@ -46,7 +46,7 @@ local server_callbacks = { } local function get_locale() - return Locales[Sky.Config.locale] or Locales["en"] + return Locales[Config.Bridge.Locale] or Locales["en"] end local function send_open_message() @@ -55,7 +55,7 @@ local function send_open_message() end local payload = device_payload - payload.lang = Sky.Config.locale + payload.lang = Config.Bridge.Locale payload.locales = get_locale().Nui SendNUIMessage({ type = "app:open", @@ -82,7 +82,7 @@ local function close_phone() is_open = false SetNuiFocus(notification_focus or sim_picker_open, notification_focus or sim_picker_open) SendNUIMessage({ type = "app:close" }) - Sky.Cb.Trigger("sky_phone:device:close", {}) + Bridge.Callbacks.Trigger("sky_phone:device:close", {}) end local function leave_call_voice() @@ -97,11 +97,11 @@ end local function join_call_voice(channel) if Config.Calls.VoiceProvider ~= "pma" then - Sky.Debug("error", "[sky_phone] Unsupported voice provider '%s'.", tostring(Config.Calls.VoiceProvider)) + Bridge.Debug("error", "[sky_phone] Unsupported voice provider '%s'.", tostring(Config.Calls.VoiceProvider)) return false end if GetResourceState("pma-voice") ~= "started" then - Sky.Debug("error", "[sky_phone] Configured pma-voice provider is not started.") + Bridge.Debug("error", "[sky_phone] Configured pma-voice provider is not started.") return false end call_channel = tonumber(channel) or 0 @@ -115,7 +115,7 @@ if Config.Phone.DevelopmentCommand then close_phone() return end - Sky.Cb.Trigger("sky_phone:device:development-open", {}) + Bridge.Callbacks.Trigger("sky_phone:device:development-open", {}) end, false) end @@ -160,7 +160,7 @@ end) for _, callback_name in ipairs(server_callbacks) do RegisterNUICallback(callback_name, function(data, cb) - local result = Sky.Cb.Trigger("sky_phone:" .. callback_name, data) + local result = Bridge.Callbacks.Trigger("sky_phone:" .. callback_name, data) if result then cb(result) return @@ -171,7 +171,7 @@ for _, callback_name in ipairs(server_callbacks) do end RegisterNetEvent("sky_phone:device:open", function(data) - Sky.Debug( + Bridge.Debug( "debug", "[sky_phone] Client received device open for IMEI %s account_linked=%s.", tostring(data.device.imei), @@ -193,14 +193,14 @@ RegisterNetEvent("sky_phone:device:invalidated", function() end) RegisterNetEvent("sky_phone:device:error", function(error_code) - Sky.Debug( + Bridge.Debug( "debug", "[sky_phone] Client received device error: %s.", tostring(error_code), { always = true } ) local message = get_locale().DeviceErrors[error_code] or get_locale().DeviceErrors.default - Sky.Show.Notification("iFruit", message, "error", 5000) + Bridge.Framework.Notify("iFruit", message, "error", 5000) end) RegisterNetEvent("sky_phone:mail:changed", function(data) diff --git a/sky_phone/source/server/calls.lua b/sky_phone/source/server/calls.lua index e04d8bc..3777cc4 100644 --- a/sky_phone/source/server/calls.lua +++ b/sky_phone/source/server/calls.lua @@ -1,6 +1,4 @@ -if not Sky.DB.AwaitMigrations("sky_phone") then - error("[sky_phone] Calling database migrations did not complete.") -end +Bridge.Database.AfterMigration("sky_phone", function() SkyPhoneCalls = {} @@ -12,7 +10,7 @@ local dialing_by_sim = {} local next_voice_channel = 10000 local function uuid() - local rows = Sky.Query("SELECT UUID() AS `id`", {}) + local rows = Bridge.Database.Query("SELECT UUID() AS `id`", {}) if not rows[1] or type(rows[1].id) ~= "string" then error("[sky_phone] Database did not generate a call UUID.") end @@ -60,7 +58,7 @@ local function scope_condition(scope, alias) end local function find_device_holder(imei) - for _, player_source in ipairs(Sky.FW.GetPlayers()) do + for _, player_source in ipairs(Bridge.Framework.GetPlayers()) do local source = tonumber(player_source) or player_source if SkyPhone.FindDeviceSlots(source, imei)[1] then return source @@ -70,7 +68,7 @@ local function find_device_holder(imei) end local function airplane_mode(imei) - local rows = Sky.Query([[ + local rows = Bridge.Database.Query([[ SELECT `payload` FROM `sky_phone_device_data` WHERE `device_imei` = ? AND `namespace` = 'settings' LIMIT 1 ]], { imei }) @@ -83,7 +81,7 @@ end local function add_call_entry(call_id, device, direction, status, other_number) local account_id, device_imei = scope_for_device(device) - Sky.Query([[ + Bridge.Database.Query([[ INSERT INTO `sky_phone_call_entries` (`call_id`, `account_id`, `device_imei`, `direction`, `status`, `other_number`) VALUES (?, ?, ?, ?, ?, ?) @@ -122,7 +120,7 @@ local function finish_call(call, status) if status == "no_answer" or status == "cancelled" then callee_status = "missed" end - Sky.DB.Transaction({ + Bridge.Database.Transaction({ { query = [[ UPDATE `sky_phone_calls` @@ -167,19 +165,19 @@ function SkyPhoneCalls.EndForSim(sim_id, reason) end function SkyPhoneCalls.LinkAccountData(account_id, imei) - local counts = Sky.Query([[ + local counts = Bridge.Database.Query([[ SELECT (SELECT COUNT(*) FROM `sky_phone_contacts` WHERE `account_id` = ?) AS `contacts`, (SELECT COUNT(*) FROM `sky_phone_call_entries` WHERE `account_id` = ?) AS `recents` ]], { account_id, account_id }) local has_cloud_data = counts[1] and ((tonumber(counts[1].contacts) or 0) > 0 or (tonumber(counts[1].recents) or 0) > 0) if has_cloud_data then - return Sky.DB.Transaction({ + return Bridge.Database.Transaction({ { query = "DELETE FROM `sky_phone_contacts` WHERE `device_imei` = ? AND `account_id` IS NULL", params = { imei } }, { query = "DELETE FROM `sky_phone_call_entries` WHERE `device_imei` = ? AND `account_id` IS NULL", params = { imei } }, }) end - return Sky.DB.Transaction({ + return Bridge.Database.Transaction({ { query = "UPDATE `sky_phone_contacts` SET `account_id` = ?, `device_imei` = NULL WHERE `device_imei` = ? AND `account_id` IS NULL", params = { account_id, imei }, @@ -192,7 +190,7 @@ function SkyPhoneCalls.LinkAccountData(account_id, imei) end function SkyPhoneCalls.CopyCloudToDevice(account_id, imei) - return Sky.DB.Transaction({ + return Bridge.Database.Transaction({ { query = "DELETE FROM `sky_phone_contacts` WHERE `device_imei` = ? AND `account_id` IS NULL", params = { imei } }, { query = "DELETE FROM `sky_phone_call_entries` WHERE `device_imei` = ? AND `account_id` IS NULL", params = { imei } }, { @@ -216,20 +214,20 @@ function SkyPhoneCalls.CopyCloudToDevice(account_id, imei) }) end -Sky.Cb.Register("sky_phone:contacts:list", function(source) +Bridge.Callbacks.Register("sky_phone:contacts:list", function(source) local scope, error_response = current_scope(source) if not scope then return error_response end local condition, params = scope_condition(scope) - local rows = Sky.Query(([[ + local rows = Bridge.Database.Query(([[ SELECT `contact_id` AS `id`, `name`, `phone_number`, `created_at`, `updated_at` FROM `sky_phone_contacts` WHERE %s ORDER BY LOWER(`name`), `phone_number` ]]):format(condition), params) return { success = true, data = rows } end) -Sky.Cb.Register("sky_phone:contacts:save", function(source, data) +Bridge.Callbacks.Register("sky_phone:contacts:save", function(source, data) if not SkyPhone.AllowOperation(source, "contact_save", 30, 60) or type(data) ~= "table" then return { success = false, error = "invalid_request" } end @@ -249,7 +247,7 @@ Sky.Cb.Register("sky_phone:contacts:save", function(source, data) 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) + 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 @@ -257,12 +255,12 @@ Sky.Cb.Register("sky_phone:contacts:save", function(source, data) for _, value in ipairs(condition_params) do params[#params + 1] = value end - Sky.Query(([[ + Bridge.Database.Query(([[ UPDATE `sky_phone_contacts` SET `name` = ?, `phone_number` = ? WHERE `contact_id` = ? AND %s ]]):format(condition), params) else - Sky.Query([[ + 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 }) @@ -273,7 +271,7 @@ Sky.Cb.Register("sky_phone:contacts:save", function(source, data) return { success = true, data = { id = id, name = name, phone_number = number } } end) -Sky.Cb.Register("sky_phone:contacts:delete", function(source, data) +Bridge.Callbacks.Register("sky_phone:contacts:delete", function(source, data) if type(data) ~= "table" or type(data.id) ~= "string" then return { success = false, error = "invalid_request" } end @@ -286,21 +284,21 @@ Sky.Cb.Register("sky_phone:contacts:delete", function(source, data) 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) + Bridge.Database.Query(("DELETE FROM `sky_phone_contacts` WHERE `contact_id` = ? AND %s"):format(condition), params) if scope.account_id then SkyPhone.NotifyAccount(scope.account_id, "sky_phone:contacts:changed", {}) end return { success = true } end) -Sky.Cb.Register("sky_phone:calls:recents", function(source) +Bridge.Callbacks.Register("sky_phone:calls:recents", function(source) local scope, error_response = current_scope(source) if not scope then return error_response end local condition, params = scope_condition(scope, "e") params[#params + 1] = Config.Calls.RecentPageSize - local rows = Sky.Query(([[ + local rows = Bridge.Database.Query(([[ SELECT e.`id`, e.`call_id`, e.`direction`, e.`status`, e.`other_number`, e.`created_at`, c.`duration_seconds` FROM `sky_phone_call_entries` e @@ -312,7 +310,7 @@ end) local function create_terminal_call(scope, number, target_sim, status) local id = uuid() - Sky.Query([[ + Bridge.Database.Query([[ INSERT INTO `sky_phone_calls` (`id`, `caller_sim_id`, `callee_sim_id`, `caller_number`, `callee_number`, `status`, `ended_at`) VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) @@ -328,7 +326,7 @@ local function create_terminal_call(scope, number, target_sim, status) } end -Sky.Cb.Register("sky_phone:calls:dial", function(source, data) +Bridge.Callbacks.Register("sky_phone:calls:dial", function(source, data) if not SkyPhone.AllowOperation(source, "call_dial", 15, 60) then return { success = false, error = "rate_limited" } end @@ -366,7 +364,7 @@ Sky.Cb.Register("sky_phone:calls:dial", function(source, data) return { success = false, error = "busy" } end dialing_by_sim[scope.device.sim_id] = true - local targets = Sky.Query([[ + local targets = Bridge.Database.Query([[ SELECT s.`id`, s.`phone_number`, d.`imei`, d.`account_id`, d.`device_name` FROM `sky_phone_sims` s LEFT JOIN `sky_phone_devices` d ON d.`sim_id` = s.`id` WHERE s.`phone_number` = ? LIMIT 1 @@ -406,7 +404,7 @@ Sky.Cb.Register("sky_phone:calls:dial", function(source, data) callee_device = target, started_at = os.time(), } - Sky.Query([[ + Bridge.Database.Query([[ INSERT INTO `sky_phone_calls` (`id`, `caller_sim_id`, `callee_sim_id`, `caller_number`, `callee_number`, `status`) VALUES (?, ?, ?, ?, ?, 'ringing') @@ -442,7 +440,7 @@ Sky.Cb.Register("sky_phone:calls:dial", function(source, data) 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) +Bridge.Callbacks.Register("sky_phone:calls:answer", function(source, data) local call = type(data) == "table" and calls[data.id] or nil if not call or call.callee_source ~= source or call.answered_at then return { success = false, error = "call_not_found" } @@ -457,14 +455,14 @@ Sky.Cb.Register("sky_phone:calls:answer", function(source, data) 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 }) + Bridge.Database.Query("UPDATE `sky_phone_calls` SET `status` = 'connected', `answered_at` = CURRENT_TIMESTAMP WHERE `id` = ?", { call.id }) + Bridge.Database.Query("UPDATE `sky_phone_call_entries` SET `status` = 'connected' WHERE `call_id` = ?", { call.id }) send_state(call, call.caller_source, "connected", call.channel) send_state(call, call.callee_source, "connected", call.channel) return { success = true } end) -Sky.Cb.Register("sky_phone:calls:decline", function(source, data) +Bridge.Callbacks.Register("sky_phone:calls:decline", function(source, data) local call = type(data) == "table" and calls[data.id] or nil if not call or call.callee_source ~= source or call.answered_at then return { success = false, error = "call_not_found" } @@ -473,7 +471,7 @@ Sky.Cb.Register("sky_phone:calls:decline", function(source, data) return { success = true } end) -Sky.Cb.Register("sky_phone:calls:hangup", function(source, data) +Bridge.Callbacks.Register("sky_phone:calls:hangup", function(source, data) local call_id = active_by_source[source] local call = call_id and calls[call_id] or nil if not call or (type(data) == "table" and data.id and data.id ~= call.id) then @@ -519,3 +517,4 @@ AddEventHandler("onResourceStop", function(resource_name) finish_call(calls[call_id], "disconnected") end end) +end) diff --git a/sky_phone/source/server/db_migrate.lua b/sky_phone/source/server/db_migrate.lua index 97c1759..536fb54 100644 --- a/sky_phone/source/server/db_migrate.lua +++ b/sky_phone/source/server/db_migrate.lua @@ -1,4 +1,4 @@ -local legacy_accounts = Sky.Query([[ +local legacy_accounts = Bridge.Database.Query([[ SELECT TABLE_NAME AS `name` FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() @@ -10,7 +10,7 @@ for _, row in ipairs(legacy_accounts) do end if account_tables.sky_phone_mail_accounts and not account_tables.sky_phone_accounts then - Sky.Query("RENAME TABLE `sky_phone_mail_accounts` TO `sky_phone_accounts`", {}) + Bridge.Database.Query("RENAME TABLE `sky_phone_mail_accounts` TO `sky_phone_accounts`", {}) elseif account_tables.sky_phone_mail_accounts and account_tables.sky_phone_accounts then error("[sky_phone] Both legacy and current iFruit account tables exist; refusing an ambiguous migration.") end @@ -250,6 +250,7 @@ local schema = { name = "sky_phone_contacts", 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 = "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" }, @@ -319,8 +320,9 @@ local schema = { }, } -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 }) +Bridge.Database.Migrate("sky_phone", schema) +Bridge.Database.EnsureIndex("sky_phone_devices", "uniq_sky_phone_devices_sim", "(`sim_id`)", { unique = true }) +Bridge.Database.Query("UPDATE `sky_phone_contacts` SET `contact_id` = `id` WHERE `contact_id` IS NULL", {}) +Bridge.Database.EnsureIndex("sky_phone_contacts", "uniq_sky_phone_contacts_account_contact", "(`account_id`, `contact_id`)", { unique = true }) +Bridge.Database.EnsureIndex("sky_phone_contacts", "uniq_sky_phone_contacts_device_contact", "(`device_imei`, `contact_id`)", { unique = true }) +Bridge.Database.CompleteMigration("sky_phone") diff --git a/sky_phone/source/server/mail.lua b/sky_phone/source/server/mail.lua index 5b8975c..8fa959d 100644 --- a/sky_phone/source/server/mail.lua +++ b/sky_phone/source/server/mail.lua @@ -1,6 +1,4 @@ -if not Sky.DB.AwaitMigrations("sky_phone") then - error("[sky_phone] Mail database migrations did not complete.") -end +Bridge.Database.AfterMigration("sky_phone", function() local function trim(value) if type(value) ~= "string" then @@ -15,7 +13,7 @@ local function validate_payload(source, operation, value) return value end - Sky.Debug( + Bridge.Debug( "warn", "[sky_phone] Invalid mail payload for %s from source %s.", operation, @@ -98,7 +96,7 @@ local function require_session(source) end local function new_database_id() - local rows = Sky.Query("SELECT UUID() AS id", {}) + 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 mail id.") end @@ -111,7 +109,7 @@ local function notify_account(account_id, event_name, data) end local function get_counts(account_id) - local rows = Sky.Query([[ + local rows = Bridge.Database.Query([[ SELECT SUM(CASE WHEN `folder` = 'inbox' AND `trashed_at` IS NULL AND `read_at` IS NULL THEN 1 ELSE 0 END) AS unread, SUM(CASE WHEN `folder` = 'inbox' AND `trashed_at` IS NULL THEN 1 ELSE 0 END) AS inbox, @@ -120,7 +118,7 @@ local function get_counts(account_id) FROM `sky_phone_mail_entries` WHERE `account_id` = ? ]], { account_id }) - local drafts = Sky.Query( + local drafts = Bridge.Database.Query( "SELECT COUNT(*) AS count FROM `sky_phone_mail_drafts` WHERE `account_id` = ?", { account_id } ) @@ -141,7 +139,7 @@ local function broadcast_mailbox_changed(account_id, counts) }) end -Sky.Cb.Register("sky_phone:mail:counts", function(source) +Bridge.Callbacks.Register("sky_phone:mail:counts", function(source) local session, error_response = require_session(source) if not session then return error_response @@ -150,7 +148,7 @@ Sky.Cb.Register("sky_phone:mail:counts", function(source) return { success = true, data = get_counts(session.id) } end) -Sky.Cb.Register("sky_phone:mail:list", function(source, data) +Bridge.Callbacks.Register("sky_phone:mail:list", function(source, data) local session, error_response = require_session(source) if not session then return error_response @@ -176,7 +174,7 @@ Sky.Cb.Register("sky_phone:mail:list", function(source, data) if folder == "drafts" then local pattern = "%" .. search .. "%" - rows = Sky.Query([[ + rows = Bridge.Database.Query([[ SELECT `id`, `recipients`, `subject`, LEFT(`body`, 180) AS `preview`, `updated_at` AS `created_at` FROM `sky_phone_mail_drafts` WHERE `account_id` = ? AND (? = '' OR `subject` LIKE ? OR `body` LIKE ? OR `recipients` LIKE ?) @@ -202,7 +200,7 @@ Sky.Cb.Register("sky_phone:mail:list", function(source, data) values[#values + 1] = limit values[#values + 1] = offset - rows = Sky.Query(([[ + rows = Bridge.Database.Query(([[ SELECT e.`id`, e.`folder`, e.`read_at`, e.`trashed_at`, m.`id` AS `message_id`, sender.`email` AS `sender`, m.`recipients`, m.`subject`, LEFT(m.`body`, 180) AS `preview`, m.`created_at` @@ -232,7 +230,7 @@ Sky.Cb.Register("sky_phone:mail:list", function(source, data) } end) -Sky.Cb.Register("sky_phone:mail:get", function(source, data) +Bridge.Callbacks.Register("sky_phone:mail:get", function(source, data) local session, error_response = require_session(source) if not session then return error_response @@ -248,7 +246,7 @@ Sky.Cb.Register("sky_phone:mail:get", function(source, data) return { success = false, error = "invalid_message" } end - local rows = Sky.Query([[ + local rows = Bridge.Database.Query([[ SELECT e.`id`, e.`folder`, e.`read_at`, e.`trashed_at`, m.`id` AS `message_id`, sender.`email` AS `sender`, m.`recipients`, m.`subject`, m.`body`, m.`created_at` FROM `sky_phone_mail_entries` e @@ -262,7 +260,7 @@ Sky.Cb.Register("sky_phone:mail:get", function(source, data) end if not rows[1].read_at then - Sky.Query( + Bridge.Database.Query( "UPDATE `sky_phone_mail_entries` SET `read_at` = CURRENT_TIMESTAMP WHERE `id` = ? AND `account_id` = ?", { entry_id, session.id } ) @@ -276,7 +274,7 @@ Sky.Cb.Register("sky_phone:mail:get", function(source, data) return { success = true, data = rows[1] } end) -Sky.Cb.Register("sky_phone:mail:get-draft", function(source, data) +Bridge.Callbacks.Register("sky_phone:mail:get-draft", function(source, data) local session, error_response = require_session(source) if not session then return error_response @@ -292,7 +290,7 @@ Sky.Cb.Register("sky_phone:mail:get-draft", function(source, data) return { success = false, error = "invalid_draft" } end - local rows = Sky.Query([[ + local rows = Bridge.Database.Query([[ SELECT `id`, `recipients`, `subject`, `body`, `created_at`, `updated_at` FROM `sky_phone_mail_drafts` WHERE `id` = ? AND `account_id` = ? @@ -306,7 +304,7 @@ Sky.Cb.Register("sky_phone:mail:get-draft", function(source, data) return { success = true, data = rows[1] } end) -Sky.Cb.Register("sky_phone:mail:save-draft", function(source, data) +Bridge.Callbacks.Register("sky_phone:mail:save-draft", function(source, data) local session, error_response = require_session(source) if not session then return error_response @@ -332,7 +330,7 @@ Sky.Cb.Register("sky_phone:mail:save-draft", function(source, data) end if id then - local result = Sky.Query([[ + local result = Bridge.Database.Query([[ UPDATE `sky_phone_mail_drafts` SET `recipients` = ?, `subject` = ?, `body` = ?, `updated_at` = CURRENT_TIMESTAMP WHERE `id` = ? AND `account_id` = ? @@ -342,7 +340,7 @@ Sky.Cb.Register("sky_phone:mail:save-draft", function(source, data) end else id = new_database_id() - Sky.Query([[ + Bridge.Database.Query([[ INSERT INTO `sky_phone_mail_drafts` (`id`, `account_id`, `recipients`, `subject`, `body`) VALUES (?, ?, ?, ?, ?) ]], { id, session.id, json.encode(recipients), subject, body }) @@ -352,7 +350,7 @@ Sky.Cb.Register("sky_phone:mail:save-draft", function(source, data) return { success = true, data = { id = id } } end) -Sky.Cb.Register("sky_phone:mail:delete-draft", function(source, data) +Bridge.Callbacks.Register("sky_phone:mail:delete-draft", function(source, data) local session, error_response = require_session(source) if not session then return error_response @@ -368,15 +366,16 @@ Sky.Cb.Register("sky_phone:mail:delete-draft", function(source, data) return { success = false, error = "invalid_draft" } end - Sky.Query( + Bridge.Database.Query( "DELETE FROM `sky_phone_mail_drafts` WHERE `id` = ? AND `account_id` = ?", { id, session.id } ) broadcast_mailbox_changed(session.id) return { success = true } end) +end) -Sky.Cb.Register("sky_phone:mail:send", function(source, data) +Bridge.Callbacks.Register("sky_phone:mail:send", function(source, data) local session, error_response = require_session(source) if not session then return error_response @@ -402,7 +401,7 @@ Sky.Cb.Register("sky_phone:mail:send", function(source, data) for index = 1, #recipients do placeholders[index] = "?" end - local recipient_accounts = Sky.Query( + local recipient_accounts = Bridge.Database.Query( ("SELECT `id`, `email` FROM `sky_phone_accounts` WHERE `email` IN (%s)"):format(table.concat(placeholders, ", ")), recipients ) @@ -444,7 +443,7 @@ Sky.Cb.Register("sky_phone:mail:send", function(source, data) } end - if not Sky.DB.Transaction(statements) then + if not Bridge.Database.Transaction(statements) then return { success = false, error = "request_failed" } end @@ -462,7 +461,7 @@ Sky.Cb.Register("sky_phone:mail:send", function(source, data) return { success = true, data = { id = message_id } } end) -Sky.Cb.Register("sky_phone:mail:set-read", function(source, data) +Bridge.Callbacks.Register("sky_phone:mail:set-read", function(source, data) local session, error_response = require_session(source) if not session then return error_response @@ -478,7 +477,7 @@ Sky.Cb.Register("sky_phone:mail:set-read", function(source, data) return { success = false, error = "invalid_message" } end - Sky.Query( + Bridge.Database.Query( ("UPDATE `sky_phone_mail_entries` SET `read_at` = %s WHERE `id` = ? AND `account_id` = ?") :format(data.read and "CURRENT_TIMESTAMP" or "NULL"), { id, session.id } @@ -487,7 +486,7 @@ Sky.Cb.Register("sky_phone:mail:set-read", function(source, data) return { success = true } end) -Sky.Cb.Register("sky_phone:mail:trash", function(source, data) +Bridge.Callbacks.Register("sky_phone:mail:trash", function(source, data) local session, error_response = require_session(source) if not session then return error_response @@ -503,7 +502,7 @@ Sky.Cb.Register("sky_phone:mail:trash", function(source, data) return { success = false, error = "invalid_message" } end - Sky.Query([[ + Bridge.Database.Query([[ UPDATE `sky_phone_mail_entries` SET `trashed_at` = CURRENT_TIMESTAMP WHERE `id` = ? AND `account_id` = ? AND `trashed_at` IS NULL ]], { id, session.id }) @@ -511,7 +510,7 @@ Sky.Cb.Register("sky_phone:mail:trash", function(source, data) return { success = true } end) -Sky.Cb.Register("sky_phone:mail:restore", function(source, data) +Bridge.Callbacks.Register("sky_phone:mail:restore", function(source, data) local session, error_response = require_session(source) if not session then return error_response @@ -527,7 +526,7 @@ Sky.Cb.Register("sky_phone:mail:restore", function(source, data) return { success = false, error = "invalid_message" } end - Sky.Query([[ + Bridge.Database.Query([[ UPDATE `sky_phone_mail_entries` SET `trashed_at` = NULL WHERE `id` = ? AND `account_id` = ? AND `trashed_at` IS NOT NULL ]], { id, session.id }) @@ -535,7 +534,7 @@ Sky.Cb.Register("sky_phone:mail:restore", function(source, data) return { success = true } end) -Sky.Cb.Register("sky_phone:mail:delete-forever", function(source, data) +Bridge.Callbacks.Register("sky_phone:mail:delete-forever", function(source, data) local session, error_response = require_session(source) if not session then return error_response @@ -551,11 +550,11 @@ Sky.Cb.Register("sky_phone:mail:delete-forever", function(source, data) return { success = false, error = "invalid_message" } end - Sky.Query([[ + Bridge.Database.Query([[ DELETE FROM `sky_phone_mail_entries` WHERE `id` = ? AND `account_id` = ? AND `trashed_at` IS NOT NULL ]], { id, session.id }) - Sky.Query([[ + Bridge.Database.Query([[ DELETE m FROM `sky_phone_mail_messages` m LEFT JOIN `sky_phone_mail_entries` e ON e.`message_id` = m.`id` WHERE e.`id` IS NULL @@ -564,17 +563,17 @@ Sky.Cb.Register("sky_phone:mail:delete-forever", function(source, data) return { success = true } end) -Sky.Cb.Register("sky_phone:mail:empty-trash", function(source) +Bridge.Callbacks.Register("sky_phone:mail:empty-trash", function(source) local session, error_response = require_session(source) if not session then return error_response end - Sky.Query( + Bridge.Database.Query( "DELETE FROM `sky_phone_mail_entries` WHERE `account_id` = ? AND `trashed_at` IS NOT NULL", { session.id } ) - Sky.Query([[ + Bridge.Database.Query([[ DELETE m FROM `sky_phone_mail_messages` m LEFT JOIN `sky_phone_mail_entries` e ON e.`message_id` = m.`id` WHERE e.`id` IS NULL diff --git a/sky_phone/source/server/notes.lua b/sky_phone/source/server/notes.lua index 556766b..785a7f6 100644 --- a/sky_phone/source/server/notes.lua +++ b/sky_phone/source/server/notes.lua @@ -1,3 +1,4 @@ +Bridge.Database.AfterMigration("sky_phone", function() SkyPhoneNotes = {} local function text_length(value) @@ -33,7 +34,7 @@ end function SkyPhoneNotes.List(account_id, imei) local condition, params = note_owner(account_id, imei) - local rows = Sky.Query(([[ + local rows = Bridge.Database.Query(([[ SELECT `id`, `title`, `body`, `pinned`, `revision`, UNIX_TIMESTAMP(`created_at`) AS `created_at_unix`, UNIX_TIMESTAMP(`updated_at`) AS `updated_at_unix` @@ -57,15 +58,16 @@ local function session_owner(source) return account and account.id or nil, session.imei end -Sky.Cb.Register("sky_phone:notes:list", function(source) +Bridge.Callbacks.Register("sky_phone:notes:list", function(source) local account_id, imei, error_response = session_owner(source) if not imei then return error_response end return { success = true, data = SkyPhoneNotes.List(account_id, imei) } end) +end) -Sky.Cb.Register("sky_phone:notes:create", function(source, data) +Bridge.Callbacks.Register("sky_phone:notes:create", function(source, data) if not SkyPhone.AllowOperation(source, "notes_write", 120, 60) then return { success = false, error = "rate_limited" } end @@ -89,13 +91,13 @@ Sky.Cb.Register("sky_phone:notes:create", function(source, data) local result if account_id then - result = Sky.Query([[ + result = Bridge.Database.Query([[ INSERT IGNORE INTO `sky_phone_notes` (`id`, `account_id`, `device_imei`, `title`, `body`, `pinned`) VALUES (?, ?, NULL, ?, ?, ?) ]], { data.id, account_id, data.title, data.body, data.pinned and 1 or 0 }) else - result = Sky.Query([[ + result = Bridge.Database.Query([[ INSERT IGNORE INTO `sky_phone_notes` (`id`, `account_id`, `device_imei`, `title`, `body`, `pinned`) VALUES (?, NULL, ?, ?, ?, ?) @@ -111,7 +113,7 @@ Sky.Cb.Register("sky_phone:notes:create", function(source, data) return { success = true, data = SkyPhoneNotes.List(account_id, imei) } end) -Sky.Cb.Register("sky_phone:notes:update", function(source, data) +Bridge.Callbacks.Register("sky_phone:notes:update", function(source, data) if not SkyPhone.AllowOperation(source, "notes_write", 120, 60) then return { success = false, error = "rate_limited" } end @@ -137,7 +139,7 @@ Sky.Cb.Register("sky_phone:notes:update", function(source, data) params[#params + 1] = value end params[#params + 1] = revision - local result = Sky.Query(([[ + local result = Bridge.Database.Query(([[ UPDATE `sky_phone_notes` SET `title` = ?, `body` = ?, `pinned` = ?, `revision` = `revision` + 1 WHERE `id` = ? AND %s AND `revision` = ? @@ -152,7 +154,7 @@ Sky.Cb.Register("sky_phone:notes:update", function(source, data) return { success = true, data = SkyPhoneNotes.List(account_id, imei) } end) -Sky.Cb.Register("sky_phone:notes:delete", function(source, data) +Bridge.Callbacks.Register("sky_phone:notes:delete", function(source, data) if not SkyPhone.AllowOperation(source, "notes_write", 120, 60) then return { success = false, error = "rate_limited" } end @@ -169,7 +171,7 @@ Sky.Cb.Register("sky_phone:notes:delete", function(source, data) for _, value in ipairs(owner_params) do params[#params + 1] = value end - Sky.Query(("DELETE FROM `sky_phone_notes` WHERE `id` = ? AND %s"):format(condition), params) + Bridge.Database.Query(("DELETE FROM `sky_phone_notes` WHERE `id` = ? AND %s"):format(condition), params) notify_owner(account_id, imei) return { success = true, data = SkyPhoneNotes.List(account_id, imei) } end) diff --git a/sky_phone/source/server/phone.lua b/sky_phone/source/server/phone.lua index 1ac3565..b8ed000 100644 --- a/sky_phone/source/server/phone.lua +++ b/sky_phone/source/server/phone.lua @@ -1,15 +1,5 @@ -Sky.Debug("debug", "[sky_phone] Server initialization started.", { always = true }) - -local migrations_ready = Sky.DB.AwaitMigrations("sky_phone") -Sky.Debug( - "debug", - "[sky_phone] Device database migrations ready: %s.", - tostring(migrations_ready), - { always = true } -) -if not migrations_ready then - error("[sky_phone] Device database migrations did not complete.") -end +Bridge.Database.AfterMigration("sky_phone", function() +Bridge.Debug("debug", "[sky_phone] Server initialization started after database migration.", { always = true }) SkyPhone = {} @@ -44,14 +34,14 @@ end local function reserve_imei() local imei = SkyPhoneImei.Reserve(function() - local rows = Sky.Query("SELECT UUID() AS `id`", {}) + local rows = Bridge.Database.Query("SELECT UUID() AS `id`", {}) local uuid = rows[1] and rows[1].id if type(uuid) ~= "string" then error("[sky_phone] Database did not generate entropy for an IMEI.") end return uuid end, function(candidate) - local result = Sky.Query([[ + local result = Bridge.Database.Query([[ INSERT IGNORE INTO `sky_phone_devices` (`imei`, `device_name`) VALUES (?, ?) ]], { candidate, Config.Phone.DeviceName }) @@ -66,14 +56,14 @@ end local function find_device_slots(source, imei) local matches = {} - for _, item in ipairs(Sky.FW.GetInventorySlotsWithItem(source, Config.Phone.Item)) do + for _, item in ipairs(Bridge.Inventory.GetSlotsWithItem(source, Config.Phone.Item)) do if item.metadata and item.metadata.imei == imei then matches[#matches + 1] = item end end if #matches > 1 then - Sky.Debug( + Bridge.Debug( "warn", "[sky_phone] Source %s has %s items with duplicated IMEI %s.", tostring(source), @@ -86,7 +76,7 @@ local function find_device_slots(source, imei) end local function resolve_used_slot(source, used_item) - Sky.Debug( + Bridge.Debug( "debug", "[sky_phone] Resolving used item for source %s: type=%s slot=%s id=%s name=%s.", tostring(source), @@ -97,10 +87,10 @@ local function resolve_used_slot(source, used_item) { always = true } ) - local slot_id = tonumber(used_item and (used_item.slot or used_item.id)) + local slot_id = used_item and (used_item.slot or used_item.id) if slot_id then - local slot = Sky.FW.GetInventorySlot(source, slot_id) - Sky.Debug( + local slot = Bridge.Inventory.GetSlot(source, slot_id) + Bridge.Debug( "debug", "[sky_phone] Inventory slot lookup for source %s slot %s returned name=%s amount=%s metadata_imei=%s.", tostring(source), @@ -115,8 +105,8 @@ local function resolve_used_slot(source, used_item) end end - local slots = Sky.FW.GetInventorySlotsWithItem(source, Config.Phone.Item) - Sky.Debug( + local slots = Bridge.Inventory.GetSlotsWithItem(source, Config.Phone.Item) + Bridge.Debug( "debug", "[sky_phone] Inventory fallback found %s phone slot candidates for source %s.", tostring(#slots), @@ -124,7 +114,7 @@ local function resolve_used_slot(source, used_item) { always = true } ) for _, candidate in ipairs(slots) do - Sky.Debug( + Bridge.Debug( "debug", "[sky_phone] Candidate slot=%s name=%s amount=%s metadata_imei=%s.", tostring(candidate.slot), @@ -138,7 +128,7 @@ local function resolve_used_slot(source, used_item) return slots[1] end - Sky.Debug( + Bridge.Debug( "warn", "[sky_phone] Usable item callback did not identify an exact phone slot for source %s (%s candidates).", tostring(source), @@ -149,32 +139,32 @@ end local function ensure_device(source, slot) local amount = tonumber(slot.amount or slot.count) or 0 - Sky.Debug( + Bridge.Debug( "debug", "[sky_phone] Ensuring device for source %s slot=%s amount=%s existing_imei=%s inventory=%s.", tostring(source), tostring(slot.slot), tostring(amount), tostring(slot.metadata and slot.metadata.imei), - tostring(Sky.FW.GetResourceName()), + tostring(Bridge.Inventory.GetResourceName()), { always = true } ) if amount ~= 1 then - Sky.Debug("warn", "[sky_phone] Phone item in slot %s is stacked for source %s.", tostring(slot.slot), tostring(source)) + Bridge.Debug("warn", "[sky_phone] Phone item in slot %s is stacked for source %s.", tostring(slot.slot), tostring(source)) return nil, "phone_stacked" end local metadata = slot.metadata or {} local imei = metadata.imei if imei and not SkyPhoneImei.IsValid(imei) then - Sky.Debug("warn", "[sky_phone] Phone item in slot %s has invalid IMEI metadata.", tostring(slot.slot)) + Bridge.Debug("warn", "[sky_phone] Phone item in slot %s has invalid IMEI metadata.", tostring(slot.slot)) return nil, "invalid_imei" end if not imei then imei = reserve_imei() metadata.imei = imei - Sky.Debug( + Bridge.Debug( "debug", "[sky_phone] Reserved IMEI %s; writing metadata to source %s slot %s.", imei, @@ -182,8 +172,8 @@ local function ensure_device(source, slot) tostring(slot.slot), { always = true } ) - local metadata_written = Sky.FW.SetInventorySlotMetadata(source, slot.slot, metadata) - Sky.Debug( + local metadata_written = Bridge.Inventory.SetSlotMetadata(source, slot.slot, metadata) + Bridge.Debug( "debug", "[sky_phone] Metadata write result for source %s slot %s IMEI %s: %s.", tostring(source), @@ -193,11 +183,11 @@ local function ensure_device(source, slot) { always = true } ) if not metadata_written then - Sky.Query("DELETE FROM `sky_phone_devices` WHERE `imei` = ?", { imei }) - Sky.Debug( + Bridge.Database.Query("DELETE FROM `sky_phone_devices` WHERE `imei` = ?", { imei }) + Bridge.Debug( "error", "[sky_phone] Inventory '%s' could not write phone metadata for source %s slot %s.", - Sky.FW.GetResourceName(), + Bridge.Inventory.GetResourceName(), tostring(source), tostring(slot.slot) ) @@ -206,7 +196,7 @@ local function ensure_device(source, slot) return imei end - Sky.Query([[ + Bridge.Database.Query([[ INSERT IGNORE INTO `sky_phone_devices` (`imei`, `device_name`) VALUES (?, ?) ]], { imei, Config.Phone.DeviceName }) @@ -215,7 +205,7 @@ local function ensure_device(source, slot) end local function load_device(imei) - local rows = Sky.Query([[ + 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` FROM `sky_phone_devices` d @@ -228,7 +218,7 @@ local function load_device(imei) end local function load_device_data(imei) - local rows = Sky.Query([[ + local rows = Bridge.Database.Query([[ SELECT `namespace`, `payload`, `revision` FROM `sky_phone_device_data` WHERE `device_imei` = ? @@ -244,7 +234,7 @@ local function load_device_data(imei) end local function account_devices(account_id, current_imei) - local rows = Sky.Query([[ + local rows = Bridge.Database.Query([[ SELECT `imei`, `device_name`, `created_at`, `updated_at` FROM `sky_phone_devices` WHERE `account_id` = ? @@ -354,7 +344,7 @@ local function link_account(source, account) if not SkyPhoneCalls.LinkAccountData(account.id, session.imei) then return { success = false, error = "request_failed" } end - if not Sky.DB.Transaction({ + if not Bridge.Database.Transaction({ { query = "UPDATE `sky_phone_devices` SET `account_id` = ? WHERE `imei` = ?", params = { account.id, session.imei }, @@ -399,7 +389,7 @@ local function authenticate(source, data, registering) end if registering then - local result = Sky.Query( + local result = Bridge.Database.Query( "INSERT IGNORE INTO `sky_phone_accounts` (`email`, `password`) VALUES (?, ?)", { email, password } ) @@ -408,7 +398,7 @@ local function authenticate(source, data, registering) end end - local accounts = Sky.Query( + local accounts = Bridge.Database.Query( "SELECT `id`, `email` FROM `sky_phone_accounts` WHERE `email` = ? AND `password` = ? LIMIT 1", { email, password } ) @@ -443,7 +433,7 @@ function SkyPhone.AllowOperation(source, operation, maximum, window_seconds) return true end if attempts.count >= maximum then - Sky.Debug( + Bridge.Debug( "warn", "[sky_phone] Rate limit '%s' exceeded by source %s.", operation, @@ -481,7 +471,7 @@ function SkyPhone.NotifyAccount(account_id, event_name, data) end function SkyPhone.NotifyAccountDevices(account_id, event_name, data) - local rows = Sky.Query([[ + local rows = Bridge.Database.Query([[ SELECT d.`imei`, d.`device_name`, settings.`payload` AS `settings` FROM `sky_phone_devices` d LEFT JOIN `sky_phone_device_data` settings @@ -493,10 +483,10 @@ function SkyPhone.NotifyAccountDevices(account_id, event_name, data) devices[row.imei] = row end - for _, player_source in ipairs(Sky.FW.GetPlayers()) do + for _, player_source in ipairs(Bridge.Framework.GetPlayers()) do local source = tonumber(player_source) or player_source local notified_devices = {} - for _, item in ipairs(Sky.FW.GetInventorySlotsWithItem(source, Config.Phone.Item)) do + 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 @@ -534,7 +524,7 @@ function SkyPhone.RefreshDevice(imei) end local function open_phone(source, used_item) - Sky.Debug( + Bridge.Debug( "debug", "[sky_phone] Usable phone callback invoked for source %s.", tostring(source), @@ -542,7 +532,7 @@ local function open_phone(source, used_item) ) local slot = resolve_used_slot(source, used_item) if not slot then - Sky.Debug( + Bridge.Debug( "debug", "[sky_phone] Phone open rejected for source %s: no exact inventory slot.", tostring(source), @@ -554,7 +544,7 @@ local function open_phone(source, used_item) local imei, error_code = ensure_device(source, slot) if not imei then - Sky.Debug( + Bridge.Debug( "debug", "[sky_phone] Phone open rejected for source %s slot %s: %s.", tostring(source), @@ -572,7 +562,7 @@ local function open_phone(source, used_item) token = ("%s:%s:%s"):format(imei, tostring(source), tostring(GetGameTimer())), } local payload = bootstrap(source) - Sky.Debug( + Bridge.Debug( "debug", "[sky_phone] Triggering client open for source %s slot %s IMEI %s account_linked=%s.", tostring(source), @@ -588,7 +578,7 @@ 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)) + Bridge.Debug("warn", "[sky_phone] Could not open ringing device %s for source %s.", tostring(imei), tostring(source)) return false end sessions[source] = { @@ -600,34 +590,34 @@ function SkyPhone.OpenDeviceForCall(source, imei) return true end -Sky.Debug( +Bridge.Debug( "debug", "[sky_phone] Registering usable item '%s' through inventory '%s'.", Config.Phone.Item, - tostring(Sky.FW.GetResourceName()), + tostring(Bridge.Inventory.GetResourceName()), { always = true } ) -local usable_registered = Sky.FW.RegisterUsableItem(Config.Phone.Item, open_phone, true) -Sky.Debug( +local usable_registered = Bridge.Inventory.RegisterUsableItem(Config.Phone.Item, open_phone) +Bridge.Debug( "debug", "[sky_phone] Usable item registration returned: %s.", tostring(usable_registered), { always = true } ) -Sky.Cb.Register("sky_phone:device:close", function(source) +Bridge.Callbacks.Register("sky_phone:device:close", function(source) sessions[source] = nil return { success = true } end) -Sky.Cb.Register("sky_phone:device:development-open", function(source) +Bridge.Callbacks.Register("sky_phone:device:development-open", function(source) if not Config.Phone.DevelopmentCommand then return { success = false, error = "disabled" } end return { success = open_phone(source, nil) } end) -Sky.Cb.Register("sky_phone:device:save", function(source, data) +Bridge.Callbacks.Register("sky_phone:device:save", function(source, data) if not SkyPhone.AllowOperation(source, "device_save", 120, 60) then return { success = false, error = "rate_limited" } end @@ -644,7 +634,7 @@ Sky.Cb.Register("sky_phone:device:save", function(source, data) return { success = false, error = "payload_too_large" } end local revision = math.max(0, math.floor(tonumber(data.revision) or 0)) - local rows = Sky.Query([[ + local rows = Bridge.Database.Query([[ SELECT `payload`, `revision` FROM `sky_phone_device_data` WHERE `device_imei` = ? AND `namespace` = ? @@ -661,7 +651,7 @@ Sky.Cb.Register("sky_phone:device:save", function(source, data) data = { payload = json.decode(rows[1].payload), revision = current_revision }, } end - local result = Sky.Query([[ + local result = Bridge.Database.Query([[ UPDATE `sky_phone_device_data` SET `payload` = ?, `revision` = `revision` + 1 WHERE `device_imei` = ? AND `namespace` = ? AND `revision` = ? @@ -676,7 +666,7 @@ Sky.Cb.Register("sky_phone:device:save", function(source, data) if revision ~= 0 then return { success = false, error = "conflict" } end - local result = Sky.Query([[ + local result = Bridge.Database.Query([[ INSERT IGNORE INTO `sky_phone_device_data` (`device_imei`, `namespace`, `payload`) VALUES (?, ?, ?) ]], { session.imei, data.namespace, encoded }) @@ -688,19 +678,19 @@ Sky.Cb.Register("sky_phone:device:save", function(source, data) end) for _, endpoint in ipairs({ "account:login", "mail:login" }) do - Sky.Cb.Register("sky_phone:" .. endpoint, function(source, data) + Bridge.Callbacks.Register("sky_phone:" .. endpoint, function(source, data) return authenticate(source, data, false) end) end for _, endpoint in ipairs({ "account:register", "mail:register" }) do - Sky.Cb.Register("sky_phone:" .. endpoint, function(source, data) + Bridge.Callbacks.Register("sky_phone:" .. endpoint, function(source, data) return authenticate(source, data, true) end) end for _, endpoint in ipairs({ "account:logout", "mail:logout" }) do - Sky.Cb.Register("sky_phone:" .. endpoint, function(source) + Bridge.Callbacks.Register("sky_phone:" .. endpoint, function(source) local account, error_response = SkyPhone.RequireAccount(source) if not account then return error_response @@ -708,13 +698,13 @@ for _, endpoint in ipairs({ "account:logout", "mail:logout" }) do 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 }) + Bridge.Database.Query("UPDATE `sky_phone_devices` SET `account_id` = NULL WHERE `imei` = ?", { account.imei }) refresh_source(source) return { success = true } end) end -Sky.Cb.Register("sky_phone:account:devices", function(source) +Bridge.Callbacks.Register("sky_phone:account:devices", function(source) local account, error_response = SkyPhone.RequireAccount(source) if not account then return error_response @@ -722,7 +712,7 @@ Sky.Cb.Register("sky_phone:account:devices", function(source) return { success = true, data = account_devices(account.id, account.imei) } end) -Sky.Cb.Register("sky_phone:account:remove-device", function(source, data) +Bridge.Callbacks.Register("sky_phone:account:remove-device", function(source, data) if not SkyPhone.AllowOperation(source, "remove_device", 10, 60) then return { success = false, error = "rate_limited" } end @@ -736,14 +726,14 @@ Sky.Cb.Register("sky_phone:account:remove-device", function(source, data) if data.imei == account.imei then return { success = false, error = "current_device" } end - local passwords = Sky.Query("SELECT `id` FROM `sky_phone_accounts` WHERE `id` = ? AND `password` = ? LIMIT 1", { + local passwords = Bridge.Database.Query("SELECT `id` FROM `sky_phone_accounts` WHERE `id` = ? AND `password` = ? LIMIT 1", { account.id, data.password, }) if not passwords[1] then return { success = false, error = "invalid_credentials" } end - local result = Sky.Query( + local result = Bridge.Database.Query( "UPDATE `sky_phone_devices` SET `account_id` = NULL WHERE `imei` = ? AND `account_id` = ?", { data.imei, account.id } ) @@ -755,7 +745,7 @@ Sky.Cb.Register("sky_phone:account:remove-device", function(source, data) return { success = true, data = account_devices(account.id, account.imei) } end) -Sky.Cb.Register("sky_phone:device:factory-reset", function(source) +Bridge.Callbacks.Register("sky_phone:device:factory-reset", function(source) if not SkyPhone.AllowOperation(source, "factory_reset", 3, 60) then return { success = false, error = "rate_limited" } end @@ -763,7 +753,7 @@ Sky.Cb.Register("sky_phone:device:factory-reset", function(source) if not session then return error_response end - if not Sky.DB.Transaction({ + if not Bridge.Database.Transaction({ { query = "DELETE FROM `sky_phone_device_data` WHERE `device_imei` = ?", params = { session.imei }, @@ -804,3 +794,4 @@ AddEventHandler("onResourceStop", function(resource_name) operation_attempts = {} end end) +end) diff --git a/sky_phone/source/server/sim.lua b/sky_phone/source/server/sim.lua index 87dce2d..601e810 100644 --- a/sky_phone/source/server/sim.lua +++ b/sky_phone/source/server/sim.lua @@ -1,6 +1,4 @@ -if not Sky.DB.AwaitMigrations("sky_phone") then - error("[sky_phone] SIM database migrations did not complete.") -end +Bridge.Database.AfterMigration("sky_phone", function() SkyPhoneSim = {} @@ -19,7 +17,7 @@ local function affected_rows(result) end local function uuid() - local rows = Sky.Query("SELECT UUID() AS `id`", {}) + 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 @@ -30,7 +28,7 @@ local function reserve_sim(sim_type) local sim_id local number = SkyPhoneSimNumber.Reserve(uuid, function(candidate) sim_id = uuid() - local result = Sky.Query([[ + local result = Bridge.Database.Query([[ INSERT IGNORE INTO `sky_phone_sims` (`id`, `phone_number`, `sim_type`) VALUES (?, ?, ?) ]], { sim_id, candidate, sim_type }) @@ -43,12 +41,13 @@ local function reserve_sim(sim_type) end local function load_sim(sim_id) - local rows = Sky.Query("SELECT * FROM `sky_phone_sims` WHERE `id` = ? LIMIT 1", { 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), @@ -58,21 +57,22 @@ local function sim_metadata(sim) 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 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 + 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 = Sky.FW.GetInventorySlotsWithItem(source, item_name) + local slots = Bridge.Inventory.GetSlotsWithItem(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)) + Bridge.Debug("warn", "[sky_phone] Could not resolve exact SIM slot for source %s.", tostring(source)) return nil end @@ -87,8 +87,8 @@ local function ensure_sim(source, slot, sim_type) 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 }) + 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 @@ -97,7 +97,7 @@ end local function list_phone_choices(source) local choices = {} - for _, slot in ipairs(Sky.FW.GetInventorySlotsWithItem(source, Config.Phone.Item)) do + 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) @@ -117,7 +117,7 @@ local function rollback_phone_metadata(source, phone_slot, old_sim) 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) + Bridge.Inventory.SetSlotMetadata(source, phone_slot.slot, metadata) end local function insert_sim(source, phone_imei, confirmed) @@ -129,7 +129,7 @@ local function insert_sim(source, phone_imei, confirmed) pending_insertions[source] = nil return { success = false, error = "sim_request_expired" } end - local sim_slot = Sky.FW.GetInventorySlot(source, pending.slot) + 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" } @@ -150,18 +150,18 @@ local function insert_sim(source, phone_imei, confirmed) 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 + if not Bridge.Inventory.SetSlotMetadata(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 + 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 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)) + 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" } @@ -182,19 +182,19 @@ local function insert_sim(source, phone_imei, confirmed) 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), + Bridge.Framework.GetIdentifier(source), + Bridge.Framework.GetFirstname(source), + Bridge.Framework.GetLastname(source), + Bridge.Framework.GetBirthdate(source), pending.sim.id, }, } end - if not Sky.DB.Transaction(transaction) then + if not Bridge.Database.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) + Bridge.Inventory.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)) + 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" } @@ -214,7 +214,7 @@ 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)) + 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 @@ -257,17 +257,17 @@ local function use_sim(source, used_item) return true end -Sky.FW.RegisterUsableItem(Config.Sim.RegisteredItem, use_sim, true) -Sky.FW.RegisterUsableItem(Config.Sim.AnonymousItem, use_sim, true) +Bridge.Inventory.RegisterUsableItem(Config.Sim.RegisteredItem, use_sim) +Bridge.Inventory.RegisterUsableItem(Config.Sim.AnonymousItem, use_sim) -Sky.Cb.Register("sky_phone:sim:insert", function(source, data) +Bridge.Callbacks.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) +Bridge.Callbacks.Register("sky_phone:sim:eject", function(source) if operation_locks[source] then return { success = false, error = "operation_in_progress" } end @@ -280,25 +280,25 @@ Sky.Cb.Register("sky_phone:sim:eject", function(source) if not sim then return { success = false, error = "no_sim" } end - local phone_slot = Sky.FW.GetInventorySlot(source, session.slot) + 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 Sky.FW.CanCarryItem(source, item_name, 1, metadata) then + if not Bridge.Inventory.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 + 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(Sky.Query("UPDATE `sky_phone_devices` SET `sim_id` = NULL WHERE `imei` = ? AND `sim_id` = ?", { + 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 - Sky.FW.RemoveItem(source, item_name, 1, nil, metadata) + 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" } @@ -313,3 +313,4 @@ AddEventHandler("playerDropped", function() pending_insertions[source] = nil operation_locks[source] = nil end) +end) diff --git a/sky_phone/sql/install.sql b/sky_phone/sql/install.sql new file mode 100644 index 0000000..773ca24 --- /dev/null +++ b/sky_phone/sql/install.sql @@ -0,0 +1,163 @@ +-- sky_phone fresh-install schema +-- MySQL/MariaDB with InnoDB and utf8mb4 support is required. +-- Runtime migrations remain enabled and handle upgrades of existing installations. + +CREATE TABLE IF NOT EXISTS `sky_phone_accounts` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `email` VARCHAR(64) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL, + `password` VARCHAR(64) NOT NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_sky_phone_mail_email` (`email`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `sky_phone_mail_messages` ( + `id` CHAR(36) NOT NULL, + `sender_account_id` BIGINT UNSIGNED NOT NULL, + `recipients` LONGTEXT NOT NULL, + `subject` VARCHAR(120) NOT NULL DEFAULT '', + `body` LONGTEXT NOT NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_sky_phone_mail_sender` (`sender_account_id`, `created_at`), + FOREIGN KEY (`sender_account_id`) REFERENCES `sky_phone_accounts` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `sky_phone_mail_entries` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `message_id` CHAR(36) NOT NULL, + `account_id` BIGINT UNSIGNED NOT NULL, + `folder` ENUM('inbox', 'sent') NOT NULL, + `read_at` DATETIME NULL, + `trashed_at` DATETIME NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_sky_phone_mail_entry` (`message_id`, `account_id`, `folder`), + KEY `idx_sky_phone_mailbox` (`account_id`, `folder`, `trashed_at`, `id`), + FOREIGN KEY (`message_id`) REFERENCES `sky_phone_mail_messages` (`id`) ON DELETE CASCADE, + FOREIGN KEY (`account_id`) REFERENCES `sky_phone_accounts` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `sky_phone_mail_drafts` ( + `id` CHAR(36) NOT NULL, + `account_id` BIGINT UNSIGNED NOT NULL, + `recipients` LONGTEXT NOT NULL, + `subject` VARCHAR(120) NOT NULL DEFAULT '', + `body` LONGTEXT NOT NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_sky_phone_mail_drafts` (`account_id`, `updated_at`), + FOREIGN KEY (`account_id`) REFERENCES `sky_phone_accounts` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `sky_phone_sims` ( + `id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, + `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, + `owner_identifier` VARCHAR(80) NULL, + `owner_firstname` VARCHAR(80) NULL, + `owner_lastname` VARCHAR(80) NULL, + `owner_birthdate` VARCHAR(32) NULL, + `registered_at` DATETIME NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_sky_phone_sim_number` (`phone_number`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `sky_phone_devices` ( + `imei` CHAR(15) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, + `account_id` BIGINT UNSIGNED NULL, + `sim_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NULL, + `device_name` VARCHAR(64) NOT NULL DEFAULT 'iFruit Phone', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`imei`), + UNIQUE KEY `uniq_sky_phone_devices_sim` (`sim_id`), + KEY `idx_sky_phone_devices_account` (`account_id`, `updated_at`), + FOREIGN KEY (`account_id`) REFERENCES `sky_phone_accounts` (`id`) ON DELETE SET NULL, + 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_device_data` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `device_imei` CHAR(15) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, + `namespace` VARCHAR(32) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, + `payload` LONGTEXT NOT NULL, + `revision` INT UNSIGNED NOT NULL DEFAULT 1, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_sky_phone_device_namespace` (`device_imei`, `namespace`), + 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_notes` ( + `id` VARCHAR(64) NOT NULL, + `account_id` BIGINT UNSIGNED NULL, + `device_imei` CHAR(15) CHARACTER SET ascii COLLATE ascii_bin NULL, + `title` VARCHAR(120) NOT NULL DEFAULT '', + `body` LONGTEXT NOT NULL, + `pinned` TINYINT(1) NOT NULL DEFAULT 0, + `revision` INT UNSIGNED NOT NULL DEFAULT 1, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_sky_phone_notes_account` (`account_id`, `updated_at`), + KEY `idx_sky_phone_notes_device` (`device_imei`, `updated_at`), + FOREIGN KEY (`account_id`) REFERENCES `sky_phone_accounts` (`id`) ON DELETE CASCADE, + 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_contacts` ( + `id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, + `contact_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NULL, + `account_id` BIGINT UNSIGNED NULL, + `device_imei` CHAR(15) CHARACTER SET ascii COLLATE ascii_bin NULL, + `name` VARCHAR(80) NOT NULL, + `phone_number` VARCHAR(24) 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 (`id`), + UNIQUE KEY `uniq_sky_phone_contacts_account_contact` (`account_id`, `contact_id`), + 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`), + FOREIGN KEY (`account_id`) REFERENCES `sky_phone_accounts` (`id`) ON DELETE CASCADE, + 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_calls` ( + `id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, + `caller_sim_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, + `callee_sim_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NULL, + `caller_number` VARCHAR(24) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, + `callee_number` VARCHAR(24) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, + `status` VARCHAR(24) NOT NULL, + `started_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `answered_at` DATETIME NULL, + `ended_at` DATETIME NULL, + `duration_seconds` INT UNSIGNED NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `idx_sky_phone_calls_caller` (`caller_sim_id`, `started_at`), + KEY `idx_sky_phone_calls_callee` (`callee_sim_id`, `started_at`), + FOREIGN KEY (`caller_sim_id`) REFERENCES `sky_phone_sims` (`id`) ON DELETE CASCADE, + 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_entries` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `call_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, + `account_id` BIGINT UNSIGNED NULL, + `device_imei` CHAR(15) CHARACTER SET ascii COLLATE ascii_bin NULL, + `direction` ENUM('incoming', 'outgoing') NOT NULL, + `status` VARCHAR(24) NOT NULL, + `other_number` VARCHAR(24) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_sky_phone_call_entries_account` (`account_id`, `created_at`), + KEY `idx_sky_phone_call_entries_device` (`device_imei`, `created_at`), + FOREIGN KEY (`call_id`) REFERENCES `sky_phone_calls` (`id`) ON DELETE CASCADE, + FOREIGN KEY (`account_id`) REFERENCES `sky_phone_accounts` (`id`) ON DELETE CASCADE, + FOREIGN KEY (`device_imei`) REFERENCES `sky_phone_devices` (`imei`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;