mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-28 23:01:37 +00:00
This reverts commit edd9677f43.
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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]],
|
||||
|
||||
@@ -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)
|
||||
@@ -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.")
|
||||
|
||||
@@ -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" })
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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`
|
||||
|
||||
@@ -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.*,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -35,7 +35,6 @@ local ALLOWED_PERMISSIONS = {
|
||||
}
|
||||
|
||||
local RESERVED_APP_IDS = {
|
||||
admin = true,
|
||||
["app-store"] = true,
|
||||
banking = true,
|
||||
crypto = true,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user