Soziales Netz im Spiel: Feed, Werbung, Markt, Kalender und Gewerbe

Jeder Spieler waehlt sich ein Handle mit @, Unternehmen und Behoerden bekommen
eigene Profile, fuer die sie Mitarbeiter freigeben.

Verwaltet wird ueber dieselbe Anmeldung wie das Webhosting: wer am IC-Computer
als Anbieter angemeldet ist, richtet Unternehmensprofile ein. Damit gibt es
genau eine Stelle, an der Verwaltungsrechte haengen - ein zweites Rechtesystem
daneben waere eine zweite Stelle, an der man jemanden zu entziehen vergisst.

Bewusst getrennt: wer fuer ein Unternehmen schreiben darf, kann sich nicht
selbst zum Verwalter machen.

Behoben gegenueber dem Ausgangsstand:

  - Die Resource startete nicht. Eine harte Abhaengigkeit zeigte auf eine
    Resource, die es nicht gibt - das verhindert den Start vollstaendig.
  - Das Schema war gegenueber dem Code stehengeblieben: eine Tabelle fehlte
    ganz, sechs Spalten fehlten. Jede Registrierung eines Handles scheiterte
    deshalb mit einem SQL-Fehler. Zwei Migrationen ziehen das nach.
  - Die Identitaet kommt jetzt von ESX statt von einem Charaktersystem, das
    hier nicht laeuft. Ohne das findet das Mailsystem die Postfaecher nicht.
  - Das Fenster hatte keinen Schliessknopf, nur ESC. Im Computerfenster ist
    diese Taste aber schon vergeben.
  - Bilder laden mit referrerpolicy="no-referrer": Bilderdienste sperren
    Hotlinks anhand der Herkunft, und die eines NUI kennen sie nicht.
  - Der Schluessel fuer den Bilder-Upload steht nicht mehr im Code, sondern in
    der server.cfg (set bleeter_imgbb_key). Er gehoert nicht in ein oeffentlich
    einsehbares Repository.

Neu: Unternehmensprofile und Mitarbeiterfreigabe, ueber Netzwerkereignisse und
Konsolenbefehle. Dazu eine Oberflaeche im IC-Computer, die dieselbe Gestaltung
benutzt - die Stildatei wird dafuer mechanisch gekapselt, statt sie nachzubauen.

Enthaelt README.md mit Einrichtung, Rechten und den Fallstricken.
This commit is contained in:
Bjoern Flessing 2026-08-09 12:00:48 +00:00
commit 20076b0aee
41 changed files with 8652 additions and 0 deletions

46
server/adapters/icweb.lua Normal file
View file

@ -0,0 +1,46 @@
-- ─────────────────────────────────────────────────────────────────────────────
-- Bleeter ← ic-web
--
-- Bleeter wird ueber dieselbe Anmeldung verwaltet wie das Webhosting: wer am
-- PC als admin@liveinvader.ls angemeldet ist, ist auch hier der Anbieter.
--
-- Damit gibt es genau eine Stelle, an der Anbieterrechte haengen ein
-- zweites Rechtesystem daneben waere eine zweite Stelle, an der man jemanden
-- vergessen kann zu entziehen.
--
-- Anbieter (superadmin) richtet Unternehmensprofile ein und verwaltet sie
-- Unternehmen geben eigene Leute fuer ihr Profil frei
-- Spieler waehlen ihr eigenes Handle mit @
--
-- Die alte Tabelle bleeter_lifeinvader_permissions bleibt als zweiter Weg
-- bestehen: sie ist der Notausgang, falls ic-web einmal nicht laeuft.
-- ─────────────────────────────────────────────────────────────────────────────
IcWebAdapter = {}
--- Anmeldung am PC, falls vorhanden.
---@return table|nil { username, role, domain, ... }
function IcWebAdapter.GetSession(source)
if GetResourceState('ic-web') ~= 'started' then return nil end
local ok, session = pcall(function()
return exports['ic-web']:GetSession(source)
end)
return ok and session or nil
end
--- Ist diese Person gerade als Anbieter angemeldet?
function IcWebAdapter.IsProvider(source)
local session = IcWebAdapter.GetSession(source)
return session ~= nil and session.role == 'superadmin'
end
--- Ist diese Person fuer eine Domaene angemeldet also Sprecher einer Stelle?
--- Wird nicht fuer Rechte benutzt, sondern nur als Vorschlag beim Einrichten
--- eines Unternehmensprofils.
---@return string|nil Domaene, wenn angemeldet
function IcWebAdapter.SessionDomain(source)
local session = IcWebAdapter.GetSession(source)
if not session then return nil end
return session.domain
end

