diff --git a/client/customapps.lua b/client/customapps.lua index 17e62445..401c04f5 100644 --- a/client/customapps.lua +++ b/client/customapps.lua @@ -7,6 +7,9 @@ local appIds = require 'client.appids' ---@type table Client job bridge (bridge.client.job): live job name/grade plus a change hook. local job = require 'bridge.client.job' +---@type table Callable check (shared.callable): app hooks arrive as function references. +local callable = require 'shared.callable' + ---@type table ---Registered third-party apps keyed by identifier. onOpen also covers lb-phone's onUse alias, and ---`jobs`/`requires` are the two gates that never reach the def - see the note in add(). @@ -386,7 +389,7 @@ function M.add(data, resource) if devices then def.devices = devices end local onOpen = data.onOpen - if type(onOpen) ~= 'function' then onOpen = data.onUse end + if not callable.is(onOpen) then onOpen = data.onUse end local entry = { def = def, resource = resource, @@ -394,9 +397,9 @@ function M.add(data, resource) -- Kept raw: server.gates is the only thing that reads a spec, and it sanitises what it is -- given. Two readers would be two chances to disagree about what a gate means. requires = type(data.requires) == 'table' and data.requires or nil, - onOpen = type(onOpen) == 'function' and onOpen or nil, - onClose = type(data.onClose) == 'function' and data.onClose or nil, - onDelete = type(data.onDelete) == 'function' and data.onDelete or nil, + onOpen = callable.is(onOpen) and onOpen or nil, + onClose = callable.is(data.onClose) and data.onClose or nil, + onDelete = callable.is(data.onDelete) and data.onDelete or nil, } registry[identifier] = entry addOrder(identifier) diff --git a/client/lockscreenwidgets.lua b/client/lockscreenwidgets.lua index 0dfa98cd..f10285c3 100644 --- a/client/lockscreenwidgets.lua +++ b/client/lockscreenwidgets.lua @@ -1,5 +1,7 @@ ---@type table Custom app registry (client.customapps), read here to confirm a widget was declared. local customApps = require 'client.customapps' +---@type table Callable check (shared.callable): an action handler arrives as a function reference. +local callable = require 'shared.callable' ---@type table "appId:widgetId" -> the resource showing it, so only the owner hides it. local active = {} @@ -39,7 +41,7 @@ exports('showLockscreenWidget', function(appId, widgetId, data, onAction) or not registered(appId, widgetId, resource) then return false end local id = key(appId, widgetId) active[id] = resource - callbacks[id] = type(onAction) == 'function' and onAction or nil + callbacks[id] = callable.is(onAction) and onAction or nil SendNUIMessage({ action = 'sd-phone:lockscreenWidget:show', data = { key = id, appId = appId, widgetId = widgetId, payload = type(data) == 'table' and data or {}, } }) diff --git a/server/bluetooth/registry.lua b/server/bluetooth/registry.lua index cf006b44..5bd980cd 100644 --- a/server/bluetooth/registry.lua +++ b/server/bluetooth/registry.lua @@ -1,5 +1,7 @@ ---@type table Pure Bluetooth maths (shared.bluetooth): validation, range, capacity. local bt = require 'shared.bluetooth' +---@type table Callable check (shared.callable): an owning script's callback is a function reference. +local callable = require 'shared.callable' ---@type table Registry module; the table returned at end of file. local registry = {} @@ -144,7 +146,7 @@ end ---@param fn function|table|nil ---@param ... any local function safely(fn, ...) - if not bt.isCallable(fn) then return end + if not callable.is(fn) then return end local ok, err = pcall(fn, ...) if not ok then print(('^1[sd-phone:bluetooth]^0 device callback errored: %s'):format(tostring(err))) diff --git a/server/compat/lbphone/messages.lua b/server/compat/lbphone/messages.lua index faff1990..2532dda1 100644 --- a/server/compat/lbphone/messages.lua +++ b/server/compat/lbphone/messages.lua @@ -6,6 +6,8 @@ local config = require 'configs.config' local actions = require 'server.messages.actions' ---@type table Shared server helpers (server.util): digit/trim sanitizers for the shim boundary. local util = require 'server.util' +---@type table Callable check (shared.callable): SendMessage's callback is a function reference. +local callable = require 'shared.callable' local registerLbExport, warnOnce = shim.registerLbExport, shim.warnOnce @@ -71,7 +73,7 @@ registerLbExport('SendMessage', function(from, to, message, attachments, cb, cha warnOnce('SendMessage.return', ('SendMessage returns a synthetic channelId of 0 (called by %s); sd-phone has no channel ids'):format(GetInvokingResource() or 'unknown')) result = { channelId = 0, messageId = messageId or 0 } end - if type(cb) == 'function' then pcall(cb, result) end + if callable.is(cb) then pcall(cb, result) end return result end) diff --git a/server/compat/qssmartphone/calls.lua b/server/compat/qssmartphone/calls.lua index aab4e7cf..ad98a635 100644 --- a/server/compat/qssmartphone/calls.lua +++ b/server/compat/qssmartphone/calls.lua @@ -1,5 +1,7 @@ ---@type table Shared shim helpers (server.compat.qssmartphone.shared): export registration + warn-once. local shim = require 'server.compat.qssmartphone.shared' +---@type table Callable check (shared.callable): an interceptor arrives as a function reference. +local callable = require 'shared.callable' ---@type table Registered call interceptors by digit-normalised phone number. ---Bookkeeping only: sd-phone rings a call straight through, so nothing ever consults these. @@ -17,7 +19,7 @@ local sd = exports['sd-phone'] ---hasCallInterceptor and getRegisteredCallInterceptors report the truth about what was asked for. registerExport('registerCallInterceptor', function(phoneNumber, callback) local number = shim.digits(phoneNumber) - if not number or type(callback) ~= 'function' then return false end + if not number or not callable.is(callback) then return false end warnOnce('registerCallInterceptor', ('call interception is not supported (registered by %s); sd-phone rings a call straight through to the recipient with no pre-ring hook, so the interceptor was recorded but will never be called and every call to that number rings normally'):format(GetInvokingResource() or 'unknown')) diff --git a/server/main.lua b/server/main.lua index 226f9dc9..3b08f937 100644 --- a/server/main.lua +++ b/server/main.lua @@ -125,7 +125,7 @@ require 'server.compat.yseries.init' require 'server.compat.qssmartphone.init' -- gksphone export compatibility shim (sd_phone_gkscompat kill switch). require 'server.compat.gksphone.init' --- roadphone export compatibility shim (sd_phone_roadcompat kill switch). +-- roadphone export compatibility shim (sd_phone_roadphonecompat kill switch). require 'server.compat.roadphone.init' ---@type table SIM feature flags (server.sim.state): active + mode. diff --git a/server/photos/init.lua b/server/photos/init.lua index 59646489..eeb1d734 100644 --- a/server/photos/init.lua +++ b/server/photos/init.lua @@ -24,6 +24,8 @@ local mediaLimit = require 'server.photos.mediaLimit' local util = require 'server.util' ---@type table AirShare core (server.share.core): per-kind delivery handler registry. local share = require 'server.share.core' +---@type table Callable check (shared.callable): uploadMedia's callback is a function reference. +local callable = require 'shared.callable' ---@type string GlobalState key carrying the game view mode every client renders with. local GAME_VIEW_MODE_KEY = 'sd-phone:gameViewMode' @@ -529,7 +531,7 @@ end) ---@param cb fun(url: string|nil, err: string|nil) ---@return boolean accepted false when the callback or payload shape is unusable exports('uploadMedia', function(dataUrl, filename, cb) - if type(cb) ~= 'function' then return false end + if not callable.is(cb) then return false end if type(dataUrl) ~= 'string' or not lib.string.startsWith(dataUrl, 'data:') then cb(nil, 'Expected a base64 data: URL') return false diff --git a/shared/bluetooth.lua b/shared/bluetooth.lua index c17cc774..2e6e8631 100644 --- a/shared/bluetooth.lua +++ b/shared/bluetooth.lua @@ -1,3 +1,6 @@ +---@type table Callable check (shared.callable): keeps a callback that arrived through an export. +local callable = require 'shared.callable' + ---@type table Pure Bluetooth maths; no natives, no state, so both sides and the test harness can ---load it unchanged. local bluetooth = {} @@ -75,17 +78,6 @@ function bluetooth.merge(device, patch) return def end ----Whether a value can be called. A function handed over through an export arrives as a function ----reference: a table with a __call metamethod, not a Lua function. ----@param value any ----@return boolean -function bluetooth.isCallable(value) - if type(value) == 'function' then return true end - if type(value) ~= 'table' then return false end - local meta = getmetatable(value) - return type(meta) == 'table' and meta.__call ~= nil -end - ---Validates a registration, returning the fields worth keeping. Rejects rather than repairs, so a ---script learns its device is wrong instead of finding it silently unreachable. ---@param def table registration passed to registerBluetoothDevice @@ -117,8 +109,8 @@ function bluetooth.validate(def) entity = entity, range = range, maxConnections = max, - onConnect = bluetooth.isCallable(def.onConnect) and def.onConnect or nil, - onDisconnect = bluetooth.isCallable(def.onDisconnect) and def.onDisconnect or nil, + onConnect = callable.is(def.onConnect) and def.onConnect or nil, + onDisconnect = callable.is(def.onDisconnect) and def.onDisconnect or nil, } end diff --git a/shared/callable.lua b/shared/callable.lua new file mode 100644 index 00000000..bb810942 --- /dev/null +++ b/shared/callable.lua @@ -0,0 +1,16 @@ +---@type table Callable module; the table returned at end of file. Shared by every export that +---accepts a callback from another resource. +local callable = {} + +---Whether a value can be called. A function handed over through an export arrives as a function +---reference: a table with a __call metamethod, not a Lua function. +---@param value any +---@return boolean +function callable.is(value) + if type(value) == 'function' then return true end + if type(value) ~= 'table' then return false end + local meta = getmetatable(value) + return type(meta) == 'table' and meta.__call ~= nil +end + +return callable