local function cleanText(value, minimum, maximum) if type(value) ~= 'string' then return nil end value = value:gsub('^%s+', ''):gsub('%s+$', '') if #value < minimum or #value > maximum then return nil end return value end local function ensureColumn(name, definition) local exists = MySQL.scalar.await([[ SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'devx_phone_contacts' AND COLUMN_NAME = ? ]], { name }) if tonumber(exists) == 0 then MySQL.query.await(('ALTER TABLE devx_phone_contacts ADD COLUMN `%s` %s'):format(name, definition)) end end MySQL.ready(function() ensureColumn('contact_type', "VARCHAR(32) NOT NULL DEFAULT 'player' AFTER phone_number") ensureColumn('metadata', 'LONGTEXT NULL AFTER contact_type') end) local function addSystemContact(characterId, name, number, contactType, metadata) characterId = tonumber(characterId) name = cleanText(name, 2, 64) number = cleanText(number, 4, 24) contactType = cleanText(contactType or 'system', 2, 32) if not characterId or not name or not number or not contactType then return false, 'Ungültiger Systemkontakt.' end local existing = MySQL.scalar.await([[ SELECT id FROM devx_phone_contacts WHERE character_id = ? AND phone_number = ? AND contact_type = ? LIMIT 1 ]], { characterId, number, contactType }) if existing then return true, tonumber(existing) end local id = MySQL.insert.await([[ INSERT INTO devx_phone_contacts (character_id, contact_name, phone_number, contact_type, metadata) VALUES (?, ?, ?, ?, ?) ]], { characterId, name, number, contactType, metadata and json.encode(metadata) or nil }) return id ~= nil, id end exports('AddSystemContact', addSystemContact) DevXRegisterCallback('phone:state', function(source) local player = exports.devx_core:GetPlayer(source) if not player then return { ok = false, error = 'Kein Charakter aktiv.' } end local contacts = MySQL.query.await([[ SELECT id, contact_name AS contactName, phone_number AS phoneNumber, contact_type AS contactType, metadata FROM devx_phone_contacts WHERE character_id = ? ORDER BY contact_name ASC ]], { player.characterId }) or {} for _, contact in ipairs(contacts) do contact.id = tonumber(contact.id) if type(contact.metadata) == 'string' and contact.metadata ~= '' then local ok, decoded = pcall(json.decode, contact.metadata) contact.metadata = ok and decoded or {} else contact.metadata = {} end end return { ok = true, phoneNumber = player.phoneNumber, rpXp = player.rpXp or 0, contacts = contacts } end) DevXRegisterCallback('phone:contactCreate', function(source, payload) local player = exports.devx_core:GetPlayer(source) if not player then return { ok = false, error = 'Kein Charakter aktiv.' } end local name = cleanText(payload.contactName, 2, 64) local number = cleanText(payload.phoneNumber, 4, 16) if not name or not number or not number:match('^[%d%-]+$') then return { ok = false, error = 'Name oder Telefonnummer ist ungültig.' } end local exists = MySQL.scalar.await('SELECT id FROM devx_characters WHERE phone_number = ? LIMIT 1', { number }) if not exists then return { ok = false, error = 'Diese Telefonnummer existiert nicht.' } end local duplicate = MySQL.scalar.await([[ SELECT id FROM devx_phone_contacts WHERE character_id = ? AND phone_number = ? LIMIT 1 ]], { player.characterId, number }) if duplicate then return { ok = false, error = 'Diese Nummer ist bereits gespeichert.' } end local id = MySQL.insert.await([[ INSERT INTO devx_phone_contacts (character_id, contact_name, phone_number, contact_type) VALUES (?, ?, ?, 'player') ]], { player.characterId, name, number }) return id and { ok = true } or { ok = false, error = 'Kontakt konnte nicht gespeichert werden.' } end) DevXRegisterCallback('phone:contactDelete', function(source, payload) local player = exports.devx_core:GetPlayer(source) local contactId = tonumber(payload.contactId) if not player or not contactId then return { ok = false, error = 'Ungültige Anfrage.' } end local contact = MySQL.single.await([[ SELECT contact_type AS contactType FROM devx_phone_contacts WHERE id = ? AND character_id = ? LIMIT 1 ]], { contactId, player.characterId }) if not contact then return { ok = false, error = 'Kontakt wurde nicht gefunden.' } end if contact.contactType ~= 'player' then return { ok = false, error = 'Dieser Kontakt gehört zu einer freigeschalteten Beziehung.' } end local changed = MySQL.update.await('DELETE FROM devx_phone_contacts WHERE id = ? AND character_id = ?', { contactId, player.characterId }) return changed and changed > 0 and { ok = true } or { ok = false, error = 'Kontakt wurde nicht gefunden.' } end) DevXRegisterCallback('phone:companionInvite', function(source) if GetResourceState('devx_stripclub') ~= 'started' then return { ok = false, error = 'Die Begleiterfunktion ist nicht aktiv.' } end return exports.devx_stripclub:InviteCompanion(source) end) DevXRegisterCallback('phone:bankOverview', function(source) local player = exports.devx_core:GetPlayer(source) if not player then return { ok = false, error = 'Kein Charakter aktiv.' } end local overview = exports.devx_banking:GetOverview(player.characterId) return overview and { ok = true, overview = overview } or { ok = false, error = 'Kein Bankkonto gefunden.' } end) DevXRegisterCallback('phone:bankTransfer', function(source, payload) local success, result = exports.devx_banking:TransferForSource(source, payload) return success and { ok = true, balance = result } or { ok = false, error = result } end)