View file

@ -0,0 +1,11 @@
IfruitAdapter = {}
function IfruitAdapter.GetAccountIdentity(source)
return {
identifier = Config.GetIdentifier(source),
charId = Config.GetCharacterId(source),
displayName = Config.GetCharacterName(source),
mailAddress = Config.GetMailAddress(source),
phoneNumber = Config.GetPhoneNumber(source)
}
end

22
server/adapters/mail.lua Normal file
View file

@ -0,0 +1,22 @@
MailAdapter = {}
function MailAdapter.OpenCompose(source, targetMail, subject)
local message = ('Mail an %s vorbereitet%s.'):format(
targetMail or 'unbekannt',
subject and subject ~= '' and (': ' .. subject) or ''
)
TriggerClientEvent('zc_ifruit:client:hardOpen', source)
TriggerClientEvent('zc_ifruit:client:openMailCompose', source, {
to = targetMail,
subject = subject or ''
})
TriggerClientEvent('zc_ifruit:client:notify', source, {
type = 'info',
message = message
})
TriggerClientEvent('bleeter:client:notify', source, {
type = 'info',
message = message
})
end

141
server/adapters/media.lua Normal file
View file

@ -0,0 +1,141 @@
MediaAdapter = {}
local function trim(value, maxLength)
local text = tostring(value or ''):match('^%s*(.-)%s*$')
if maxLength and #text > maxLength then
text = text:sub(1, maxLength)
end
return text
end
local function extensionFromUrl(url)
return tostring(url or ''):match('%.([%w]+)%??[^/]*$')
end
local function extensionFromName(name)
return tostring(name or ''):match('%.([%w]+)$')
end
local function isAllowedExtension(ext)
ext = tostring(ext or ''):lower()
for _, allowed in ipairs(Config.Media.allowedExtensions or {}) do
if ext == allowed then
return true
end
end
return false
end
local function mimeExtension(mime)
return ({
['image/jpeg'] = 'jpg',
['image/jpg'] = 'jpg',
['image/png'] = 'png'
})[tostring(mime or ''):lower()]
end
local function urlEncode(value)
return tostring(value or ''):gsub('\n', '\r\n'):gsub('([^%w%-_%.~])', function(char)
return string.format('%%%02X', string.byte(char))
end)
end
local function decodeJson(value)
if not value or value == '' then return nil end
local ok, result = pcall(json.decode, value)
if ok then return result end
return nil
end
function MediaAdapter.IsAllowedExternalUrl(url)
url = tostring(url or '')
if Config.Media.requireHttps and not url:match('^https://') then
return false, 'url_must_be_https'
end
if url:match('^https://i%.ibb%.co/') or url:match('^https://ibb%.co/') then
return true
end
local ext = extensionFromUrl(url)
if not ext then return false, 'missing_extension' end
ext = ext:lower()
if isAllowedExtension(ext) then return true end
return false, 'unsupported_extension'
end
function MediaAdapter.ValidateUpload(payload)
if type(payload) ~= 'table' then return false, 'invalid_payload' end
local name = trim(payload.name, 180)
local mime = trim(payload.mime, 80):lower()
local data = tostring(payload.data or '')
local size = tonumber(payload.size) or 0
if data == '' then return false, 'missing_data' end
if size <= 0 then return false, 'missing_size' end
if size > (Config.Media.maxBytes or 2097152) then return false, 'file_too_large' end
local ext = extensionFromName(name) or mimeExtension(mime)
if not isAllowedExtension(ext) then return false, 'unsupported_extension' end
if mimeExtension(mime) and not isAllowedExtension(mimeExtension(mime)) then return false, 'unsupported_mime' end
return true, {
name = name ~= '' and name or ('bleeter_%s.%s'):format(os.time(), ext),
mime = mime,
data = data,
size = size,
extension = ext
}
end
function MediaAdapter.Upload(payload, cb)
local ok, upload = MediaAdapter.ValidateUpload(payload)
if not ok then
cb(false, upload)
return
end
if Config.Media.provider ~= 'imgbb' then
cb(false, 'provider_not_configured')
return
end
-- Erst die Konsolenvariable, dann die Config. So bleibt der Schluessel
-- aus der Versionsverwaltung heraus.
local apiKey = trim(GetConvar('bleeter_imgbb_key', Config.Media.imgbbApiKey or ''))
if apiKey == '' then
cb(false, 'missing_imgbb_key')
return
end
local body = ('image=%s&name=%s'):format(urlEncode(upload.data), urlEncode(upload.name))
local url = ('https://api.imgbb.com/1/upload?key=%s'):format(urlEncode(apiKey))
PerformHttpRequest(url, function(statusCode, responseText)
if statusCode < 200 or statusCode >= 300 then
cb(false, 'upload_failed')
return
end
local decoded = decodeJson(responseText)
local data = decoded and decoded.data
local imageUrl = data and (data.url or data.display_url)
if not imageUrl or imageUrl == '' then
cb(false, 'missing_upload_url')
return
end
cb(true, {
url = imageUrl,
deleteUrl = data.delete_url,
size = upload.size,
mime = upload.mime,
name = upload.name
})
end, 'POST', body, {
['Content-Type'] = 'application/x-www-form-urlencoded'
})
end

