mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-04 09:13:24 +00:00
ENH - add standalone framework and inventory bridges
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user