ENH - add standalone framework and inventory bridges

This commit is contained in:
DerEchteAlec
2026-08-05 20:14:47 +02:00
parent 13ddeb893b
commit b9ca1f1153
28 changed files with 1552 additions and 207 deletions
+41 -3
View File
@@ -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
+13
View File
@@ -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 = {
+17 -4
View File
@@ -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'
@@ -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)
@@ -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
@@ -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)
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
+30
View File
@@ -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
+11 -11
View File
@@ -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)
+30 -31
View File
@@ -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)
+9 -7
View File
@@ -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")
+35 -36
View File
@@ -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
+11 -9
View File
@@ -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)
+61 -70
View File
@@ -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)
+37 -36
View File
@@ -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)
+163
View File
@@ -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;