-- ───────────────────────────────────────────────────────────────────────────── -- Bleeter – Unternehmensprofile und Mitarbeiterfreigabe -- -- Drei Ebenen, angelehnt an ic-web: -- -- Anbieter am PC als admin@liveinvader.ls angemeldet. Richtet -- Unternehmensprofile ein und setzt den ersten Verantwortlichen. -- Unternehmen wer im Profil can_manage_members hat, gibt weitere Leute frei. -- Spieler waehlen ihr eigenes Handle mit @ (registerAccount in main.lua). -- -- Bewusst getrennt von "darf posten": wer fuer ein Unternehmen schreiben darf, -- soll sich nicht selbst zum Verwalter machen koennen. -- ───────────────────────────────────────────────────────────────────────────── local trim = BleeterInternal.trim local notify = BleeterInternal.notify local decodeBool = BleeterInternal.decodeBool local pushData = BleeterInternal.pushData local writeAudit = BleeterInternal.writeAudit -- Profiltypen, die ein Unternehmen darstellen. 'private' gehoert einer Person -- und wird nicht hier vergeben, sondern vom Spieler selbst gewaehlt. local BUSINESS_TYPES = { small_business = true, company = true, authority = true, lifeinvader = true, } local function reply(src, action, ok, data, err) TriggerClientEvent('bleeter:client:data', src, { ok = true, data = { action = action, ok = ok == true, payload = data, error = err }, }) end local function isProvider(src) -- Serverkonsole. Netzwerkereignisse haben nie die Quelle 0, das kann also -- niemand von aussen erreichen – es ist der Zugang des Betreibers. if not src or src == 0 then return true end return BleeterPermissions.HasLifeinvaderPermission(src, 'profile.staff') end local function charIdOf(src) return trim(Config.GetCharacterId(src), 80) end --- Handle pruefen. Gibt (handle, nil) oder (nil, fehlertext) zurueck. --- --- Die Liste der reservierten Handles wird hier bewusst NICHT geprueft. --- Sie soll verhindern, dass sich Spieler Namen wie 'lspd' oder 'weazel' --- greifen – geprueft wird sie deshalb bei der Selbstregistrierung --- (registerAccount in main.lua). --- --- Hierher kommt nur der Anbieter, und der vergibt diese Namen ja gerade: --- @weazelnews soll bei Weazel News landen. Eine Pruefung an dieser Stelle --- kann niemanden schuetzen, sondern nur die Stelle blockieren, die die Namen --- verteilt. local function checkHandle(raw) local handle = trim(raw, 40):lower() if #handle < Config.Handles.minLength or #handle > Config.Handles.maxLength then return nil, ('Handle muss %d bis %d Zeichen lang sein.') :format(Config.Handles.minLength, Config.Handles.maxLength) end if not handle:match(Config.Handles.pattern) then return nil, 'Handle enthält ungültige Zeichen (nur a-z, 0-9, . _ - erlaubt).' end if MySQL.single.await('SELECT 1 FROM bleeter_profiles WHERE handle = ? LIMIT 1', { handle }) then return nil, 'Dieses Handle ist bereits vergeben.' end return handle, nil end -- ── Unternehmensprofil einrichten ─────────────────────────────────────────── -- -- Als Funktion, nicht nur als Ereignis: derselbe Ablauf wird auch vom -- Chatbefehl benutzt. Zwei Wege, eine Pruefung. local function createBusinessProfile(src, payload) if not isProvider(src) then return reply(src, 'createBusinessProfile', false, nil, 'Nur die Lifeinvader-Verwaltung darf Unternehmensprofile einrichten.') end payload = type(payload) == 'table' and payload or {} local profileType = trim(payload.profileType, 24) if not BUSINESS_TYPES[profileType] then return reply(src, 'createBusinessProfile', false, nil, 'Unbekannte Profilart.') end local handle, err = checkHandle(payload.handle) if not handle then return reply(src, 'createBusinessProfile', false, nil, err) end local displayName = trim(payload.displayName, 80) if displayName == '' then displayName = handle end -- Der erste Verantwortliche wird ueber die Server-Id gewaehlt; den -- Charakter dazu sucht der Server selbst. local ownerSource = tonumber(payload.ownerSource) local ownerCharId = ownerSource and charIdOf(ownerSource) or '' local profileId = MySQL.insert.await([[ INSERT INTO bleeter_profiles (account_id, profile_type, handle, display_name, email_contact, bio, owner_source, created_by_char_id) VALUES (NULL, ?, ?, ?, ?, '', 'lifeinvader', ?) ]], { profileType, handle, displayName, trim(payload.emailContact, 120):lower(), charIdOf(src), }) if not profileId then return reply(src, 'createBusinessProfile', false, nil, 'Anlegen fehlgeschlagen.') end -- Ohne Verantwortlichen waere das Profil nicht bedienbar: es gehoert -- keinem Konto, der Zugang laeuft ausschliesslich ueber die Mitgliedschaft. if ownerCharId ~= '' then MySQL.insert.await([[ INSERT INTO bleeter_profile_members (profile_id, char_id, role, can_post, can_edit_profile, can_manage_members, source) VALUES (?, ?, 'owner', 1, 1, 1, 'lifeinvader') ]], { profileId, ownerCharId }) if ownerSource then notify(ownerSource, ('Du verwaltest jetzt das Bleeter-Profil @%s.'):format(handle), 'success') pushData(ownerSource) end end local account = BleeterInternal.ensureAccount(src) if account then writeAudit(account, nil, 'profile.business.create', 'profile', profileId, nil, { handle = handle, profile_type = profileType, owner = ownerCharId, }) end reply(src, 'createBusinessProfile', true, { id = profileId, handle = handle }) notify(src, ('Profil @%s eingerichtet.'):format(handle), 'success') pushData(src) return true, handle end RegisterNetEvent('bleeter:server:createBusinessProfile', function(payload) createBusinessProfile(source, payload) end) -- ── Profile, die diese Person verwalten darf ──────────────────────────────── RegisterNetEvent('bleeter:server:listBusinessProfiles', function() local src = source local rows if isProvider(src) then rows = MySQL.query.await([[ SELECT p.id, p.handle, p.display_name, p.profile_type, p.is_active, p.is_locked, p.is_verified, (SELECT COUNT(*) FROM bleeter_profile_members m WHERE m.profile_id = p.id) AS member_count FROM bleeter_profiles p WHERE p.profile_type <> 'private' ORDER BY p.display_name ]]) or {} else rows = MySQL.query.await([[ SELECT p.id, p.handle, p.display_name, p.profile_type, p.is_active, p.is_locked, p.is_verified, (SELECT COUNT(*) FROM bleeter_profile_members m2 WHERE m2.profile_id = p.id) AS member_count FROM bleeter_profile_members m JOIN bleeter_profiles p ON p.id = m.profile_id WHERE m.char_id = ? AND m.can_manage_members = 1 ORDER BY p.display_name ]], { charIdOf(src) }) or {} end reply(src, 'listBusinessProfiles', true, { profiles = rows, provider = isProvider(src) }) end) -- ── Mitarbeiter eines Profils ─────────────────────────────────────────────── RegisterNetEvent('bleeter:server:listProfileMembers', function(payload) local src = source local profileId = tonumber(payload and payload.profileId) if not profileId then return end if not BleeterPermissions.CanManageMembers(src, charIdOf(src), profileId) then return reply(src, 'listProfileMembers', false, nil, 'Keine Berechtigung.') end local rows = MySQL.query.await([[ SELECT m.char_id, m.role, m.can_post, m.can_edit_profile, m.can_manage_members, m.source, m.updated_at FROM bleeter_profile_members m WHERE m.profile_id = ? ORDER BY m.role, m.char_id ]], { profileId }) or {} -- Namen gibt es nur fuer Anwesende. Wer offline ist, wird ueber die -- Charakter-Id angezeigt – ein Namensnachschlag in einem fremden Schema -- waere ein Alleingang, der bei der naechsten Aenderung dort ausfaellt. local online = {} for _, playerSrc in ipairs(GetPlayers()) do online[charIdOf(tonumber(playerSrc))] = { source = tonumber(playerSrc), name = Config.GetCharacterName(tonumber(playerSrc)), } end for _, row in ipairs(rows) do local who = online[row.char_id] row.name = who and who.name or row.char_id row.online = who ~= nil end reply(src, 'listProfileMembers', true, { profileId = profileId, members = rows }) end) -- ── Auswahlliste: wer ist gerade da ───────────────────────────────────────── RegisterNetEvent('bleeter:server:listCandidates', function(payload) local src = source local profileId = tonumber(payload and payload.profileId) -- Auch ohne Profil-Id nutzbar: der Anbieter braucht die Liste schon beim -- Einrichten, wenn es das Profil noch gar nicht gibt. if profileId and not BleeterPermissions.CanManageMembers(src, charIdOf(src), profileId) then return reply(src, 'listCandidates', false, nil, 'Keine Berechtigung.') end if not profileId and not isProvider(src) then return reply(src, 'listCandidates', false, nil, 'Keine Berechtigung.') end local existing = {} if profileId then for _, row in ipairs(MySQL.query.await( 'SELECT char_id FROM bleeter_profile_members WHERE profile_id = ?', { profileId }) or {}) do existing[row.char_id] = true end end local out = {} for _, playerSrc in ipairs(GetPlayers()) do local id = tonumber(playerSrc) local charId = charIdOf(id) if charId ~= '' and not existing[charId] then out[#out + 1] = { source = id, name = Config.GetCharacterName(id) } end end table.sort(out, function(a, b) return a.name < b.name end) reply(src, 'listCandidates', true, { candidates = out }) end) -- ── Mitarbeiter freigeben oder Rechte aendern ─────────────────────────────── local function setProfileMember(src, payload) payload = type(payload) == 'table' and payload or {} local profileId = tonumber(payload.profileId) if not profileId then return end local ownCharId = charIdOf(src) if not BleeterPermissions.CanManageMembers(src, ownCharId, profileId) then return reply(src, 'setProfileMember', false, nil, 'Keine Berechtigung.') end -- Ziel ueber die Server-Id: der Client soll keine Charakter-Ids kennen -- muessen, und der Server sucht sie ohnehin selbst. local targetSource = tonumber(payload.targetSource) local targetCharId = targetSource and charIdOf(targetSource) or trim(payload.charId, 80) if not targetCharId or targetCharId == '' then return reply(src, 'setProfileMember', false, nil, 'Diese Person ist nicht (mehr) online.') end if targetCharId == ownCharId and not isProvider(src) then return reply(src, 'setProfileMember', false, nil, 'Deine eigenen Rechte kannst du hier nicht ändern.') end local profile = MySQL.single.await( 'SELECT id, handle, profile_type FROM bleeter_profiles WHERE id = ?', { profileId }) if not profile then return reply(src, 'setProfileMember', false, nil, 'Profil nicht gefunden.') end if profile.profile_type == 'private' then return reply(src, 'setProfileMember', false, nil, 'Ein privates Profil gehört genau einer Person.') end if decodeBool(payload.remove) then MySQL.update.await( 'DELETE FROM bleeter_profile_members WHERE profile_id = ? AND char_id = ?', { profileId, targetCharId }) if targetSource then pushData(targetSource) end reply(src, 'setProfileMember', true) notify(src, 'Freigabe entfernt.', 'success') return true end MySQL.insert.await([[ INSERT INTO bleeter_profile_members (profile_id, char_id, role, can_post, can_edit_profile, can_manage_members, source) VALUES (?, ?, ?, ?, ?, ?, 'ingame') ON DUPLICATE KEY UPDATE role = VALUES(role), can_post = VALUES(can_post), can_edit_profile = VALUES(can_edit_profile), can_manage_members = VALUES(can_manage_members), source = VALUES(source) ]], { profileId, targetCharId, trim(payload.role, 40) ~= '' and trim(payload.role, 40) or 'member', decodeBool(payload.canPost) and 1 or 0, decodeBool(payload.canEditProfile) and 1 or 0, decodeBool(payload.canManageMembers) and 1 or 0, }) local account = BleeterInternal.ensureAccount(src) if account then writeAudit(account, nil, 'profile.member.set', 'profile', profileId, nil, { handle = profile.handle, target = targetCharId, }) end if targetSource then notify(targetSource, ('Du bist jetzt für @%s freigegeben.'):format(profile.handle), 'success') pushData(targetSource) end reply(src, 'setProfileMember', true) notify(src, 'Freigabe gespeichert.', 'success') return true end RegisterNetEvent('bleeter:server:setProfileMember', function(payload) setProfileMember(source, payload) end) -- ───────────────────────────────────────────────────────────────────────────── -- Befehle -- -- Solange es im Bleeter-Fenster noch keine Verwaltungsseite gibt, laeuft die -- Einrichtung ueber diese Befehle. Die Rechtepruefung ist dieselbe wie oben: -- im Spiel entscheidet die Anmeldung am PC, in der Serverkonsole der Betreiber. -- ───────────────────────────────────────────────────────────────────────────── local function usage(src, ...) for _, line in ipairs({ ... }) do if src == 0 then print(line) else notify(src, line, 'info') end end end RegisterCommand('bleeterprofil', function(src, args) if src ~= 0 and not isProvider(src) then return notify(src, 'Dafür musst du als admin@liveinvader.ls angemeldet sein.', 'error') end local handle = args[1] local profileType = args[2] local ownerSource = tonumber(args[3]) if not handle or not profileType then return usage(src, '/bleeterprofil [server-id des Verantwortlichen]', 'Arten: small_business, company, authority, lifeinvader') end local displayName = table.concat(args, ' ', 4) createBusinessProfile(src, { handle = handle, profileType = profileType, ownerSource = ownerSource, displayName = displayName ~= '' and displayName or nil, }) end, false) RegisterCommand('bleeterfrei', function(src, args) local handle = tostring(args[1] or ''):lower() local targetSource = tonumber(args[2]) local rights = tostring(args[3] or 'post') if handle == '' or not targetSource then return usage(src, '/bleeterfrei [post|edit|manage|weg]', 'post = darf schreiben, edit = darf das Profil ändern,', 'manage = darf zusätzlich Leute freigeben, weg = Freigabe entfernen') end local profile = MySQL.single.await( 'SELECT id FROM bleeter_profiles WHERE handle = ?', { handle }) if not profile then return usage(src, ('Profil @%s gibt es nicht.'):format(handle)) end setProfileMember(src, { profileId = profile.id, targetSource = targetSource, remove = rights == 'weg', role = rights == 'manage' and 'manager' or 'member', canPost = rights ~= 'weg', canEditProfile = rights == 'edit' or rights == 'manage', canManageMembers = rights == 'manage', }) end, false) RegisterCommand('bleeterprofile', function(src) if src ~= 0 and not isProvider(src) then return notify(src, 'Dafür musst du als admin@liveinvader.ls angemeldet sein.', 'error') end local rows = MySQL.query.await([[ SELECT p.handle, p.display_name, p.profile_type, p.is_locked, (SELECT COUNT(*) FROM bleeter_profile_members m WHERE m.profile_id = p.id) AS members FROM bleeter_profiles p WHERE p.profile_type <> 'private' ORDER BY p.display_name ]]) or {} if src ~= 0 then return notify(src, ('%d Unternehmensprofile – Liste steht in der Serverkonsole.') :format(#rows), 'info') end print(('[bleeter] %d Unternehmensprofile:'):format(#rows)) for _, r in ipairs(rows) do print((' @%-24s %-16s %2d Mitarbeiter%s'):format( r.handle, r.profile_type, r.members, (r.is_locked == 1 or r.is_locked == true) and ' [gesperrt]' or '')) end end, false)