pc-live/server/database.lua

420 lines
15 KiB
Lua
Raw Normal View History

IC-Computer: Schreibtisch, Fenster und Apps Ein Computer im Spiel. Man tritt an ein Terminal, drueckt E, und bekommt eine Oberflaeche mit Mail, Browser, Adressbuch, Kalender und weiteren Anwendungen. Andere Resources haengen sich als App ein. Auf ESX umgestellt: - Fuenf harte Abhaengigkeiten zeigten auf Resources, die es hier nicht gibt. Eine fehlende Abhaengigkeit verhindert den Start vollstaendig - die Resource waere nie hochgekommen. - Die Bruecke zum Framework neu geschrieben; Charakterdaten, Aktenverwaltung und Immobilien ausgebaut, weil die zugehoerigen Systeme fehlen. - pc_live_devices fehlte im Schema, wird vom Code aber gelesen. Ohne die Tabelle ist kein fester PC ansprechbar. Ein PC braucht ausserdem zwei Eintraege mit derselben Kennung: Standort in der Datenbank, Kamerafahrt in der config.lua. Mail ueberarbeitet: - Postfaecher haengen an einer Anmeldung statt an der Person. Mehrere Leute koennen dasselbe Firmenpostfach gleichzeitig offen haben. - Ordner gehoeren zum Postfach, nicht zur Person. Eine Mail an ein geteiltes Postfach wird je Empfaenger einmal gespeichert; damit das Einsortieren trotzdem fuer alle gilt, tragen alle Kopien einer Zustellung dieselbe Kennung. Gelesen und geloescht bleibt persoenlich - das ist keine Eigenschaft der Nachricht, sondern der Person. - Signaturen gehoeren ebenfalls zum Postfach. - Kalender koennen privat, geteilt oder oeffentlich sein. Ein oeffentlicher Termin haengt bewusst auch an einem Postfach, sonst koennte ihn spaeter niemand mehr aendern oder absagen. Neue Apps: Webhosting, Bleeter und dessen Verwaltung. Behoben: - prompt(), confirm() und alert() froren das NUI ein. FiveMs CEF hat keinen Handler fuer die eingebauten Browserdialoge - der Aufruf oeffnet nichts und kehrt nie zurueck. Ersetzt durch eigene Dialoge. - Die Fenster tragen data-wid, keine id. Wer sie mit getElementById sucht, schreibt ins Leere und das Fenster bleibt leer. Enthaelt README.md mit Einrichtung und PLUGINS.md: eine Anleitung, wie man eine eigene App baut und einhaengt, mit vollstaendigem Beispiel von der Tabelle bis zum Schreibtischsymbol.
2026-08-09 12:02:11 +00:00
-- ─────────────────────────────────────────────────────────────────────────────
-- Database layer all MySQL calls live here
-- ─────────────────────────────────────────────────────────────────────────────
DB = {}
-- ── Users ──────────────────────────────────────────────────────────────────
---@param identifier string
---@param displayName string
---@return table?
function DB.GetOrCreateUser(identifier, displayName)
local row = MySQL.single.await(
'SELECT * FROM pc_live_users WHERE identifier = ?',
{ identifier }
)
if row then return row end
MySQL.insert.await(
'INSERT INTO pc_live_users (identifier, display_name, settings_json) VALUES (?, ?, ?)',
{ identifier, displayName, '{}' }
)
return MySQL.single.await(
'SELECT * FROM pc_live_users WHERE identifier = ?',
{ identifier }
)
end
---@param identifier string
---@param settings table
function DB.SaveSettings(identifier, settings)
MySQL.update.await(
'UPDATE pc_live_users SET settings_json = ? WHERE identifier = ?',
{ json.encode(settings), identifier }
)
end
-- ── Installs ───────────────────────────────────────────────────────────────
---@param userId number
---@return table[]
function DB.GetInstalls(userId)
return MySQL.query.await(
'SELECT * FROM pc_live_installs WHERE user_id = ?',
{ userId }
)
end
---@param userId number
---@param appId string
---@param version string
function DB.InstallApp(userId, appId, version)
local existing = MySQL.single.await(
'SELECT id FROM pc_live_installs WHERE user_id = ? AND app_id = ?',
{ userId, appId }
)
if existing then
MySQL.update.await(
'UPDATE pc_live_installs SET version = ?, installed_at = NOW() WHERE id = ?',
{ version, existing.id }
)
else
MySQL.insert.await(
'INSERT INTO pc_live_installs (user_id, app_id, version, installed_at) VALUES (?, ?, ?, NOW())',
{ userId, appId, version }
)
end
end
---@param userId number
---@param appId string
function DB.UninstallApp(userId, appId)
MySQL.update.await(
'DELETE FROM pc_live_installs WHERE user_id = ? AND app_id = ?',
{ userId, appId }
)
end
-- ── Mail ───────────────────────────────────────────────────────────────────
---@param toId string
---@return table[]
function DB.GetInbox(toId)
return MySQL.query.await(
'SELECT * FROM pc_live_mail WHERE to_identifier = ? ORDER BY sent_at DESC LIMIT 100',
{ toId }
)
end
---@param id number
---@param toId string
---@return table?
function DB.GetMail(id, toId)
return MySQL.single.await(
'SELECT * FROM pc_live_mail WHERE id = ? AND to_identifier = ?',
{ id, toId }
)
end
---@param toId string
---@param fromId string
---@param subject string
---@param body string
---@return number inserted id
function DB.SendMail(toId, fromId, subject, body)
return MySQL.insert.await(
'INSERT INTO pc_live_mail (to_identifier, from_identifier, subject, body, is_read, sent_at) VALUES (?, ?, ?, ?, 0, NOW())',
{ toId, fromId, subject, body }
)
end
---@param toId string Empfänger-Identifier (aufgelöst)
---@param fromDisplay string Absender-Anzeige (E-Mail oder Identifier)
---@param senderId string Roher Identifier des Absenders (für Gesendet-Abfrage)
---@param toAddress string Originale Empfängeradresse
---@param subject string
---@param body string
---@return number
--- deliveryId klammert die Kopien einer Zustellung zusammen: eine Mail an ein
--- geteiltes Postfach wird je Empfaenger einmal gespeichert, soll aber fuer
--- alle im selben Ordner landen.
function DB.SendMailExtended(toId, fromDisplay, senderId, toAddress, subject, body, deliveryId)
return MySQL.insert.await(
'INSERT INTO pc_live_mail'
.. ' (to_identifier, from_identifier, sender_id, to_address, subject, body,'
.. ' delivery_id, is_read, sent_at)'
.. ' VALUES (?, ?, ?, ?, ?, ?, ?, 0, NOW())',
{ toId, fromDisplay, senderId, toAddress, subject, body, deliveryId }
)
end
---@param id number
---@param toId string
function DB.MarkRead(id, toId)
MySQL.update.await(
'UPDATE pc_live_mail SET is_read = 1 WHERE id = ? AND to_identifier = ?',
{ id, toId }
)
end
---@param id number
---@param toId string
function DB.DeleteMail(id, toId)
MySQL.update.await(
'DELETE FROM pc_live_mail WHERE id = ? AND to_identifier = ?',
{ id, toId }
)
end
---@param toId string
---@return number
function DB.CountUnread(toId)
local row = MySQL.single.await(
'SELECT COUNT(*) as cnt FROM pc_live_mail WHERE to_identifier = ? AND is_read = 0',
{ toId }
)
return row and row.cnt or 0
end
---@param fromId string Spieler-Identifier
---@return table[]
function DB.GetSent(fromId)
return MySQL.query.await(
'SELECT * FROM pc_live_mail'
.. ' WHERE from_identifier = ? OR sender_id = ?'
.. ' ORDER BY sent_at DESC LIMIT 100',
{ fromId, fromId }
)
end
-- ── Mail Folders ───────────────────────────────────────────────────────────
--
-- Ordner gehoeren zum Postfach, nicht zur Person: ein geteiltes Postfach wird
-- von mehreren bedient und braucht dieselbe Struktur fuer alle.
--
-- address = '' → persoenliche Ordner (Spalte address IS NULL)
-- address ~= '' → Ordner dieses Postfachs
--
-- Der Aufrufer muss vorher pruefen, ob die Person das Postfach benutzen darf.
---@param identifier string
---@param address string|nil
---@return table[]
function DB.GetFolders(identifier, address)
if address and address ~= '' then
return MySQL.query.await(
'SELECT * FROM pc_live_mail_folders WHERE address = ? ORDER BY name ASC',
{ address }
) or {}
end
return MySQL.query.await(
'SELECT * FROM pc_live_mail_folders WHERE identifier = ? AND address IS NULL ORDER BY name ASC',
{ identifier }
) or {}
end
---@param identifier string
---@param address string|nil
---@param name string
---@return number
function DB.CreateFolder(identifier, address, name)
return MySQL.insert.await(
'INSERT INTO pc_live_mail_folders (identifier, address, name) VALUES (?, ?, ?)',
{ identifier, (address ~= '' and address or nil), name }
)
end
--- Gibt den Ordner zurueck, wenn er zu diesem Postfach gehoert sonst nil.
--- Damit haengt jede Ordneraktion an einer Besitzpruefung statt an der Id
--- allein, die der Client frei mitschicken kann.
---@param folderId number
---@param identifier string
---@param address string|nil
---@return table|nil
function DB.GetFolder(folderId, identifier, address)
if address and address ~= '' then
return MySQL.single.await(
'SELECT * FROM pc_live_mail_folders WHERE id = ? AND address = ?',
{ folderId, address }
)
end
return MySQL.single.await(
'SELECT * FROM pc_live_mail_folders WHERE id = ? AND identifier = ? AND address IS NULL',
{ folderId, identifier }
)
end
---@param folderId number
---@param identifier string
---@param address string|nil
function DB.DeleteFolder(folderId, identifier, address)
-- Mails zurueck in den Posteingang. Bei einem geteilten Postfach betrifft
-- das die Kopien aller Beteiligten, sonst blieben deren Mails in einem
-- Ordner haengen, den es nicht mehr gibt.
if address and address ~= '' then
MySQL.update.await(
'UPDATE pc_live_mail SET folder_id = NULL WHERE folder_id = ? AND to_address = ?',
{ folderId, address }
)
else
MySQL.update.await(
'UPDATE pc_live_mail SET folder_id = NULL WHERE folder_id = ? AND to_identifier = ?',
{ folderId, identifier }
)
end
MySQL.update.await('DELETE FROM pc_live_mail_folders WHERE id = ?', { folderId })
end
--- Mail einsortieren.
--- Im geteilten Postfach wandern alle Kopien derselben Zustellung mit, damit
--- die Ablage fuer alle gleich aussieht.
---@param mailId number
---@param folderId number|nil nil = Posteingang
---@param identifier string
---@param address string|nil
function DB.MoveMailToFolder(mailId, folderId, identifier, address)
local mail = MySQL.single.await(
'SELECT id, delivery_id, to_address FROM pc_live_mail WHERE id = ? AND to_identifier = ?',
{ mailId, identifier }
)
if not mail then return end
local shared = address and address ~= '' and mail.to_address == address
if shared and mail.delivery_id then
MySQL.update.await(
'UPDATE pc_live_mail SET folder_id = ? WHERE delivery_id = ? AND to_address = ?',
{ folderId, mail.delivery_id, address }
)
return
end
-- Aeltere Mails haben keine delivery_id dann bleibt es bei der Kopie.
MySQL.update.await(
'UPDATE pc_live_mail SET folder_id = ? WHERE id = ? AND to_identifier = ?',
{ folderId, mailId, identifier }
)
end
---@param folderId number
---@param identifier string
---@return table[]
function DB.GetFolderMails(folderId, identifier)
return MySQL.query.await(
'SELECT * FROM pc_live_mail WHERE folder_id = ? AND to_identifier = ? ORDER BY sent_at DESC',
{ folderId, identifier }
) or {}
end
---@param identifier string
---@return table[]
function DB.GetInboxOnly(identifier)
return MySQL.query.await(
'SELECT * FROM pc_live_mail WHERE to_identifier = ? AND folder_id IS NULL ORDER BY sent_at DESC LIMIT 100',
{ identifier }
)
end
-- ── Calendar ───────────────────────────────────────────────────────────────
--
-- Ein Termin ist entweder privat (address IS NULL) oder gehoert einem Postfach.
-- Postfachtermine sind entweder nur fuer dessen Leute sichtbar ('shared') oder
-- fuer alle ('public') bearbeiten darf sie in beiden Faellen nur, wer das
-- Postfach bedienen darf.
--- Alle Termine, die diese Person sehen darf.
---@param identifier string
---@param addresses string[] Postfaecher, auf die sie Zugriff hat
---@return table[]
function DB.GetCalendar(identifier, addresses)
addresses = addresses or {}
local params = { identifier }
local clauses = { '(c.identifier = ? AND c.address IS NULL)' }
if #addresses > 0 then
local marks = {}
for _, addr in ipairs(addresses) do
marks[#marks + 1] = '?'
params[#params + 1] = addr
end
clauses[#clauses + 1] = ('(c.address IN (%s))'):format(table.concat(marks, ','))
end
-- Oeffentliche Termine sieht jeder, auch ohne Zugriff auf das Postfach.
clauses[#clauses + 1] = "(c.visibility = 'public')"
return MySQL.query.await(
'SELECT c.* FROM pc_live_calendar c WHERE '
.. table.concat(clauses, ' OR ') .. ' ORDER BY c.start_at ASC',
params
) or {}
end
---@param id number
---@return table|nil
function DB.GetCalendarEvent(id)
return MySQL.single.await('SELECT * FROM pc_live_calendar WHERE id = ?', { id })
end
---@param identifier string
---@param address string|nil '' oder nil = privat
---@param visibility string 'private' | 'shared' | 'public'
---@param data table
---@return number
function DB.AddCalendarEvent(identifier, address, visibility, data)
return MySQL.insert.await(
'INSERT INTO pc_live_calendar'
.. ' (identifier, address, visibility, title, description, start_at, end_at, color)'
.. ' VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
{ identifier, (address ~= '' and address or nil), visibility,
data.title, data.description or '', data.start_at,
data.end_at or nil, data.color or '#00aaff' }
)
end
--- Aendern. Die Berechtigung prueft der Aufrufer (server/calendar.lua)
--- hier steht keine Bedingung mehr, weil ein Postfachtermin auch von jemandem
--- geaendert werden darf, der ihn nicht angelegt hat.
---@param id number
---@param address string|nil
---@param visibility string
---@param data table
function DB.UpdateCalendarEvent(id, address, visibility, data)
MySQL.update.await(
'UPDATE pc_live_calendar SET address=?, visibility=?, title=?, description=?,'
.. ' start_at=?, end_at=?, color=? WHERE id=?',
{ (address ~= '' and address or nil), visibility,
data.title, data.description or '', data.start_at,
data.end_at or nil, data.color or '#00aaff', id }
)
end
---@param id number
function DB.DeleteCalendarEvent(id)
MySQL.update.await('DELETE FROM pc_live_calendar WHERE id = ?', { id })
end
-- ── Contacts ───────────────────────────────────────────────────────────────
---@param identifier string
---@return table[]
function DB.GetContacts(identifier)
return MySQL.query.await(
'SELECT * FROM pc_live_contacts WHERE identifier = ? ORDER BY name ASC',
{ identifier }
)
end
---@param identifier string
---@param data table
---@return number
function DB.AddContact(identifier, data)
return MySQL.insert.await(
'INSERT INTO pc_live_contacts (identifier, name, email, phone, notes) VALUES (?, ?, ?, ?, ?)',
{ identifier, data.name, data.email or '', data.phone or '', data.notes or '' }
)
end
---@param id number
---@param identifier string
---@param data table
function DB.UpdateContact(id, identifier, data)
MySQL.update.await(
'UPDATE pc_live_contacts SET name=?, email=?, phone=?, notes=? WHERE id=? AND identifier=?',
{ data.name, data.email or '', data.phone or '', data.notes or '', id, identifier }
)
end
---@param id number
---@param identifier string
function DB.DeleteContact(id, identifier)
MySQL.update.await(
'DELETE FROM pc_live_contacts WHERE id = ? AND identifier = ?',
{ id, identifier }
)
end