mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-28 21:01:30 +00:00
ENH - centralize phone defaults and fixed permissions (#26)
* ENH - centralize phone configuration defaults * FIX - enforce fixed phone permissions
This commit is contained in:
@@ -138,10 +138,10 @@ Sky Phone is built to be the **free FiveM phone you can choose without accepting
|
||||
| `smx-inventory` (`smx`) | Yes | Yes | ESX only; one metadata record per configured item name through the player metadata bridge |
|
||||
| `lj-inventory` (`lj`) | Yes | Yes | QBCore inventory with item `info` metadata |
|
||||
| `qb-inventory` (`qb`) | Yes | Yes | Uses item `info` metadata |
|
||||
| `hex_4_inventory` (`hex`) | **No metadata support** | **No, Unique Phones are not possible** | ESX only; set `Config.Phone.Unique = false` and `Config.Sim.Enabled = false` |
|
||||
| Native ESX inventory (`esx`) | **No metadata support** | **No, Unique Phones are not possible** | Count-based items; set `Config.Phone.Unique = false` and `Config.Sim.Enabled = false` |
|
||||
| `hex_4_inventory` (`hex`) | **No metadata support** | **No, Unique Phones are not possible** | ESX only; Sky Phone automatically disables unique phones and physical SIM cards |
|
||||
| Native ESX inventory (`esx`) | **No metadata support** | **No, Unique Phones are not possible** | Count-based items; Sky Phone automatically disables unique phones and physical SIM cards |
|
||||
|
||||
`hex_4_inventory` and native ESX inventory cannot persist per-item metadata. Unique Phones and physical SIM cards are therefore unavailable with these adapters.
|
||||
`hex_4_inventory` and native ESX inventory cannot persist per-item metadata. Sky Phone therefore forces `Config.Phone.Unique` and `Config.Sim.Enabled` to `false` at runtime whenever either adapter is active.
|
||||
|
||||
`Config.Bridge.Inventory = "auto"` detects framework-compatible adapters in the table order. This deliberately matches the Sky inventory priority so a dedicated inventory is selected before a compatibility resource it may run beside. You may configure either the short value shown in parentheses or the exact resource name.
|
||||
|
||||
@@ -216,6 +216,7 @@ The files contain clearly separated sections for:
|
||||
| Section | Purpose |
|
||||
| --- | --- |
|
||||
| `Config.Bridge` | Framework, inventory, language, callback timeout, and debug mode |
|
||||
| `Config.CommandPermissions` | Fixed groups for the admin panel, test data, verification commands, and social moderation |
|
||||
| `Config.Phone` | Phone item, movement, unique-device mode, and development command |
|
||||
| `Config.Sim` | Physical or virtual SIM behavior and number formatting |
|
||||
| `Config.Calls` / `Config.Radio` | Voice providers, call behavior, radio limits, and permissions |
|
||||
@@ -241,21 +242,31 @@ Config.PhoneConfigurator = {
|
||||
}
|
||||
```
|
||||
|
||||
When enabled, `config.lua` and the server-only `media.lua` are first-run defaults. Sky Phone creates
|
||||
the `sky_phone_configurator` table automatically, loads its saved values before framework and phone
|
||||
When enabled, the generated `source/shared/config_default.lua` is the shipped first-run baseline.
|
||||
The frontend build recreates this file from `config.lua` and the server-only `media.lua`; do not edit
|
||||
the generated snapshot directly. Sky Phone creates the `sky_phone_configurator` table automatically,
|
||||
loads its saved values before framework and phone
|
||||
modules initialize, and exposes the editor through `/phonepanel`. Nothing autosaves: stage changes
|
||||
in the Phone Configurator tool and press the green check. Saving verifies both SQL payloads and then
|
||||
applies the new server, client, media, app, item, command, provider, animation, and UI values through
|
||||
Sky Phone's internal runtime refresh. It does not execute a resource restart command.
|
||||
|
||||
Every `Config.*` value from `config.lua`, including server-only sections, and every value from
|
||||
`Config.Media` is discovered automatically. The bootstrap switch above intentionally remains
|
||||
file-owned because it decides whether SQL configuration is loaded. Lists, nested objects, vectors,
|
||||
Every configurable `Config.*` value from `config.lua`, including server-only sections, and every
|
||||
value from `Config.Media` is discovered automatically. The bootstrap switch and
|
||||
`Config.CommandPermissions` intentionally remain file-owned: the switch decides whether SQL
|
||||
configuration is loaded, while permissions must remain authoritative outside the panel. The fixed
|
||||
permission table is never displayed or overwritten by the Phone Configurator, and its stable keys do
|
||||
not change when their commands are renamed in the panel. Lists, nested objects, vectors,
|
||||
and numeric-keyed Lua tables use structured editors instead of raw JSON. Shipped schema rows stay
|
||||
editable but cannot be renamed, converted, or removed. Every list and table still accepts any number
|
||||
of additional rows; administrator-added rows remain removable. Company job keys are intentionally
|
||||
fully removable because `Config.Companies.Definitions` is a freely managed job collection.
|
||||
|
||||
ESX and QBCore use the groups listed in `Config.CommandPermissions`. Qbox checks the configured ACE
|
||||
objects first and then its framework groups. The standard Qbox `permissions.cfg` grants the `admin`
|
||||
ACE to `group.admin`, so an identifier assigned to `group.admin` can open `/phonepanel` with the
|
||||
shipped `phonepanel` permission list. Restart `sky_phone` after changing fixed permissions.
|
||||
|
||||
Media API keys and server peppers are never returned in plaintext to the NUI. Existing secrets are
|
||||
shown only as configured and are replaced only when an administrator enters a new value.
|
||||
|
||||
@@ -365,7 +376,7 @@ Do not configure an LB Phone client event or client export. Sky Phone registers
|
||||
|
||||
The server registers `Config.Phone.Item` as usable for every supported inventory adapter: `ak47`, `codem`, `core`, `jaksam`, `jpr`, `lj`, `mf`, `one`, `origen`, `ox`, `ps`, `qb`, `qs`, `smx`, `tgiann`, `hex`, and `esx`. Resource startup fails visibly if the selected adapter or its resource is unavailable.
|
||||
|
||||
The `hex` and `esx` adapters use ESX's count-based item API. They require both `Config.Phone.Unique = false` and `Config.Sim.Enabled = false` because this API cannot persist per-item phone or physical SIM metadata. `auto` selects `hex` when `hex_4_inventory` is started and otherwise falls back to `esx` on an ESX server when no metadata-capable inventory is detected.
|
||||
The `hex` and `esx` adapters use ESX's count-based item API, which cannot persist per-item phone or physical SIM metadata. Sky Phone automatically forces `Config.Phone.Unique = false` and `Config.Sim.Enabled = false` while either adapter is active. `auto` selects `hex` when `hex_4_inventory` is started and otherwise falls back to `esx` on an ESX server when no metadata-capable inventory is detected.
|
||||
|
||||
### QBCore-style item tables
|
||||
|
||||
|
||||
+40
-5
@@ -2,15 +2,49 @@ const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
|
||||
const frontendRoot = __dirname
|
||||
const resourceDirectory = path.join(frontendRoot, '..', 'sky_phone')
|
||||
const sourceDirectory = path.join(frontendRoot, 'dist')
|
||||
const targetDirectory = path.join(
|
||||
frontendRoot,
|
||||
'..',
|
||||
'sky_phone',
|
||||
const targetDirectory = path.join(resourceDirectory, 'source', 'html')
|
||||
const configDefaultPath = path.join(
|
||||
resourceDirectory,
|
||||
'source',
|
||||
'html',
|
||||
'shared',
|
||||
'config_default.lua',
|
||||
)
|
||||
|
||||
function readLuaSource(...segments) {
|
||||
return fs
|
||||
.readFileSync(path.join(resourceDirectory, ...segments), 'utf8')
|
||||
.replace(/\r\n?/g, '\n')
|
||||
.trimEnd()
|
||||
}
|
||||
|
||||
function generateConfigDefault() {
|
||||
const configSource = readLuaSource('config', 'config.lua').replace(
|
||||
/\n?-- CONFIG_DEFAULT_EXCLUDE_START[\s\S]*?-- CONFIG_DEFAULT_EXCLUDE_END\n?/g,
|
||||
'\n',
|
||||
)
|
||||
const mediaSource = readLuaSource('config', 'media.lua')
|
||||
const generatedSource = [
|
||||
'-- GENERATED by frontend/build.cjs from config/config.lua and config/media.lua - do not edit.',
|
||||
'-- Escrowed snapshot of the shipped defaults used by the Phone Configurator.',
|
||||
'local realConfig = Config',
|
||||
'Config = {}',
|
||||
'',
|
||||
configSource,
|
||||
'',
|
||||
mediaSource,
|
||||
'',
|
||||
'ConfigDefaults = Config',
|
||||
'Config = realConfig',
|
||||
'',
|
||||
].join('\n')
|
||||
|
||||
fs.mkdirSync(path.dirname(configDefaultPath), { recursive: true })
|
||||
fs.writeFileSync(configDefaultPath, generatedSource, 'utf8')
|
||||
console.log(`Generated Phone Configurator defaults at ${configDefaultPath}`)
|
||||
}
|
||||
|
||||
if (!fs.existsSync(sourceDirectory)) {
|
||||
throw new Error(
|
||||
'Missing frontend/dist. Run vite build before publishing the NUI.',
|
||||
@@ -28,4 +62,5 @@ const normalizedIndex = fs
|
||||
.replace(/\n[ \t]*\n([ \t]*<\/body>)/g, '\n$1')
|
||||
fs.writeFileSync(targetIndex, normalizedIndex)
|
||||
|
||||
generateConfigDefault()
|
||||
console.log(`Published NUI to ${targetDirectory}`)
|
||||
|
||||
@@ -54,6 +54,14 @@ const config = readFileSync(
|
||||
new URL('../../../sky_phone/config/config.lua', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
const configDefaultsSource = config.replace(
|
||||
/\r?\n?-- CONFIG_DEFAULT_EXCLUDE_START[\s\S]*?-- CONFIG_DEFAULT_EXCLUDE_END\r?\n?/g,
|
||||
'\n',
|
||||
)
|
||||
const mediaConfig = readFileSync(
|
||||
new URL('../../../sky_phone/config/media.lua', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
const schema = readFileSync(
|
||||
new URL('../../../sky_phone/sql/install.sql', import.meta.url),
|
||||
'utf8',
|
||||
@@ -69,6 +77,17 @@ const configuratorServer = readFileSync(
|
||||
),
|
||||
'utf8',
|
||||
)
|
||||
const configDefault = readFileSync(
|
||||
new URL(
|
||||
'../../../sky_phone/source/shared/config_default.lua',
|
||||
import.meta.url,
|
||||
),
|
||||
'utf8',
|
||||
)
|
||||
const frontendBuild = readFileSync(
|
||||
new URL('../../build.cjs', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
const configuratorClient = readFileSync(
|
||||
new URL(
|
||||
'../../../sky_phone/source/client/phone_configurator.lua',
|
||||
@@ -216,12 +235,14 @@ describe('standalone admin panel contracts', () => {
|
||||
it('authorizes every server request without requiring a phone session', () => {
|
||||
expect(server).not.toContain('SkyPhone.RequireSession(source)')
|
||||
expect(server).toContain(
|
||||
'Bridge.Framework.HasAdminGroup(source, Config.AdminPanel.AdminGroups)',
|
||||
'Bridge.Framework.HasPermission(source, "phonepanel")',
|
||||
)
|
||||
expect(server).toContain('Config.AdminPanel.ReadRequestsPerMinute')
|
||||
expect(server).toContain('Config.AdminPanel.ActionRequestsPerMinute')
|
||||
expect(server).toContain('Config.AdminPanel.CredentialRevealsPerMinute')
|
||||
expect(config).toContain('Config.AdminPanel = {')
|
||||
expect(config).toContain('Config.CommandPermissions = {')
|
||||
expect(config).not.toContain('AdminGroups =')
|
||||
})
|
||||
|
||||
it('opens directly from the configurable command with dedicated focus', () => {
|
||||
@@ -272,6 +293,12 @@ describe('standalone admin panel contracts', () => {
|
||||
expect(schema).toContain(
|
||||
'CREATE TABLE IF NOT EXISTS `sky_phone_configurator`',
|
||||
)
|
||||
expect(
|
||||
manifest.indexOf("'source/shared/config_default.lua'"),
|
||||
).toBeGreaterThan(manifest.indexOf("'config/media.lua'"))
|
||||
expect(manifest.indexOf("'source/shared/config_default.lua'")).toBeLessThan(
|
||||
manifest.indexOf("'source/server/phone_configurator.lua'"),
|
||||
)
|
||||
expect(
|
||||
manifest.indexOf("'source/server/phone_configurator.lua'"),
|
||||
).toBeLessThan(manifest.indexOf("'source/bridge/server/framework.lua'"))
|
||||
@@ -281,11 +308,36 @@ describe('standalone admin panel contracts', () => {
|
||||
expect(configuratorServer).toContain('AND `revision` = ?')
|
||||
expect(configuratorServer).toContain('configurator_enabled')
|
||||
expect(configuratorServer).toMatch(
|
||||
/for key, value in pairs\(Config\)[\s\S]*?key ~= "Media"[\s\S]*?key ~= "PhoneConfigurator"/,
|
||||
/for key, value in pairs\(ConfigDefaults\)[\s\S]*?key ~= "Media"[\s\S]*?key ~= "PhoneConfigurator"/,
|
||||
)
|
||||
expect(configuratorServer).toContain(
|
||||
'default_media = serialize_value(Config.Media)',
|
||||
'default_media = serialize_value(ConfigDefaults.Media)',
|
||||
)
|
||||
expect(configDefault).toContain(
|
||||
'GENERATED by frontend/build.cjs from config/config.lua and config/media.lua',
|
||||
)
|
||||
expect(configDefault).toContain('local realConfig = Config')
|
||||
expect(configDefault).toContain('ConfigDefaults = Config')
|
||||
expect(configDefault.replace(/\r\n?/g, '\n').trimEnd()).toBe(
|
||||
[
|
||||
'-- GENERATED by frontend/build.cjs from config/config.lua and config/media.lua - do not edit.',
|
||||
'-- Escrowed snapshot of the shipped defaults used by the Phone Configurator.',
|
||||
'local realConfig = Config',
|
||||
'Config = {}',
|
||||
'',
|
||||
configDefaultsSource.replace(/\r\n?/g, '\n').trimEnd(),
|
||||
'',
|
||||
mediaConfig.replace(/\r\n?/g, '\n').trimEnd(),
|
||||
'',
|
||||
'ConfigDefaults = Config',
|
||||
'Config = realConfig',
|
||||
].join('\n'),
|
||||
)
|
||||
expect(frontendBuild).toContain("readLuaSource('config', 'config.lua')")
|
||||
expect(frontendBuild).toContain("readLuaSource('config', 'media.lua')")
|
||||
expect(frontendBuild).toContain("'config_default.lua'")
|
||||
expect(configDefault).not.toContain('Config.PhoneConfigurator')
|
||||
expect(configDefault).not.toContain('Config.CommandPermissions')
|
||||
expect(configuratorServer).toContain(
|
||||
'build_sections("config", stored_config',
|
||||
)
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
function source(path: string): string {
|
||||
return readFileSync(new URL(path, import.meta.url), 'utf8')
|
||||
}
|
||||
|
||||
const config = source('../../sky_phone/config/config.lua')
|
||||
const configDefault = source('../../sky_phone/source/shared/config_default.lua')
|
||||
const framework = source('../../sky_phone/source/bridge/server/framework.lua')
|
||||
const qbox = source('../../sky_phone/source/bridge/server/frameworks/qbox.lua')
|
||||
const configurator = source(
|
||||
'../../sky_phone/source/server/phone_configurator.lua',
|
||||
)
|
||||
const configuratorFixture = source('../testserver/configurator-fixture.cjs')
|
||||
|
||||
describe('fixed server permissions', () => {
|
||||
it('defines every protected phone capability only in config.lua', () => {
|
||||
expect(config).toContain('Config.CommandPermissions = {')
|
||||
for (const permission of [
|
||||
'phonepanel',
|
||||
'phonetestdata',
|
||||
'fliptokverify',
|
||||
'picstagramverify',
|
||||
'picstagramadmin',
|
||||
]) {
|
||||
expect(config).toMatch(new RegExp(`\\s${permission} = \\{`))
|
||||
}
|
||||
expect(config).not.toContain('AdminGroups =')
|
||||
expect(configDefault).not.toContain('Config.PhoneConfigurator')
|
||||
expect(configDefault).not.toContain('Config.CommandPermissions')
|
||||
expect(configDefault).not.toContain('AdminGroups =')
|
||||
})
|
||||
|
||||
it('keeps fixed permissions outside SQL and removes legacy group fields', () => {
|
||||
expect(configurator).toContain('key ~= "CommandPermissions"')
|
||||
expect(configurator).toContain('if key ~= "CommandPermissions" then')
|
||||
expect(configuratorFixture).toContain('delete config.CommandPermissions')
|
||||
for (const path of [
|
||||
'AdminPanel.AdminGroups',
|
||||
'TestData.AdminGroups',
|
||||
'FlipTok.AdminGroups',
|
||||
'Picstagram.AdminGroups',
|
||||
]) {
|
||||
expect(configurator).toContain(`["${path}"] = true`)
|
||||
}
|
||||
expect(configurator).toContain(
|
||||
'Phone Configurator enabled: file-based settings from config.lua',
|
||||
)
|
||||
expect(configurator).toContain(
|
||||
'(except Config.CommandPermissions) and media.lua are disabled',
|
||||
)
|
||||
})
|
||||
|
||||
it('authorizes Qbox through ACE before retaining framework group support', () => {
|
||||
expect(framework).toContain(
|
||||
'local groups = Config.CommandPermissions[permission]',
|
||||
)
|
||||
expect(qbox).toContain(
|
||||
'if IsPlayerAceAllowed(tostring(source), group) then',
|
||||
)
|
||||
expect(qbox.indexOf('IsPlayerAceAllowed')).toBeLessThan(
|
||||
qbox.indexOf('exports.qbx_core:HasGroup'),
|
||||
)
|
||||
})
|
||||
|
||||
it('uses stable permission identifiers for every protected operation', () => {
|
||||
const expectations = [
|
||||
['../../sky_phone/source/server/admin.lua', 'phonepanel'],
|
||||
['../../sky_phone/source/server/testdata.lua', 'phonetestdata'],
|
||||
['../../sky_phone/source/server/fliptok.lua', 'fliptokverify'],
|
||||
['../../sky_phone/source/server/picstagram.lua', 'picstagramverify'],
|
||||
['../../sky_phone/source/server/picstagram.lua', 'picstagramadmin'],
|
||||
] as const
|
||||
|
||||
for (const [path, permission] of expectations) {
|
||||
expect(source(path)).toContain(
|
||||
`Bridge.Framework.HasPermission(source, "${permission}")`,
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -52,7 +52,7 @@ describe('phone inventory contracts', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('auto-detects registered inventories and limits metadata-free adapters', () => {
|
||||
it('auto-detects registered inventories and forces metadata-free adapters into compatible modes', () => {
|
||||
const inventoryBridge = readResourceFile(
|
||||
'source/bridge/server/inventory.lua',
|
||||
)
|
||||
@@ -65,8 +65,17 @@ describe('phone inventory contracts', () => {
|
||||
)
|
||||
expect(inventoryBridge).toContain('configured_inventory = adapter.name')
|
||||
expect(inventoryBridge).toContain('selected_adapter.metadata == false')
|
||||
expect(inventoryBridge).toContain('Config.Phone.Unique ~= false')
|
||||
expect(inventoryBridge).toContain('Config.Sim.Enabled ~= false')
|
||||
expect(inventoryBridge).toContain('Config.Phone.Unique = false')
|
||||
expect(inventoryBridge).toContain('Config.Sim.Enabled = false')
|
||||
expect(inventoryBridge).toContain(
|
||||
'does not support item metadata; unique phones and physical SIM cards were disabled automatically',
|
||||
)
|
||||
expect(inventoryBridge).toContain(
|
||||
'AddEventHandler("sky_phone:configurator:serverUpdated"',
|
||||
)
|
||||
expect(inventoryBridge).not.toContain(
|
||||
'cannot store unique phone or physical SIM metadata',
|
||||
)
|
||||
})
|
||||
|
||||
it('provides the LB IsOpen export alias from the authoritative client state', () => {
|
||||
|
||||
@@ -945,7 +945,6 @@ const adminPanelFallbackLocales = {
|
||||
entry: 'Entry',
|
||||
general: 'General',
|
||||
subtabs: {
|
||||
AdminGroups: 'Admin Groups',
|
||||
Dictionaries: 'Dictionaries',
|
||||
Clips: 'Clips',
|
||||
Transforms: 'Transforms',
|
||||
|
||||
@@ -16,9 +16,7 @@ describe('admin configurator descriptions', () => {
|
||||
expect(configuratorDescriptionKey('Media.RequestTimeoutMs', 10000)).toBe(
|
||||
'milliseconds',
|
||||
)
|
||||
expect(configuratorDescriptionKey('AdminPanel.AdminGroups', [])).toBe(
|
||||
'access',
|
||||
)
|
||||
expect(configuratorDescriptionKey('Radio.AllowedJobs', [])).toBe('access')
|
||||
expect(configuratorDescriptionKey('FiveManage.ApiKey', '')).toBe(
|
||||
'credential',
|
||||
)
|
||||
|
||||
@@ -661,6 +661,7 @@ function loadConfiguratorSections() {
|
||||
)
|
||||
const media = mediaRoot.Media
|
||||
delete config.PhoneConfigurator
|
||||
delete config.CommandPermissions
|
||||
delete config.Media
|
||||
return [...buildSections('config', config), ...buildSections('media', media)]
|
||||
}
|
||||
|
||||
@@ -61,14 +61,19 @@ function countStructure(structure: ConfiguratorStructure | undefined): number {
|
||||
}
|
||||
|
||||
describe('admin configurator fixture', () => {
|
||||
it('exposes every config.lua root through the full live preview', () => {
|
||||
it('exposes every SQL-managed config.lua root through the full live preview', () => {
|
||||
const sections = loadConfiguratorSections()
|
||||
const fields = sections.flatMap((section) => section.fields)
|
||||
const roots = [
|
||||
...configSource.matchAll(/^\s{0,4}Config\.([A-Za-z0-9_]+)\s*=/gm),
|
||||
]
|
||||
.map((match) => match[1])
|
||||
.filter((root) => root !== 'Media' && root !== 'PhoneConfigurator')
|
||||
.filter(
|
||||
(root) =>
|
||||
root !== 'Media' &&
|
||||
root !== 'PhoneConfigurator' &&
|
||||
root !== 'CommandPermissions',
|
||||
)
|
||||
|
||||
expect(sections).toHaveLength(45)
|
||||
expect(
|
||||
|
||||
+15
-2
@@ -134,7 +134,7 @@ Sky Phone is built to be the **free FiveM phone you can choose without accepting
|
||||
- `hex_4_inventory`
|
||||
- Native ESX inventory
|
||||
|
||||
`mf-inventory` and `smx-inventory` are supported with ESX. The native ESX and HEX adapters use count-based items and therefore require unique phones and physical SIM cards to be disabled.
|
||||
`mf-inventory` and `smx-inventory` are supported with ESX. The native ESX and HEX adapters use count-based items, so Sky Phone automatically disables unique phones and physical SIM cards while either adapter is active.
|
||||
|
||||
### Voice
|
||||
|
||||
@@ -196,6 +196,7 @@ The files contain clearly separated sections for:
|
||||
| Section | Purpose |
|
||||
| --- | --- |
|
||||
| `Config.Bridge` | Framework, inventory, language, callback timeout, and debug mode |
|
||||
| `Config.CommandPermissions` | Fixed groups for the admin panel, test data, verification commands, and social moderation |
|
||||
| `Config.Phone` | Phone item, movement, unique-device mode, and development command |
|
||||
| `Config.Sim` | Physical or virtual SIM behavior and number formatting |
|
||||
| `Config.Calls` / `Config.Radio` | Voice providers, call behavior, radio limits, and permissions |
|
||||
@@ -211,6 +212,18 @@ The files contain clearly separated sections for:
|
||||
|
||||
Restart `sky_phone` after changing Lua configuration.
|
||||
|
||||
When `Config.PhoneConfigurator.Enabled` is enabled, the generated `source/shared/config_default.lua`
|
||||
provides the shipped SQL baseline. The frontend build recreates it from `config.lua` and the
|
||||
server-only `media.lua`; do not edit the generated snapshot directly.
|
||||
|
||||
`Config.PhoneConfigurator` and `Config.CommandPermissions` remain file-owned and are omitted from
|
||||
the generated default snapshot. Permissions are not displayed in the Phone Configurator and stay
|
||||
authoritative while SQL configuration is enabled. Their stable keys do not change when a command is
|
||||
renamed in the panel. ESX and QBCore use their framework permissions. Qbox checks the configured ACE
|
||||
objects first and then its framework groups, so the standard `permissions.cfg` mapping from
|
||||
`group.admin` to the `admin` ACE works with the shipped `phonepanel` permission list. Restart
|
||||
`sky_phone` after changing fixed permissions.
|
||||
|
||||
### Language
|
||||
|
||||
Available locales:
|
||||
@@ -312,7 +325,7 @@ Do not configure an LB Phone client event or client export. Sky Phone registers
|
||||
|
||||
The server registers `Config.Phone.Item` as usable for every supported inventory adapter: `ox`, `qb`, `lj`, `qs`, `codem`, `core`, `mf`, `smx`, `hex`, and `esx`. Resource startup fails visibly if the selected adapter cannot complete that registration.
|
||||
|
||||
The `hex` and `esx` adapters use ESX's count-based item API. They require both `Config.Phone.Unique = false` and `Config.Sim.Enabled = false` because this API cannot persist per-item phone or physical SIM metadata. `auto` selects `hex` when `hex_4_inventory` is started and otherwise falls back to `esx` on an ESX server when no metadata-capable inventory is detected.
|
||||
The `hex` and `esx` adapters use ESX's count-based item API, which cannot persist per-item phone or physical SIM metadata. Sky Phone automatically forces `Config.Phone.Unique = false` and `Config.Sim.Enabled = false` while either adapter is active. `auto` selects `hex` when `hex_4_inventory` is started and otherwise falls back to `esx` on an ESX server when no metadata-capable inventory is detected.
|
||||
|
||||
### QBCore-style item tables
|
||||
|
||||
|
||||
@@ -10,12 +10,27 @@
|
||||
Keep option names unchanged. Restart sky_phone after editing this file.
|
||||
]]
|
||||
|
||||
-- When enabled, config.lua and media.lua only provide first-run defaults.
|
||||
-- The active configuration is loaded from SQL and managed through /phonepanel.
|
||||
-- CONFIG_DEFAULT_EXCLUDE_START
|
||||
-- When enabled, the active configuration is loaded from SQL and managed through
|
||||
-- /phonepanel. Frontend builds snapshot the shipped defaults from config.lua and
|
||||
-- media.lua into source/shared/config_default.lua.
|
||||
Config.PhoneConfigurator = {
|
||||
Enabled = true,
|
||||
}
|
||||
|
||||
-- Fixed server permissions. These values remain authoritative even while the
|
||||
-- Phone Configurator is enabled and are intentionally not shown in its panel.
|
||||
-- Group names use the active framework's permissions. On Qbox they also match
|
||||
-- ACE objects such as "admin" from the standard permissions.cfg.
|
||||
Config.CommandPermissions = {
|
||||
phonepanel = { "god", "superadmin", "admin" },
|
||||
phonetestdata = { "god", "superadmin", "admin" },
|
||||
fliptokverify = { "god", "superadmin", "admin" },
|
||||
picstagramverify = { "god", "superadmin", "admin" },
|
||||
picstagramadmin = { "god", "superadmin", "admin" },
|
||||
}
|
||||
-- CONFIG_DEFAULT_EXCLUDE_END
|
||||
|
||||
-- =============================================================================
|
||||
-- Core, framework and device
|
||||
-- =============================================================================
|
||||
@@ -34,7 +49,7 @@ Config.Command = "phone"
|
||||
|
||||
Config.Phone = {
|
||||
Item = "phone",
|
||||
Unique = true, -- true: data follows each phone item; false: one persistent phone per character; hex/esx require false
|
||||
Unique = true, -- true: data follows each phone item; false: one persistent phone per character; forced false for metadata-free inventories
|
||||
Keybind = "F1", -- false disables the configurable phone key mapping
|
||||
AllowMovement = true, -- true: game input stays active while the mobile phone is open
|
||||
HoldToLook = {
|
||||
@@ -49,7 +64,6 @@ Config.TestData = {
|
||||
Enabled = false, -- development/test servers only; keep disabled in production
|
||||
Command = "phonetestdata",
|
||||
AdminOnly = false, -- enable only on development servers; every run is scoped to the executing player's phone
|
||||
AdminGroups = { "admin", "superadmin" },
|
||||
}
|
||||
|
||||
Config.CustomApps = {
|
||||
@@ -76,7 +90,6 @@ Config.Security = {
|
||||
Config.AdminPanel = {
|
||||
Enabled = true,
|
||||
Command = "phonepanel",
|
||||
AdminGroups = { "admin", "superadmin" },
|
||||
MaximumPlayers = 128,
|
||||
ReadRequestsPerMinute = 60,
|
||||
ActionRequestsPerMinute = 30,
|
||||
@@ -86,7 +99,7 @@ Config.AdminPanel = {
|
||||
}
|
||||
|
||||
Config.Sim = {
|
||||
Enabled = true, -- false: devices receive a persistent random number automatically; hex/esx require false
|
||||
Enabled = true, -- false: devices receive a persistent random number automatically; forced false for metadata-free inventories
|
||||
RegisteredItem = "sky_phone_sim_registered",
|
||||
AnonymousItem = "sky_phone_sim_anonymous",
|
||||
NumberLength = 10, -- total number of digits, including NumberPrefix
|
||||
@@ -485,7 +498,6 @@ Config.FlipTok = {
|
||||
MaxPostMedia = 10,
|
||||
MusicTracks = {},
|
||||
VerifyCommand = "fliptokverify",
|
||||
AdminGroups = { "admin" },
|
||||
ReportWebhookConvar = "sky_phone_fliptok_report_webhook",
|
||||
}
|
||||
|
||||
@@ -507,7 +519,6 @@ Config.Picstagram = {
|
||||
ReportDetailsMaxLength = 500,
|
||||
ReportReasons = { "spam", "harassment", "dangerous", "illegal", "other" },
|
||||
VerifyCommand = "picstagramverify",
|
||||
AdminGroups = { "admin" },
|
||||
}
|
||||
|
||||
Config.Feather = {
|
||||
|
||||
@@ -2139,7 +2139,6 @@ Locales["de"] = {
|
||||
}
|
||||
|
||||
Locales["de"].Nui.AdminPanel.configurator.table.subtabs = {
|
||||
AdminGroups = "Admin-Gruppen",
|
||||
Dictionaries = "Animationsdateien",
|
||||
Clips = "Clips",
|
||||
Transforms = "Transformationen",
|
||||
|
||||
@@ -2139,7 +2139,6 @@ Locales["en"] = {
|
||||
}
|
||||
|
||||
Locales["en"].Nui.AdminPanel.configurator.table.subtabs = {
|
||||
AdminGroups = "Admin Groups",
|
||||
Dictionaries = "Dictionaries",
|
||||
Clips = "Clips",
|
||||
Transforms = "Transforms",
|
||||
|
||||
@@ -2139,7 +2139,6 @@ Locales["es"] = {
|
||||
}
|
||||
|
||||
Locales["es"].Nui.AdminPanel.configurator.table.subtabs = {
|
||||
AdminGroups = "Grupos de administradores",
|
||||
Dictionaries = "Diccionarios",
|
||||
Clips = "Los clips",
|
||||
Transforms = "Transformaciones",
|
||||
|
||||
@@ -2,19 +2,25 @@
|
||||
Sky Phone media configuration
|
||||
|
||||
This file is loaded on the server only. Keep API keys private. When the
|
||||
phone configurator is enabled in config.lua, these values are first-run
|
||||
defaults and the active media configuration is loaded from SQL.
|
||||
phone configurator is enabled in config.lua, the active media configuration
|
||||
is loaded from SQL and frontend builds copy the shipped values from this
|
||||
file into source/shared/config_default.lua.
|
||||
]]
|
||||
|
||||
Config.Media = {
|
||||
GiphyApiKey = "", -- Paste the GIPHY API key here.
|
||||
-- Create or sign in to an account at https://developers.giphy.com/, open
|
||||
-- the Developer Dashboard, choose "Create an API Key", and paste it here.
|
||||
GiphyApiKey = "",
|
||||
GifPageSize = 24,
|
||||
GifRating = "pg-13",
|
||||
UrlMaxLength = 2048,
|
||||
AllowedGifHosts = { "giphy.com" },
|
||||
|
||||
-- Create or sign in at https://fivemanage.com/, create or select your team,
|
||||
-- then open Dashboard > Tokens and create a token with Media access. Keep
|
||||
-- this server-only token private and paste it here.
|
||||
FiveManage = {
|
||||
ApiKey = "", -- Paste a newly generated FiveManage V3 Media API token here.
|
||||
ApiKey = "",
|
||||
BaseUrl = "https://api.fivemanage.com/api/v3/file",
|
||||
RequestTimeoutMs = 10000,
|
||||
UploadTimeoutMs = 25000,
|
||||
|
||||
@@ -79,6 +79,7 @@ server_scripts {
|
||||
'config/locales/en.lua',
|
||||
'config/locales/de.lua',
|
||||
'config/locales/es.lua',
|
||||
'source/shared/config_default.lua',
|
||||
'source/server/nui_build_check.lua',
|
||||
'source/server/update_check.lua',
|
||||
'source/bridge/server/database.lua',
|
||||
|
||||
@@ -19,3 +19,23 @@ Bridge.Framework.Name = configured_framework
|
||||
function Bridge.Framework.GetName()
|
||||
return Bridge.Framework.Name
|
||||
end
|
||||
|
||||
function Bridge.Framework.HasPermission(source, permission)
|
||||
if type(permission) ~= "string" or permission == "" then
|
||||
error("[sky_phone] Permission identifiers must be non-empty strings.")
|
||||
end
|
||||
|
||||
local groups = Config.CommandPermissions[permission]
|
||||
if type(groups) ~= "table" or #groups == 0 then
|
||||
local message = "[sky_phone] Config.CommandPermissions.%s must contain at least one group."
|
||||
error(message:format(permission))
|
||||
end
|
||||
for index, group in ipairs(groups) do
|
||||
if type(group) ~= "string" or group == "" then
|
||||
local message = "[sky_phone] Config.CommandPermissions.%s[%s] must be a non-empty string."
|
||||
error(message:format(permission, index))
|
||||
end
|
||||
end
|
||||
|
||||
return Bridge.Framework.HasAdminGroup(source, groups)
|
||||
end
|
||||
|
||||
@@ -24,6 +24,13 @@ function Bridge.Framework.HasAdminGroup(source, groups)
|
||||
if not player then
|
||||
return false
|
||||
end
|
||||
|
||||
for _, group in ipairs(groups) do
|
||||
if IsPlayerAceAllowed(tostring(source), group) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return exports.qbx_core:HasGroup(source, groups)
|
||||
end
|
||||
|
||||
|
||||
@@ -61,13 +61,30 @@ end
|
||||
|
||||
Bridge.Inventory.Name = configured_inventory
|
||||
|
||||
if selected_adapter.metadata == false then
|
||||
if Config.Phone.Unique ~= false or Config.Sim.Enabled ~= false then
|
||||
Bridge.Inventory.ConfigurationError = ("[sky_phone] Inventory '%s' cannot store unique phone or physical SIM metadata. Set Config.Phone.Unique = false and Config.Sim.Enabled = false, or configure a metadata-capable inventory.")
|
||||
:format(configured_inventory)
|
||||
local metadata_free_inventory = selected_adapter.metadata == false
|
||||
|
||||
local function enforce_metadata_compatibility()
|
||||
if not metadata_free_inventory then
|
||||
return
|
||||
end
|
||||
|
||||
local modes_changed = Config.Phone.Unique ~= false or Config.Sim.Enabled ~= false
|
||||
Config.Phone.Unique = false
|
||||
Config.Sim.Enabled = false
|
||||
|
||||
if modes_changed then
|
||||
Bridge.Debug(
|
||||
"warn",
|
||||
"[sky_phone] Inventory '%s' does not support item metadata; unique phones and physical SIM cards were disabled automatically.",
|
||||
configured_inventory
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
enforce_metadata_compatibility()
|
||||
|
||||
AddEventHandler("sky_phone:configurator:serverUpdated", enforce_metadata_compatibility)
|
||||
|
||||
function Bridge.Inventory.NormalizeItem(item, metadata_field, fallback_slot)
|
||||
if type(item) ~= "table" then
|
||||
return nil
|
||||
|
||||
@@ -98,7 +98,7 @@ end
|
||||
|
||||
local function require_admin(source, operation, maximum)
|
||||
if not Config.AdminPanel.Enabled
|
||||
or not Bridge.Framework.HasAdminGroup(source, Config.AdminPanel.AdminGroups)
|
||||
or not Bridge.Framework.HasPermission(source, "phonepanel")
|
||||
then
|
||||
Bridge.Debug("warn", "[sky_phone] Rejected admin panel access from source %s.", tostring(source))
|
||||
return nil, { success = false, error = "not_authorized" }
|
||||
@@ -121,7 +121,7 @@ local function run_admin_command(command_source)
|
||||
TriggerClientEvent("sky_phone:admin:command-error", player_source, "disabled")
|
||||
return
|
||||
end
|
||||
if not Bridge.Framework.HasAdminGroup(player_source, Config.AdminPanel.AdminGroups) then
|
||||
if not Bridge.Framework.HasPermission(player_source, "phonepanel") then
|
||||
Bridge.Debug(
|
||||
"warn",
|
||||
"[sky_phone] Rejected admin panel command from source %s.",
|
||||
|
||||
@@ -943,7 +943,7 @@ local function run_verify_command(source, arguments)
|
||||
notificationType = notification_type,
|
||||
})
|
||||
end
|
||||
if source ~= 0 and not Bridge.Framework.HasAdminGroup(source, Config.FlipTok.AdminGroups) then
|
||||
if source ~= 0 and not Bridge.Framework.HasPermission(source, "fliptokverify") then
|
||||
send_command_feedback(command_locale.noPermission, "error")
|
||||
Bridge.Debug(
|
||||
"warn",
|
||||
|
||||
@@ -17,6 +17,21 @@ local updated_at
|
||||
local updated_by_name
|
||||
local is_sequence
|
||||
|
||||
local FIXED_CONFIG_PATHS = {
|
||||
CommandPermissions = true,
|
||||
["AdminPanel.AdminGroups"] = true,
|
||||
["TestData.AdminGroups"] = true,
|
||||
["FlipTok.AdminGroups"] = true,
|
||||
["Picstagram.AdminGroups"] = true,
|
||||
}
|
||||
|
||||
if configurator_enabled then
|
||||
print(
|
||||
"[sky_phone] Phone Configurator enabled: file-based settings from config.lua " ..
|
||||
"(except Config.CommandPermissions) and media.lua are disabled; SQL configuration is active."
|
||||
)
|
||||
end
|
||||
|
||||
local CLIENT_CONFIG_KEYS = {
|
||||
AdminPanel = true,
|
||||
Animations = true,
|
||||
@@ -252,7 +267,7 @@ local function upgrade_legacy_map(defaults, saved)
|
||||
return { __skyType = "map", entries = entries }
|
||||
end
|
||||
|
||||
local function merge_values(defaults, saved, path)
|
||||
local function merge_values(defaults, saved, path, excluded_paths)
|
||||
path = path or ""
|
||||
if type(defaults) ~= "table" or type(saved) ~= "table" then
|
||||
return copy_value(saved)
|
||||
@@ -278,7 +293,12 @@ local function merge_values(defaults, saved, path)
|
||||
key = entry.key,
|
||||
keyType = entry.keyType,
|
||||
value = saved_entry
|
||||
and merge_values(entry.value, saved_entry.value, path .. "." .. tostring(entry.key))
|
||||
and merge_values(
|
||||
entry.value,
|
||||
saved_entry.value,
|
||||
path .. "." .. tostring(entry.key),
|
||||
excluded_paths
|
||||
)
|
||||
or copy_value(entry.value),
|
||||
}
|
||||
included[identity] = true
|
||||
@@ -298,7 +318,7 @@ local function merge_values(defaults, saved, path)
|
||||
local merged = copy_value(saved)
|
||||
for index, child in ipairs(defaults) do
|
||||
merged[index] = saved[index] ~= nil
|
||||
and merge_values(child, saved[index], path .. "." .. tostring(index))
|
||||
and merge_values(child, saved[index], path .. "." .. tostring(index), excluded_paths)
|
||||
or copy_value(child)
|
||||
end
|
||||
return merged
|
||||
@@ -309,11 +329,13 @@ local function merge_values(defaults, saved, path)
|
||||
|
||||
local merged = copy_value(defaults)
|
||||
for key, child in pairs(saved) do
|
||||
if merged[key] ~= nil then
|
||||
local child_path = path == "" and tostring(key) or (path .. "." .. tostring(key))
|
||||
merged[key] = merge_values(merged[key], child, child_path)
|
||||
else
|
||||
merged[key] = copy_value(child)
|
||||
local child_path = path == "" and tostring(key) or (path .. "." .. tostring(key))
|
||||
if not excluded_paths or not excluded_paths[child_path] then
|
||||
if merged[key] ~= nil then
|
||||
merged[key] = merge_values(merged[key], child, child_path, excluded_paths)
|
||||
else
|
||||
merged[key] = copy_value(child)
|
||||
end
|
||||
end
|
||||
end
|
||||
return merged
|
||||
@@ -356,10 +378,12 @@ local function apply_runtime_configuration()
|
||||
|
||||
local runtime_config = deserialize_value(stored_config)
|
||||
for key, value in pairs(runtime_config) do
|
||||
if type(Config[key]) == "table" and type(value) == "table" then
|
||||
apply_runtime_table(Config[key], value)
|
||||
else
|
||||
Config[key] = value
|
||||
if key ~= "CommandPermissions" then
|
||||
if type(Config[key]) == "table" and type(value) == "table" then
|
||||
apply_runtime_table(Config[key], value)
|
||||
else
|
||||
Config[key] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
local runtime_media = deserialize_value(stored_media)
|
||||
@@ -1032,7 +1056,12 @@ local function read_stored_row()
|
||||
end
|
||||
|
||||
local function apply_stored_row(row)
|
||||
stored_config = merge_values(default_config, decode_payload(row.config_payload, "config"), "")
|
||||
stored_config = merge_values(
|
||||
default_config,
|
||||
decode_payload(row.config_payload, "config"),
|
||||
"",
|
||||
FIXED_CONFIG_PATHS
|
||||
)
|
||||
stored_media = merge_values(default_media, decode_payload(row.media_payload, "media"), "")
|
||||
revision = tonumber(row.revision) or 1
|
||||
updated_at = row.updated_at
|
||||
@@ -1040,12 +1069,12 @@ local function apply_stored_row(row)
|
||||
end
|
||||
|
||||
default_config = {}
|
||||
for key, value in pairs(Config) do
|
||||
if key ~= "Media" and key ~= "PhoneConfigurator" then
|
||||
for key, value in pairs(ConfigDefaults) do
|
||||
if key ~= "Media" and key ~= "PhoneConfigurator" and key ~= "CommandPermissions" then
|
||||
default_config[key] = serialize_value(value)
|
||||
end
|
||||
end
|
||||
default_media = serialize_value(Config.Media)
|
||||
default_media = serialize_value(ConfigDefaults.Media)
|
||||
|
||||
Bridge.Database.Migrate("sky_phone_configurator", { SkyPhoneConfiguratorSchema })
|
||||
Bridge.Database.Query(([[
|
||||
|
||||
@@ -319,7 +319,7 @@ local function create_activity(recipient_id, actor_id, kind, post_id)
|
||||
end
|
||||
|
||||
local function is_admin(source)
|
||||
return Bridge.Framework.HasAdminGroup(source, Config.Picstagram.AdminGroups)
|
||||
return Bridge.Framework.HasPermission(source, "picstagramadmin")
|
||||
end
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:picstagram:register", function(source, data)
|
||||
@@ -1464,7 +1464,7 @@ local function run_verify_command(source, args)
|
||||
notificationType = notification_type,
|
||||
})
|
||||
end
|
||||
if source ~= 0 and not Bridge.Framework.HasAdminGroup(source, Config.Picstagram.AdminGroups) then
|
||||
if source ~= 0 and not Bridge.Framework.HasPermission(source, "picstagramverify") then
|
||||
send_command_feedback(command_locale.noPermission, "error")
|
||||
Bridge.Debug(
|
||||
"warn",
|
||||
|
||||
@@ -1045,7 +1045,7 @@ local function run_test_data_command(source)
|
||||
Bridge.Debug("warn", "[sky_phone] The test data command must be run by an in-game player.")
|
||||
return
|
||||
end
|
||||
if Config.TestData.AdminOnly and not Bridge.Framework.HasAdminGroup(source, Config.TestData.AdminGroups) then
|
||||
if Config.TestData.AdminOnly and not Bridge.Framework.HasPermission(source, "phonetestdata") then
|
||||
Bridge.Debug("warn", "[sky_phone] Source %s attempted to run the restricted test data command.", tostring(source))
|
||||
TriggerClientEvent("sky_phone:testdata:feedback", source, false)
|
||||
return
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,64 @@
|
||||
Config = {
|
||||
Bridge = {
|
||||
Framework = "qbox",
|
||||
},
|
||||
CommandPermissions = {
|
||||
phonepanel = { "god", "superadmin", "admin" },
|
||||
},
|
||||
}
|
||||
|
||||
Bridge = {
|
||||
Framework = {},
|
||||
}
|
||||
|
||||
function GetResourceState(resource)
|
||||
assert(resource == "qbx_core")
|
||||
return "started"
|
||||
end
|
||||
|
||||
function GetPlayers()
|
||||
return { "1" }
|
||||
end
|
||||
|
||||
local ace_permissions = {}
|
||||
local framework_permission = false
|
||||
|
||||
function IsPlayerAceAllowed(source, permission)
|
||||
assert(source == "1")
|
||||
return ace_permissions[permission] == true
|
||||
end
|
||||
|
||||
exports = {
|
||||
qbx_core = {
|
||||
GetPlayer = function(_, source)
|
||||
if tonumber(source) ~= 1 then
|
||||
return nil
|
||||
end
|
||||
return {
|
||||
PlayerData = {
|
||||
citizenid = "test-citizen",
|
||||
},
|
||||
}
|
||||
end,
|
||||
HasGroup = function(_, source, groups)
|
||||
assert(source == 1)
|
||||
assert(groups == Config.CommandPermissions.phonepanel)
|
||||
return framework_permission
|
||||
end,
|
||||
},
|
||||
}
|
||||
|
||||
dofile("sky_phone/source/bridge/server/framework.lua")
|
||||
dofile("sky_phone/source/bridge/server/frameworks/qbox.lua")
|
||||
|
||||
ace_permissions.admin = true
|
||||
assert(Bridge.Framework.HasPermission(1, "phonepanel"), "Qbox ACE permissions must grant access")
|
||||
|
||||
ace_permissions.admin = false
|
||||
framework_permission = true
|
||||
assert(Bridge.Framework.HasPermission(1, "phonepanel"), "Qbox framework groups must remain supported")
|
||||
|
||||
local success = pcall(Bridge.Framework.HasPermission, 1, "missing")
|
||||
assert(not success, "Missing fixed permission definitions must fail loudly")
|
||||
|
||||
print("framework permission tests passed")
|
||||
@@ -1,3 +1,9 @@
|
||||
local event_handlers = {}
|
||||
|
||||
AddEventHandler = function(event_name, callback)
|
||||
event_handlers[event_name] = callback
|
||||
end
|
||||
|
||||
local function reset_bridge(inventory_name, unique_phones, sim_cards_enabled)
|
||||
Config = {
|
||||
Bridge = {
|
||||
@@ -193,11 +199,18 @@ exports = {
|
||||
}
|
||||
load_inventory_contract("sky_phone/source/bridge/server/inventory/esx.lua")
|
||||
|
||||
local ok, configuration_error = pcall(Bridge.Inventory.RegisterUsableItem, "phone", function()
|
||||
end)
|
||||
assert(not ok)
|
||||
assert(configuration_error:find("Config.Phone.Unique = false", 1, true))
|
||||
assert(configuration_error:find("Config.Sim.Enabled = false", 1, true))
|
||||
assert(Config.Phone.Unique == false)
|
||||
assert(Config.Sim.Enabled == false)
|
||||
assert(Bridge.Inventory.ConfigurationError == nil)
|
||||
|
||||
Config.Phone.Unique = true
|
||||
Config.Sim.Enabled = true
|
||||
event_handlers["sky_phone:configurator:serverUpdated"]()
|
||||
assert(Config.Phone.Unique == false)
|
||||
assert(Config.Sim.Enabled == false)
|
||||
|
||||
assert(Bridge.Inventory.RegisterUsableItem("phone", function()
|
||||
end))
|
||||
|
||||
local manifest_file = assert(io.open("sky_phone/fxmanifest.lua", "rb"))
|
||||
local manifest = manifest_file:read("*a")
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
AddEventHandler = function()
|
||||
end
|
||||
|
||||
local function reset_bridge(inventory_name, resource_name, inventory_export, framework_name, extra_exports)
|
||||
Config = {
|
||||
Bridge = { Inventory = inventory_name },
|
||||
|
||||
@@ -25,7 +25,7 @@ Bridge = {
|
||||
Debug = function()
|
||||
end,
|
||||
Framework = {
|
||||
HasAdminGroup = function()
|
||||
HasPermission = function()
|
||||
return true
|
||||
end,
|
||||
},
|
||||
@@ -42,7 +42,6 @@ Bridge = {
|
||||
|
||||
Config = {
|
||||
AdminPanel = {
|
||||
AdminGroups = { "admin" },
|
||||
Command = "phoneadmin",
|
||||
Enabled = true,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user