-- ───────────────────────────────────────────────────────────────────────────── -- 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