Revert "ADD - add in-game phone administration and configuration (#21)" (#24)

This reverts commit edd9677f43.
This commit is contained in:
DerEchteAlec
2026-08-21 21:49:05 +02:00
committed by GitHub
parent 21913b585c
commit 5f8c835685
74 changed files with 331 additions and 12864 deletions
+1 -19
View File
@@ -10,12 +10,6 @@
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.PhoneConfigurator = {
Enabled = true,
}
-- =============================================================================
-- Core, framework and device
-- =============================================================================
@@ -54,7 +48,7 @@ Config.CustomApps = {
Enabled = true,
BundledApps = true,
ExternalApps = true,
Debug = false, -- detailed client traces for exports, registration, catalog sync and lifecycle events
Debug = true, -- detailed client traces for exports, registration, catalog sync and lifecycle events
ReadyTimeoutMs = 8000,
MaximumMessageBytes = 65536,
MaximumStorageBytesPerApp = 262144,
@@ -71,18 +65,6 @@ Config.Security = {
AttemptsPerMinute = 12,
}
Config.AdminPanel = {
Enabled = true,
Command = "phonepanel",
AdminGroups = { "admin", "superadmin" },
MaximumPlayers = 128,
ReadRequestsPerMinute = 60,
ActionRequestsPerMinute = 30,
CredentialRevealsPerMinute = 6,
AuditLimit = 40,
ActivityLimit = 40,
}
Config.Sim = {
Enabled = true, -- false: devices receive a persistent random number automatically; hex/esx require false
RegisteredItem = "sky_phone_sim_registered",
-95
View File
@@ -1,13 +1,5 @@
Locales["de"] = {
CommandDescription = "Öffne dein Handy.",
AdminCommand = {
CommandDescription = "Öffne Handy-Administration und -Konfiguration.",
Errors = {
disabled = "Das Handy-Admin-Panel ist deaktiviert.",
not_authorized = "Du hast keinen Zugriff auf das Handy-Admin-Panel.",
default = "Das Handy-Admin-Panel konnte nicht geöffnet werden.",
},
},
Controls = {
OpenPhone = "Handy öffnen",
},
@@ -201,28 +193,6 @@ Locales["de"] = {
contacts = { name = "Favoriten", description = "Rufe deine Lieblingskontakte an oder schreibe ihnen.", choose = "Lieblingskontakte" },
},
},
AdminPanel = {
name = "Phone Admin", subtitle = "Administration", navigation = "Admin-Navigation", refresh = "Admin-Daten aktualisieren", loading = "Geschützte Daten werden geladen...",
tabs = { overview = "Übersicht", players = "Spieler", devices = "Geräte", apps = "Apps", accounts = "Accounts", messages = "Nachrichten", calls = "Anrufe", moderation = "Moderation", audit = "Audit", configurator = "Phone Configurator" },
overview = { eyebrow = "Server", title = "Dashboard", body = "Spieler, Geräte, Apps und Handydaten.", stats = "Server-Handystatistik", online = "Online", devices = "Geräte", accounts = "Accounts", audit = "Audit-Einträge", control = "Navigation", features = "Module", featuresBody = "Öffne ein Verwaltungsmodul.", recent = "Letzte Aktivitäten", playerFeature = "Identität, Finanzen, Job und Dienst", deviceFeature = "IMEI, SIM, Nummer und Aktivität", appFeature = "Handy-Apps installieren oder entfernen", accountFeature = "Accountzugriff und geschützte Zugangsdaten", messageFeature = "Letzte SMS-Aktivitäten prüfen", callFeature = "Letzte Anrufaktivitäten prüfen", moderationFeature = "Zugriff, Nummer oder Gerätedaten zurücksetzen", auditFeature = "Sensible Admin-Aktionen prüfen", configuratorFeature = "config.lua und media.lua über SQL verwalten" },
appearance = { eyebrow = "Darstellung", title = "Akzentfarbe", body = "Ändere den Akzent im gesamten Admin-Arbeitsbereich.", colors = { emerald = "Smaragd", blue = "Blau", violet = "Violett", orange = "Orange", red = "Rot" } },
configurator = { context = "Runtime-Konfiguration", eyebrow = "Systemwerkzeug", sections = "Konfiguration", search = "Einstellungen oder Pfade suchen", configScope = "config.lua", mediaScope = "media.lua", noResults = "Keine passenden Einstellungen", loading = "SQL-Konfiguration wird geladen...", title = "Phone Configurator", body = "Verwalte Handy- und Media-Einstellungen im geschützten Admin-Bereich.", disabledTitle = "SQL-Konfiguration ist nicht aktiv", disabledBody = "Aktiviere den Configurator am Anfang der config.lua und starte sky_phone neu. Bis dahin bleiben die Dateiwerte aktiv und die Bearbeitung gesperrt.", manualSave = "Manuelles Speichern", refreshNotice = "Nichts wird automatisch gespeichert. Der grüne Haken prüft config.lua und media.lua in SQL und aktualisiert die aktive Server-, Client-, Media- und UI-Konfiguration sofort intern.", fieldCount = "{count} Felder", secretConfigured = "Secret gesetzt · Ersatzwert eingeben", invalidValue = "Prüfe den markierten Tabellen- oder Zahlenwert.", saved = "SQL-Konfiguration gespeichert und übernommen.", descriptions = { featureToggle = "Schaltet {name} ein oder aus.", boolean = "Legt fest, ob {name} erlaubt ist.", number = "Legt den Zahlenwert für {name} fest.", text = "Legt den Textwert für {name} fest.", optionalText = "Legt den optionalen Wert für {name} fest; der Schalter deaktiviert ihn.", list = "Verwaltet alle Einträge für {name}.", table = "Bündelt die zusammengehörigen Einstellungen für {name}.", credential = "Speichert den geschützten Zugangsschlüssel für {name}.", url = "Legt die URL oder den Endpunkt für {name} fest.", hosts = "Legt die erlaubten Domains für {name} fest.", milliseconds = "Legt die Zeit für {name} in Millisekunden fest.", seconds = "Legt die Zeit für {name} in Sekunden fest.", rateLimit = "Begrenzt die Aktionen für {name} pro Minute.", byteLimit = "Legt die maximal erlaubte Datengröße für {name} fest.", textLimit = "Legt die maximal erlaubte Textlänge für {name} fest.", distance = "Legt die Distanz in der Spielwelt für {name} fest.", coordinates = "Legt Weltkoordinaten oder Ausrichtung für {name} fest.", gameAsset = "Legt das GTA-Modell oder Prop für {name} fest.", animation = "Legt die Animationsdatei für {name} fest.", access = "Legt Jobs, Gruppen oder Berechtigungsstufen für {name} fest.", integration = "Wählt Framework oder Anbieter für {name} aus.", path = "Legt den Speicher- oder Ressourcenpfad für {name} fest.", color = "Legt die Oberflächenfarbe für {name} fest.", displayText = "Legt den Spielern angezeigten Text für {name} fest.", phoneNumber = "Legt die Telefon- oder Servicenummer für {name} fest.", routing = "Steuert die Verteilung eingehender Anfragen für {name}.", command = "Legt den Chat-Befehl zum Öffnen oder Ausführen von {name} fest.", locale = "Wählt die Sprache für {name} aus.", debug = "Steuert ausführliche Diagnoseausgaben für {name}.", mediaQuality = "Legt Medienqualität oder Lautstärke für {name} fest.", amount = "Legt die maximale oder angezeigte Menge für {name} fest." }, table = { list = "Liste", table = "Schlüsseltabelle", vector = "Vektor", entry = "Eintrag", general = "Allgemein", addRow = "Zeile hinzufügen", addField = "Feld hinzufügen", remove = "Entfernen", emptyList = "Noch keine Zeilen. Füge die erste Zeile mit Plus hinzu.", emptyTable = "Noch keine Felder. Füge unten den ersten Schlüssel hinzu.", keyPlaceholder = "Neuer Schlüssel", convertToList = "Als Liste nutzen", convertToMap = "Als typisierte Schlüsseltabelle nutzen", convertToTable = "Als Schlüsseltabelle nutzen", types = { string = "Text", number = "Zahl", boolean = "Schalter", list = "Liste", table = "Tabelle" } } },
players = { eyebrow = "Aktive Sitzungen", title = "Online-Spieler", online = "Jetzt online", empty = "Keine Spieler gefunden", emptyBody = "Passe die Suche an oder aktualisiere die Spielerliste." },
search = { players = "Name, ID, Job oder Nummer suchen", apps = "Apps suchen", clear = "Suche leeren" },
detail = { character = "Charakterprofil", data = "Spielerdatenübersicht", cash = "Bargeld", bank = "Bank", job = "Job", duty = "Dienst", onDuty = "Im Dienst", offDuty = "Außer Dienst", identity = "Identität", playerData = "Spielerdaten", identifier = "Charakter-Identifier", birthdate = "Geburtsdatum", grade = "Job-Rang", unknown = "Unbekannt" },
devices = { eyebrow = "Gerätesteuerung", title = "Handys", body = "Prüfe alle Handys des ausgewählten Spielers.", choose = "Handy auswählen", empty = "Kein Handy gefunden", emptyBody = "Dieser Spieler besitzt aktuell kein verwaltbares Handy.", noNumber = "Keine Telefonnummer", noSim = "Keine SIM", imei = "IMEI", updated = "Letzte Aktivität", apps = "Zugewiesene Apps", account = "Verknüpfter Account" },
credentials = { eyebrow = "Geschützte Daten", title = "Zugangsdaten", email = "iFruit-E-Mail", password = "iFruit-Passwort", reveal = "Anzeigen", copy = "Passwort kopieren", copied = "Passwort kopiert.", noAccount = "Mit diesem Handy ist kein iFruit-Account verknüpft.", passcode = "Gerätecode", passcodeHashed = "{length}-stellige PIN · sicher gehasht und nicht wiederherstellbar", passcodeDisabled = "Kein Gerätecode eingerichtet", revealTitle = "Geschütztes Passwort anzeigen?", revealBody = "Diese Aktion wird serverseitig autorisiert, rate-limitiert und im Admin-Audit protokolliert.", cancel = "Abbrechen", confirmReveal = "Passwort anzeigen" },
apps = { eyebrow = "Fernverwaltung", title = "App-Zugriff", description = "Lege App-Zugriffe für dieses Gerät als Entwurf fest. Erst Speichern übernimmt die Änderungen.", installed = "Installiert", available = "Verfügbar", protected = "System-App", changes = "{count} offene Änderungen" },
activity = { protected = "Geschützte Aktivität", messagesTitle = "Nachrichten", messagesBody = "Letzte SMS-Aktivitäten der ausgewählten SIM.", callsTitle = "Anrufe", callsBody = "Letzte Anrufaktivitäten der ausgewählten SIM.", loading = "Aktivitäten werden geladen...", incoming = "Eingehend", outgoing = "Ausgehend", mediaMessage = "{type}-Nachricht", noMessages = "Keine Nachrichtenaktivität gefunden.", noCalls = "Keine Anrufaktivität gefunden.", status = { completed = "Abgeschlossen", missed = "Verpasst", rejected = "Abgelehnt", busy = "Besetzt", unanswered = "Unbeantwortet", cancelled = "Abgebrochen", failed = "Fehlgeschlagen", ringing = "Klingelt" } },
moderation = {
eyebrow = "Geräteverwaltung", title = "Moderationsaktionen", body = "Jede Aktion wird serverseitig autorisiert, rate-limitiert und protokolliert.", resetPasscode = "Gerätecode zurücksetzen", resetPasscodeBody = "Entfernt die Geräte-PIN und fehlgeschlagene Versuche.", changeNumber = "Nummer ändern", changeNumberBody = "Weist der aktuellen SIM eine neue eindeutige Nummer zu.", factoryReset = "Werksreset", factoryResetBody = "Löscht lokale Gerätedaten und trennt den Account.", saveFirst = "Speichere oder verwirf zuerst offene App-Änderungen.", phoneNumber = "Neue Telefonnummer", phoneNumberPlaceholder = "Vollständige konfigurierte Nummer eingeben", typeToConfirm = "Tippe {word}, um den Werksreset zu bestätigen.", confirmWord = "RESET", cancel = "Abbrechen", ["reset-passcodeSuccess"] = "Gerätecode zurückgesetzt.", ["change-numberSuccess"] = "Telefonnummer geändert.", ["factory-resetSuccess"] = "Werksreset abgeschlossen.",
dialogs = { ["reset-passcodeTitle"] = "Gerätecode zurücksetzen?", ["reset-passcodeBody"] = "Der Spieler kann dieses Handy danach ohne die bisherige PIN entsperren.", ["change-numberTitle"] = "Telefonnummer ändern?", ["change-numberBody"] = "Die neue Nummer muss dem Serverformat entsprechen und eindeutig sein.", ["factory-resetTitle"] = "Werksreset für dieses Handy?", ["factory-resetBody"] = "Lokale Gerätedaten, App-Einstellungen, Sicherheit und der verknüpfte Account werden gelöscht. Das kann nicht rückgängig gemacht werden." },
confirm = { ["reset-passcode"] = "Gerätecode zurücksetzen", ["change-number"] = "Nummer ändern", ["factory-reset"] = "Werksreset" },
},
audit = { eyebrow = "Nachvollziehbarkeit", title = "Audit-Verlauf", body = "Sensible Anzeigen und App-Änderungen werden hier protokolliert.", empty = "Noch keine Admin-Aktionen", emptyBody = "Geschützte Aktionen erscheinen hier, nachdem sie ausgeführt wurden.", by = "{actor} · Ziel-ID {target}", actions = { grant_app = "App installiert", revoke_app = "App entfernt", reveal_account_password = "Passwort angezeigt", view_messages = "Nachrichten angesehen", view_calls = "Anrufe angesehen", reset_passcode = "Gerätecode zurückgesetzt", change_number = "Telefonnummer geändert", factory_reset = "Werksreset ausgeführt", save_configuration = "Konfiguration gespeichert" } },
editor = { brand = "SKY PHONE", workspace = "ADMIN", players = "Spielerverzeichnis", audit = "Audit-Protokoll", selectPlayer = "Wähle einen Spieler, um Identität, Geräte, Zugangsdaten und App-Zugriffe zu prüfen.", save = "Änderungen speichern", saveHint = "Offene Änderungen übernehmen", saved = "Änderungen gespeichert.", unsaved = "Ungespeicherte Änderungen", close = "Admin-Panel schließen", refresh = "Live-Daten aktualisieren", online = "LIVE", profile = "PROFIL", financial = "FINANZEN", device = "GERÄT", security = "SICHERHEIT", noAutoSave = "Manuelles Speichern", noAutoSaveBody = "Änderungen bleiben lokal, bis der grüne Haken gedrückt wird.", discardTitle = "Ungespeicherte Änderungen verwerfen?", discardBody = "Deine vorgemerkten App- oder Konfigurationsänderungen wurden noch nicht gespeichert.", keepEditing = "Weiter bearbeiten", discard = "Änderungen verwerfen", saveFailed = "Einige Änderungen konnten nicht gespeichert werden.", noSelection = "Kein Spieler ausgewählt" },
errors = { not_authorized = "Du hast keinen Zugriff auf das Admin-Panel.", rate_limited = "Zu viele Admin-Anfragen. Bitte warte.", player_unavailable = "Dieser Spieler ist nicht mehr online.", device_not_owned = "Dieses Handy gehört nicht mehr zum ausgewählten Spieler.", invalid_app = "Diese App ist nicht auf dem Server registriert.", app_protected = "Diese System-App kann nicht entfernt werden.", revision_conflict = "Die Daten wurden zwischenzeitlich geändert. Öffne den Bereich neu und versuche es erneut.", configurator_disabled = "Aktiviere zuerst den Phone Configurator in der config.lua.", invalid_field = "Dieses Konfigurationsfeld ist nicht mehr verfügbar.", invalid_value = "Ein Konfigurationswert ist ungültig.", account_not_found = "Mit diesem Handy ist kein iFruit-Account verknüpft.", invalid_phone_number = "Gib eine Telefonnummer im konfigurierten Serverformat ein.", phone_number_unchanged = "Diese SIM verwendet diese Telefonnummer bereits.", phone_number_taken = "Diese Telefonnummer ist bereits vergeben.", no_sim = "Dieses Handy besitzt keine änderbare SIM.", passcode_not_set = "Für dieses Handy ist kein Gerätecode eingerichtet.", device_not_found = "Dieses Handy existiert nicht mehr.", metadata_unsupported = "Die Inventar-Metadaten des Handys konnten nicht aktualisiert werden.", invalid_request = "Die Admin-Anfrage war ungültig.", request_failed = "Die Admin-Anfrage ist fehlgeschlagen.", default = "Das Admin-Panel ist vorübergehend nicht verfügbar." },
},
Apps = {
health = {
name = "Gesundheit",
@@ -2137,68 +2107,3 @@ Locales["de"] = {
},
},
}
Locales["de"].Nui.AdminPanel.configurator.table.subtabs = {
AdminGroups = "Admin-Gruppen",
Dictionaries = "Animationsdateien",
Clips = "Clips",
Transforms = "Transformationen",
RateLimits = "Ratenlimits",
Publishers = "Herausgeber",
ExternalPingResources = "Externe Ping-Ressourcen",
Markets = "Märkte",
TrustedAdapters = "Vertrauenswürdige Adapter",
AllowedDisappearTimers = "Erlaubte Löschzeiten",
MusicTracks = "Musiktitel",
VehicleImages = "Fahrzeugbilder",
Custom = "Benutzerdefiniert",
Valet = "Parkservice",
AutoPriority = "Automatische Priorität",
Camera = "Kamera",
Categories = "Kategorien",
Districts = "Bezirke",
PhotoGradients = "Fotoverläufe",
LbPhone = "LB Phone",
Tracks = "Titel",
Props = "Props",
CustomLocations = "Benutzerdefinierte Standorte",
Animation = "Animation",
ReportReasons = "Meldegründe",
DisplayName = "Anzeigename",
Hud = "HUD",
Badge = "Abzeichen",
LockedChannels = "Gesperrte Kanäle",
NumberGroups = "Nummerngruppen",
CustomFare = "Benutzerdefinierter Tarif",
DriverJobs = "Fahrerjobs",
Services = "Dienstleistungen",
QuickLocations = "Schnellziele",
AllowedJobs = "Erlaubte Jobs",
Websites = "Webseiten",
}
Locales["de"].Nui.AdminPanel.appearance.title = "Oberfläche"
Locales["de"].Nui.AdminPanel.appearance.body = "Passe Farben und Typografie im gesamten Admin-Bereich an."
Locales["de"].Nui.AdminPanel.appearance.controls = {
customColor = "Eigene Farbe",
customColorBody = "Wähle einen beliebigen RGB-Akzent oder trage die Farbwerte ein.",
red = "R",
green = "G",
blue = "B",
hex = "HEX-Farbe",
value = "Wert",
fontFamily = "Schriftart",
fontFamilyBody = "Wähle die Schrift für das gesamte Admin-Panel.",
fontSize = "Schriftgröße",
fontSizeBody = "Skaliere Text und Bedienelemente für eine angenehme Lesbarkeit.",
fonts = {
inter = "Inter",
system = "System",
classic = "Klassisch",
verdana = "Verdana",
tahoma = "Tahoma",
trebuchet = "Trebuchet",
georgia = "Georgia",
mono = "Monospace",
},
}
-95
View File
@@ -1,13 +1,5 @@
Locales["en"] = {
CommandDescription = "Open your phone.",
AdminCommand = {
CommandDescription = "Open phone administration and configuration.",
Errors = {
disabled = "The phone admin panel is disabled.",
not_authorized = "You do not have access to the phone admin panel.",
default = "The phone admin panel could not be opened.",
},
},
Controls = {
OpenPhone = "Open phone",
},
@@ -201,28 +193,6 @@ Locales["en"] = {
contacts = { name = "Favorites", description = "Call or message your favorite contacts.", choose = "Favorite Contacts" },
},
},
AdminPanel = {
name = "Phone Admin", subtitle = "Administration", navigation = "Admin navigation", refresh = "Refresh admin data", loading = "Loading protected data...",
tabs = { overview = "Overview", players = "Players", devices = "Devices", apps = "Apps", accounts = "Accounts", messages = "Messages", calls = "Calls", moderation = "Moderation", audit = "Audit", configurator = "Phone configurator" },
overview = { eyebrow = "Server", title = "Dashboard", body = "Players, devices, apps, and phone data.", stats = "Server phone statistics", online = "Online", devices = "Devices", accounts = "Accounts", audit = "Audit entries", control = "Navigation", features = "Modules", featuresBody = "Open an administration module.", recent = "Recent activity", playerFeature = "Identity, finances, job, and duty", deviceFeature = "IMEI, SIM, number, and activity", appFeature = "Install or remove phone apps", accountFeature = "Account access and protected credentials", messageFeature = "Review recent SMS activity", callFeature = "Review recent call activity", moderationFeature = "Reset access, number, or device data", auditFeature = "Review sensitive admin actions", configuratorFeature = "Manage config.lua and media.lua through SQL" },
appearance = { eyebrow = "Appearance", title = "Accent color", body = "Change the accent across the complete admin workspace.", colors = { emerald = "Emerald", blue = "Blue", violet = "Violet", orange = "Orange", red = "Red" } },
configurator = { context = "Runtime configuration", eyebrow = "System tool", sections = "Configuration", search = "Search settings or paths", configScope = "config.lua", mediaScope = "media.lua", noResults = "No matching settings", loading = "Loading SQL configuration...", title = "Phone configurator", body = "Manage phone and media settings from the protected admin workspace.", disabledTitle = "SQL configuration is not active", disabledBody = "Enable the configurator at the beginning of config.lua and restart sky_phone. Until then, file values remain active and editing is locked.", manualSave = "Manual save", refreshNotice = "Nothing is written automatically. The green check verifies config.lua and media.lua in SQL and refreshes the active server, client, media and UI configuration immediately.", fieldCount = "{count} fields", secretConfigured = "Secret configured · enter a replacement", invalidValue = "Check the highlighted table or number value.", saved = "SQL configuration saved and applied.", descriptions = { featureToggle = "Turns {name} on or off.", boolean = "Controls whether {name} is allowed.", number = "Sets the numeric value for {name}.", text = "Sets the text value used for {name}.", optionalText = "Sets the optional value for {name}; switch it off to disable it.", list = "Manages all entries used for {name}.", table = "Groups the related settings for {name}.", credential = "Stores the protected credential used by {name}.", url = "Sets the URL or endpoint used by {name}.", hosts = "Defines which domains are allowed for {name}.", milliseconds = "Sets the timing for {name} in milliseconds.", seconds = "Sets the timing for {name} in seconds.", rateLimit = "Limits how many {name} actions are allowed per minute.", byteLimit = "Sets the maximum data size allowed for {name}.", textLimit = "Sets the maximum text length allowed for {name}.", distance = "Sets the world distance used for {name}.", coordinates = "Sets the world coordinates or orientation for {name}.", gameAsset = "Sets the GTA model or prop used for {name}.", animation = "Sets the animation asset used for {name}.", access = "Defines the jobs, groups or permission level for {name}.", integration = "Selects the connected framework or provider for {name}.", path = "Sets the storage or resource path used for {name}.", color = "Sets the interface color used for {name}.", displayText = "Sets the text shown to players for {name}.", phoneNumber = "Sets the phone or service number used for {name}.", routing = "Controls how incoming requests are routed for {name}.", command = "Sets the chat command used to open or run {name}.", locale = "Selects the language used for {name}.", debug = "Controls detailed diagnostic output for {name}.", mediaQuality = "Sets the media quality or volume used for {name}.", amount = "Sets the maximum or displayed amount for {name}." }, table = { list = "List", table = "Key table", vector = "Vector", entry = "Entry", general = "General", addRow = "Add row", addField = "Add field", remove = "Remove", emptyList = "No rows yet. Add the first row with plus.", emptyTable = "No fields yet. Add the first key below.", keyPlaceholder = "New key", convertToList = "Use list", convertToMap = "Use typed key table", convertToTable = "Use key table", types = { string = "Text", number = "Number", boolean = "Switch", list = "List", table = "Table" } } },
players = { eyebrow = "Active sessions", title = "Online players", online = "Online now", empty = "No players found", emptyBody = "Adjust the search or refresh the live player list." },
search = { players = "Search name, ID, job, or number", apps = "Search apps", clear = "Clear search" },
detail = { character = "Character profile", data = "Player data overview", cash = "Cash", bank = "Bank", job = "Job", duty = "Duty", onDuty = "On duty", offDuty = "Off duty", identity = "Identity", playerData = "Player data", identifier = "Character identifier", birthdate = "Birthdate", grade = "Job grade", unknown = "Unknown" },
devices = { eyebrow = "Device control", title = "Phones", body = "Inspect every phone assigned to the selected player.", choose = "Choose phone", empty = "No phone found", emptyBody = "This player currently has no phone device that can be managed.", noNumber = "No phone number", noSim = "No SIM", imei = "IMEI", updated = "Last activity", apps = "Claimed apps", account = "Linked account" },
credentials = { eyebrow = "Protected data", title = "Credentials", email = "iFruit email", password = "iFruit password", reveal = "Reveal", copy = "Copy password", copied = "Password copied.", noAccount = "No iFruit account is linked to this phone.", passcode = "Device passcode", passcodeHashed = "{length}-digit PIN · securely hashed and not recoverable", passcodeDisabled = "No passcode configured", revealTitle = "Reveal protected password?", revealBody = "This action is server-authorized, rate-limited, and written to the admin audit log.", cancel = "Cancel", confirmReveal = "Reveal password" },
apps = { eyebrow = "Remote management", title = "App access", description = "Stage app access for this device. Nothing changes until you save.", installed = "Installed", available = "Available", protected = "System app", changes = "{count} pending changes" },
activity = { protected = "Protected activity", messagesTitle = "Messages", messagesBody = "Recent SMS activity for the selected SIM.", callsTitle = "Calls", callsBody = "Recent call activity for the selected SIM.", loading = "Loading activity...", incoming = "Incoming", outgoing = "Outgoing", mediaMessage = "{type} message", noMessages = "No message activity found.", noCalls = "No call activity found.", status = { completed = "Completed", missed = "Missed", rejected = "Rejected", busy = "Busy", unanswered = "Unanswered", cancelled = "Cancelled", failed = "Failed", ringing = "Ringing" } },
moderation = {
eyebrow = "Device administration", title = "Moderation actions", body = "Every action is server-authorized, rate-limited, and audited.", resetPasscode = "Reset passcode", resetPasscodeBody = "Remove the device PIN and clear failed attempts.", changeNumber = "Change number", changeNumberBody = "Assign a new unique number to the current SIM.", factoryReset = "Factory reset", factoryResetBody = "Clear local device data and disconnect the account.", saveFirst = "Save or discard pending app changes first.", phoneNumber = "New phone number", phoneNumberPlaceholder = "Enter the full configured number", typeToConfirm = "Type {word} to confirm the factory reset.", confirmWord = "RESET", cancel = "Cancel", ["reset-passcodeSuccess"] = "Passcode reset.", ["change-numberSuccess"] = "Phone number changed.", ["factory-resetSuccess"] = "Phone factory reset completed.",
dialogs = { ["reset-passcodeTitle"] = "Reset device passcode?", ["reset-passcodeBody"] = "The player can unlock this phone without the previous PIN afterward.", ["change-numberTitle"] = "Change phone number?", ["change-numberBody"] = "The new number must match the configured server number format and be unique.", ["factory-resetTitle"] = "Factory reset this phone?", ["factory-resetBody"] = "This clears local device data, app settings, security, and the linked account. This cannot be undone." },
confirm = { ["reset-passcode"] = "Reset passcode", ["change-number"] = "Change number", ["factory-reset"] = "Factory reset" },
},
audit = { eyebrow = "Accountability", title = "Audit trail", body = "Sensitive reveals and remote app changes are recorded here.", empty = "No admin actions yet", emptyBody = "Protected actions will appear here after they are performed.", by = "{actor} · target ID {target}", actions = { grant_app = "App installed", revoke_app = "App removed", reveal_account_password = "Password revealed", view_messages = "Messages viewed", view_calls = "Calls viewed", reset_passcode = "Passcode reset", change_number = "Phone number changed", factory_reset = "Phone factory reset", save_configuration = "Configuration saved" } },
editor = { brand = "SKY PHONE", workspace = "ADMIN", players = "Player directory", audit = "Audit log", selectPlayer = "Select a player to inspect identity, devices, credentials, and app access.", save = "Save changes", saveHint = "Apply pending changes", saved = "Changes saved.", unsaved = "Unsaved changes", close = "Close admin panel", refresh = "Refresh live data", online = "LIVE", profile = "PROFILE", financial = "FINANCIAL", device = "DEVICE", security = "SECURITY", noAutoSave = "Manual save", noAutoSaveBody = "Changes stay local until the green check is pressed.", discardTitle = "Discard unsaved changes?", discardBody = "Your staged app or configuration changes have not been saved.", keepEditing = "Keep editing", discard = "Discard changes", saveFailed = "Some changes could not be saved.", noSelection = "No player selected" },
errors = { not_authorized = "You do not have access to the admin panel.", rate_limited = "Too many admin requests. Please wait.", player_unavailable = "That player is no longer online.", device_not_owned = "That phone no longer belongs to the selected player.", invalid_app = "That app is not registered on the server.", app_protected = "This system app cannot be removed.", revision_conflict = "The data changed in the meantime. Reopen the section and try again.", configurator_disabled = "Enable the phone configurator in config.lua first.", invalid_field = "That configuration field is no longer available.", invalid_value = "A configuration value is invalid.", account_not_found = "No iFruit account is linked to this phone.", invalid_phone_number = "Enter a phone number in the configured server format.", phone_number_unchanged = "This SIM already uses that phone number.", phone_number_taken = "That phone number is already assigned.", no_sim = "This phone has no SIM that can be changed.", passcode_not_set = "This phone has no passcode configured.", device_not_found = "This phone no longer exists.", metadata_unsupported = "The phone inventory metadata could not be updated.", invalid_request = "The admin request was invalid.", request_failed = "The admin request failed.", default = "The admin panel is temporarily unavailable." },
},
Apps = {
health = {
name = "Health",
@@ -2137,68 +2107,3 @@ Locales["en"] = {
},
},
}
Locales["en"].Nui.AdminPanel.configurator.table.subtabs = {
AdminGroups = "Admin Groups",
Dictionaries = "Dictionaries",
Clips = "Clips",
Transforms = "Transforms",
RateLimits = "Rate Limits",
Publishers = "Publishers",
ExternalPingResources = "External Ping Resources",
Markets = "Markets",
TrustedAdapters = "Trusted Adapters",
AllowedDisappearTimers = "Allowed Disappear Timers",
MusicTracks = "Music Tracks",
VehicleImages = "Vehicle Images",
Custom = "Custom",
Valet = "Valet",
AutoPriority = "Automatic Priority",
Camera = "Camera",
Categories = "Categories",
Districts = "Districts",
PhotoGradients = "Photo Gradients",
LbPhone = "LB Phone",
Tracks = "Tracks",
Props = "Props",
CustomLocations = "Custom Locations",
Animation = "Animation",
ReportReasons = "Report Reasons",
DisplayName = "Display Name",
Hud = "HUD",
Badge = "Badge",
LockedChannels = "Locked Channels",
NumberGroups = "Number Groups",
CustomFare = "Custom Fare",
DriverJobs = "Driver Jobs",
Services = "Services",
QuickLocations = "Quick Locations",
AllowedJobs = "Allowed Jobs",
Websites = "Websites",
}
Locales["en"].Nui.AdminPanel.appearance.title = "Interface"
Locales["en"].Nui.AdminPanel.appearance.body = "Personalize colors and typography across the admin workspace."
Locales["en"].Nui.AdminPanel.appearance.controls = {
customColor = "Custom color",
customColorBody = "Choose any RGB accent or enter its channel values.",
red = "R",
green = "G",
blue = "B",
hex = "HEX color",
value = "value",
fontFamily = "Font family",
fontFamilyBody = "Choose the typeface used by the complete admin panel.",
fontSize = "Font size",
fontSizeBody = "Scale text and controls for comfortable readability.",
fonts = {
inter = "Inter",
system = "System",
classic = "Classic",
verdana = "Verdana",
tahoma = "Tahoma",
trebuchet = "Trebuchet",
georgia = "Georgia",
mono = "Monospace",
},
}
+2 -3
View File
@@ -1,9 +1,8 @@
--[[
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.
This file is loaded on the server only. Keep API keys private and restart
sky_phone after changing providers, upload limits, or import sources.
]]
Config.Media = {
+1 -5
View File
@@ -32,9 +32,8 @@ client_scripts {
'config/config.lua',
'config/locales/en.lua',
'config/locales/de.lua',
'source/bridge/client/callbacks.lua',
'source/client/phone_configurator.lua',
'source/bridge/client/framework.lua',
'source/bridge/client/callbacks.lua',
'source/bridge/client/housing.lua',
'source/bridge/client/housing/*.lua',
'source/bridge/client/calls.lua',
@@ -81,9 +80,7 @@ server_scripts {
'source/server/update_check.lua',
'source/bridge/server/database.lua',
'source/bridge/server/migrations.lua',
'source/server/phone_configurator_schema.lua',
'source/bridge/server/callbacks.lua',
'source/server/phone_configurator.lua',
'source/bridge/server/framework.lua',
'source/bridge/server/frameworks/*.lua',
'source/bridge/server/housing.lua',
@@ -104,7 +101,6 @@ server_scripts {
'source/server/phone.lua',
'source/server/device_directory.lua',
'source/server/db_migrate.lua',
'source/server/admin.lua',
'source/server/lb_phone_migration.lua',
'source/server/custom_app_storage.lua',
'source/server/payphones.lua',
+3 -16
View File
@@ -21,24 +21,11 @@ local function resolve_framework()
return nil
end
local function refresh_framework()
local resolved = resolve_framework()
if resolved ~= "esx" and resolved ~= "qbox" and resolved ~= "qb" then
error("[sky_phone] No supported framework is running. Configure Config.Bridge.Framework.")
end
if resolved == framework_name then
return
end
framework_name = resolved
esx = nil
qb = nil
framework_name = resolve_framework()
if not framework_name then
error("[sky_phone] No supported framework is running. Configure Config.Bridge.Framework.")
end
refresh_framework()
AddEventHandler("sky_phone:configurator:updated", refresh_framework)
function Bridge.Framework.GetName()
return framework_name
end
+2 -17
View File
@@ -1,5 +1,4 @@
local provider
local configured_voice_provider = Config.Radio.VoiceProvider
local provider_resources = {
yaca = "yaca-voice",
pma = "pma-voice",
@@ -139,7 +138,8 @@ function Bridge.Radio.Join(primary, secondary)
return false
end
local function leave_provider(selected)
function Bridge.Radio.Leave()
local selected = resolve_provider()
if selected == "yaca" then
exports["yaca-voice"]:enableRadio(false)
elseif selected == "pma" then
@@ -151,10 +151,6 @@ local function leave_provider(selected)
end
end
function Bridge.Radio.Leave()
leave_provider(resolve_provider())
end
function Bridge.Radio.SetVolume(volume)
local selected = resolve_provider()
if selected == "yaca" then
@@ -169,17 +165,6 @@ function Bridge.Radio.SetVolume(volume)
end
end
AddEventHandler("sky_phone:configurator:updated", function()
if configured_voice_provider == Config.Radio.VoiceProvider then
return
end
leave_provider(provider)
provider = nil
configured_voice_provider = Config.Radio.VoiceProvider
TriggerEvent("sky_phone:client:radioProviderUpdated")
end)
AddEventHandler("onResourceStop", function(resource_name)
if resource_name == GetCurrentResourceName() then
Bridge.Radio.Leave()
@@ -111,8 +111,3 @@ end
AddEventHandler("onResourceStart", reset_provider)
AddEventHandler("onResourceStop", reset_provider)
AddEventHandler("sky_phone:configurator:serverUpdated", function()
selected_provider_name = nil
last_provider_state = nil
end)
-6
View File
@@ -384,12 +384,6 @@ reevaluate = function(force)
end)
end
AddEventHandler("sky_phone:configurator:updated", function()
animation_state.revision = animation_state.revision + 1
cleanup_phone()
reevaluate(true)
end)
local function reset_animation_state()
animation_state.phone_open = false
animation_state.call_state = nil
+9 -39
View File
@@ -1567,49 +1567,19 @@ SkyPhoneApps.SendCatalog = send_catalog
SkyPhoneApps.SetPhoneOpen = set_phone_open
SkyPhoneApps.RegisterClientHooks = register_bundled_client_hooks
local function refresh_bundled_apps()
local manifests = Config.CustomApps.Enabled and Config.CustomApps.BundledApps
and SkyPhoneApps.GetBundledManifests()
or {}
local desired_ids = {}
for index = 1, #manifests do
desired_ids[manifests[index].id] = true
end
local removals = {}
for app_id, app in pairs(apps_by_id) do
if app.catalog.bundled and not desired_ids[app_id] then
removals[#removals + 1] = app_id
if Config.CustomApps.Enabled and Config.CustomApps.BundledApps then
local bundled = SkyPhoneApps.GetBundledManifests()
for index = 1, #bundled do
local registered, register_error = register_app(normalize_bundled_manifest(bundled[index]))
if not registered then
error(("[sky_phone] Could not register bundled custom app '%s': %s"):format(
bundled[index].id,
register_error
))
end
end
for index = 1, #removals do
remove_registered_app(removals[index], true, false)
end
for index = 1, #manifests do
local manifest = manifests[index]
local existing = apps_by_id[manifest.id]
local normalized = normalize_bundled_manifest(manifest)
if existing and existing.catalog.bundled then
normalized.hooks = existing.hooks
apps_by_id[manifest.id] = normalized
elseif not existing then
local registered, register_error = register_app(normalized)
if not registered then
error(("[sky_phone] Could not register bundled custom app '%s': %s"):format(
manifest.id,
register_error
))
end
end
end
sync_catalog()
end
refresh_bundled_apps()
AddEventHandler("sky_phone:configurator:updated", refresh_bundled_apps)
SkyPhoneApps.CompatibilityCore = {
Add = add_compatibility_app,
AddServerAuthorized = add_server_authorized_compatibility_app,
+11 -47
View File
@@ -3,12 +3,19 @@ SkyPhoneFocus = {}
local blocked_phone_controls = { 24, 140, 141, 142, 257, 263, 264 }
local blocked_phone_look_controls = { 1, 2, 3, 4, 5, 6 }
local focused_control_groups = { 0, 1, 2 }
local hold_to_look_enabled = false
local hold_to_look_control
local hold_to_look_config = Config.Phone.HoldToLook
local hold_to_look_enabled = type(hold_to_look_config) == "table" and hold_to_look_config.Enabled == true
local hold_to_look_control = hold_to_look_enabled and tonumber(hold_to_look_config.Control) or nil
if hold_to_look_enabled and (
not hold_to_look_control
or hold_to_look_control ~= math.floor(hold_to_look_control)
or hold_to_look_control < 0
) then
error("[sky_phone] Config.Phone.HoldToLook.Control must be a non-negative whole control index.")
end
local state = {
admin_panel_open = false,
activity_suspended = false,
allow_movement = false,
allow_movement = Config.Phone.AllowMovement,
call_focus = false,
camera_active = false,
camera_nui_focused = true,
@@ -26,23 +33,6 @@ local block_game = false
local block_look = false
local game_input = false
local function refresh_focus_configuration()
local hold_to_look_config = Config.Phone.HoldToLook
local enabled = type(hold_to_look_config) == "table" and hold_to_look_config.Enabled == true
local control = enabled and tonumber(hold_to_look_config.Control) or nil
if enabled and (
not control
or control ~= math.floor(control)
or control < 0
) then
error("[sky_phone] Config.Phone.HoldToLook.Control must be a non-negative whole control index.")
end
hold_to_look_enabled = enabled
hold_to_look_control = control
state.allow_movement = Config.Phone.AllowMovement == true
end
local function allows_game_input(value)
local allowed = value.external_game_input
if allowed == nil then
@@ -51,13 +41,6 @@ local function allows_game_input(value)
return allowed == true
end
refresh_focus_configuration()
AddEventHandler("sky_phone:configurator:updated", function()
refresh_focus_configuration()
SkyPhoneFocus.Reapply()
end)
function SkyPhoneFocus.ApplyFocusedControls()
for _, group in ipairs(focused_control_groups) do
DisableAllControlActions(group)
@@ -78,15 +61,6 @@ function SkyPhoneFocus.ApplyGameInputControls(block_look)
end
function SkyPhoneFocus.Resolve(state)
if state.admin_panel_open then
return {
block_game = true,
cursor = true,
focused = true,
game_input = false,
keep_input = false,
}
end
if state.activity_suspended then
return { block_game = false, cursor = false, focused = false, game_input = false, keep_input = false }
end
@@ -168,15 +142,6 @@ function SkyPhoneFocus.SetPhone(open, cursor_disabled)
SkyPhoneFocus.Reapply()
end
function SkyPhoneFocus.SetAdminPanel(open)
state.admin_panel_open = open == true
if state.admin_panel_open then
state.notification_focus = false
state.text_input_focused = false
end
SkyPhoneFocus.Reapply()
end
function SkyPhoneFocus.SetCall(active)
state.call_focus = active == true
SkyPhoneFocus.Reapply()
@@ -210,7 +175,6 @@ function SkyPhoneFocus.SetExternalGameInput(owner_resource, allow_game_input)
end
function SkyPhoneFocus.Reset()
state.admin_panel_open = false
state.activity_suspended = false
state.call_focus = false
state.camera_active = false
-5
View File
@@ -48,11 +48,6 @@ local vehicle_mods = {
local phone_locale = SkyPhoneLocales.Resolve(Config.Bridge.Locale)
local garage_locale = phone_locale.Nui.Apps.garage
AddEventHandler("sky_phone:configurator:updated", function()
phone_locale = SkyPhoneLocales.Resolve(Config.Bridge.Locale)
garage_locale = phone_locale.Nui.Apps.garage
end)
local function vehicle_kind(model_hash, fallback)
if IsThisModelABoat(model_hash) then
return "boat"
+27 -186
View File
@@ -8,17 +8,6 @@ local equipped_phone_number = nil
local nui_generation = 0
local live_activity_active = false
local open_home_requested = false
local admin_panel_open = false
local suggested_admin_command = nil
local suggested_test_data_command = nil
local active_development_command = nil
local registered_development_commands = {}
local active_key_mapping_command = nil
local active_key_mapping_key = nil
local key_mapping_revision = 0
local refresh_development_command
local refresh_phone_key_mapping
local refresh_test_data_command_suggestion
local function get_equipped_phone_number()
if not device_payload or not device_payload.device.sim then
@@ -75,26 +64,6 @@ Bridge.Debug("debug", "[sky_phone] Client script initialized.", { always = true
local locale, locale_name = SkyPhoneLocales.Resolve(Config.Bridge.Locale)
local function send_admin_panel_open()
SendNUIMessage({
type = "admin:open",
data = {
lang = locale_name,
locales = locale.Nui,
fallbackLocales = Locales.en.Nui,
},
})
end
local function close_admin_panel()
if not admin_panel_open then
return
end
admin_panel_open = false
SkyPhoneFocus.SetAdminPanel(false)
SendNUIMessage({ type = "admin:close" })
end
local function send_open_message()
if not device_payload then
return
@@ -113,39 +82,6 @@ local function send_open_message()
open_home_requested = false
end
local function refresh_admin_command_suggestion()
if suggested_admin_command then
TriggerEvent("chat:removeSuggestion", "/" .. suggested_admin_command)
suggested_admin_command = nil
end
if Config.AdminPanel.Enabled then
suggested_admin_command = Config.AdminPanel.Command
TriggerEvent(
"chat:addSuggestion",
"/" .. suggested_admin_command,
locale.AdminCommand.CommandDescription
)
end
end
AddEventHandler("sky_phone:configurator:updated", function()
locale, locale_name = SkyPhoneLocales.Resolve(Config.Bridge.Locale)
refresh_development_command()
refresh_phone_key_mapping()
refresh_test_data_command_suggestion()
refresh_admin_command_suggestion()
SkyPhoneApps.SendCatalog()
if is_open and device_payload then
device_payload.lang = locale_name
device_payload.locales = locale.Nui
device_payload.fallbackLocales = Locales.en.Nui
SendNUIMessage({ type = "device:updated", data = device_payload })
end
if admin_panel_open then
send_admin_panel_open()
end
end)
local function open_phone()
if is_open or not device_payload then
return
@@ -216,46 +152,18 @@ AddEventHandler("sky_phone:client:forceClose", function()
close_phone()
end)
local function run_development_command()
if is_open or open_requested then
close_phone()
return
end
open_without_focus = false
Bridge.Callbacks.Trigger("sky_phone:device:development-open", {})
if Config.Phone.DevelopmentCommand then
RegisterCommand(Config.Command, function()
if is_open or open_requested then
close_phone()
return
end
open_without_focus = false
Bridge.Callbacks.Trigger("sky_phone:device:development-open", {})
end, false)
end
refresh_development_command = function()
local command_name = Config.Phone.DevelopmentCommand and Config.Command or nil
if command_name ~= nil and (type(command_name) ~= "string" or command_name == "") then
error("[sky_phone] Config.Command must be a non-empty command name.")
end
if active_development_command then
TriggerEvent("chat:removeSuggestion", "/" .. active_development_command)
end
active_development_command = command_name
if not command_name then
return
end
if not registered_development_commands[command_name] then
registered_development_commands[command_name] = true
RegisterCommand(command_name, function()
if active_development_command == command_name and Config.Phone.DevelopmentCommand then
run_development_command()
end
end, false)
end
TriggerEvent("chat:addSuggestion", "/" .. command_name, locale.CommandDescription)
end
refresh_development_command()
local function run_phone_toggle()
RegisterCommand("sky_phone_toggle", function()
if is_open or open_requested then
close_phone()
return
@@ -263,7 +171,7 @@ local function run_phone_toggle()
open_without_focus = false
Bridge.Callbacks.Trigger("sky_phone:device:open-request", {})
end
end, false)
RegisterCommand("sky_phone_live_activity_open", function()
if not live_activity_active or is_open or open_requested then
@@ -277,36 +185,6 @@ RegisterCommand("sky_phone_live_activity_open", function()
end
end, false)
RegisterCommand("sky_phone_toggle", run_phone_toggle, false)
refresh_phone_key_mapping = function()
local key_name = Config.Phone.Keybind
if key_name ~= false and key_name ~= nil and (type(key_name) ~= "string" or key_name == "") then
error("[sky_phone] Config.Phone.Keybind must be a non-empty keyboard key name or false.")
end
if key_name == active_key_mapping_key then
return
end
active_key_mapping_key = key_name
active_key_mapping_command = nil
if not key_name then
return
end
key_mapping_revision = key_mapping_revision + 1
local command_name = "sky_phone_toggle_config_" .. key_mapping_revision
active_key_mapping_command = command_name
RegisterCommand(command_name, function()
if active_key_mapping_command == command_name then
run_phone_toggle()
end
end, false)
RegisterKeyMapping(command_name, locale.Controls.OpenPhone, "keyboard", key_name)
end
refresh_phone_key_mapping()
RegisterKeyMapping(
"sky_phone_live_activity_open",
locale.Controls.OpenPhone,
@@ -314,18 +192,13 @@ RegisterKeyMapping(
"SPACE"
)
refresh_test_data_command_suggestion = function()
if suggested_test_data_command then
TriggerEvent("chat:removeSuggestion", "/" .. suggested_test_data_command)
suggested_test_data_command = nil
if Config.Phone.Keybind then
if type(Config.Phone.Keybind) ~= "string" or Config.Phone.Keybind == "" then
error("[sky_phone] Config.Phone.Keybind must be a non-empty keyboard key name or false.")
end
if Config.TestData.Enabled then
suggested_test_data_command = Config.TestData.Command
TriggerEvent("chat:addSuggestion", "/" .. suggested_test_data_command, locale.TestData.CommandDescription)
end
end
refresh_test_data_command_suggestion()
RegisterKeyMapping("sky_phone_toggle", locale.Controls.OpenPhone, "keyboard", Config.Phone.Keybind)
end
RegisterNetEvent("sky_phone:testdata:feedback", function(success, detail)
local test_data_locale = locale.TestData
@@ -351,9 +224,6 @@ RegisterNUICallback("ui:ready", function(data, cb)
if open_requested and device_payload then
send_open_message()
end
if admin_panel_open then
send_admin_panel_open()
end
SkyPhoneCalls.ReplayNui()
SkyPhoneSimPicker.ReplayNui()
SkyPhoneFocus.Reapply()
@@ -399,42 +269,12 @@ RegisterNUICallback("ui:opened", function(data, cb)
cb({ success = true })
end)
RegisterNetEvent("sky_phone:admin:launch", function()
if is_open or open_requested then
close_phone()
end
admin_panel_open = true
SkyPhoneFocus.SetAdminPanel(true)
send_admin_panel_open()
end)
RegisterNetEvent("sky_phone:admin:command-error", function(error_code)
local messages = locale.AdminCommand.Errors
Bridge.Framework.Notify(
"iFruit",
messages[error_code] or messages.default,
"error",
5000
)
end)
RegisterNUICallback("admin:close", function(data, cb)
if type(data) ~= "table" then
cb({ success = false, error = "invalid_request" })
return
end
close_admin_panel()
cb({ success = true })
end)
RegisterNUICallback("ui:input-focus", function(data, cb)
if type(data) ~= "table" or type(data.active) ~= "boolean" then
cb({ success = false, error = "invalid_request" })
return
end
SkyPhoneFocus.SetTextInputFocused(
data.active and (is_open or open_requested or admin_panel_open)
)
SkyPhoneFocus.SetTextInputFocused(data.active and (is_open or open_requested))
cb({ success = true })
end)
@@ -513,7 +353,12 @@ RegisterNetEvent("sky_phone:device:error", function(error_code)
end)
CreateThread(function()
refresh_admin_command_suggestion()
if Config.Phone.DevelopmentCommand then
TriggerEvent("chat:addSuggestion", "/" .. Config.Command, locale.CommandDescription)
end
if Config.TestData.Enabled then
TriggerEvent("chat:addSuggestion", "/" .. Config.TestData.Command, locale.TestData.CommandDescription)
end
end)
AddEventHandler("onResourceStop", function(resource_name)
@@ -524,20 +369,16 @@ AddEventHandler("onResourceStop", function(resource_name)
is_open = false
open_requested = false
open_without_focus = false
admin_panel_open = false
TriggerEvent("sky_phone:animation:reset")
SkyPhoneCalls.Reset()
SkyPhoneSimPicker.Reset()
SkyPhoneFocus.Reset()
if active_development_command then
TriggerEvent("chat:removeSuggestion", "/" .. active_development_command)
if Config.Phone.DevelopmentCommand then
TriggerEvent("chat:removeSuggestion", "/" .. Config.Command)
end
if suggested_test_data_command then
TriggerEvent("chat:removeSuggestion", "/" .. suggested_test_data_command)
end
if suggested_admin_command then
TriggerEvent("chat:removeSuggestion", "/" .. suggested_admin_command)
if Config.TestData.Enabled then
TriggerEvent("chat:removeSuggestion", "/" .. Config.TestData.Command)
end
end)
@@ -159,11 +159,6 @@ end)
local phone_locale = SkyPhoneLocales.Resolve(Config.Bridge.Locale)
local app_locales = phone_locale.Nui.Apps
AddEventHandler("sky_phone:configurator:updated", function()
phone_locale = SkyPhoneLocales.Resolve(Config.Bridge.Locale)
app_locales = phone_locale.Nui.Apps
end)
RegisterNetEvent("sky_phone:mail:new", function(data)
local locale = app_locales.mail
data.title = locale.name
@@ -1,9 +1,5 @@
local callback_groups = {
account = [[login register logout devices remove-device]],
admin = [[
bootstrap player save-apps reveal-password activity
reset-passcode change-number factory-reset configurator save-configurator
]],
banking = [[overview transfer]],
billing = [[overview list detail markRead pay dispute]],
calendar = [[list create update delete]],
+8 -26
View File
@@ -21,16 +21,10 @@ local remote_visuals = {}
local custom_payphone_props = {}
local configured_models = {}
local function refresh_configured_models()
configured_models = {}
for _, model_name in ipairs(Config.Payphones.Props or {}) do
configured_models[joaat(model_name)] = model_name
end
for _, model_name in ipairs(Config.Payphones.Props or {}) do
configured_models[joaat(model_name)] = model_name
end
refresh_configured_models()
local function valid_visual_number(value, maximum)
local number = tonumber(value)
if not number or number ~= number or math.abs(number) > maximum then
@@ -162,23 +156,6 @@ end
CreateThread(spawn_custom_payphones)
local function clear_custom_payphones()
for _, entity in ipairs(custom_payphone_props) do
if DoesEntityExist(entity) then
SetEntityAsMissionEntity(entity, true, true)
DeleteEntity(entity)
end
end
custom_payphone_props = {}
end
AddEventHandler("sky_phone:configurator:updated", function()
locale = SkyPhoneLocales.Resolve(Config.Bridge.Locale)
refresh_configured_models()
clear_custom_payphones()
CreateThread(spawn_custom_payphones)
end)
local function load_animation(dictionary)
if HasAnimDictLoaded(dictionary) then
return true
@@ -749,5 +726,10 @@ AddEventHandler("onResourceStop", function(resource_name)
for _, id in ipairs(visual_ids) do
restore_remote_visual(id)
end
clear_custom_payphones()
for _, entity in ipairs(custom_payphone_props) do
if DoesEntityExist(entity) then
SetEntityAsMissionEntity(entity, true, true)
DeleteEntity(entity)
end
end
end)
@@ -1,83 +0,0 @@
local function deserialize_value(value)
if type(value) ~= "table" then
return value
end
if value.__skyType == "vector2" then
return vector2(tonumber(value.x) or 0.0, tonumber(value.y) or 0.0)
end
if value.__skyType == "vector3" then
return vector3(tonumber(value.x) or 0.0, tonumber(value.y) or 0.0, tonumber(value.z) or 0.0)
end
if value.__skyType == "vector4" then
return vector4(
tonumber(value.x) or 0.0,
tonumber(value.y) or 0.0,
tonumber(value.z) or 0.0,
tonumber(value.w) or 0.0
)
end
if value.__skyType == "map" then
local decoded = {}
for _, entry in ipairs(value.entries or {}) do
local key = entry.keyType == "number" and tonumber(entry.key) or entry.key
decoded[key] = deserialize_value(entry.value)
end
return decoded
end
local decoded = {}
for key, child in pairs(value) do
decoded[key] = deserialize_value(child)
end
return decoded
end
local function apply_runtime_table(current, replacement)
for key in pairs(current) do
if replacement[key] == nil then
current[key] = nil
end
end
for key, value in pairs(replacement) do
local current_value = current[key]
if type(current_value) == "table" and type(value) == "table" then
apply_runtime_table(current_value, value)
else
current[key] = value
end
end
end
local function apply_runtime_config(payload)
if type(payload) ~= "table" or payload.enabled ~= true then
return
end
if type(payload.config) ~= "table" then
error("[sky_phone] Phone configurator received an invalid client configuration payload.")
end
local runtime_config = deserialize_value(payload.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
end
end
TriggerEvent("sky_phone:configurator:updated", tonumber(payload.revision) or 0)
end
RegisterNetEvent("sky_phone:configurator:sync", function(payload)
apply_runtime_config(payload)
end)
local response = Bridge.Callbacks.Trigger("sky_phone:configurator:runtime", {})
if not response or not response.success or type(response.data) ~= "table" then
if Config.PhoneConfigurator.Enabled then
error("[sky_phone] Phone configurator failed to load the client runtime configuration.")
end
return
end
apply_runtime_config(response.data)
-12
View File
@@ -44,18 +44,6 @@ local CLIENT_CAPABILITIES = {
side = "client",
}
local function refresh_configured_capabilities()
local enabled = Config.CustomApps.Enabled == true
local external = enabled and Config.CustomApps.ExternalApps == true
CLIENT_CAPABILITIES.features.customApps.enabled = enabled
CLIENT_CAPABILITIES.features.customApps.external = external
CLIENT_CAPABILITIES.features.notifications.customApps = external
end
refresh_configured_capabilities()
AddEventHandler("sky_phone:configurator:updated", refresh_configured_capabilities)
local custom_app_api = SkyPhoneApps.ClientPublicApi
if type(custom_app_api) ~= "table" then
error("[sky_phone] Client public API initialized before the custom app API.")
-19
View File
@@ -29,10 +29,6 @@ local function send_hud_config()
SendNUIMessage({ type = "radio:hud-config", data = get_hud_config() })
end
AddEventHandler("sky_phone:configurator:updated", function()
send_hud_config()
end)
local function send_hud_members()
local combined = {}
for channel_id = 1, 2 do
@@ -160,21 +156,6 @@ local function leave_radio()
return request("disconnect")
end
AddEventHandler("sky_phone:client:radioProviderUpdated", function()
if current_primary <= 0 then
return
end
if not Bridge.Radio.Join(current_primary, current_secondary) then
request("disconnect")
current_primary = 0
current_secondary = 0
clear_hud_members()
return
end
Bridge.Radio.SetVolume(current_volume)
end)
RegisterNUICallback("radio:get", function(data, cb)
if type(data) ~= "table" then
cb({ success = false, error = "invalid_request" })
-930
View File
@@ -1,930 +0,0 @@
Bridge.Database.AfterMigration("sky_phone", function()
local BUILTIN_APPS = {
banking = true,
billing = true,
calculator = true,
calendar = true,
camera = true,
citymarkt = true,
citywarn = true,
clock = true,
companies = true,
crewlink = true,
crypto = true,
darkchat = true,
feather = true,
flare = true,
fliptok = true,
garage = true,
health = true,
house = true,
["app-store"] = true,
["local-pages"] = true,
mail = true,
map = true,
memos = true,
memory = true,
messages = true,
minesweeper = true,
music = true,
["neon-drop"] = true,
notes = true,
["number-merge"] = true,
phone = true,
photos = true,
picstagram = true,
radio = true,
settings = true,
["sky-flappy"] = true,
skyride = true,
snake = true,
["tower-stack"] = true,
weather = true,
["weazel-news"] = true,
}
local DEFAULT_INSTALLED_APPS = {
["app-store"] = true,
calculator = true,
calendar = true,
camera = true,
citywarn = true,
clock = true,
health = true,
mail = true,
map = true,
memos = true,
messages = true,
notes = true,
phone = true,
photos = true,
settings = true,
weather = true,
}
local PROTECTED_APPS = {
["app-store"] = true,
camera = true,
citywarn = true,
health = true,
mail = true,
messages = true,
phone = true,
photos = true,
settings = true,
}
local function affected_rows(result)
if type(result) == "number" then
return result
end
return type(result) == "table" and tonumber(result.affectedRows) or 0
end
local function trim(value)
if type(value) ~= "string" then
return ""
end
return value:match("^%s*(.-)%s*$")
end
local function player_name(source)
local first_name = trim(Bridge.Framework.GetFirstname(source))
local last_name = trim(Bridge.Framework.GetLastname(source))
local character_name = trim((first_name .. " " .. last_name))
return character_name ~= "" and character_name or GetPlayerName(source) or ("Player %s"):format(source)
end
local function require_admin(source, operation, maximum)
if not Config.AdminPanel.Enabled
or not Bridge.Framework.HasAdminGroup(source, Config.AdminPanel.AdminGroups)
then
Bridge.Debug("warn", "[sky_phone] Rejected admin panel access from source %s.", tostring(source))
return nil, { success = false, error = "not_authorized" }
end
if not SkyPhone.AllowOperation(source, "admin_" .. operation, maximum, 60) then
return nil, { success = false, error = "rate_limited" }
end
return true
end
local active_admin_command
local function run_admin_command(command_source)
local player_source = tonumber(command_source)
if not player_source or player_source < 1 then
Bridge.Debug("warn", "[sky_phone] The admin panel command can only be used by a player.")
return
end
if not Config.AdminPanel.Enabled then
TriggerClientEvent("sky_phone:admin:command-error", player_source, "disabled")
return
end
if not Bridge.Framework.HasAdminGroup(player_source, Config.AdminPanel.AdminGroups) then
Bridge.Debug(
"warn",
"[sky_phone] Rejected admin panel command from source %s.",
tostring(player_source)
)
TriggerClientEvent("sky_phone:admin:command-error", player_source, "not_authorized")
return
end
TriggerClientEvent("sky_phone:admin:launch", player_source)
end
local function register_admin_command()
local command_name = Config.AdminPanel.Command
if type(command_name) ~= "string" or command_name == "" then
error("[sky_phone] Config.AdminPanel.Command must be a non-empty command name.")
end
if command_name == active_admin_command then
return
end
active_admin_command = command_name
RegisterCommand(command_name, function(command_source)
if active_admin_command == command_name then
run_admin_command(command_source)
end
end, false)
end
register_admin_command()
AddEventHandler("sky_phone:configurator:serverUpdated", function()
register_admin_command()
end)
local function normalize_source(value)
local player_source = tonumber(value)
if not player_source or player_source < 1 or player_source ~= math.floor(player_source) then
return nil
end
if not Bridge.Framework.GetIdentifier(player_source) then
return nil
end
return player_source
end
local function collect_device_imeis(source, identifier)
local imeis = {}
local seen = {}
local function add_imei(imei)
if SkyPhoneImei.IsValid(imei) and not seen[imei] then
seen[imei] = true
imeis[#imeis + 1] = imei
end
end
for _, item in ipairs(Bridge.Inventory.GetSlotsWithItem(source, Config.Phone.Item)) do
add_imei(item.metadata and item.metadata.imei)
end
local rows = Bridge.Database.Query([[
SELECT `device_imei` AS `imei`
FROM `sky_phone_character_devices`
WHERE `owner_identifier` = ?
UNION
SELECT device.`imei`
FROM `sky_phone_devices` device
JOIN `sky_phone_sims` sim ON sim.`id` = device.`sim_id`
WHERE sim.`owner_identifier` = ?
]], { identifier, identifier })
for _, row in ipairs(rows) do
add_imei(row.imei)
end
table.sort(imeis)
return imeis
end
local function normalize_app_ids(value)
local normalized = {}
local seen = {}
if type(value) ~= "table" then
return normalized
end
for index = 1, #value do
local app_id = value[index]
if type(app_id) == "string"
and #app_id > 0
and #app_id <= 64
and not seen[app_id]
then
seen[app_id] = true
normalized[#normalized + 1] = app_id
end
end
return normalized
end
local function load_app_payload(encoded)
if type(encoded) ~= "string" or encoded == "" then
return {}, {}, {}
end
local payload = json.decode(encoded)
if type(payload) ~= "table" then
error("[sky_phone] Stored admin target app payload is not a JSON object.")
end
return payload, normalize_app_ids(payload.claimedApps), normalize_app_ids(payload.uninstalledApps)
end
local function load_player_devices(source, identifier)
local imeis = collect_device_imeis(source, identifier)
if #imeis == 0 then
return {}
end
local placeholders = {}
for index = 1, #imeis do
placeholders[index] = "?"
end
local rows = Bridge.Database.Query(([[
SELECT device.`imei`, device.`device_name`, device.`created_at`, device.`updated_at`,
sim.`phone_number`, sim.`sim_type`, sim.`registered_at`,
account.`id` AS `account_id`, account.`email` AS `account_email`,
security.`passcode_length`, security.`failed_attempts`, security.`locked_until`,
app_data.`payload` AS `apps_payload`, app_data.`revision` AS `apps_revision`
FROM `sky_phone_devices` device
LEFT JOIN `sky_phone_sims` sim ON sim.`id` = device.`sim_id`
LEFT JOIN `sky_phone_accounts` account ON account.`id` = device.`account_id`
LEFT JOIN `sky_phone_device_security` security ON security.`device_imei` = device.`imei`
LEFT JOIN `sky_phone_device_data` app_data
ON app_data.`device_imei` = device.`imei` AND app_data.`namespace` = 'apps'
WHERE device.`imei` IN (%s)
ORDER BY device.`updated_at` DESC, device.`imei` ASC
]]):format(table.concat(placeholders, ", ")), imeis)
local devices = {}
for _, row in ipairs(rows) do
local _, claimed_apps, uninstalled_apps = load_app_payload(row.apps_payload)
devices[#devices + 1] = {
imei = row.imei,
name = row.device_name,
createdAt = row.created_at,
updatedAt = row.updated_at,
number = row.phone_number,
simType = row.sim_type,
simRegistered = row.registered_at ~= nil,
account = row.account_id and {
id = tonumber(row.account_id),
email = row.account_email,
passwordAvailable = true,
} or nil,
security = {
enabled = row.passcode_length ~= nil,
length = row.passcode_length and tonumber(row.passcode_length) or nil,
failedAttempts = tonumber(row.failed_attempts) or 0,
lockedUntil = tonumber(row.locked_until) or 0,
},
apps = {
claimed = claimed_apps,
uninstalled = uninstalled_apps,
revision = tonumber(row.apps_revision) or 0,
},
}
end
return devices
end
local function build_player_summary(source)
local identifier = Bridge.Framework.GetIdentifier(source)
local devices = load_player_devices(source, identifier)
local job = Bridge.Framework.GetJob(source)
return {
source = source,
identifier = identifier,
name = player_name(source),
serverName = GetPlayerName(source) or "",
job = job.label ~= "" and job.label or job.name,
grade = job.grade,
onDuty = job.onDuty,
deviceCount = #devices,
phoneNumber = devices[1] and devices[1].number or nil,
}
end
local function list_players()
local sources = Bridge.Framework.GetPlayers()
table.sort(sources, function(left, right)
return tonumber(left) < tonumber(right)
end)
local players = {}
local maximum = math.max(1, math.floor(tonumber(Config.AdminPanel.MaximumPlayers) or 128))
for index = 1, math.min(#sources, maximum) do
local player_source = tonumber(sources[index])
if player_source and Bridge.Framework.GetIdentifier(player_source) then
players[#players + 1] = build_player_summary(player_source)
end
end
return players
end
local function load_player_detail(source)
local identifier = Bridge.Framework.GetIdentifier(source)
local job = Bridge.Framework.GetJob(source)
return {
source = source,
identifier = identifier,
name = player_name(source),
serverName = GetPlayerName(source) or "",
firstName = trim(Bridge.Framework.GetFirstname(source)),
lastName = trim(Bridge.Framework.GetLastname(source)),
birthdate = trim(Bridge.Framework.GetBirthdate(source)),
job = {
name = job.name,
label = job.label,
grade = job.grade,
gradeLabel = job.gradeLabel,
onDuty = job.onDuty,
},
money = {
bank = tonumber(Bridge.Framework.GetMoney(source, "bank")) or 0,
cash = tonumber(Bridge.Framework.GetMoney(source, "cash")) or 0,
currency = Config.Banking.Currency,
},
devices = load_player_devices(source, identifier),
}
end
local function find_owned_device(source, imei)
local identifier = Bridge.Framework.GetIdentifier(source)
for _, device in ipairs(load_player_devices(source, identifier)) do
if device.imei == imei then
return device, identifier
end
end
return nil, identifier
end
local function load_device_sim(imei)
local rows = Bridge.Database.Query([[
SELECT sim.`id`, sim.`phone_number`
FROM `sky_phone_devices` device
JOIN `sky_phone_sims` sim ON sim.`id` = device.`sim_id`
WHERE device.`imei` = ?
LIMIT 1
]], { imei })
return rows[1]
end
local function app_metadata(app_id)
if BUILTIN_APPS[app_id] then
return {
defaultInstalled = DEFAULT_INSTALLED_APPS[app_id] == true,
removable = PROTECTED_APPS[app_id] ~= true,
}
end
if SkyPhoneApps.GetPolicy(app_id) then
return { defaultInstalled = false, removable = true }
end
return nil
end
local function remove_app_id(values, app_id)
local next_values = {}
for index = 1, #values do
if values[index] ~= app_id then
next_values[#next_values + 1] = values[index]
end
end
return next_values
end
local function add_app_id(values, app_id)
for index = 1, #values do
if values[index] == app_id then
return values
end
end
values[#values + 1] = app_id
return values
end
local function write_audit(actor_source, target_source, target_identifier, imei, action, details)
local actor_identifier = Bridge.Framework.GetIdentifier(actor_source)
local result = Bridge.Database.Query([[
INSERT INTO `sky_phone_admin_audit`
(`actor_identifier`, `actor_name`, `target_identifier`, `target_source`, `device_imei`, `action`, `details`)
VALUES (?, ?, ?, ?, ?, ?, ?)
]], {
actor_identifier,
player_name(actor_source),
target_identifier,
target_source,
imei,
action,
json.encode(details or {}),
})
if affected_rows(result) ~= 1 then
error("[sky_phone] Admin audit insert did not affect exactly one row.")
end
end
local function load_audit()
local limit = math.max(1, math.min(100, math.floor(tonumber(Config.AdminPanel.AuditLimit) or 40)))
local rows = Bridge.Database.Query(([[
SELECT `id`, `actor_name`, `target_identifier`, `target_source`, `device_imei`,
`action`, `details`, `created_at`
FROM `sky_phone_admin_audit`
ORDER BY `id` DESC
LIMIT %s
]]):format(limit), {})
local audit = {}
for _, row in ipairs(rows) do
audit[#audit + 1] = {
id = tonumber(row.id),
actorName = row.actor_name,
targetIdentifier = row.target_identifier,
targetSource = row.target_source and tonumber(row.target_source) or nil,
deviceImei = row.device_imei,
action = row.action,
details = json.decode(row.details),
createdAt = row.created_at,
}
end
return audit
end
Bridge.Callbacks.Register("sky_phone:admin:bootstrap", function(source)
local authorized, error_response = require_admin(
source,
"bootstrap",
Config.AdminPanel.ReadRequestsPerMinute
)
if not authorized then
return error_response
end
local players = list_players()
local totals = Bridge.Database.Query([[
SELECT
(SELECT COUNT(*) FROM `sky_phone_devices`) AS `devices`,
(SELECT COUNT(*) FROM `sky_phone_accounts`) AS `accounts`
]], {})
return {
success = true,
data = {
players = players,
stats = {
online = #players,
devices = tonumber(totals[1] and totals[1].devices) or 0,
accounts = tonumber(totals[1] and totals[1].accounts) or 0,
},
audit = load_audit(),
},
}
end)
Bridge.Callbacks.Register("sky_phone:admin:player", function(source, data)
local authorized, error_response = require_admin(
source,
"player",
Config.AdminPanel.ReadRequestsPerMinute
)
if not authorized then
return error_response
end
local target_source = normalize_source(data and data.source)
if not target_source then
return { success = false, error = "player_unavailable" }
end
return { success = true, data = load_player_detail(target_source) }
end)
Bridge.Callbacks.Register("sky_phone:admin:save-apps", function(source, data)
local authorized, error_response = require_admin(
source,
"save_apps",
Config.AdminPanel.ActionRequestsPerMinute
)
if not authorized then
return error_response
end
if type(data) ~= "table"
or not SkyPhoneImei.IsValid(data.imei)
or type(data.revision) ~= "number"
or data.revision < 0
or data.revision ~= math.floor(data.revision)
or type(data.changes) ~= "table"
or #data.changes < 1
or #data.changes > 128
then
return { success = false, error = "invalid_request" }
end
local target_source = normalize_source(data.source)
if not target_source then
return { success = false, error = "player_unavailable" }
end
local target_device, target_identifier = find_owned_device(target_source, data.imei)
if not target_device then
return { success = false, error = "device_not_owned" }
end
local normalized_changes = {}
local seen_apps = {}
for index = 1, #data.changes do
local change = data.changes[index]
if type(change) ~= "table"
or type(change.appId) ~= "string"
or #change.appId < 1
or #change.appId > 64
or type(change.installed) ~= "boolean"
or seen_apps[change.appId]
then
return { success = false, error = "invalid_request" }
end
local metadata = app_metadata(change.appId)
if not metadata then
return { success = false, error = "invalid_app" }
end
if not change.installed and not metadata.removable then
return { success = false, error = "app_protected" }
end
seen_apps[change.appId] = true
normalized_changes[#normalized_changes + 1] = {
appId = change.appId,
installed = change.installed,
metadata = metadata,
}
end
local rows = Bridge.Database.Query([[
SELECT `payload`, `revision`
FROM `sky_phone_device_data`
WHERE `device_imei` = ? AND `namespace` = 'apps'
LIMIT 1
]], { data.imei })
local current_revision = tonumber(rows[1] and rows[1].revision) or 0
if current_revision ~= data.revision then
return { success = false, error = "revision_conflict" }
end
local payload, claimed_apps, uninstalled_apps = load_app_payload(rows[1] and rows[1].payload)
for index = 1, #normalized_changes do
local change = normalized_changes[index]
if change.installed then
uninstalled_apps = remove_app_id(uninstalled_apps, change.appId)
if not change.metadata.defaultInstalled then
claimed_apps = add_app_id(claimed_apps, change.appId)
end
else
claimed_apps = remove_app_id(claimed_apps, change.appId)
uninstalled_apps = add_app_id(uninstalled_apps, change.appId)
end
end
payload.claimedApps = claimed_apps
payload.uninstalledApps = #uninstalled_apps > 0 and uninstalled_apps or nil
local encoded = json.encode(payload)
if #encoded > 100000 then
return { success = false, error = "payload_too_large" }
end
if rows[1] then
local result = Bridge.Database.Query([[
UPDATE `sky_phone_device_data`
SET `payload` = ?, `revision` = `revision` + 1
WHERE `device_imei` = ? AND `namespace` = 'apps' AND `revision` = ?
]], { encoded, data.imei, data.revision })
if affected_rows(result) ~= 1 then
return { success = false, error = "revision_conflict" }
end
else
local result = Bridge.Database.Query([[
INSERT IGNORE INTO `sky_phone_device_data` (`device_imei`, `namespace`, `payload`)
VALUES (?, 'apps', ?)
]], { data.imei, encoded })
if affected_rows(result) ~= 1 then
return { success = false, error = "revision_conflict" }
end
end
for index = 1, #normalized_changes do
local change = normalized_changes[index]
write_audit(
source,
target_source,
target_identifier,
data.imei,
change.installed and "grant_app" or "revoke_app",
{ appId = change.appId }
)
end
SkyPhone.RefreshDevice(data.imei)
return { success = true, data = load_player_detail(target_source) }
end)
Bridge.Callbacks.Register("sky_phone:admin:reveal-password", function(source, data)
local authorized, error_response = require_admin(
source,
"reveal_password",
Config.AdminPanel.CredentialRevealsPerMinute
)
if not authorized then
return error_response
end
if type(data) ~= "table" or not SkyPhoneImei.IsValid(data.imei) then
return { success = false, error = "invalid_request" }
end
local target_source = normalize_source(data.source)
if not target_source then
return { success = false, error = "player_unavailable" }
end
local device, target_identifier = find_owned_device(target_source, data.imei)
if not device then
return { success = false, error = "device_not_owned" }
end
local accounts = Bridge.Database.Query([[
SELECT account.`email`, account.`password`
FROM `sky_phone_devices` device
JOIN `sky_phone_accounts` account ON account.`id` = device.`account_id`
WHERE device.`imei` = ?
LIMIT 1
]], { data.imei })
if not accounts[1] then
return { success = false, error = "account_not_found" }
end
write_audit(
source,
target_source,
target_identifier,
data.imei,
"reveal_account_password",
{ email = accounts[1].email }
)
return {
success = true,
data = {
email = accounts[1].email,
password = accounts[1].password,
},
}
end)
Bridge.Callbacks.Register("sky_phone:admin:activity", function(source, data)
local authorized, error_response = require_admin(
source,
"activity",
Config.AdminPanel.ReadRequestsPerMinute
)
if not authorized then
return error_response
end
if type(data) ~= "table"
or not SkyPhoneImei.IsValid(data.imei)
or (data.kind ~= "messages" and data.kind ~= "calls")
then
return { success = false, error = "invalid_request" }
end
local target_source = normalize_source(data.source)
if not target_source then
return { success = false, error = "player_unavailable" }
end
local device, target_identifier = find_owned_device(target_source, data.imei)
if not device then
return { success = false, error = "device_not_owned" }
end
local audit_action = data.kind == "messages" and "view_messages" or "view_calls"
local sim = load_device_sim(data.imei)
if not sim then
write_audit(
source,
target_source,
target_identifier,
data.imei,
audit_action,
{ count = 0 }
)
return { success = true, data = { kind = data.kind, entries = {} } }
end
local limit = math.max(1, math.min(100, math.floor(tonumber(Config.AdminPanel.ActivityLimit) or 40)))
local entries = {}
if data.kind == "messages" then
local rows = Bridge.Database.Query(([[
SELECT `id`, `sender_sim_id`, `sender_number`, `recipient_number`, `message_type`,
`body`, `read_at`, `created_at`
FROM `sky_phone_sms_messages`
WHERE `sender_sim_id` = ? OR `recipient_sim_id` = ?
ORDER BY `created_at` DESC
LIMIT %s
]]):format(limit), { sim.id, sim.id })
for _, row in ipairs(rows) do
local outgoing = row.sender_sim_id == sim.id
entries[#entries + 1] = {
id = row.id,
direction = outgoing and "outgoing" or "incoming",
otherNumber = outgoing and row.recipient_number or row.sender_number,
messageType = row.message_type,
body = row.body,
readAt = row.read_at,
createdAt = row.created_at,
}
end
else
local rows = Bridge.Database.Query(([[
SELECT `id`, `caller_sim_id`, `caller_number`, `callee_number`, `status`,
`started_at`, `answered_at`, `ended_at`, `duration_seconds`
FROM `sky_phone_calls`
WHERE `caller_sim_id` = ? OR `callee_sim_id` = ?
ORDER BY `started_at` DESC
LIMIT %s
]]):format(limit), { sim.id, sim.id })
for _, row in ipairs(rows) do
local outgoing = row.caller_sim_id == sim.id
entries[#entries + 1] = {
id = row.id,
direction = outgoing and "outgoing" or "incoming",
otherNumber = outgoing and row.callee_number or row.caller_number,
status = row.status,
startedAt = row.started_at,
answeredAt = row.answered_at,
endedAt = row.ended_at,
durationSeconds = tonumber(row.duration_seconds) or 0,
}
end
end
write_audit(
source,
target_source,
target_identifier,
data.imei,
audit_action,
{ count = #entries }
)
return { success = true, data = { kind = data.kind, entries = entries } }
end)
Bridge.Callbacks.Register("sky_phone:admin:reset-passcode", function(source, data)
local authorized, error_response = require_admin(
source,
"reset_passcode",
Config.AdminPanel.ActionRequestsPerMinute
)
if not authorized then
return error_response
end
if type(data) ~= "table" or not SkyPhoneImei.IsValid(data.imei) then
return { success = false, error = "invalid_request" }
end
local target_source = normalize_source(data.source)
if not target_source then
return { success = false, error = "player_unavailable" }
end
local device, target_identifier = find_owned_device(target_source, data.imei)
if not device then
return { success = false, error = "device_not_owned" }
end
local result = Bridge.Database.Query(
"DELETE FROM `sky_phone_device_security` WHERE `device_imei` = ?",
{ data.imei }
)
if affected_rows(result) ~= 1 then
return { success = false, error = "passcode_not_set" }
end
write_audit(source, target_source, target_identifier, data.imei, "reset_passcode", {})
SkyPhone.RefreshDevice(data.imei)
return { success = true, data = load_player_detail(target_source) }
end)
Bridge.Callbacks.Register("sky_phone:admin:change-number", function(source, data)
local authorized, error_response = require_admin(
source,
"change_number",
Config.AdminPanel.ActionRequestsPerMinute
)
if not authorized then
return error_response
end
if type(data) ~= "table"
or not SkyPhoneImei.IsValid(data.imei)
or (type(data.phoneNumber) ~= "string" and type(data.phoneNumber) ~= "number")
then
return { success = false, error = "invalid_request" }
end
local target_source = normalize_source(data.source)
if not target_source then
return { success = false, error = "player_unavailable" }
end
local device, target_identifier = find_owned_device(target_source, data.imei)
if not device then
return { success = false, error = "device_not_owned" }
end
local sim = load_device_sim(data.imei)
if not sim then
return { success = false, error = "no_sim" }
end
local changed, number_or_error = SkyPhoneSim.ChangeNumber(
target_source,
data.imei,
sim.id,
data.phoneNumber
)
if not changed then
return { success = false, error = number_or_error }
end
SkyPhoneCompanies.ClearCallAvailability(target_source)
SkyPhoneCalls.EndForSim(sim.id, "number_changed")
write_audit(source, target_source, target_identifier, data.imei, "change_number", {
previousNumber = sim.phone_number,
phoneNumber = number_or_error,
})
SkyPhone.RefreshDevice(data.imei)
return { success = true, data = load_player_detail(target_source) }
end)
Bridge.Callbacks.Register("sky_phone:admin:factory-reset", function(source, data)
local authorized, error_response = require_admin(
source,
"factory_reset",
Config.AdminPanel.ActionRequestsPerMinute
)
if not authorized then
return error_response
end
if type(data) ~= "table" or not SkyPhoneImei.IsValid(data.imei) then
return { success = false, error = "invalid_request" }
end
local target_source = normalize_source(data.source)
if not target_source then
return { success = false, error = "player_unavailable" }
end
local device, target_identifier = find_owned_device(target_source, data.imei)
if not device then
return { success = false, error = "device_not_owned" }
end
local reset, phone_number_or_error = SkyPhonePersistence.FactoryReset(data.imei)
if not reset then
return { success = false, error = phone_number_or_error }
end
write_audit(source, target_source, target_identifier, data.imei, "factory_reset", {})
if phone_number_or_error then
TriggerEvent("sky_phone:server:factoryReset", target_source, phone_number_or_error)
end
SkyPhone.RefreshDevice(data.imei)
return { success = true, data = load_player_detail(target_source) }
end)
Bridge.Callbacks.Register("sky_phone:admin:configurator", function(source)
local authorized, error_response = require_admin(
source,
"configurator_read",
Config.AdminPanel.ReadRequestsPerMinute
)
if not authorized then
return error_response
end
return { success = true, data = SkyPhoneConfigurator.GetAdminData() }
end)
Bridge.Callbacks.Register("sky_phone:admin:save-configurator", function(source, data)
local authorized, error_response = require_admin(
source,
"configurator_save",
Config.AdminPanel.ActionRequestsPerMinute
)
if not authorized then
return error_response
end
if type(data) ~= "table" or type(data.changes) ~= "table" then
return { success = false, error = "invalid_request" }
end
local actor_identifier = Bridge.Framework.GetIdentifier(source)
if not actor_identifier then
return { success = false, error = "player_unavailable" }
end
local actor_name = player_name(source)
local response = SkyPhoneConfigurator.Save(
data.revision,
data.changes,
actor_identifier,
actor_name
)
if response.success then
write_audit(source, source, actor_identifier, nil, "save_configuration", {
changeCount = #data.changes,
revision = response.data.revision,
})
end
return response
end)
end)
+3 -13
View File
@@ -1206,18 +1206,12 @@ Bridge.Callbacks.Register("sky_phone:calls:dial", function(source, data)
end)
local payphone_models = {}
local function refresh_payphone_models()
payphone_models = {}
for _, model_name in ipairs(Config.Payphones.Props or {}) do
if type(model_name) == "string" then
payphone_models[model_name] = true
end
for _, model_name in ipairs(Config.Payphones.Props or {}) do
if type(model_name) == "string" then
payphone_models[model_name] = true
end
end
refresh_payphone_models()
if Config.Payphones.Enabled and not next(payphone_models) then
Bridge.Debug(
"error",
@@ -1226,10 +1220,6 @@ if Config.Payphones.Enabled and not next(payphone_models) then
)
end
AddEventHandler("sky_phone:configurator:serverUpdated", function()
refresh_payphone_models()
end)
local function valid_payphone_position(source, detected_booth)
local ped = GetPlayerPed(source)
if not ped or ped == 0 then
+3 -16
View File
@@ -905,22 +905,9 @@ local function cleanup_retained_data()
end
end
local function refresh_runtime_configuration()
definitions = {}
definition_ids = {}
definitions_by_job = {}
service_lines_by_number = {}
validate_configuration()
seed_companies()
tombstone_removed_companies()
end
refresh_runtime_configuration()
AddEventHandler("sky_phone:configurator:serverUpdated", function()
refresh_runtime_configuration()
end)
validate_configuration()
seed_companies()
tombstone_removed_companies()
cleanup_retained_data()
CreateThread(function()
-4
View File
@@ -1,9 +1,5 @@
Bridge.Database.AfterMigration("sky_phone", function()
local password_pepper = tostring(Config.Server.CrewLinkPasswordPepper or "")
AddEventHandler("sky_phone:configurator:serverUpdated", function()
password_pepper = tostring(Config.Server.CrewLinkPasswordPepper or "")
end)
local role_levels = {
guest = 1,
member = 2,
+2 -15
View File
@@ -316,16 +316,9 @@ local function ceil_div(value, divisor)
end
local function initialize_markets()
local next_markets = {}
local next_market_order = {}
local next_market_dynamics = {}
for _, config in ipairs(Config.Crypto.Markets) do
if next_markets[config.Id] then
error(("[sky_phone] Config.Crypto.Markets contains duplicate id '%s'."):format(config.Id))
end
next_markets[config.Id] = config
next_market_order[#next_market_order + 1] = config.Id
next_market_dynamics[config.Id] = market_dynamics[config.Id]
markets[config.Id] = config
market_order[#market_order + 1] = config.Id
Bridge.Database.Query([[
INSERT INTO `sky_phone_crypto_markets`
(`id`,`asset_scale`,`price_scale`,`issued_supply`,`price`,`version`,`status`)
@@ -364,10 +357,6 @@ local function initialize_markets()
INSERT INTO `sky_phone_crypto_balances` (`account_id`,`asset_id`,`available`)
VALUES ('treasury', 'CASH', ?) ON DUPLICATE KEY UPDATE `account_id` = VALUES(`account_id`)
]], { Config.Crypto.TreasuryCash * Config.Crypto.PriceScale })
markets = next_markets
market_order = next_market_order
market_dynamics = next_market_dynamics
market_cursor = math.min(market_cursor, math.max(#market_order, 1))
end
local function require_phone(source)
@@ -1368,8 +1357,6 @@ ensure_schema()
migrate_crypto_keys()
initialize_markets()
AddEventHandler("sky_phone:configurator:serverUpdated", initialize_markets)
local function reconcile_settlements(include_recent)
local age_clause = include_recent and "" or " AND settlement.`updated_at` < DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 5 MINUTE)"
local rows = Bridge.Database.Query([[
@@ -1,6 +1,8 @@
Bridge.Database.AfterMigration("sky_phone", function()
local STORAGE_JSON_MAX_DEPTH = 8
local STORAGE_JSON_MAX_NODES = 512
local STORAGE_KEY_MAX_LENGTH = math.min(Config.CustomApps.MaximumStorageKeyLength, 64)
local STORAGE_VALUE_MAX_BYTES = math.min(Config.CustomApps.MaximumStorageValueBytes, 65536)
local storage_write_locks = {}
local function affected_rows(result)
@@ -25,7 +27,7 @@ local function validate_storage_json(value, depth, state)
return value == value and value ~= math.huge and value ~= -math.huge
end
if value_type == "string" then
return #value <= math.min(Config.CustomApps.MaximumStorageValueBytes, 65536)
return #value <= STORAGE_VALUE_MAX_BYTES
end
if value_type ~= "table" or state.seen[value] then
return false
@@ -118,7 +120,7 @@ local function validate_request(source, data, operation)
end
if type(data.key) ~= "string"
or #data.key == 0
or #data.key > math.min(Config.CustomApps.MaximumStorageKeyLength, 64)
or #data.key > STORAGE_KEY_MAX_LENGTH
or not data.key:match("^[%w._-]+$")
then
return nil, nil, { success = false, error = "invalid_storage_key" }
@@ -203,7 +205,7 @@ Bridge.Callbacks.Register("sky_phone:custom-app:storage:set", function(source, d
local encoded, payload = pcall(json.encode, request.value)
if not encoded
or type(payload) ~= "string"
or #payload > math.min(Config.CustomApps.MaximumStorageValueBytes, 65536)
or #payload > STORAGE_VALUE_MAX_BYTES
then
return { success = false, error = "storage_value_too_large" }
end
+16 -34
View File
@@ -474,44 +474,26 @@ SkyPhoneApps.ServerPublicApi = {
UpdateCustomAppPolicyFromAdapter = update_custom_app_policy_from_adapter,
}
local function refresh_bundled_policies()
local removals = {}
for app_id, policy in pairs(policies_by_id) do
if policy.bundled then
removals[#removals + 1] = app_id
end
end
for _, app_id in ipairs(removals) do
remove_policy(app_id)
end
if Config.CustomApps.Enabled and Config.CustomApps.BundledApps then
local bundled = SkyPhoneApps.GetBundledManifests()
for index = 1, #bundled do
local manifest = bundled[index]
local registered, register_error = register_policy({
adapterResource = nil,
bundled = true,
id = manifest.id,
ownerResource = current_resource,
permissions = build_permission_set(manifest.permissions),
})
if not registered then
error(("[sky_phone] Could not register bundled custom app policy '%s': %s"):format(
manifest.id,
register_error
))
end
if Config.CustomApps.Enabled and Config.CustomApps.BundledApps then
local bundled = SkyPhoneApps.GetBundledManifests()
for index = 1, #bundled do
local manifest = bundled[index]
local registered, register_error = register_policy({
adapterResource = nil,
bundled = true,
id = manifest.id,
ownerResource = current_resource,
permissions = build_permission_set(manifest.permissions),
})
if not registered then
error(("[sky_phone] Could not register bundled custom app policy '%s': %s"):format(
manifest.id,
register_error
))
end
end
end
refresh_bundled_policies()
AddEventHandler("sky_phone:configurator:serverUpdated", function()
refresh_bundled_policies()
end)
AddEventHandler("onResourceStop", function(resource_name)
if resource_name == current_resource then
return
-42
View File
@@ -416,46 +416,6 @@ local schema = {
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
{
name = "sky_phone_admin_audit",
columns = {
{ name = "id", type = "BIGINT UNSIGNED NOT NULL AUTO_INCREMENT" },
{
name = "actor_identifier",
type = "VARCHAR(80) NOT NULL",
characterSet = "ascii",
collation = "ascii_bin",
},
{ name = "actor_name", type = "VARCHAR(120) NOT NULL" },
{
name = "target_identifier",
type = "VARCHAR(80) NOT NULL",
characterSet = "ascii",
collation = "ascii_bin",
},
{ name = "target_source", type = "INT UNSIGNED NULL" },
{
name = "device_imei",
type = "CHAR(15) NULL",
characterSet = "ascii",
collation = "ascii_bin",
},
{
name = "action",
type = "VARCHAR(48) NOT NULL",
characterSet = "ascii",
collation = "ascii_bin",
},
{ name = "details", type = "LONGTEXT NOT NULL" },
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
},
primaryKey = "id",
indexes = {
{ name = "idx_sky_phone_admin_audit_created", columns = "(`created_at`, `id`)" },
{ name = "idx_sky_phone_admin_audit_target", columns = "(`target_identifier`, `created_at`)" },
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
{
name = "sky_phone_notes",
columns = {
@@ -2949,8 +2909,6 @@ local schema = {
},
}
schema[#schema + 1] = SkyPhoneConfiguratorSchema
Bridge.Database.Migrate("sky_phone", schema)
Bridge.Database.Query([[
INSERT IGNORE INTO `sky_phone_fliptok_video_media` (`video_id`, `media_id`, `sort_order`)
+15 -50
View File
@@ -14,27 +14,7 @@ local custom_music_extensions = {
wav = true,
webm = true,
}
local password_pepper = ""
local function refresh_runtime_configuration()
password_pepper = tostring(Config.Server.FlipTokPasswordPepper or "")
music_tracks = {}
music_track_list = {}
for _, track in ipairs(Config.FlipTok.MusicTracks) do
local id = tostring(track.Id or track.id or "")
local title = tostring(track.Title or track.title or "")
local artist = tostring(track.Artist or track.artist or "")
local url = tostring(track.Url or track.url or "")
if id == "" or title == "" or artist == "" or url == "" then
error("[sky_phone] Every configured FlipTok music track requires Id, Title, Artist, and Url.")
end
local item = { id = id, title = title, artist = artist, url = url }
music_tracks[id] = item
music_track_list[#music_track_list + 1] = item
end
end
refresh_runtime_configuration()
local password_pepper = tostring(Config.Server.FlipTokPasswordPepper or "")
if password_pepper == "" then
Bridge.Debug(
@@ -44,9 +24,18 @@ if password_pepper == "" then
)
end
AddEventHandler("sky_phone:configurator:serverUpdated", function()
refresh_runtime_configuration()
end)
for _, track in ipairs(Config.FlipTok.MusicTracks) do
local id = tostring(track.Id or track.id or "")
local title = tostring(track.Title or track.title or "")
local artist = tostring(track.Artist or track.artist or "")
local url = tostring(track.Url or track.url or "")
if id == "" or title == "" or artist == "" or url == "" then
error("[sky_phone] Every configured FlipTok music track requires Id, Title, Artist, and Url.")
end
local item = { id = id, title = title, artist = artist, url = url }
music_tracks[id] = item
music_track_list[#music_track_list + 1] = item
end
local function trim(value)
if type(value) ~= "string" then return nil end
@@ -924,10 +913,7 @@ Bridge.Callbacks.Register("sky_phone:fliptok:delete", function(source, data)
return affected > 0 and { success = true } or { success = false, error = "video_not_found" }
end)
local active_verify_command = nil
local registered_verify_commands = {}
local function run_verify_command(source, arguments)
RegisterCommand(Config.FlipTok.VerifyCommand, function(source, arguments)
local command_locale = SkyPhoneLocales.Resolve(Config.Bridge.Locale).FlipTokCommand
local function command_message(template, values)
return template:gsub("{(%w+)}", function(key) return values[key] or "" end)
@@ -987,26 +973,5 @@ local function run_verify_command(source, arguments)
state = verified and command_locale.verified or command_locale.unverified,
})
send_command_feedback(message, "success")
end
local function refresh_verify_command()
local command_name = Config.FlipTok.VerifyCommand
if type(command_name) ~= "string" or command_name == "" then
error("[sky_phone] Config.FlipTok.VerifyCommand must be a non-empty command name.")
end
active_verify_command = command_name
if registered_verify_commands[command_name] then
return
end
registered_verify_commands[command_name] = true
RegisterCommand(command_name, function(source, arguments)
if active_verify_command == command_name then
run_verify_command(source, arguments)
end
end, false)
end
refresh_verify_command()
AddEventHandler("sky_phone:configurator:serverUpdated", refresh_verify_command)
end, false)
end)
+5 -15
View File
@@ -8,22 +8,12 @@ local offer_responses = { accepted = true, rejected = true }
local public_statuses = { active = true, reserved = true }
local seller_statuses = { active = true, reserved = true, sold = true, removed = true }
local function refresh_runtime_configuration()
categories = {}
districts = {}
for _, category in ipairs(Config.Marketplace.Categories) do
categories[category] = true
end
for _, district in ipairs(Config.Marketplace.Districts) do
districts[district] = true
end
for _, category in ipairs(Config.Marketplace.Categories) do
categories[category] = true
end
for _, district in ipairs(Config.Marketplace.Districts) do
districts[district] = true
end
refresh_runtime_configuration()
AddEventHandler("sky_phone:configurator:serverUpdated", function()
refresh_runtime_configuration()
end)
local function trim(value)
if type(value) ~= "string" then
+5 -15
View File
@@ -226,18 +226,14 @@ local function validate_website(definition)
return nil, "invalid_required_ace"
end
local website = {}
for key, value in pairs(definition) do
website[key] = value
end
website._adapter = adapter
website._media_types = allowed_media_types
local valid, validation_error = adapter.Validate(website)
definition._adapter = adapter
definition._media_types = allowed_media_types
local valid, validation_error = adapter.Validate(definition)
if not valid then
return nil, validation_error
end
return website
return definition
end
local function build_registry()
@@ -259,7 +255,7 @@ local function build_registry()
if website_error == "missing_api_key" then
Bridge.Debug(
"warn",
"[sky_phone] Media import source '%s' at index %s is disabled because Config.Media.FiveManage.ApiKey is empty. Add a FiveManage V3 token with Media access in the Phone Configurator and save it.",
"[sky_phone] Media import source '%s' at index %s is disabled because Config.Media.FiveManage.ApiKey is empty in server-only config/media.lua. Add a FiveManage V3 token with Media access and restart sky_phone.",
tostring(source_name or "unknown"),
tostring(index)
)
@@ -760,9 +756,3 @@ function SkyPhoneMediaImport.Initialize()
import_candidates[source] = nil
end)
end
AddEventHandler("sky_phone:configurator:serverUpdated", function()
if initialized then
build_registry()
end
end)
-6
View File
@@ -218,10 +218,6 @@ local function normalize_server_tracks()
return
end
server_tracks = {}
server_tracks_by_id = {}
configured_track_ids = {}
for index, configured in ipairs(Config.Music.Tracks or {}) do
local id = type(configured) == "table" and trim(configured.Id) or nil
local title = type(configured) == "table" and trim(configured.Title) or nil
@@ -304,8 +300,6 @@ end
normalize_server_tracks()
AddEventHandler("sky_phone:configurator:serverUpdated", normalize_server_tracks)
local function session_owner(source)
local session, error_response = SkyPhone.RequireSession(source)
if not session then
+2 -17
View File
@@ -1,23 +1,8 @@
Bridge.Database.AfterMigration("sky_phone", function()
local categories = {}
local districts = {}
local function refresh_runtime_configuration()
categories = {}
districts = {}
for _, value in ipairs(Config.LocalPages.Categories) do
categories[value] = true
end
for _, value in ipairs(Config.Marketplace.Districts) do
districts[value] = true
end
end
refresh_runtime_configuration()
AddEventHandler("sky_phone:configurator:serverUpdated", function()
refresh_runtime_configuration()
end)
for _, value in ipairs(Config.LocalPages.Categories) do categories[value] = true end
for _, value in ipairs(Config.Marketplace.Districts) do districts[value] = true end
local function trim(value)
if type(value) ~= "string" then return nil end
+26 -36
View File
@@ -54,6 +54,8 @@ Bridge.Debug("debug", "[sky_phone] Server initialization started after database
SkyPhone = {}
local unique_phones = Config.Phone.Unique ~= false
local sim_cards_enabled = Config.Sim.Enabled ~= false
local sessions = {}
local preferred_device_imeis = {}
local equipped_phone_numbers = {}
@@ -275,7 +277,7 @@ end
local function find_device_slots(source, imei)
local matches = {}
local slots = Bridge.Inventory.GetSlotsWithItem(source, Config.Phone.Item)
if Config.Phone.Unique == false then
if not unique_phones then
if not slots[1] then
return matches
end
@@ -366,7 +368,7 @@ local function resolve_used_slot(source, used_item)
)
end
if Config.Phone.Unique == false and #slots > 0 then
if not unique_phones and #slots > 0 then
return slots[1]
end
@@ -414,7 +416,7 @@ local function ensure_device(source, slot)
tostring(Bridge.Inventory.GetResourceName()),
{ always = true }
)
if Config.Phone.Unique == false then
if not unique_phones then
if amount < 1 then
return nil, "phone_slot_missing"
end
@@ -559,7 +561,7 @@ local function bootstrap(source, security, security_loaded)
sim = device.sim_id and {
id = device.sim_id,
number = device.phone_number,
removable = Config.Sim.Enabled ~= false and tonumber(device.sim_is_virtual) ~= 1,
removable = sim_cards_enabled and tonumber(device.sim_is_virtual) ~= 1,
type = device.sim_type,
registered = device.registered_at ~= nil,
} or nil,
@@ -614,7 +616,7 @@ local function resolve_equipped_phone_number(source)
end
local imei
if Config.Phone.Unique ~= false then
if unique_phones then
table.sort(slots, function(left, right)
local left_slot = tonumber(left.slot)
local right_slot = tonumber(right.slot)
@@ -800,7 +802,7 @@ function SkyPhone.NotifyAccountDevices(account_id, event_name, data)
for _, player_source in ipairs(Bridge.Framework.GetPlayers()) do
local source = tonumber(player_source) or player_source
if Config.Phone.Unique == false then
if not unique_phones then
if Bridge.Inventory.GetSlotsWithItem(source, Config.Phone.Item)[1] then
local imei = load_character_device(source)
local device = imei and devices[imei] or nil
@@ -966,38 +968,26 @@ function SkyPhone.OpenDeviceForCall(source, imei)
return true
end
local registered_usable_items = {}
local function register_configured_phone_item()
local item_name = Config.Phone.Item
if registered_usable_items[item_name] then
return
end
Bridge.Debug(
"debug",
"[sky_phone] Registering usable item '%s' through inventory '%s'.",
item_name,
Bridge.Debug(
"debug",
"[sky_phone] Registering usable item '%s' through inventory '%s'.",
Config.Phone.Item,
tostring(Bridge.Inventory.GetResourceName()),
{ always = true }
)
local usable_registered = Bridge.Inventory.RegisterUsableItem(Config.Phone.Item, open_phone)
if not usable_registered then
error(("[sky_phone] Inventory '%s' did not register phone item '%s' as usable."):format(
tostring(Bridge.Inventory.GetResourceName()),
{ always = true }
)
if not Bridge.Inventory.RegisterUsableItem(item_name, function(...)
if Config.Phone.Item == item_name then
open_phone(...)
end
end) then
error(("[sky_phone] Inventory '%s' did not register phone item '%s' as usable."):format(
tostring(Bridge.Inventory.GetResourceName()),
tostring(item_name)
))
end
registered_usable_items[item_name] = true
tostring(Config.Phone.Item)
))
end
register_configured_phone_item()
AddEventHandler("sky_phone:configurator:serverUpdated", function()
register_configured_phone_item()
end)
Bridge.Debug(
"debug",
"[sky_phone] Usable item registration returned: %s.",
tostring(usable_registered),
{ always = true }
)
Bridge.Callbacks.Register("sky_phone:device:close", function(source)
sessions[source] = nil
File diff suppressed because it is too large Load Diff
@@ -1,23 +0,0 @@
SkyPhoneConfiguratorSchema = {
name = "sky_phone_configurator",
columns = {
{ name = "id", type = "TINYINT UNSIGNED NOT NULL" },
{ name = "config_payload", type = "LONGTEXT NOT NULL" },
{ name = "media_payload", type = "LONGTEXT NOT NULL" },
{ name = "revision", type = "INT UNSIGNED NOT NULL DEFAULT 1" },
{
name = "updated_by_identifier",
type = "VARCHAR(80) NULL",
characterSet = "ascii",
collation = "ascii_bin",
},
{ name = "updated_by_name", type = "VARCHAR(120) NULL" },
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
{
name = "updated_at",
type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP",
},
},
primaryKey = "id",
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
}
+51 -69
View File
@@ -1,6 +1,4 @@
Bridge.Database.AfterMigration("sky_phone", function()
SkyPhonePersistence = {}
local max_device_data_bytes = 100000
local allowed_device_namespaces = {
settings = true,
@@ -152,69 +150,6 @@ Bridge.Callbacks.Register("sky_phone:notifications:save", function(source, data)
return { success = true }
end)
function SkyPhonePersistence.FactoryReset(imei)
if not SkyPhoneImei.IsValid(imei) then
return false, "invalid_request"
end
local device = SkyPhone.LoadDevice(imei)
if not device then
return false, "device_not_found"
end
local phone_number = device and device.phone_number or nil
local media_remote_ids = SkyPhoneMedia.GetDeviceRemoteIds(imei)
if not Bridge.Database.Transaction({
{
query = "DELETE FROM `sky_phone_device_security` WHERE `device_imei` = ?",
params = { imei },
},
{
query = "DELETE FROM `sky_phone_device_data` WHERE `device_imei` = ?",
params = { imei },
},
{
query = "DELETE FROM `sky_phone_custom_app_data` WHERE `device_imei` = ?",
params = { imei },
},
{
query = "DELETE FROM `sky_phone_notes` WHERE `device_imei` = ? AND `account_id` IS NULL",
params = { imei },
},
{
query = "DELETE FROM `sky_phone_media` WHERE `device_imei` = ? AND `account_id` IS NULL",
params = { imei },
},
{
query = "DELETE FROM `sky_phone_music_playlists` WHERE `device_imei` = ? AND `account_id` IS NULL",
params = { imei },
},
{
query = "DELETE FROM `sky_phone_music_youtube_songs` WHERE `device_imei` = ? AND `account_id` IS NULL",
params = { imei },
},
{
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 },
},
{
query = "DELETE FROM `sky_phone_fliptok_sessions` WHERE `device_imei` = ?",
params = { imei },
},
{
query = "UPDATE `sky_phone_devices` SET `account_id` = NULL, `device_name` = ? WHERE `imei` = ?",
params = { Config.Phone.DeviceName, imei },
},
}) then
return false, "request_failed"
end
SkyPhoneMedia.CleanupRemoteFiles(media_remote_ids)
return true, phone_number
end
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" }
@@ -223,11 +158,58 @@ Bridge.Callbacks.Register("sky_phone:device:factory-reset", function(source)
if not session then
return error_response
end
local reset, phone_number = SkyPhonePersistence.FactoryReset(session.imei)
if not reset then
return { success = false, error = phone_number }
local device = SkyPhone.LoadDevice(session.imei)
local phone_number = device and device.phone_number or nil
local media_remote_ids = SkyPhoneMedia.GetDeviceRemoteIds(session.imei)
if not Bridge.Database.Transaction({
{
query = "DELETE FROM `sky_phone_device_security` WHERE `device_imei` = ?",
params = { session.imei },
},
{
query = "DELETE FROM `sky_phone_device_data` WHERE `device_imei` = ?",
params = { session.imei },
},
{
query = "DELETE FROM `sky_phone_custom_app_data` WHERE `device_imei` = ?",
params = { session.imei },
},
{
query = "DELETE FROM `sky_phone_notes` WHERE `device_imei` = ? AND `account_id` IS NULL",
params = { session.imei },
},
{
query = "DELETE FROM `sky_phone_media` WHERE `device_imei` = ? AND `account_id` IS NULL",
params = { session.imei },
},
{
query = "DELETE FROM `sky_phone_music_playlists` WHERE `device_imei` = ? AND `account_id` IS NULL",
params = { session.imei },
},
{
query = "DELETE FROM `sky_phone_music_youtube_songs` WHERE `device_imei` = ? AND `account_id` IS NULL",
params = { session.imei },
},
{
query = "DELETE FROM `sky_phone_contacts` WHERE `device_imei` = ? AND `account_id` IS NULL",
params = { session.imei },
},
{
query = "DELETE FROM `sky_phone_call_entries` WHERE `device_imei` = ? AND `account_id` IS NULL",
params = { session.imei },
},
{
query = "DELETE FROM `sky_phone_fliptok_sessions` WHERE `device_imei` = ?",
params = { session.imei },
},
{
query = "UPDATE `sky_phone_devices` SET `account_id` = NULL, `device_name` = ? WHERE `imei` = ?",
params = { Config.Phone.DeviceName, session.imei },
},
}) then
return { success = false, error = "request_failed" }
end
SkyPhoneMedia.CleanupRemoteFiles(media_remote_ids)
session.unlocked = true
if phone_number then
TriggerEvent("sky_phone:server:factoryReset", source, phone_number)
@@ -2,10 +2,6 @@ Bridge.Database.AfterMigration("sky_phone", function()
SkyPhoneSecurity = {}
local passcode_pepper = tostring(Config.Server.PasscodePepper or "")
AddEventHandler("sky_phone:configurator:serverUpdated", function()
passcode_pepper = tostring(Config.Server.PasscodePepper or "")
end)
if passcode_pepper == "" then
Bridge.Debug(
"warn",
+6 -40
View File
@@ -1,16 +1,6 @@
Bridge.Database.AfterMigration("sky_phone", function()
local report_reasons = {}
local password_pepper = ""
local function refresh_runtime_configuration()
password_pepper = tostring(Config.Server.PicstagramPasswordPepper or "")
report_reasons = {}
for index = 1, #Config.Picstagram.ReportReasons do
report_reasons[Config.Picstagram.ReportReasons[index]] = true
end
end
refresh_runtime_configuration()
local password_pepper = tostring(Config.Server.PicstagramPasswordPepper or "")
if password_pepper == "" then
Bridge.Debug(
@@ -20,9 +10,9 @@ if password_pepper == "" then
)
end
AddEventHandler("sky_phone:configurator:serverUpdated", function()
refresh_runtime_configuration()
end)
for index = 1, #Config.Picstagram.ReportReasons do
report_reasons[Config.Picstagram.ReportReasons[index]] = true
end
local function trim(value)
if type(value) ~= "string" then
@@ -1444,10 +1434,7 @@ Bridge.Callbacks.Register("sky_phone:picstagram:admin-resolve-report", function(
return { success = true }
end)
local active_verify_command = nil
local registered_verify_commands = {}
local function run_verify_command(source, args)
RegisterCommand(Config.Picstagram.VerifyCommand, function(source, args)
local command_locale = SkyPhoneLocales.Resolve(Config.Bridge.Locale).PicstagramCommand
local function command_message(template, values)
return template:gsub("{(%w+)}", function(key)
@@ -1502,26 +1489,5 @@ local function run_verify_command(source, args)
handle = handle,
state = state == 1 and command_locale.verified or command_locale.unverified,
}), "success")
end
local function refresh_verify_command()
local command_name = Config.Picstagram.VerifyCommand
if type(command_name) ~= "string" or command_name == "" then
error("[sky_phone] Config.Picstagram.VerifyCommand must be a non-empty command name.")
end
active_verify_command = command_name
if registered_verify_commands[command_name] then
return
end
registered_verify_commands[command_name] = true
RegisterCommand(command_name, function(source, args)
if active_verify_command == command_name then
run_verify_command(source, args)
end
end, false)
end
refresh_verify_command()
AddEventHandler("sky_phone:configurator:serverUpdated", refresh_verify_command)
end, false)
end)
-12
View File
@@ -25,18 +25,6 @@ local SERVER_CAPABILITIES = {
side = "server",
}
local function refresh_configured_capabilities()
local enabled = Config.CustomApps.Enabled == true
local external = enabled and Config.CustomApps.ExternalApps == true
SERVER_CAPABILITIES.features.customApps.enabled = enabled
SERVER_CAPABILITIES.features.customApps.external = external
SERVER_CAPABILITIES.features.notifications.customApps = external
end
refresh_configured_capabilities()
AddEventHandler("sky_phone:configurator:serverUpdated", refresh_configured_capabilities)
local custom_app_api = SkyPhoneApps.ServerPublicApi
if type(custom_app_api) ~= "table" then
error("[sky_phone] Server public API initialized before the custom app policy API.")
+23 -107
View File
@@ -2,29 +2,22 @@ Bridge.Database.AfterMigration("sky_phone", function()
SkyPhoneSim = {}
local valid_number_configuration, number_configuration_error = SkyPhoneSimNumber.ValidateConfiguration(
Config.Sim.NumberLength,
Config.Sim.NumberPrefix
)
if not valid_number_configuration then
error(("[sky_phone] Invalid SIM number configuration: %s."):format(number_configuration_error))
end
local unique_phones = Config.Phone.Unique ~= false
local sim_cards_enabled = Config.Sim.Enabled ~= false
local pending_insertions = {}
local operation_locks = {}
local sim_types = {}
local function validate_number_configuration()
local valid, validation_error = SkyPhoneSimNumber.ValidateConfiguration(
Config.Sim.NumberLength,
Config.Sim.NumberPrefix
)
if not valid then
error(("[sky_phone] Invalid SIM number configuration: %s."):format(validation_error))
end
end
local function refresh_sim_types()
sim_types = {
[Config.Sim.RegisteredItem] = "registered",
[Config.Sim.AnonymousItem] = "anonymous",
}
end
validate_number_configuration()
refresh_sim_types()
local sim_types = {
[Config.Sim.RegisteredItem] = "registered",
[Config.Sim.AnonymousItem] = "anonymous",
}
local function affected_rows(result)
if type(result) == "number" then
@@ -83,7 +76,7 @@ local function sim_metadata(sim)
end
local function set_phone_sim_metadata(source, phone_slot, sim)
if Config.Phone.Unique == false then
if not unique_phones then
return true
end
if not phone_slot then
@@ -109,7 +102,7 @@ local function prepare_device(source, phone_slot, imei)
end
local sim = device.sim_id and load_sim(device.sim_id) or nil
if Config.Sim.Enabled ~= false then
if sim_cards_enabled then
if sim and tonumber(sim.is_virtual) == 1 then
local result = Bridge.Database.Query([[
UPDATE `sky_phone_devices`
@@ -168,62 +161,6 @@ end
SkyPhoneSim.PrepareDevice = prepare_device
function SkyPhoneSim.ChangeNumber(source, imei, sim_id, value)
local number = SkyPhoneSimNumber.Normalize(value, Config.Sim.NumberLength, Config.Sim.NumberPrefix)
if not number or SkyPhoneCompanies.IsServiceNumber(number) then
return false, "invalid_phone_number"
end
local sim = load_sim(sim_id)
if not sim then
return false, "no_sim"
end
if sim.phone_number == number then
return false, "phone_number_unchanged"
end
local existing = Bridge.Database.Query(
"SELECT `id` FROM `sky_phone_sims` WHERE `phone_number` = ? AND `id` <> ? LIMIT 1",
{ number, sim_id }
)
if existing[1] then
return false, "phone_number_taken"
end
local phone_slot
if Config.Phone.Unique ~= false then
for _, slot in ipairs(Bridge.Inventory.GetSlotsWithItem(source, Config.Phone.Item)) do
if slot.metadata and slot.metadata.imei == imei then
phone_slot = slot
break
end
end
end
local previous_number = sim.phone_number
sim.phone_number = number
if phone_slot and not set_phone_sim_metadata(source, phone_slot, sim) then
return false, "metadata_unsupported"
end
local result = Bridge.Database.Query([[
UPDATE IGNORE `sky_phone_sims`
SET `phone_number` = ?
WHERE `id` = ? AND `phone_number` = ?
]], { number, sim_id, previous_number })
if affected_rows(result) ~= 1 then
if phone_slot then
sim.phone_number = previous_number
if not set_phone_sim_metadata(source, phone_slot, sim) then
error("[sky_phone] Could not restore SIM metadata after a failed admin number change.")
end
end
return false, "phone_number_taken"
end
return true, number
end
local function resolve_used_sim(source, used_item, item_name)
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
@@ -315,7 +252,7 @@ local function rollback_phone_metadata(source, phone_slot, old_sim)
end
local function insert_sim(source, phone_imei, confirmed)
if Config.Sim.Enabled == false then
if not sim_cards_enabled then
return { success = false, error = "disabled" }
end
if operation_locks[source] then
@@ -419,7 +356,7 @@ local function insert_sim(source, phone_imei, confirmed)
end
local function use_sim(source, used_item)
if Config.Sim.Enabled == false then
if not sim_cards_enabled then
return false
end
local item_name = used_item and used_item.name
@@ -474,26 +411,11 @@ end
-- Register the guarded callbacks in both modes so a resource-only restart can
-- replace registrations left behind in framework-owned usable-item tables.
local registered_sim_items = {}
local function register_configured_sim_items()
for item_name in pairs(sim_types) do
if not registered_sim_items[item_name] then
local registered_item_name = item_name
Bridge.Inventory.RegisterUsableItem(registered_item_name, function(...)
if Config.Sim.Enabled ~= false and sim_types[registered_item_name] then
use_sim(...)
end
end)
registered_sim_items[registered_item_name] = true
end
end
end
register_configured_sim_items()
Bridge.Inventory.RegisterUsableItem(Config.Sim.RegisteredItem, use_sim)
Bridge.Inventory.RegisterUsableItem(Config.Sim.AnonymousItem, use_sim)
Bridge.Callbacks.Register("sky_phone:sim:insert", function(source, data)
if Config.Sim.Enabled == false then
if not sim_cards_enabled then
return { success = false, error = "disabled" }
end
if type(data) ~= "table" or not SkyPhoneImei.IsValid(data.imei) then
@@ -511,7 +433,7 @@ Bridge.Callbacks.Register("sky_phone:sim:picker-close", function(source)
end)
Bridge.Callbacks.Register("sky_phone:sim:eject", function(source)
if Config.Sim.Enabled == false then
if not sim_cards_enabled then
return { success = false, error = "disabled" }
end
if operation_locks[source] then
@@ -564,13 +486,7 @@ AddEventHandler("playerDropped", function()
operation_locks[source] = nil
end)
AddEventHandler("sky_phone:configurator:serverUpdated", function()
validate_number_configuration()
refresh_sim_types()
register_configured_sim_items()
end)
if Config.Sim.Enabled ~= false then
if sim_cards_enabled then
Bridge.Database.Query([[
UPDATE `sky_phone_devices` d
INNER JOIN `sky_phone_sims` s ON s.`id` = d.`sim_id`
+12 -22
View File
@@ -11,30 +11,20 @@ local online_drivers = {}
local operation_locks = {}
local services = {}
local function refresh_runtime_configuration()
if Config.SkyRide.DistanceUnit ~= "kilometer" and Config.SkyRide.DistanceUnit ~= "mile" then
error(("[sky_phone] Invalid SkyRide distance unit '%s'."):format(tostring(Config.SkyRide.DistanceUnit)))
end
local next_services = {}
for index = 1, #Config.SkyRide.Services do
local service = Config.SkyRide.Services[index]
if service.Id ~= "taxi" and service.Id ~= "comfort" and service.Id ~= "xl" and service.Id ~= "premium" then
error(("[sky_phone] Invalid SkyRide service class '%s'."):format(tostring(service.Id)))
end
if next_services[service.Id] then
error(("[sky_phone] Duplicate SkyRide service class '%s'."):format(service.Id))
end
next_services[service.Id] = service
end
services = next_services
if Config.SkyRide.DistanceUnit ~= "kilometer" and Config.SkyRide.DistanceUnit ~= "mile" then
error(("[sky_phone] Invalid SkyRide distance unit '%s'."):format(tostring(Config.SkyRide.DistanceUnit)))
end
refresh_runtime_configuration()
AddEventHandler("sky_phone:configurator:serverUpdated", function()
refresh_runtime_configuration()
end)
for index = 1, #Config.SkyRide.Services do
local service = Config.SkyRide.Services[index]
if service.Id ~= "taxi" and service.Id ~= "comfort" and service.Id ~= "xl" and service.Id ~= "premium" then
error(("[sky_phone] Invalid SkyRide service class '%s'."):format(tostring(service.Id)))
end
if services[service.Id] then
error(("[sky_phone] Duplicate SkyRide service class '%s'."):format(service.Id))
end
services[service.Id] = service
end
local ride_select = [[
SELECT r.*,
+6 -32
View File
@@ -1,5 +1,9 @@
Bridge.Database.AfterMigration("sky_phone", function()
if not Config.TestData.Enabled then
return
end
local photo_urls = {
city = "https://images.unsplash.com/photo-1519501025264-65ba15a82390?auto=format&fit=crop&w=1200&q=80",
car = "https://images.unsplash.com/photo-1493238792000-8113da705763?auto=format&fit=crop&w=1200&q=80",
@@ -1037,10 +1041,7 @@ local function seed_for_source(source)
return account.email
end
local function run_test_data_command(source)
if not Config.TestData.Enabled then
return
end
RegisterCommand(Config.TestData.Command, function(source)
if source <= 0 then
Bridge.Debug("warn", "[sky_phone] The test data command must be run by an in-game player.")
return
@@ -1065,34 +1066,7 @@ local function run_test_data_command(source)
end
Bridge.Debug("info", "[sky_phone] Test data seeded for source %s.", tostring(source), { always = true })
TriggerClientEvent("sky_phone:testdata:feedback", source, true, result)
end
local active_test_data_command = nil
local registered_test_data_commands = {}
local function refresh_test_data_command()
active_test_data_command = Config.TestData.Enabled and Config.TestData.Command or nil
if not active_test_data_command then
return
end
if type(active_test_data_command) ~= "string" or active_test_data_command == "" then
error("[sky_phone] Config.TestData.Command must be a non-empty command name.")
end
if registered_test_data_commands[active_test_data_command] then
return
end
local command_name = active_test_data_command
registered_test_data_commands[command_name] = true
RegisterCommand(command_name, function(source)
if active_test_data_command == command_name then
run_test_data_command(source)
end
end, false)
end
refresh_test_data_command()
AddEventHandler("sky_phone:configurator:serverUpdated", refresh_test_data_command)
end, false)
AddEventHandler("playerDropped", function()
seed_attempts[source] = nil
+16 -27
View File
@@ -55,35 +55,24 @@ if type(config.Categories) ~= "table" then
end
local categories = {}
local function refresh_categories()
local next_categories = {}
for _, category in ipairs(config.Categories or {}) do
if type(category) ~= "string" or #category < 1 or #category > 32
or not category:match("^[%l_]+$") or next_categories[category]
then
error(("[sky_phone] Invalid Weazel News category '%s'."):format(tostring(category)))
end
next_categories[category] = true
for _, category in ipairs(config.Categories or {}) do
if type(category) ~= "string" or #category < 1 or #category > 32
or not category:match("^[%l_]+$") or categories[category]
then
error(("[sky_phone] Invalid Weazel News category '%s'."):format(tostring(category)))
end
categories[category] = true
end
for category in pairs(supported_categories) do
if not categories[category] then
error(("[sky_phone] Config.WeazelNews.Categories is missing supported category '%s'."):format(category))
end
end
for category in pairs(categories) do
if not supported_categories[category] then
error(("[sky_phone] Config.WeazelNews.Categories contains unsupported category '%s'."):format(category))
end
for category in pairs(supported_categories) do
if not next_categories[category] then
error(("[sky_phone] Config.WeazelNews.Categories is missing supported category '%s'."):format(category))
end
end
for category in pairs(next_categories) do
if not supported_categories[category] then
error(("[sky_phone] Config.WeazelNews.Categories contains unsupported category '%s'."):format(category))
end
end
categories = next_categories
end
refresh_categories()
AddEventHandler("sky_phone:configurator:serverUpdated", function()
refresh_categories()
end)
if type(config.AllowedJobs) ~= "table" then
error("[sky_phone] Config.WeazelNews.AllowedJobs must be a table.")
-1
View File
@@ -35,7 +35,6 @@ local ALLOWED_PERMISSIONS = {
}
local RESERVED_APP_IDS = {
admin = true,
["app-store"] = true,
banking = true,
crypto = true,
-27
View File
@@ -185,33 +185,6 @@ CREATE TABLE IF NOT EXISTS `sky_phone_device_security` (
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_admin_audit` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`actor_identifier` VARCHAR(80) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`actor_name` VARCHAR(120) NOT NULL,
`target_identifier` VARCHAR(80) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`target_source` INT UNSIGNED NULL,
`device_imei` CHAR(15) CHARACTER SET ascii COLLATE ascii_bin NULL,
`action` VARCHAR(48) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`details` LONGTEXT NOT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_sky_phone_admin_audit_created` (`created_at`, `id`),
KEY `idx_sky_phone_admin_audit_target` (`target_identifier`, `created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `sky_phone_configurator` (
`id` TINYINT UNSIGNED NOT NULL,
`config_payload` LONGTEXT NOT NULL,
`media_payload` LONGTEXT NOT NULL,
`revision` INT UNSIGNED NOT NULL DEFAULT 1,
`updated_by_identifier` VARCHAR(80) CHARACTER SET ascii COLLATE ascii_bin NULL,
`updated_by_name` VARCHAR(120) NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) 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,