View file

@ -0,0 +1,39 @@
SuperPcAdapter = {}
local businessStatus = {}
function SuperPcAdapter.GetBusinessStatus(handle)
return businessStatus[handle] or 'closed'
end
function SuperPcAdapter.SetBusinessStatus(handle, status)
if not handle or handle == '' then return false end
if status ~= 'open' and status ~= 'closed' then return false end
businessStatus[handle] = status
return true
end
function SuperPcAdapter.GetProfileMemberships(_source)
return {}
end
function SuperPcAdapter.HasLifeinvaderPermission(_source, _permission)
local identity = IfruitAdapter and IfruitAdapter.GetAccountIdentity and IfruitAdapter.GetAccountIdentity(_source)
local charId = identity and identity.charId or ''
local permission = tostring(_permission or '')
if charId == '' or permission == '' then
return false
end
local row = MySQL.single.await([[
SELECT id
FROM bleeter_lifeinvader_permissions
WHERE char_id = ?
AND allowed = 1
AND permission IN (?, '*', 'admin')
LIMIT 1
]], { charId, permission })
return row ~= nil
end

1627
server/main.lua Normal file

File diff suppressed because it is too large Load diff

437
server/management.lua Normal file
View file

@ -0,0 +1,437 @@
-- ─────────────────────────────────────────────────────────────────────────────
-- 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 <handle> <art> [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 <handle> <server-id> [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)

56
server/permissions.lua Normal file
View file

@ -0,0 +1,56 @@
BleeterPermissions = {}
local feedPostRights = {
private = { home = true },
small_business = { advertising = true },
company = { advertising = true },
authority = { home = true, advertising = true },
lifeinvader = {}
}
function BleeterPermissions.CanPost(profileType, feedType)
local rights = feedPostRights[profileType or ''] or {}
return rights[feedType or ''] == true
end
function BleeterPermissions.CanInteract(profile)
return profile and profile.is_locked ~= true and profile.is_active ~= false
end
--- Anbieterrechte (Lifeinvader).
---
--- Erste Quelle ist die Anmeldung am PC: wer als admin@liveinvader.ls
--- angemeldet ist, hat alle Rechte. Damit haengt Bleeter an derselben Stelle
--- wie das Webhosting, statt eine zweite Rechteliste zu fuehren.
---
--- Die alte Tabelle bleibt als zweiter Weg bestehen der Notausgang, falls
--- ic-web einmal nicht laeuft.
function BleeterPermissions.HasLifeinvaderPermission(source, permission)
if IcWebAdapter and IcWebAdapter.IsProvider(source) then
return true
end
if SuperPcAdapter and SuperPcAdapter.HasLifeinvaderPermission then
return SuperPcAdapter.HasLifeinvaderPermission(source, permission)
end
return false
end
--- Darf diese Person die Mitglieder dieses Profils verwalten?
--- Der Anbieter immer; sonst nur, wer im Profil ausdruecklich dafuer
--- eingetragen ist. Wer nur posten darf, soll sich nicht selbst weitere
--- Rechte holen koennen.
function BleeterPermissions.CanManageMembers(source, charId, profileId)
if BleeterPermissions.HasLifeinvaderPermission(source, 'profile.staff') then
return true
end
if not charId or charId == '' or not profileId then return false end
local row = MySQL.single.await([[
SELECT 1 AS ok FROM bleeter_profile_members
WHERE profile_id = ? AND char_id = ? AND can_manage_members = 1
LIMIT 1
]], { profileId, charId })
return row ~= nil
end