-- ===================================================================== -- aether-police / Modul: Fahndungen -- -- Fahndungen entstehen ausschliesslich durch einen Beamten. Es gibt -- keinen Automatismus, der jemanden aufgrund von Vorgängen zur Fahndung -- ausschreibt — und keinen, der eine Fahndung von selbst aufhebt. -- ===================================================================== Kern.RegistriereModul({ id = 'fahndung', name = 'Fahndungen', icon = '🔍', reihenfolge = 9, recht = 'fahndung.erstellen', beschreibung = 'Personen- und Sachfahndung', }) -- --------------------------------------------------------------------- -- Hilfsfunktionen -- --------------------------------------------------------------------- local GEFAHRSTUFEN = { normal = true, bewaffnet = true, gefaehrlich = true } --- Ermittelt die Bezeichnung eines Fahndungsziels --- @param typ string 'person' | 'fahrzeug' --- @param zielId string --- @return string|nil Bezeichnung, string|nil Fehler local function BezeichnungFuerZiel(typ, zielId) if typ == 'person' then local charakterId = tonumber(zielId) if not charakterId then return nil, 'Ungültige Personenkennung.' end local person = MySQL.query.await( 'SELECT firstname, lastname, dob FROM aether_characters WHERE id = ? AND deleted = 0', { charakterId }) if not person or not person[1] then return nil, 'Diese Person existiert nicht.' end return ('%s %s (geb. %s)'):format( person[1].firstname, person[1].lastname, tostring(person[1].dob):sub(1, 10)) end -- Fahrzeug local kennzeichen = tostring(zielId):upper() local fahrzeug = MySQL.query.await([[ SELECT v.plate, v.model, v.props, c.firstname, c.lastname FROM aether_vehicles v LEFT JOIN aether_characters c ON c.id = v.owner WHERE UPPER(v.plate) = ? ]], { kennzeichen }) if not fahrzeug or not fahrzeug[1] then return nil, 'Kein Fahrzeug mit diesem Kennzeichen.' end local name = fahrzeug[1].model if fahrzeug[1].props then local erfolg, props = pcall(json.decode, fahrzeug[1].props) if erfolg and type(props) == 'table' and props.name then name = props.name end end return ('%s · %s'):format(fahrzeug[1].plate, name) end --- Reichert eine Fahndung um die Daten ihres Ziels an --- @param fahndung table local function ZielDatenErgaenzen(fahndung) if fahndung.typ == 'person' then local person = MySQL.query.await([[ SELECT c.id, c.firstname, c.lastname, c.dob, p.lichtbild, p.warnhinweis, p.gefahrenstufe FROM aether_characters c LEFT JOIN police_person p ON p.`character` = c.id WHERE c.id = ? ]], { tonumber(fahndung.ziel_id) }) fahndung.ziel = person and person[1] or nil else local fahrzeug = MySQL.query.await([[ SELECT v.plate, v.model, v.props, v.state, v.owner, c.firstname, c.lastname FROM aether_vehicles v LEFT JOIN aether_characters c ON c.id = v.owner WHERE v.plate = ? ]], { fahndung.ziel_id }) if fahrzeug and fahrzeug[1] then local eintrag = fahrzeug[1] if eintrag.props then local erfolg, props = pcall(json.decode, eintrag.props) if erfolg and type(props) == 'table' then eintrag.name = props.name eintrag.farbe = props.farbe end end eintrag.props = nil fahndung.ziel = eintrag end end end -- ===================================================================== -- ÜBERSICHT -- ===================================================================== Kern.RegistriereAktion('fahndung', 'liste', nil, function(quelle, beamter, daten) local bedingungen, werte = { '1=1' }, {} -- Standardmäßig nur aktive Fahndungen if daten.archiv then bedingungen[#bedingungen + 1] = 'f.aktiv = 0' else bedingungen[#bedingungen + 1] = 'f.aktiv = 1' end if daten.typ and daten.typ ~= '' then bedingungen[#bedingungen + 1] = 'f.typ = ?' werte[#werte + 1] = daten.typ end if daten.gefahr and daten.gefahr ~= '' then bedingungen[#bedingungen + 1] = 'f.gefahr = ?' werte[#werte + 1] = daten.gefahr end if type(daten.begriff) == 'string' and #daten.begriff >= 2 then bedingungen[#bedingungen + 1] = '(f.bezeichnung LIKE ? OR f.grund LIKE ?)' local muster = '%' .. daten.begriff .. '%' werte[#werte + 1] = muster werte[#werte + 1] = muster end local fahndungen = MySQL.query.await(([[ SELECT f.id, f.typ, f.ziel_id, f.bezeichnung, f.grund, f.gefahr, f.prioritaet, f.hinweise, f.bild, f.aktiv, f.erstellt_am, f.aufgehoben_am, c.firstname, c.lastname, p.dienstnummer, a.firstname AS a_vorname, a.lastname AS a_nachname FROM police_fahndung f LEFT JOIN aether_characters c ON c.id = f.erstellt_von LEFT JOIN police_personal p ON p.`character` = f.erstellt_von LEFT JOIN aether_characters a ON a.id = f.aufgehoben_von WHERE %s ORDER BY f.prioritaet ASC, f.id DESC LIMIT 80 ]]):format(table.concat(bedingungen, ' AND ')), werte) or {} -- Zahlen für die Übersicht local zahlen = MySQL.query.await([[ SELECT gefahr, COUNT(*) AS anzahl FROM police_fahndung WHERE aktiv = 1 GROUP BY gefahr ]]) or {} local bestand = { normal = 0, bewaffnet = 0, gefaehrlich = 0 } for _, zeile in ipairs(zahlen) do bestand[zeile.gefahr] = zeile.anzahl end return { fahndungen = fahndungen, bestand = bestand, rechte = { erstellen = Rechte.Hat(beamter, 'fahndung.erstellen'), aufheben = Rechte.Hat(beamter, 'fahndung.aufheben'), }, } end) Kern.RegistriereAktion('fahndung', 'oeffnen', nil, function(quelle, beamter, daten) local fahndungId = tonumber(daten.id) if not fahndungId then return nil, 'Ungültige Kennung.' end local zeilen = MySQL.query.await([[ SELECT f.*, c.firstname, c.lastname, p.dienstnummer, a.firstname AS a_vorname, a.lastname AS a_nachname FROM police_fahndung f LEFT JOIN aether_characters c ON c.id = f.erstellt_von LEFT JOIN police_personal p ON p.`character` = f.erstellt_von LEFT JOIN aether_characters a ON a.id = f.aufgehoben_von WHERE f.id = ? ]], { fahndungId }) if not zeilen or not zeilen[1] then return nil, 'Fahndung nicht gefunden.' end local fahndung = zeilen[1] ZielDatenErgaenzen(fahndung) return { fahndung = fahndung, historie = Protokoll.Lies({ zielTyp = 'fahndung', zielId = fahndungId, limit = 20 }), rechte = { bearbeiten = Rechte.Hat(beamter, 'fahndung.erstellen'), aufheben = Rechte.Hat(beamter, 'fahndung.aufheben'), }, } end) -- ===================================================================== -- AUSSCHREIBEN -- ===================================================================== Kern.RegistriereAktion('fahndung', 'erstellen', 'fahndung.erstellen', function(quelle, beamter, daten) local typ = (daten.typ == 'fahrzeug') and 'fahrzeug' or 'person' local zielId = tostring(daten.zielId or ''):gsub('^%s+', ''):gsub('%s+$', '') if zielId == '' then return nil, 'Kein Ziel angegeben.' end if typ == 'fahrzeug' then zielId = zielId:upper() end -- Ziel muss existieren local bezeichnung, fehler = BezeichnungFuerZiel(typ, zielId) if not bezeichnung then return nil, fehler end -- Doppelte aktive Fahndung verhindern local vorhanden = MySQL.scalar.await( 'SELECT id FROM police_fahndung WHERE typ = ? AND ziel_id = ? AND aktiv = 1', { typ, zielId }) if vorhanden then return nil, 'Für dieses Ziel besteht bereits eine aktive Fahndung.' end local grund = type(daten.grund) == 'string' and daten.grund:gsub('^%s+', ''):gsub('%s+$', '') or '' if #grund < 5 then return nil, 'Bitte einen aussagekräftigen Fahndungsgrund angeben.' end local gefahr = GEFAHRSTUFEN[daten.gefahr] and daten.gefahr or 'normal' local prioritaet = math.max(1, math.min(4, tonumber(daten.prioritaet) or 3)) local fahndungId = MySQL.insert.await([[ INSERT INTO police_fahndung (typ, ziel_id, bezeichnung, grund, gefahr, prioritaet, hinweise, bild, erstellt_von) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ]], { typ, zielId, bezeichnung, grund:sub(1, 512), gefahr, prioritaet, type(daten.hinweise) == 'string' and daten.hinweise or nil, type(daten.bild) == 'string' and daten.bild:sub(1, 2000) or nil, beamter.charakterId, }) Protokoll.Schreibe(beamter, 'fahndung', 'Fahndung ausgeschrieben', { typ = 'fahndung', id = fahndungId }, { ziel = typ .. ':' .. zielId, gefahr = gefahr, prioritaet = prioritaet }, grund) -- Alle Beamten im Dienst informieren; Gefahrenstufe bestimmt die Dringlichkeit Hinweise.Erstelle(nil, { kategorie = 'fahndung', prioritaet = gefahr == 'gefaehrlich' and 'kritisch' or gefahr == 'bewaffnet' and 'hoch' or 'normal', titel = ('Fahndung: %s'):format(bezeichnung), text = ('%s · Gefahrenstufe: %s · ausgeschrieben von %s') :format(grund, gefahr, beamter.dienstnummer), zielTyp = 'fahndung', zielId = fahndungId, }) Kern.Loese('fahndung.erstellt', fahndungId, beamter) return { id = fahndungId, bezeichnung = bezeichnung } end) -- ===================================================================== -- BEARBEITEN -- ===================================================================== Kern.RegistriereAktion('fahndung', 'bearbeiten', 'fahndung.erstellen', function(quelle, beamter, daten) local fahndungId = tonumber(daten.id) if not fahndungId then return nil, 'Ungültige Kennung.' end local fahndung = MySQL.query.await( 'SELECT gefahr, prioritaet, aktiv, bezeichnung FROM police_fahndung WHERE id = ?', { fahndungId }) if not fahndung or not fahndung[1] then return nil, 'Fahndung nicht gefunden.' end if fahndung[1].aktiv == 0 then return nil, 'Aufgehobene Fahndungen können nicht geändert werden.' end local gefahr = GEFAHRSTUFEN[daten.gefahr] and daten.gefahr or fahndung[1].gefahr local prioritaet = math.max(1, math.min(4, tonumber(daten.prioritaet) or fahndung[1].prioritaet)) MySQL.update.await([[ UPDATE police_fahndung SET grund = ?, gefahr = ?, prioritaet = ?, hinweise = ?, bild = ? WHERE id = ? ]], { type(daten.grund) == 'string' and daten.grund:sub(1, 512) or nil, gefahr, prioritaet, type(daten.hinweise) == 'string' and daten.hinweise or nil, type(daten.bild) == 'string' and daten.bild:sub(1, 2000) or nil, fahndungId, }) Protokoll.Schreibe(beamter, 'fahndung', 'Fahndung geändert', { typ = 'fahndung', id = fahndungId }, { gefahr = gefahr, prioritaet = prioritaet }) -- Bei Höherstufung erneut alarmieren if gefahr ~= fahndung[1].gefahr and (gefahr == 'gefaehrlich' or gefahr == 'bewaffnet') then Hinweise.Erstelle(nil, { kategorie = 'fahndung', prioritaet = gefahr == 'gefaehrlich' and 'kritisch' or 'hoch', titel = ('Gefahrenstufe erhöht: %s'):format(fahndung[1].bezeichnung), text = ('Neue Einstufung: %s'):format(gefahr), zielTyp = 'fahndung', zielId = fahndungId, }) end return { ok = true } end) -- ===================================================================== -- AUFHEBEN -- -- Eine Fahndung endet nie von selbst — auch nicht nach einer Festnahme. -- ===================================================================== Kern.RegistriereAktion('fahndung', 'aufheben', 'fahndung.aufheben', function(quelle, beamter, daten) local fahndungId = tonumber(daten.id) if not fahndungId then return nil, 'Ungültige Kennung.' end local grund = type(daten.grund) == 'string' and daten.grund:gsub('^%s+', ''):gsub('%s+$', '') or '' if #grund < 3 then return nil, 'Bitte einen Grund für die Aufhebung angeben.' end local fahndung = MySQL.query.await( 'SELECT bezeichnung, aktiv FROM police_fahndung WHERE id = ?', { fahndungId }) if not fahndung or not fahndung[1] then return nil, 'Fahndung nicht gefunden.' end if fahndung[1].aktiv == 0 then return nil, 'Diese Fahndung ist bereits aufgehoben.' end MySQL.update.await([[ UPDATE police_fahndung SET aktiv = 0, aufgehoben_von = ?, aufgehoben_am = NOW() WHERE id = ? ]], { beamter.charakterId, fahndungId }) Protokoll.Schreibe(beamter, 'fahndung', 'Fahndung aufgehoben', { typ = 'fahndung', id = fahndungId }, { bezeichnung = fahndung[1].bezeichnung }, grund) Hinweise.Erstelle(nil, { kategorie = 'fahndung', prioritaet = 'niedrig', titel = ('Fahndung aufgehoben: %s'):format(fahndung[1].bezeichnung), text = ('%s · durch %s'):format(grund, beamter.dienstnummer), }) Kern.Loese('fahndung.aufgehoben', fahndungId, beamter) return { ok = true } end) -- ===================================================================== -- ZIELSUCHE FÜR DIE OBERFLÄCHE -- ===================================================================== Kern.RegistriereAktion('fahndung', 'zielSuche', 'fahndung.erstellen', function(quelle, beamter, daten) local begriff = type(daten.begriff) == 'string' and daten.begriff or '' if #begriff < 2 then return { treffer = {} } end local muster = '%' .. begriff .. '%' if daten.typ == 'fahrzeug' then local fahrzeuge = MySQL.query.await([[ SELECT v.plate, v.model, v.props, c.firstname, c.lastname FROM aether_vehicles v LEFT JOIN aether_characters c ON c.id = v.owner WHERE UPPER(v.plate) LIKE ? OR v.model LIKE ? LIMIT 20 ]], { muster:upper(), muster }) or {} local treffer = {} for _, fahrzeug in ipairs(fahrzeuge) do local name = fahrzeug.model if fahrzeug.props then local erfolg, props = pcall(json.decode, fahrzeug.props) if erfolg and type(props) == 'table' and props.name then name = props.name end end treffer[#treffer + 1] = { id = fahrzeug.plate, titel = fahrzeug.plate .. ' · ' .. name, unter = 'Halter: ' .. ((fahrzeug.firstname or 'unbekannt') .. ' ' .. (fahrzeug.lastname or '')), } end return { treffer = treffer } end local personen = MySQL.query.await([[ SELECT id, firstname, lastname, dob FROM aether_characters WHERE deleted = 0 AND (firstname LIKE ? OR lastname LIKE ? OR CONCAT(firstname, ' ', lastname) LIKE ?) LIMIT 20 ]], { muster, muster, muster }) or {} local treffer = {} for _, person in ipairs(personen) do treffer[#treffer + 1] = { id = tostring(person.id), titel = person.firstname .. ' ' .. person.lastname, unter = 'geb. ' .. tostring(person.dob):sub(1, 10), } end return { treffer = treffer } end) -- ===================================================================== -- EXPORT — Auskunft für andere Module -- ===================================================================== --- Besteht eine aktive Fahndung gegen ein Ziel? exports('BestehtFahndung', function(typ, zielId) local treffer = MySQL.scalar.await( 'SELECT id FROM police_fahndung WHERE typ = ? AND ziel_id = ? AND aktiv = 1', { typ, tostring(zielId) }) return treffer ~= nil end)