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.
This commit is contained in:
commit
be502d2834
46 changed files with 13119 additions and 0 deletions
373
server/mail.lua
Normal file
373
server/mail.lua
Normal file
|
|
@ -0,0 +1,373 @@
|
|||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- Mail handlers
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Mail = {}
|
||||
|
||||
-- Rate limiting: identifier -> { count, resetAt }
|
||||
local _rateMap = {}
|
||||
|
||||
--- Darf diese Person das Postfach benutzen?
|
||||
--- '' bedeutet "persoenliches Postfach" und ist immer erlaubt. Ohne diese
|
||||
--- Pruefung koennte der Client jede beliebige Adresse mitschicken und damit in
|
||||
--- fremden Ordnern lesen und schreiben.
|
||||
---@return string die gepruefte Adresse, oder '' bei fehlendem Zugriff
|
||||
local function CheckMailbox(identifier, address)
|
||||
address = tostring(address or '')
|
||||
if address == '' then return '' end
|
||||
if GetResourceState('ic-mail') ~= 'started' then return '' end
|
||||
|
||||
local ok = exports['ic-mail']:CanAccessAddress(identifier, address)
|
||||
return ok and address or ''
|
||||
end
|
||||
|
||||
--- Postfachliste samt Signaturen an einen Spieler schicken.
|
||||
--- Steht hier als Funktion, weil sie aus mehreren Ereignissen gebraucht wird –
|
||||
--- ein TriggerEvent auf das Netzwerkereignis waere kein Ersatz: dort ist
|
||||
--- `source` nicht der Spieler, und der Aufruf liefe ins Leere.
|
||||
local function SendMailboxes(src, identifier)
|
||||
local mailboxes = {}
|
||||
local personalAddr = ''
|
||||
|
||||
if GetResourceState('ic-mail') == 'started' then
|
||||
personalAddr = exports['ic-mail']:GetPrimaryAddress(identifier) or ''
|
||||
local addrs = exports['ic-mail']:GetAddressesForIdentifier(identifier) or {}
|
||||
table.sort(addrs, function(a, b)
|
||||
if a.type == 'personal' and b.type ~= 'personal' then return true end
|
||||
if a.type ~= 'personal' and b.type == 'personal' then return false end
|
||||
return (a.address or '') < (b.address or '')
|
||||
end)
|
||||
mailboxes = addrs
|
||||
end
|
||||
|
||||
local personalSig = ''
|
||||
for _, mb in ipairs(mailboxes) do
|
||||
if mb.address == personalAddr then personalSig = mb.signature or '' end
|
||||
end
|
||||
|
||||
TriggerClientEvent('pc-live:client:sharedMailboxes', src, mailboxes, personalAddr, personalSig)
|
||||
end
|
||||
|
||||
local function CheckRateLimit(identifier)
|
||||
local now = os.time()
|
||||
local entry = _rateMap[identifier]
|
||||
if not entry or now >= entry.resetAt then
|
||||
_rateMap[identifier] = { count = 1, resetAt = now + 60 }
|
||||
return true
|
||||
end
|
||||
if entry.count >= Config.MailRateLimit then
|
||||
return false
|
||||
end
|
||||
entry.count = entry.count + 1
|
||||
return true
|
||||
end
|
||||
|
||||
-- ── Fetch inbox ─────────────────────────────────────────────────────────────
|
||||
RegisterNetEvent('pc-live:server:getInbox', function()
|
||||
local src = source
|
||||
local user = ActiveSessions[src]
|
||||
if not user then return end
|
||||
|
||||
local rows = DB.GetInboxOnly(user.identifier)
|
||||
local folders = DB.GetFolders(user.identifier, '')
|
||||
TriggerClientEvent('pc-live:client:inboxData', src, rows, folders)
|
||||
end)
|
||||
|
||||
-- ── Fetch sent ───────────────────────────────────────────────────────────────
|
||||
RegisterNetEvent('pc-live:server:getSent', function()
|
||||
local src = source
|
||||
local user = ActiveSessions[src]
|
||||
if not user then return end
|
||||
|
||||
local rows = DB.GetSent(user.identifier)
|
||||
TriggerClientEvent('pc-live:client:sentData', src, rows)
|
||||
end)
|
||||
|
||||
-- ── Fetch folder mails ───────────────────────────────────────────────────────
|
||||
RegisterNetEvent('pc-live:server:getFolderMails', function(folderId, address)
|
||||
local src = source
|
||||
local user = ActiveSessions[src]
|
||||
if not user then return end
|
||||
if type(folderId) ~= "number" then return end
|
||||
|
||||
local box = CheckMailbox(user.identifier, address)
|
||||
if not DB.GetFolder(folderId, user.identifier, box) then return end
|
||||
|
||||
local rows = DB.GetFolderMails(folderId, user.identifier)
|
||||
TriggerClientEvent('pc-live:client:folderMailsData', src, folderId, rows)
|
||||
end)
|
||||
|
||||
-- ── Ordner eines Postfachs holen ─────────────────────────────────────────────
|
||||
-- Wird beim Wechsel des Postfachs gerufen: jedes Postfach hat eigene Ordner.
|
||||
RegisterNetEvent('pc-live:server:getFolders', function(address)
|
||||
local src = source
|
||||
local user = ActiveSessions[src]
|
||||
if not user then return end
|
||||
|
||||
local box = CheckMailbox(user.identifier, address)
|
||||
TriggerClientEvent('pc-live:client:foldersData', src, box,
|
||||
DB.GetFolders(user.identifier, box))
|
||||
end)
|
||||
|
||||
-- ── Create folder ────────────────────────────────────────────────────────────
|
||||
RegisterNetEvent('pc-live:server:createFolder', function(name, address)
|
||||
local src = source
|
||||
local user = ActiveSessions[src]
|
||||
if not user then return end
|
||||
name = Utils.Sanitize(tostring(name or "")):sub(1, 60)
|
||||
if #name < 1 then return end
|
||||
|
||||
local box = CheckMailbox(user.identifier, address)
|
||||
local id = DB.CreateFolder(user.identifier, box, name)
|
||||
|
||||
TriggerClientEvent('pc-live:client:folderCreated', src, { id = id, name = name, address = box })
|
||||
|
||||
-- Im geteilten Postfach sehen die anderen den Ordner sofort.
|
||||
if box ~= '' then
|
||||
for playerSrc, session in pairs(ActiveSessions) do
|
||||
if playerSrc ~= src and CheckMailbox(session.identifier, box) ~= '' then
|
||||
TriggerClientEvent('pc-live:client:foldersData', playerSrc, box,
|
||||
DB.GetFolders(session.identifier, box))
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- ── Delete folder ────────────────────────────────────────────────────────────
|
||||
RegisterNetEvent('pc-live:server:deleteFolder', function(folderId, address)
|
||||
local src = source
|
||||
local user = ActiveSessions[src]
|
||||
if not user then return end
|
||||
if type(folderId) ~= "number" then return end
|
||||
|
||||
local box = CheckMailbox(user.identifier, address)
|
||||
if not DB.GetFolder(folderId, user.identifier, box) then return end
|
||||
|
||||
DB.DeleteFolder(folderId, user.identifier, box)
|
||||
TriggerClientEvent('pc-live:client:folderDeleted', src, folderId, box)
|
||||
|
||||
if box ~= '' then
|
||||
for playerSrc, session in pairs(ActiveSessions) do
|
||||
if playerSrc ~= src and CheckMailbox(session.identifier, box) ~= '' then
|
||||
TriggerClientEvent('pc-live:client:folderDeleted', playerSrc, folderId, box)
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- ── Move mail to folder ──────────────────────────────────────────────────────
|
||||
RegisterNetEvent('pc-live:server:moveMail', function(mailId, folderId, address)
|
||||
local src = source
|
||||
local user = ActiveSessions[src]
|
||||
if not user then return end
|
||||
if type(mailId) ~= "number" then return end
|
||||
-- folderId may be nil (move to inbox)
|
||||
if folderId ~= nil and type(folderId) ~= "number" then return end
|
||||
|
||||
local box = CheckMailbox(user.identifier, address)
|
||||
if folderId ~= nil and not DB.GetFolder(folderId, user.identifier, box) then return end
|
||||
|
||||
DB.MoveMailToFolder(mailId, folderId, user.identifier, box)
|
||||
TriggerClientEvent('pc-live:client:mailMoved', src, mailId, folderId)
|
||||
end)
|
||||
|
||||
-- ── Read mail ───────────────────────────────────────────────────────────────
|
||||
RegisterNetEvent('pc-live:server:readMail', function(mailId)
|
||||
local src = source
|
||||
local user = ActiveSessions[src]
|
||||
if not user then return end
|
||||
if type(mailId) ~= "number" then return end
|
||||
|
||||
local mail = DB.GetMail(mailId, user.identifier)
|
||||
if not mail then return end
|
||||
|
||||
DB.MarkRead(mailId, user.identifier)
|
||||
mail.is_read = 1
|
||||
TriggerClientEvent('pc-live:client:mailContent', src, mail)
|
||||
end)
|
||||
|
||||
-- ── Send mail ────────────────────────────────────────────────────────────────
|
||||
RegisterNetEvent('pc-live:server:sendMail', function(payload)
|
||||
local src = source
|
||||
local user = ActiveSessions[src]
|
||||
if not user then return end
|
||||
|
||||
-- Validate payload shape
|
||||
if type(payload) ~= "table" then return end
|
||||
|
||||
local toId = Utils.Sanitize(tostring(payload.to or "")):sub(1, 120)
|
||||
local subject = Utils.Sanitize(tostring(payload.subject or "")):sub(1, Config.MailMaxSubject)
|
||||
local body = Utils.Sanitize(tostring(payload.body or "")):sub(1, Config.MailMaxBody)
|
||||
|
||||
if #toId < 3 or #subject < 1 or #body < 1 then
|
||||
Bridge.Notify(src, "Invalid mail data.", "error")
|
||||
return
|
||||
end
|
||||
|
||||
-- Rate limit
|
||||
if not CheckRateLimit(user.identifier) then
|
||||
Bridge.Notify(src, "You are sending mails too fast. Please wait.", "error")
|
||||
return
|
||||
end
|
||||
|
||||
-- Absender-Adresse ermitteln (ic-mail optional)
|
||||
local fromDisplay = user.identifier
|
||||
if GetResourceState and GetResourceState('ic-mail') == 'started' then
|
||||
local addr = exports['ic-mail']:GetPrimaryAddress(user.identifier)
|
||||
if addr then fromDisplay = addr end
|
||||
|
||||
-- Versand aus geteiltem Postfach: Zugriff prüfen
|
||||
local fromMailbox = Utils.Sanitize(tostring(payload.fromMailbox or '')):sub(1, 120)
|
||||
if fromMailbox ~= '' and fromMailbox ~= fromDisplay then
|
||||
local addrs = exports['ic-mail']:GetAddressesForIdentifier(user.identifier) or {}
|
||||
for _, a in ipairs(addrs) do
|
||||
if a.address == fromMailbox then
|
||||
fromDisplay = fromMailbox
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Empfänger auflösen: @-Adresse → Identifier-Liste, sonst direkt
|
||||
local recipients = {}
|
||||
if toId:find('@') and GetResourceState and GetResourceState('ic-mail') == 'started' then
|
||||
local resolved = exports['ic-mail']:ResolveAddress(toId)
|
||||
if resolved and resolved.identifiers and #resolved.identifiers > 0 then
|
||||
recipients = resolved.identifiers
|
||||
else
|
||||
Bridge.Notify(src, "Empfänger-Adresse nicht gefunden.", "error")
|
||||
return
|
||||
end
|
||||
else
|
||||
recipients = { toId }
|
||||
end
|
||||
|
||||
-- Eine Kennung fuer diese Zustellung. Alle Kopien tragen sie, damit das
|
||||
-- Einsortieren in einen Ordner im geteilten Postfach fuer alle gilt.
|
||||
local deliveryId = ('%08x-%04x-%04x-%04x-%012x'):format(
|
||||
os.time(), math.random(0, 0xFFFF), math.random(0, 0xFFFF),
|
||||
math.random(0, 0xFFFF), math.random(0, 0xFFFFFFFFFFFF))
|
||||
|
||||
-- Mail an alle Empfänger senden (pc_live_mail + ic-mail Dual-Write)
|
||||
for _, recipId in ipairs(recipients) do
|
||||
DB.SendMailExtended(recipId, fromDisplay, user.identifier, toId, subject, body, deliveryId)
|
||||
-- ic-mail parallel benachrichtigen (zentrale Mailplattform)
|
||||
pcall(function()
|
||||
if GetResourceState('ic-mail') == 'started' then
|
||||
exports['ic-mail']:SendSystemMail(recipId, subject, body)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
Bridge.Notify(src, "Mail gesendet.", "success")
|
||||
TriggerClientEvent('pc-live:client:mailSent', src)
|
||||
|
||||
-- Online-Empfänger benachrichtigen
|
||||
for playerSrc, session in pairs(ActiveSessions) do
|
||||
for _, recipId in ipairs(recipients) do
|
||||
if session.identifier == recipId then
|
||||
TriggerClientEvent('pc-live:client:newMailNotify', playerSrc)
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- ── Delete mail ─────────────────────────────────────────────────────────────
|
||||
RegisterNetEvent('pc-live:server:deleteMail', function(mailId)
|
||||
local src = source
|
||||
local user = ActiveSessions[src]
|
||||
if not user then return end
|
||||
if type(mailId) ~= "number" then return end
|
||||
|
||||
DB.DeleteMail(mailId, user.identifier)
|
||||
TriggerClientEvent('pc-live:client:mailDeleted', src, mailId)
|
||||
end)
|
||||
|
||||
-- ── Geteilte Postfächer (ic-mail Integration) ────────────────────────────────
|
||||
RegisterNetEvent('pc-live:server:getSharedMailboxes', function()
|
||||
local src = source
|
||||
local user = ActiveSessions[src]
|
||||
if not user then return end
|
||||
|
||||
SendMailboxes(src, user.identifier)
|
||||
end)
|
||||
|
||||
-- ── Mail-Adresse prüfen (Ersteinrichtung) ────────────────────────────────────
|
||||
|
||||
RegisterNetEvent('pc-live:server:checkMailAddress', function()
|
||||
local src = source
|
||||
local user = ActiveSessions[src]
|
||||
if not user then return end
|
||||
|
||||
if GetResourceState('ic-mail') ~= 'started' then return end
|
||||
|
||||
local existing = exports['ic-mail']:GetPrimaryAddress(user.identifier)
|
||||
if existing then
|
||||
-- Adresse vorhanden → Status senden, Postfach lädt der Client selbst nach
|
||||
TriggerClientEvent('pc-live:client:mailAddressStatus', src, true, existing, nil)
|
||||
return
|
||||
end
|
||||
|
||||
local realms = exports['ic-mail']:GetRealms() or {}
|
||||
local citizenRealms = {}
|
||||
for _, r in ipairs(realms) do
|
||||
if r.realm_type == 'citizen' and r.active then
|
||||
table.insert(citizenRealms, r)
|
||||
end
|
||||
end
|
||||
TriggerClientEvent('pc-live:client:mailAddressStatus', src, false, nil, citizenRealms)
|
||||
end)
|
||||
|
||||
RegisterNetEvent('pc-live:server:createMailAddress', function(username)
|
||||
local src = source
|
||||
local user = ActiveSessions[src]
|
||||
if not user then return end
|
||||
if type(username) ~= 'string' or #username < 2 or #username > 40 then
|
||||
TriggerClientEvent('pc-live:client:mailCreateResult', src, false, 'Benutzername muss 2-40 Zeichen haben')
|
||||
return
|
||||
end
|
||||
username = username:lower():gsub('[^a-z0-9._%-]', ''):gsub('^%.+', ''):gsub('%.+$', '')
|
||||
if #username < 2 then
|
||||
TriggerClientEvent('pc-live:client:mailCreateResult', src, false, 'Ungültiger Benutzername')
|
||||
return
|
||||
end
|
||||
if GetResourceState('ic-mail') ~= 'started' then
|
||||
TriggerClientEvent('pc-live:client:mailCreateResult', src, false, 'Mail-System nicht verfügbar')
|
||||
return
|
||||
end
|
||||
local existing = exports['ic-mail']:GetPrimaryAddress(user.identifier)
|
||||
if existing then
|
||||
TriggerClientEvent('pc-live:client:mailCreateResult', src, true, nil, existing)
|
||||
return
|
||||
end
|
||||
local addr = exports['ic-mail']:EnsurePersonalAddress(user.identifier, username)
|
||||
if addr then
|
||||
TriggerClientEvent('pc-live:client:mailCreateResult', src, true, nil, addr)
|
||||
else
|
||||
TriggerClientEvent('pc-live:client:mailCreateResult', src, false, 'Benutzername bereits vergeben')
|
||||
end
|
||||
end)
|
||||
|
||||
-- ── Signaturen (ic-mail) ─────────────────────────────────────────────────────
|
||||
-- Die Signatur gehoert zum Postfach: eine Firmenadresse traegt dieselbe
|
||||
-- Fusszeile, egal wer sie gerade bedient. Geprueft wird in ic-mail.
|
||||
|
||||
RegisterNetEvent('pc-live:server:setSignature', function(address, signature)
|
||||
local src = source
|
||||
local user = ActiveSessions[src]
|
||||
if not user then return end
|
||||
|
||||
if GetResourceState('ic-mail') ~= 'started' then
|
||||
TriggerClientEvent('pc-live:client:signatureSaved', src, false, 'Mailsystem nicht verfügbar.')
|
||||
return
|
||||
end
|
||||
|
||||
local ok, err = exports['ic-mail']:SetSignature(
|
||||
tostring(address or ''), user.identifier, tostring(signature or ''))
|
||||
|
||||
TriggerClientEvent('pc-live:client:signatureSaved', src, ok == true, err)
|
||||
|
||||
-- Postfachliste nachschieben, damit die neue Signatur sofort greift.
|
||||
if ok then SendMailboxes(src, user.identifier) end
|
||||
end)
|
||||
Loading…
Add table
Add a link
Reference in a new issue