// ── What the permission screen reads (D162, D163, U-1) ──────────────────── // // The screen follows uMod PermissionsManager's flow — a server, then players ⇄ // groups, then a subject, then a plugin's permissions with Granted / Revoked — // and every toggle on it carries its own state on that server. This file // assembles what that needs in one read per request: // // • plugins grouped by the plugin that REGISTERED each permission (§0.1), // never by the name's prefix — `zonemanager.ignoreflag.nokits` is // ZoneManager's. A name no plugin owns (Carbon's built-in modules) is // grouped by its prefix, and says so. // • the groups on the server (D189), with where else each one is. // • every subject holding anything there, named by linked account and in-game // name, or Steam id when there is neither (D163). // • the raw facts the toggle states are computed from: what the site wants and // why (its sources), what has landed (the pushed ledger), and what the last // report said did not. const db = require('./permissions.db') const model = require('./permissions.model') const servers = require('../servers/servers.model') /** The servers, their policy and sync state, and every row waiting for a person. */ async function overview() { const [serverRows, sync, policies, drift] = await Promise.all([ servers.listForAdmin(), db.listSync(), db.listPolicies(), db.listDrift(), ]) const syncById = new Map(sync.map((row) => [row.serverId, model.shapeSync(row)])) const policyById = new Map(policies.map((row) => [row.serverId, row.policy])) return { servers: serverRows.map((row) => ({ id: row.id, name: row.name || row.id, policy: policyById.get(row.id) || 'auto-adopt', sync: syncById.get(row.id) || null, })), drift: drift.map((row) => ({ ...row, detail: row.detail === undefined ? null : row.detail })), } } /** A permission's plugin button: its registering plugin, or its prefix. */ function pluginOf(row) { if (row.owner) return { key: `plugin:${row.owner}`, label: row.owner, registered: true } const prefix = row.permission.includes('.') ? row.permission.slice(0, row.permission.indexOf('.')) : row.permission return { key: `prefix:${prefix}`, label: prefix, registered: false } } /** * Everything the screen shows for one server. Null for a server the site does * not have. */ async function serverView(serverId) { const serverRows = await servers.listForAdmin() const server = serverRows.find((row) => row.id === serverId) if (!server) return null const [authored, catalogue, pushed, sync, policies, drift, links] = await Promise.all([ model.readAuthored(), db.listCatalogue(), db.listPushed(serverId), db.listSync(), db.listPolicies(), db.listDrift(), db.listLinksNamed(), ]) const serverIds = serverRows.map((row) => row.id) const desired = model.buildDesired(serverId, authored) const byGroup = model.serversByGroup(authored.groupServers) const syncRow = sync.find((row) => row.serverId === serverId) const linkBySteam = new Map(links.map((row) => [row.steamId, row])) // ── Plugins, by who registered each permission ── const plugins = new Map() for (const row of catalogue.filter((r) => r.serverId === serverId)) { const plugin = pluginOf(row) if (!plugins.has(plugin.key)) plugins.set(plugin.key, { ...plugin, permissions: [] }) plugins.get(plugin.key).permissions.push(row.permission) } // ── Groups on this server ── const chat = model.chatByGroup(authored.groupChat) const onServer = model.groupsOn(serverId, authored) const permissionsByGroup = new Map() for (const row of authored.groupPermissions) { if (!permissionsByGroup.has(row.groupId)) permissionsByGroup.set(row.groupId, []) permissionsByGroup.get(row.groupId).push(model.normaliseName(row.permission)) } const members = new Map() for (const row of authored.members) { if (!members.has(row.groupId)) members.set(row.groupId, new Map()) const byUser = members.get(row.groupId) if (!byUser.has(row.userId)) byUser.set(row.userId, { userId: row.userId, username: row.username, steamIds: [] }) if (row.steamId) byUser.get(row.userId).steamIds.push(row.steamId) } const groups = onServer.map((group) => { const reach = model.groupReach(group, byGroup, serverIds) return { id: group.id, name: group.name, title: group.title, rank: group.rank, parent: group.parent, source: group.source, builtin: model.BUILTIN_GROUPS.has(group.name), allServers: group.allServers, servers: reach, shared: model.isShared(group, byGroup), permissions: (permissionsByGroup.get(group.id) || []).sort(), members: [...((members.get(group.id) || new Map()).values())], steamMembers: authored.steamMembers.filter((m) => m.groupId === group.id).map((m) => m.steamId), chat: chat.get(group.id) || null, } }) // ── Subjects: every Steam id holding anything here, by the desired set ── const subjects = new Map() const subject = (steamId) => { if (!subjects.has(steamId)) subjects.set(steamId, { steamId, grants: [], groups: [] }) return subjects.get(steamId) } for (const row of desired.rows) { if (row.kind === 'grant') { subject(row.subject).grants.push({ permission: row.object, sources: desired.sources.get(model.rowKey(row)) || [] }) } else if (row.kind === 'member') { subject(row.subject).groups.push(row.object) } } // A grant kept off this server by an exception still belongs on the screen: // it is "on every server except this one", and the toggle can take it back. const exceptions = authored.exceptions.filter((e) => e.serverId === serverId) const grantById = new Map(authored.grants.map((g) => [`user:${g.id}`, g])) for (const g of authored.steamGrants) grantById.set(`steam:${g.id}`, g) const excepted = [] for (const e of exceptions) { const grant = grantById.get(`${e.holder}:${e.grantId}`) if (!grant) continue const steamIds = e.holder === 'steam' ? [grant.steamId] : (authored.steamIdsByUser.get(grant.userId) || []) for (const steamId of steamIds) { subject(steamId) excepted.push({ id: e.id, steamId, permission: model.normaliseName(grant.permission), holder: e.holder, grantId: e.grantId }) } } const steamIds = [...subjects.keys()] const names = new Map((await db.namesFor(steamIds)).map((row) => [row.steamId, row.name])) const players = [...subjects.values()] .map((s) => { const link = linkBySteam.get(s.steamId) return { ...s, name: names.get(s.steamId) || (link && link.playerName) || null, account: link ? { userId: link.userId, username: link.username } : null, } }) .sort((a, b) => (a.name || a.steamId).localeCompare(b.name || b.steamId)) const report = syncRow ? model.shapeSync(syncRow).report : null const policy = (policies.find((row) => row.serverId === serverId) || {}).policy || 'auto-adopt' return { server: { id: server.id, name: server.name || server.id }, servers: serverRows.map((row) => ({ id: row.id, name: row.name || row.id })), policy, sync: syncRow ? model.shapeSync(syncRow) : null, plugins: [...plugins.values()].sort((a, b) => Number(b.registered) - Number(a.registered) || a.label.localeCompare(b.label)), groups, players, excepted, // What has landed on this server: `grant steamId permission`, `member steamId // group`, `group-permission group permission`. landed: pushed .filter((row) => row.kind === 'grant' || row.kind === 'member' || row.kind === 'group-permission') .map(model.rowKey), report: report ? { unresolved: report.unresolved || [], pending: report.pending || [], notLanded: report.notLanded || [], } : null, drift: drift.filter((row) => row.serverId === serverId), } } module.exports = { overview, serverView, pluginOf }