- aether-medical: Stationen/Belegung, Intensivstation, Blutbank, Fuhrpark, Nachrichten/Aushänge, Berichte und Textbausteine (server + NUI) - aether-admin: vollständiges Server-Admin-Panel als echtes FiveM-Resource, an aether-core gekoppelt (Rechte über aether_users.admin_group), mit WebRTC-Live-Kameras (Bildschirmfreigabe) und separatem Media-Server (SFU) - Prototypen admin/ vervollständigt - SQL-Schema aether_admin.sql - Docker-Compose mit MariaDB und idempotenter Auto-Migration (schema_migrations) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
555 lines
20 KiB
Lua
555 lines
20 KiB
Lua
-- =====================================================================
|
|
-- Aether Phone — Serverlogik
|
|
-- Telefonnummer, Kontakte, Nachrichten, Anrufe und Sicherheit.
|
|
-- Die PIN verlaesst den Server nicht.
|
|
-- =====================================================================
|
|
|
|
local Core = exports['aether-core']
|
|
local Inventory = exports['aether-inventory']
|
|
|
|
-- Laufende Anrufe: [anruferQuelle] = { ziel = quelle, nummer = ..., seit = ... }
|
|
local aktiveAnrufe = {}
|
|
|
|
-- Wer hat sein Handy gerade entsperrt? (fuer Aktionen, die das voraussetzen)
|
|
local entsperrt = {}
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Hilfsfunktionen
|
|
-- ---------------------------------------------------------------------
|
|
|
|
local function CharakterVon(quelle)
|
|
return Core:HoleCharakterId(quelle)
|
|
end
|
|
|
|
--- Erzeugt eine freie Telefonnummer im Format 555-XXXX
|
|
--- @return string
|
|
local function ErzeugeNummer()
|
|
for _ = 1, 50 do
|
|
local nummer = ('555-%04d'):format(math.random(0, 9999))
|
|
local belegt = MySQL.scalar.await('SELECT `character` FROM aether_phone WHERE number = ?', { nummer })
|
|
if not belegt then return nummer end
|
|
end
|
|
-- Notfalls eine Nummer mit anderer Vorwahl
|
|
return ('%03d-%04d'):format(math.random(200, 999), math.random(0, 9999))
|
|
end
|
|
|
|
--- Laedt (oder erzeugt) den Handy-Datensatz eines Charakters
|
|
--- @param charakterId number Charakter-ID
|
|
--- @return table
|
|
local function HoleGeraet(charakterId)
|
|
local zeilen = MySQL.query.await('SELECT * FROM aether_phone WHERE `character` = ?', { charakterId })
|
|
|
|
if zeilen and zeilen[1] then return zeilen[1] end
|
|
|
|
local nummer = ErzeugeNummer()
|
|
MySQL.insert.await('INSERT INTO aether_phone (`character`, number) VALUES (?, ?)',
|
|
{ charakterId, nummer })
|
|
Aether.Info(('Telefonnummer %s fuer Charakter #%d vergeben.'):format(nummer, charakterId))
|
|
|
|
local neue = MySQL.query.await('SELECT * FROM aether_phone WHERE `character` = ?', { charakterId })
|
|
return neue[1]
|
|
end
|
|
|
|
--- Sucht den Charakter zu einer Telefonnummer
|
|
--- @param nummer string Telefonnummer
|
|
--- @return number|nil Charakter-ID
|
|
local function CharakterZuNummer(nummer)
|
|
return MySQL.scalar.await('SELECT `character` FROM aether_phone WHERE number = ?', { nummer })
|
|
end
|
|
|
|
--- Sucht die Server-ID zu einer Telefonnummer (nur wenn online)
|
|
--- @param nummer string Telefonnummer
|
|
--- @return number|nil
|
|
local function QuelleZuNummer(nummer)
|
|
local charakterId = CharakterZuNummer(nummer)
|
|
if not charakterId then return nil end
|
|
return Core:HoleQuelleNachCharakter(charakterId)
|
|
end
|
|
|
|
--- Prueft die Hardware: Handy, SIM und Akku muessen vorhanden sein
|
|
--- @param quelle number Server-ID
|
|
--- @return boolean hatHandy, boolean hatSim
|
|
local function PruefeHardware(quelle)
|
|
local handy = (Inventory:ZaehleItem(quelle, Handy.ITEM_HANDY) or 0) > 0
|
|
local sim = (Inventory:ZaehleItem(quelle, Handy.ITEM_SIM) or 0) > 0
|
|
return handy, sim
|
|
end
|
|
|
|
--- Laedt die Kontakte eines Charakters
|
|
local function HoleKontakte(charakterId)
|
|
local zeilen = MySQL.query.await(
|
|
'SELECT id, name, number FROM aether_phone_contacts WHERE `character` = ? ORDER BY name ASC',
|
|
{ charakterId }) or {}
|
|
|
|
local liste = {}
|
|
-- Notrufe stehen immer oben
|
|
for nummer, eintrag in pairs(Handy.NOTRUFE) do
|
|
liste[#liste + 1] = { id = 0, name = eintrag.name, nummer = nummer, notruf = true }
|
|
end
|
|
for _, zeile in ipairs(zeilen) do
|
|
liste[#liste + 1] = { id = zeile.id, name = zeile.name, nummer = zeile.number }
|
|
end
|
|
return liste
|
|
end
|
|
|
|
--- Laedt die Unterhaltungen (letzte Nachricht je Gegenstelle)
|
|
local function HoleChats(nummer)
|
|
local zeilen = MySQL.query.await([[
|
|
SELECT sender, receiver, text, gelesen, created_at
|
|
FROM aether_phone_messages
|
|
WHERE sender = ? OR receiver = ?
|
|
ORDER BY id DESC
|
|
LIMIT 300
|
|
]], { nummer, nummer }) or {}
|
|
|
|
local chats = {}
|
|
for _, zeile in ipairs(zeilen) do
|
|
local gegenstelle = (zeile.sender == nummer) and zeile.receiver or zeile.sender
|
|
chats[gegenstelle] = chats[gegenstelle] or {}
|
|
table.insert(chats[gegenstelle], 1, {
|
|
von = (zeile.sender == nummer) and 'me' or 'them',
|
|
text = zeile.text,
|
|
zeit = tostring(zeile.created_at):sub(12, 16),
|
|
})
|
|
end
|
|
return chats
|
|
end
|
|
|
|
--- Laedt den Anrufverlauf
|
|
local function HoleAnrufe(nummer)
|
|
local zeilen = MySQL.query.await([[
|
|
SELECT caller, callee, verpasst, created_at
|
|
FROM aether_phone_calls
|
|
WHERE caller = ? OR callee = ?
|
|
ORDER BY id DESC LIMIT 30
|
|
]], { nummer, nummer }) or {}
|
|
|
|
local liste = {}
|
|
for _, zeile in ipairs(zeilen) do
|
|
local eingehend = zeile.callee == nummer
|
|
liste[#liste + 1] = {
|
|
nummer = eingehend and zeile.caller or zeile.callee,
|
|
art = eingehend and 'eingehend' or 'ausgehend',
|
|
verpasst = zeile.verpasst == 1,
|
|
zeit = tostring(zeile.created_at):sub(1, 16),
|
|
}
|
|
end
|
|
return liste
|
|
end
|
|
|
|
--- Baut den kompletten Zustand fuer die Oberflaeche (ohne PIN!)
|
|
--- @param quelle number Server-ID
|
|
--- @return table|nil
|
|
local function BaueZustand(quelle)
|
|
local charakterId = CharakterVon(quelle)
|
|
if not charakterId then return nil end
|
|
|
|
local person = Core:HoleSpielerInfo(quelle)
|
|
local geraet = HoleGeraet(charakterId)
|
|
local hatHandy, hatSim = PruefeHardware(quelle)
|
|
|
|
local dock = { 'phone', 'messages', 'bank', 'camera' }
|
|
local erfolg, entschluesselt = pcall(json.decode, geraet.dock or '')
|
|
if erfolg and type(entschluesselt) == 'table' then dock = entschluesselt end
|
|
|
|
local freigeschaltet = {}
|
|
local ok2, entsch2 = pcall(json.decode, geraet.unlocked or '{}')
|
|
if ok2 and type(entsch2) == 'table' then freigeschaltet = entsch2 end
|
|
|
|
return {
|
|
name = ((person and person.vorname or '') .. ' ' .. (person and person.nachname or '')),
|
|
nummer = geraet.number,
|
|
kontostand = person and person.konto or 0,
|
|
kontonummer = person and person.iban or '—',
|
|
theme = geraet.theme,
|
|
wallpaper = geraet.wallpaper,
|
|
dockApps = dock,
|
|
freigeschaltet = freigeschaltet,
|
|
handy = hatHandy,
|
|
sim = hatSim,
|
|
akku = geraet.battery,
|
|
-- Die PIN selbst wird NIE geschickt, nur ob eine eingerichtet ist
|
|
pinGesetzt = geraet.pin ~= nil,
|
|
faceId = geraet.faceid == 1,
|
|
kontakte = HoleKontakte(charakterId),
|
|
chats = HoleChats(geraet.number),
|
|
anrufe = HoleAnrufe(geraet.number),
|
|
}
|
|
end
|
|
|
|
--- Schickt den Zustand an einen Spieler
|
|
local function SendeZustand(quelle, hinweis)
|
|
local zustand = BaueZustand(quelle)
|
|
if not zustand then return end
|
|
zustand.hinweis = hinweis
|
|
TriggerClientEvent('aether:phone:zustand', quelle, zustand)
|
|
end
|
|
|
|
local function Ablehnen(quelle, grund)
|
|
TriggerClientEvent('aether:phone:fehler', quelle, grund)
|
|
end
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Handy oeffnen
|
|
-- ---------------------------------------------------------------------
|
|
RegisterNetEvent('aether:phone:oeffnen', function()
|
|
local quelle = source
|
|
local charakterId = CharakterVon(quelle)
|
|
if not charakterId then return end
|
|
|
|
local hatHandy = PruefeHardware(quelle)
|
|
if not hatHandy then
|
|
Ablehnen(quelle, 'Du hast kein Handy dabei.')
|
|
return
|
|
end
|
|
|
|
local geraet = HoleGeraet(charakterId)
|
|
if geraet.battery <= 0 then
|
|
Ablehnen(quelle, 'Der Akku ist leer.')
|
|
return
|
|
end
|
|
|
|
local zustand = BaueZustand(quelle)
|
|
if zustand then
|
|
TriggerClientEvent('aether:phone:zeige', quelle, zustand)
|
|
end
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Sicherheit: Ersteinrichtung und Entsperren
|
|
-- ---------------------------------------------------------------------
|
|
RegisterNetEvent('aether:phone:pinEinrichten', function(pin, faceId)
|
|
local quelle = source
|
|
local charakterId = CharakterVon(quelle)
|
|
if not charakterId then return end
|
|
|
|
if type(pin) ~= 'string' or not pin:match('^%d%d%d%d$') then
|
|
Ablehnen(quelle, 'Der Code muss aus vier Ziffern bestehen.')
|
|
return
|
|
end
|
|
|
|
MySQL.update.await('UPDATE aether_phone SET pin = ?, faceid = ? WHERE `character` = ?',
|
|
{ pin, faceId and 1 or 0, charakterId })
|
|
|
|
entsperrt[quelle] = true
|
|
TriggerClientEvent('aether:phone:entsperrt', quelle, true)
|
|
end)
|
|
|
|
RegisterNetEvent('aether:phone:entsperren', function(pin)
|
|
local quelle = source
|
|
local charakterId = CharakterVon(quelle)
|
|
if not charakterId then return end
|
|
|
|
local gespeichert = MySQL.scalar.await(
|
|
'SELECT pin FROM aether_phone WHERE `character` = ?', { charakterId })
|
|
|
|
if gespeichert and gespeichert == pin then
|
|
entsperrt[quelle] = true
|
|
TriggerClientEvent('aether:phone:entsperrt', quelle, true)
|
|
else
|
|
entsperrt[quelle] = false
|
|
TriggerClientEvent('aether:phone:entsperrt', quelle, false)
|
|
end
|
|
end)
|
|
|
|
--- Entsperren per Face-ID (nur wenn dafuer eingerichtet)
|
|
RegisterNetEvent('aether:phone:faceId', function()
|
|
local quelle = source
|
|
local charakterId = CharakterVon(quelle)
|
|
if not charakterId then return end
|
|
|
|
local aktiv = MySQL.scalar.await(
|
|
'SELECT faceid FROM aether_phone WHERE `character` = ?', { charakterId })
|
|
|
|
if aktiv == 1 then
|
|
entsperrt[quelle] = true
|
|
TriggerClientEvent('aether:phone:entsperrt', quelle, true)
|
|
else
|
|
TriggerClientEvent('aether:phone:entsperrt', quelle, false)
|
|
end
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Kontakte
|
|
-- ---------------------------------------------------------------------
|
|
RegisterNetEvent('aether:phone:kontaktSpeichern', function(name, nummer, kontaktId)
|
|
local quelle = source
|
|
local charakterId = CharakterVon(quelle)
|
|
if not charakterId or not entsperrt[quelle] then return end
|
|
|
|
if type(name) ~= 'string' or #name < 1 or #name > 48 then
|
|
Ablehnen(quelle, 'Bitte einen Namen angeben.') return
|
|
end
|
|
if not Handy.NummerGueltig(nummer) then
|
|
Ablehnen(quelle, 'Ungültige Telefonnummer.') return
|
|
end
|
|
|
|
if type(kontaktId) == 'number' and kontaktId > 0 then
|
|
MySQL.update.await(
|
|
'UPDATE aether_phone_contacts SET name = ?, number = ? WHERE id = ? AND `character` = ?',
|
|
{ name, nummer, kontaktId, charakterId })
|
|
else
|
|
MySQL.insert.await(
|
|
'INSERT INTO aether_phone_contacts (`character`, name, number) VALUES (?, ?, ?)',
|
|
{ charakterId, name, nummer })
|
|
end
|
|
|
|
SendeZustand(quelle, 'Kontakt gespeichert.')
|
|
end)
|
|
|
|
RegisterNetEvent('aether:phone:kontaktLoeschen', function(kontaktId)
|
|
local quelle = source
|
|
local charakterId = CharakterVon(quelle)
|
|
if not charakterId or not entsperrt[quelle] then return end
|
|
if type(kontaktId) ~= 'number' then return end
|
|
|
|
MySQL.update.await('DELETE FROM aether_phone_contacts WHERE id = ? AND `character` = ?',
|
|
{ kontaktId, charakterId })
|
|
SendeZustand(quelle, 'Kontakt gelöscht.')
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Nachrichten
|
|
-- ---------------------------------------------------------------------
|
|
RegisterNetEvent('aether:phone:senden', function(anNummer, text)
|
|
local quelle = source
|
|
local charakterId = CharakterVon(quelle)
|
|
if not charakterId or not entsperrt[quelle] then return end
|
|
|
|
local hatHandy, hatSim = PruefeHardware(quelle)
|
|
if not hatHandy or not hatSim then
|
|
Ablehnen(quelle, 'Ohne SIM-Karte kannst du keine Nachrichten senden.')
|
|
return
|
|
end
|
|
|
|
if not Handy.NummerGueltig(anNummer) then
|
|
Ablehnen(quelle, 'Ungültige Telefonnummer.') return
|
|
end
|
|
if type(text) ~= 'string' then return end
|
|
|
|
text = text:gsub('^%s+', ''):gsub('%s+$', '')
|
|
if #text < 1 or #text > 512 then
|
|
Ablehnen(quelle, 'Die Nachricht ist leer oder zu lang.') return
|
|
end
|
|
|
|
local geraet = HoleGeraet(charakterId)
|
|
|
|
MySQL.insert.await(
|
|
'INSERT INTO aether_phone_messages (sender, receiver, text) VALUES (?, ?, ?)',
|
|
{ geraet.number, anNummer, text })
|
|
|
|
-- Empfaenger benachrichtigen, falls online
|
|
local zielQuelle = QuelleZuNummer(anNummer)
|
|
if zielQuelle then
|
|
TriggerClientEvent('aether:phone:neueNachricht', zielQuelle, {
|
|
von = geraet.number,
|
|
text = text,
|
|
zeit = os.date('%H:%M'),
|
|
})
|
|
SendeZustand(zielQuelle)
|
|
end
|
|
|
|
SendeZustand(quelle)
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Anrufe
|
|
-- ---------------------------------------------------------------------
|
|
RegisterNetEvent('aether:phone:anrufen', function(nummer)
|
|
local quelle = source
|
|
local charakterId = CharakterVon(quelle)
|
|
if not charakterId or not entsperrt[quelle] then return end
|
|
|
|
local hatHandy, hatSim = PruefeHardware(quelle)
|
|
if not hatHandy or not hatSim then
|
|
Ablehnen(quelle, 'Ohne SIM-Karte kannst du nicht telefonieren.')
|
|
return
|
|
end
|
|
if not Handy.NummerGueltig(nummer) then
|
|
Ablehnen(quelle, 'Ungültige Telefonnummer.') return
|
|
end
|
|
|
|
local geraet = HoleGeraet(charakterId)
|
|
if nummer == geraet.number then
|
|
Ablehnen(quelle, 'Du kannst dich nicht selbst anrufen.') return
|
|
end
|
|
|
|
-- Notrufe gehen an die Leitstelle der jeweiligen Fraktion
|
|
if Handy.NOTRUFE[nummer] then
|
|
TriggerEvent('aether:phone:notruf', quelle, Handy.NOTRUFE[nummer].fraktion, geraet.number)
|
|
MySQL.insert('INSERT INTO aether_phone_calls (caller, callee) VALUES (?, ?)',
|
|
{ geraet.number, nummer })
|
|
TriggerClientEvent('aether:phone:anrufStatus', quelle, 'notruf',
|
|
Handy.NOTRUFE[nummer].name)
|
|
return
|
|
end
|
|
|
|
local zielQuelle = QuelleZuNummer(nummer)
|
|
if not zielQuelle then
|
|
MySQL.insert('INSERT INTO aether_phone_calls (caller, callee, verpasst) VALUES (?, ?, 1)',
|
|
{ geraet.number, nummer })
|
|
TriggerClientEvent('aether:phone:anrufStatus', quelle, 'nichtErreichbar')
|
|
return
|
|
end
|
|
|
|
-- Ist die Gegenstelle schon im Gespraech?
|
|
for _, anruf in pairs(aktiveAnrufe) do
|
|
if anruf.ziel == zielQuelle or anruf.anrufer == zielQuelle then
|
|
TriggerClientEvent('aether:phone:anrufStatus', quelle, 'besetzt')
|
|
return
|
|
end
|
|
end
|
|
|
|
aktiveAnrufe[quelle] = {
|
|
anrufer = quelle, ziel = zielQuelle,
|
|
nummer = geraet.number, zielNummer = nummer, angenommen = false,
|
|
}
|
|
|
|
TriggerClientEvent('aether:phone:eingehenderAnruf', zielQuelle, geraet.number)
|
|
TriggerClientEvent('aether:phone:anrufStatus', quelle, 'klingelt', nummer)
|
|
end)
|
|
|
|
RegisterNetEvent('aether:phone:annehmen', function()
|
|
local quelle = source
|
|
|
|
for anruferQuelle, anruf in pairs(aktiveAnrufe) do
|
|
if anruf.ziel == quelle then
|
|
anruf.angenommen = true
|
|
MySQL.insert('INSERT INTO aether_phone_calls (caller, callee) VALUES (?, ?)',
|
|
{ anruf.nummer, anruf.zielNummer })
|
|
TriggerClientEvent('aether:phone:anrufStatus', anruferQuelle, 'verbunden')
|
|
TriggerClientEvent('aether:phone:anrufStatus', quelle, 'verbunden')
|
|
return
|
|
end
|
|
end
|
|
end)
|
|
|
|
RegisterNetEvent('aether:phone:auflegen', function()
|
|
local quelle = source
|
|
|
|
for anruferQuelle, anruf in pairs(aktiveAnrufe) do
|
|
if anruferQuelle == quelle or anruf.ziel == quelle then
|
|
-- Nicht angenommene Anrufe als verpasst protokollieren
|
|
if not anruf.angenommen then
|
|
MySQL.insert('INSERT INTO aether_phone_calls (caller, callee, verpasst) VALUES (?, ?, 1)',
|
|
{ anruf.nummer, anruf.zielNummer })
|
|
end
|
|
TriggerClientEvent('aether:phone:anrufStatus', anruferQuelle, 'beendet')
|
|
TriggerClientEvent('aether:phone:anrufStatus', anruf.ziel, 'beendet')
|
|
aktiveAnrufe[anruferQuelle] = nil
|
|
return
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Einstellungen speichern
|
|
-- ---------------------------------------------------------------------
|
|
RegisterNetEvent('aether:phone:einstellungen', function(einstellungen)
|
|
local quelle = source
|
|
local charakterId = CharakterVon(quelle)
|
|
if not charakterId or not entsperrt[quelle] then return end
|
|
if type(einstellungen) ~= 'table' then return end
|
|
|
|
local theme = (einstellungen.theme == 'light') and 'light' or 'dark'
|
|
local wallpaper = tostring(einstellungen.wallpaper or 'wp-aurora'):sub(1, 32)
|
|
local dock = type(einstellungen.dockApps) == 'table' and einstellungen.dockApps or nil
|
|
|
|
MySQL.update.await([[
|
|
UPDATE aether_phone SET theme = ?, wallpaper = ?, dock = COALESCE(?, dock)
|
|
WHERE `character` = ?
|
|
]], { theme, wallpaper, dock and json.encode(dock) or nil, charakterId })
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Akkuverbrauch
|
|
-- ---------------------------------------------------------------------
|
|
CreateThread(function()
|
|
while true do
|
|
Wait(Handy.AKKU_INTERVALL)
|
|
|
|
for _, quelle in ipairs(GetPlayers()) do
|
|
quelle = tonumber(quelle)
|
|
local charakterId = CharakterVon(quelle)
|
|
|
|
if charakterId then
|
|
local hatHandy = PruefeHardware(quelle)
|
|
if hatHandy then
|
|
local geraet = HoleGeraet(charakterId)
|
|
if geraet.battery > 0 then
|
|
local neu = math.max(0, geraet.battery - 1)
|
|
MySQL.update('UPDATE aether_phone SET battery = ? WHERE `character` = ?',
|
|
{ neu, charakterId })
|
|
TriggerClientEvent('aether:phone:akku', quelle, neu)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
|
|
--- Akku austauschen: verbraucht ein Akku-Item und laedt auf 100 %
|
|
RegisterNetEvent('aether:phone:akkuTauschen', function()
|
|
local quelle = source
|
|
local charakterId = CharakterVon(quelle)
|
|
if not charakterId then return end
|
|
|
|
if not Inventory:EntferneItem(quelle, Handy.ITEM_AKKU, 1) then
|
|
Ablehnen(quelle, 'Du hast keinen Ersatz-Akku dabei.')
|
|
return
|
|
end
|
|
|
|
MySQL.update.await('UPDATE aether_phone SET battery = 100 WHERE `character` = ?', { charakterId })
|
|
TriggerClientEvent('aether:phone:akku', quelle, 100)
|
|
SendeZustand(quelle, 'Akku getauscht.')
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Aufraeumen
|
|
-- ---------------------------------------------------------------------
|
|
RegisterNetEvent('aether:phone:schliessen', function()
|
|
entsperrt[source] = false
|
|
end)
|
|
|
|
AddEventHandler('playerDropped', function()
|
|
local quelle = source
|
|
entsperrt[quelle] = nil
|
|
|
|
-- Laufende Anrufe beenden
|
|
for anruferQuelle, anruf in pairs(aktiveAnrufe) do
|
|
if anruferQuelle == quelle or anruf.ziel == quelle then
|
|
local anderer = (anruferQuelle == quelle) and anruf.ziel or anruferQuelle
|
|
TriggerClientEvent('aether:phone:anrufStatus', anderer, 'beendet')
|
|
aktiveAnrufe[anruferQuelle] = nil
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- =====================================================================
|
|
-- EXPORTS
|
|
-- =====================================================================
|
|
|
|
--- Liefert die Telefonnummer eines Spielers
|
|
exports('HoleNummer', function(quelle)
|
|
local charakterId = CharakterVon(quelle)
|
|
if not charakterId then return nil end
|
|
return HoleGeraet(charakterId).number
|
|
end)
|
|
|
|
--- Schickt eine Nachricht von einer Dienstnummer an einen Spieler
|
|
--- (z. B. Rechnung vom EMS, Bestaetigung eines Jobs)
|
|
exports('SendeNachricht', function(anNummer, vonNummer, text)
|
|
if not Handy.NummerGueltig(anNummer) then return false end
|
|
|
|
MySQL.insert('INSERT INTO aether_phone_messages (sender, receiver, text) VALUES (?, ?, ?)',
|
|
{ vonNummer or 'System', anNummer, tostring(text):sub(1, 512) })
|
|
|
|
local zielQuelle = QuelleZuNummer(anNummer)
|
|
if zielQuelle then
|
|
TriggerClientEvent('aether:phone:neueNachricht', zielQuelle, {
|
|
von = vonNummer or 'System', text = text, zeit = os.date('%H:%M'),
|
|
})
|
|
end
|
|
return true
|
|
end)
|