Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions client/customapps.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, {def: table, resource: string, jobs: table?, requires: table?, onOpen: function?, onClose: function?, onDelete: function?}>
---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().
Expand Down Expand Up @@ -386,17 +389,17 @@ 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,
jobs = readJobs(data.job),
-- 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)
Expand Down
4 changes: 3 additions & 1 deletion client/lockscreenwidgets.lua
Original file line number Diff line number Diff line change
@@ -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<string, string> "appId:widgetId" -> the resource showing it, so only the owner hides it.
local active = {}
Expand Down Expand Up @@ -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 {},
} })
Expand Down
4 changes: 3 additions & 1 deletion server/bluetooth/registry.lua
Original file line number Diff line number Diff line change
@@ -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 = {}
Expand Down Expand Up @@ -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)))
Expand Down
4 changes: 3 additions & 1 deletion server/compat/lbphone/messages.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)

Expand Down
4 changes: 3 additions & 1 deletion server/compat/qssmartphone/calls.lua
Original file line number Diff line number Diff line change
@@ -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<string, function[]> Registered call interceptors by digit-normalised phone number.
---Bookkeeping only: sd-phone rings a call straight through, so nothing ever consults these.
Expand All @@ -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'))

Expand Down
2 changes: 1 addition & 1 deletion server/main.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion server/photos/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 <const> = 'sd-phone:gameViewMode'
Expand Down Expand Up @@ -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
Expand Down
18 changes: 5 additions & 13 deletions shared/bluetooth.lua
Original file line number Diff line number Diff line change
@@ -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 = {}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
16 changes: 16 additions & 0 deletions shared/callable.lua
Original file line number Diff line number Diff line change
@@ -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
Loading