From 99ee40d05c0d6dde78d39592741c77193b3f881f Mon Sep 17 00:00:00 2001 From: Guangcong Luo Date: Fri, 17 Apr 2020 01:36:02 -0700 Subject: [PATCH] Setting a room rank can no longer demote globals Previously, if you gave e.g. roomvoice to a global moderator, that would demote their room rank to voice. Now, they will remain a moderator, with "voice" only appearing in `/roomauth` and `/auth`. (Includes a refactor of Config.groups) --- server/chat-commands/core.ts | 2 +- server/chat-commands/moderation.ts | 6 +- server/chat-commands/room-settings.ts | 6 +- server/config-loader.ts | 100 +++++++++++++++++++++++++- server/global-variables.d.ts | 6 +- server/index.ts | 1 - server/rooms.ts | 25 +++++-- server/users.ts | 93 ++---------------------- 8 files changed, 135 insertions(+), 104 deletions(-) diff --git a/server/chat-commands/core.ts b/server/chat-commands/core.ts index 2a5316f0d1a00..ba968515fe755 100644 --- a/server/chat-commands/core.ts +++ b/server/chat-commands/core.ts @@ -1242,7 +1242,7 @@ export const commands: ChatCommands = { } if (Config.pmmodchat) { const userGroup = user.group; - if (Config.groupsranking.indexOf(userGroup) < Config.groupsranking.indexOf(Config.pmmodchat)) { + if (Config.groupsranking.indexOf(userGroup) < Config.groupsranking.indexOf(Config.pmmodchat as GroupSymbol)) { const groupName = Config.groups[Config.pmmodchat].name || Config.pmmodchat; this.popupReply(`Because moderated chat is set, you must be of rank ${groupName} or higher to challenge users.`); return false; diff --git a/server/chat-commands/moderation.ts b/server/chat-commands/moderation.ts index 6c45b3de6cc99..660742fa9eb11 100644 --- a/server/chat-commands/moderation.ts +++ b/server/chat-commands/moderation.ts @@ -86,7 +86,7 @@ export const commands: ChatCommands = { return this.errorReply(`User '${name}' is unregistered, and so can't be promoted.`); } - const currentGroup = room.getAuth({id: userid, group: (Users.usergroups[userid] || ' ').charAt(0)}); + const currentGroup = room.auth[userid] || Config.groupsranking[0]; let nextGroup = target as GroupSymbol; if (target === 'deauth') nextGroup = Config.groupsranking[0]; if (!nextGroup) { @@ -106,7 +106,7 @@ export const commands: ChatCommands = { } if (!user.can('makeroom')) { if (currentGroup !== ' ' && !user.can(`room${Config.groups[currentGroup]?.id || 'voice'}`, null, room)) { - return this.errorReply(`/${cmd} - Access denied for promoting/demoting from ${(Config.groups[currentGroup] ? Config.groups[currentGroup].name : "an undefined group")}.`); + return this.errorReply(`/${cmd} - Access denied for promoting/demoting from ${Config.groups[currentGroup]?.name || currentGroup}.`); } if (nextGroup !== ' ' && !user.can('room' + Config.groups[nextGroup].id, null, room)) { return this.errorReply(`/${cmd} - Access denied for promoting/demoting to ${Config.groups[nextGroup].name}.`); @@ -1087,7 +1087,7 @@ export const commands: ChatCommands = { } if (!cmd.startsWith('global')) { let groupid = Config.groups[nextGroup].id; - if (!groupid && nextGroup === Config.groupsranking[0]) groupid = 'deauth'; + if (!groupid && nextGroup === Config.groupsranking[0]) groupid = 'deauth' as ID; if (Config.groups[nextGroup].globalonly) return this.errorReply(`Did you mean "/global${groupid}"?`); if (Config.groups[nextGroup].roomonly) return this.errorReply(`Did you mean "/room${groupid}"?`); return this.errorReply(`Did you mean "/room${groupid}" or "/global${groupid}"?`); diff --git a/server/chat-commands/room-settings.ts b/server/chat-commands/room-settings.ts index 97741f8ce84d3..9856f6b59380a 100644 --- a/server/chat-commands/room-settings.ts +++ b/server/chat-commands/room-settings.ts @@ -68,7 +68,7 @@ export const commands: ChatCommands = { user.can('modchatall', null, room) ? Config.groupsranking.indexOf(room.getAuth(user)) : 1; - if (room.modchat && room.modchat.length <= 1 && Config.groupsranking.indexOf(room.modchat) > threshold) { + if (room.modchat && room.modchat.length <= 1 && Config.groupsranking.indexOf(room.modchat as GroupSymbol) > threshold) { return this.errorReply(`/modchat - Access denied for changing a setting higher than ${Config.groupsranking[threshold]}.`); } if ((room as GameRoom).requestModchat) { @@ -100,7 +100,7 @@ export const commands: ChatCommands = { this.errorReply(`The rank '${target}' was unrecognized as a modchat level.`); return this.parse('/help modchat'); } - if (Config.groupsranking.indexOf(target) > threshold) { + if (Config.groupsranking.indexOf(target as GroupSymbol) > threshold) { return this.errorReply(`/modchat - Access denied for setting higher than ${Config.groupsranking[threshold]}.`); } room.modchat = target; @@ -1321,7 +1321,7 @@ export const roomSettings: SettingsHandler[] = [ 'off', 'autoconfirmed', 'trusted', - ...RANKS.slice(1, threshold + 1), + ...RANKS.slice(1, threshold! + 1), ].map(rank => [rank, (rank === 'off' ? !room.modchat : rank === room.modchat) || `modchat ${rank || 'off'}`]); return { diff --git a/server/config-loader.ts b/server/config-loader.ts index 54daa60be1fee..2c2c464476fdb 100644 --- a/server/config-loader.ts +++ b/server/config-loader.ts @@ -7,15 +7,113 @@ import * as defaults from '../config/config-example'; +export interface GroupInfo { + symbol: GroupSymbol; + id: ID; + name: string; + rank: number; + inherit?: GroupSymbol; + jurisdiction?: string; + globalGroupInPersonalRoom?: GroupSymbol; + [k: string]: string | true | number | undefined; +} + +export type ConfigType = typeof defaults & { + groups: {[symbol: string]: GroupInfo}, + groupsranking: GroupSymbol[], + greatergroupscache: {[combo: string]: GroupSymbol}, + [k: string]: any, +}; + const CONFIG_PATH = require.resolve('../config/config'); export function load(invalidate = false) { if (invalidate) delete require.cache[CONFIG_PATH]; // eslint-disable-next-line @typescript-eslint/no-var-requires - const config: Config = Object.assign({}, defaults, require('../config/config')); + const config = Object.assign({}, defaults, require('../config/config')) as ConfigType; // config.routes is nested - we need to ensure values are set for its keys as well. config.routes = Object.assign({}, defaults.routes, config.routes); + cacheGroupData(config); return config; } +export function cacheGroupData(config: ConfigType) { + if (config.groups) { + // Support for old config groups format. + // Should be removed soon. + console.error( + `You are using a deprecated version of user group specification in config.\n` + + `Support for this will be removed soon.\n` + + `Please ensure that you update your config.js to the new format (see config-example.js, line 220).\n` + ); + } else { + config.punishgroups = Object.create(null); + config.groups = Object.create(null); + config.groupsranking = []; + config.greatergroupscache = Object.create(null); + } + + const groups = config.groups; + const punishgroups = config.punishgroups; + const cachedGroups: {[k: string]: 'processing' | true} = {}; + + function cacheGroup(sym: string, groupData: GroupInfo) { + if (cachedGroups[sym] === 'processing') return false; // cyclic inheritance. + + if (cachedGroups[sym] !== true && groupData['inherit']) { + cachedGroups[sym] = 'processing'; + const inheritGroup = groups[groupData['inherit']]; + if (cacheGroup(groupData['inherit'], inheritGroup)) { + // Add lower group permissions to higher ranked groups, + // preserving permissions specifically declared for the higher group. + for (const key in inheritGroup) { + if (key in groupData) continue; + groupData[key] = inheritGroup[key]; + } + } + delete groupData['inherit']; + } + return (cachedGroups[sym] = true); + } + + if (config.grouplist) { // Using new groups format. + const grouplist = config.grouplist as any; + const numGroups = grouplist.length; + for (let i = 0; i < numGroups; i++) { + const groupData = grouplist[i]; + + // punish groups + if (groupData.punishgroup) { + punishgroups[groupData.id] = groupData; + continue; + } + + groupData.rank = numGroups - i - 1; + groups[groupData.symbol] = groupData; + config.groupsranking.unshift(groupData.symbol); + } + } + + for (const sym in groups) { + const groupData = groups[sym]; + cacheGroup(sym, groupData); + } + + // hardcode default punishgroups. + if (!punishgroups.locked) { + punishgroups.locked = { + name: 'Locked', + id: 'locked', + symbol: '\u203d', + }; + } + if (!punishgroups.muted) { + punishgroups.muted = { + name: 'Muted', + id: 'muted', + symbol: '!', + }; + } +} + export const Config = load(); diff --git a/server/global-variables.d.ts b/server/global-variables.d.ts index 16a0682f55776..f6ed159776ace 100644 --- a/server/global-variables.d.ts +++ b/server/global-variables.d.ts @@ -6,9 +6,7 @@ import * as TeamValidatorAsyncType from './team-validator-async'; import * as UsersType from './users'; import * as VerifierType from './verifier'; -import * as ConfigType from "../config/config-example"; - -import * as StreamsType from '../lib/streams'; +import {ConfigType} from "../server/config-loader"; import {IPTools as IPToolsType} from './ip-tools'; import {LadderStore as LadderStoreType} from './ladders-remote'; @@ -30,7 +28,7 @@ declare global { __version: {head: string, origin?: string, tree?: string}; } } - const Config: typeof ConfigType & AnyObject; + const Config: ConfigType; const Chat: typeof ChatType.Chat; const IPTools: typeof IPToolsType; const Ladders: typeof LaddersType; diff --git a/server/index.ts b/server/index.ts index 2faa1fb6efe57..cccee3d291ee6 100644 --- a/server/index.ts +++ b/server/index.ts @@ -90,7 +90,6 @@ if (Config.watchconfig) { FS(require.resolve('../config/config')).onModify(() => { try { global.Config = ConfigLoader.load(true); - if (global.Users) Users.cacheGroupData(); Monitor.notice('Reloaded ../config/config.js'); } catch (e) { Monitor.adminlog("Error reloading ../config/config.js: " + e.stack); diff --git a/server/rooms.ts b/server/rooms.ts index e948ce7288d0c..08cf5a93378a0 100644 --- a/server/rooms.ts +++ b/server/rooms.ts @@ -328,16 +328,31 @@ export abstract class BasicRoom { * Gets the group symbol of a user in the room. */ getAuth(user: {id: ID, group: GroupSymbol} | User): GroupSymbol { + const globalGroup = this.auth && this.isPrivate === true ? ' ' : user.group; + if (this.auth && user.id in this.auth) { - return this.auth[user.id]; + // room has roomauth + // authority is whichever is higher between roomauth and global auth + const roomGroup = this.auth[user.id]; + let greaterGroup = Config.greatergroupscache[`${roomGroup}${globalGroup}`]; + if (!greaterGroup) { + const roomRank = (Config.groups[roomGroup] || {rank: 0}).rank; + const globalRank = (Config.groups[globalGroup] || {rank: 0}).rank; + if (roomGroup === Users.PLAYER_SYMBOL || roomGroup === Users.HOST_SYMBOL || roomGroup === '#') { + // Player, Host, and Room Owner always trump higher global rank + greaterGroup = roomGroup; + } else { + greaterGroup = (roomRank > globalRank ? roomGroup : globalGroup); + } + Config.greatergroupscache[`${roomGroup}${globalGroup}`] = greaterGroup; + } + return greaterGroup; } + if (this.parent) { return this.parent.getAuth(user); } - if (this.auth && this.isPrivate === true) { - return ' '; - } - return user.group; + return globalGroup; } checkModjoin(user: User) { if (this.staffRoom && !user.isStaff && (!this.auth || (this.auth[user.id] || ' ') === ' ')) return false; diff --git a/server/users.ts b/server/users.ts index a4f316be5d5fb..62f92c4efdc82 100644 --- a/server/users.ts +++ b/server/users.ts @@ -193,86 +193,6 @@ function exportUsergroups() { } void importUsergroups(); -function cacheGroupData() { - if (Config.groups) { - // Support for old config groups format. - // Should be removed soon. - console.error( - `You are using a deprecated version of user group specification in config.\n` + - `Support for this will be removed soon.\n` + - `Please ensure that you update your config.js to the new format (see config-example.js, line 220).\n` - ); - } else { - Config.punishgroups = Object.create(null); - Config.groups = Object.create(null); - Config.groupsranking = []; - } - - const groups = Config.groups; - const punishgroups = Config.punishgroups; - const cachedGroups: {[k: string]: 'processing' | true} = {}; - - function cacheGroup(sym: string, groupData: AnyObject) { - if (cachedGroups[sym] === 'processing') return false; // cyclic inheritance. - - if (cachedGroups[sym] !== true && groupData['inherit']) { - cachedGroups[sym] = 'processing'; - const inheritGroup = groups[groupData['inherit']]; - if (cacheGroup(groupData['inherit'], inheritGroup)) { - // Add lower group permissions to higher ranked groups, - // preserving permissions specifically declared for the higher group. - for (const key in inheritGroup) { - if (key in groupData) continue; - groupData[key] = inheritGroup[key]; - } - } - delete groupData['inherit']; - } - return (cachedGroups[sym] = true); - } - - if (Config.grouplist) { // Using new groups format. - const grouplist = Config.grouplist; - const numGroups = grouplist.length; - for (let i = 0; i < numGroups; i++) { - const groupData = grouplist[i]; - - // punish groups - if (groupData.punishgroup) { - punishgroups[groupData.id] = groupData; - continue; - } - - // @ts-ignore - dyanmically assigned property - groupData.rank = numGroups - i - 1; - groups[groupData.symbol] = groupData; - Config.groupsranking.unshift(groupData.symbol); - } - } - - for (const sym in groups) { - const groupData = groups[sym]; - cacheGroup(sym, groupData); - } - - // hardcode default punishgroups. - if (!punishgroups.locked) { - punishgroups.locked = { - name: 'Locked', - id: 'locked', - symbol: '\u203d', - }; - } - if (!punishgroups.muted) { - punishgroups.muted = { - name: 'Muted', - id: 'muted', - symbol: '!', - }; - } -} -cacheGroupData(); - function setOfflineGroup(name: string, group: GroupSymbol, forceTrusted?: boolean) { if (!group) throw new Error(`Falsy value passed to setOfflineGroup`); const userid = toID(name); @@ -641,11 +561,11 @@ export class User extends Chat.MessageContext { } let group: GroupSymbol; - let targetGroup = ''; + let targetGroup: GroupSymbol | '' = ''; let targetUser = null; if (typeof target === 'string') { - targetGroup = target; + targetGroup = target as GroupSymbol; } else { targetUser = target; } @@ -686,7 +606,7 @@ export class User extends Chat.MessageContext { if (jurisdiction.includes('s') && targetUser === this) { return true; } - if (jurisdiction.includes('u') && Config.groupsranking.indexOf(group) > Config.groupsranking.indexOf(targetGroup)) { + if (jurisdiction.includes('u') && Config.groupsranking.indexOf(group) > Config.groupsranking.indexOf(targetGroup as GroupSymbol)) { return true; } } @@ -1167,7 +1087,8 @@ export class User extends Chat.MessageContext { this.avatar = Config.customavatars[this.id]; } - this.isStaff = Config.groups[this.group] && (Config.groups[this.group].lock || Config.groups[this.group].root); + const groupInfo = Config.groups[this.group]; + this.isStaff = !!(groupInfo && (groupInfo.lock || groupInfo.root)); if (!this.isStaff) { const staffRoom = Rooms.get('staff'); this.isStaff = !!staffRoom?.auth?.[this.id]; @@ -1198,7 +1119,8 @@ export class User extends Chat.MessageContext { if (!group) throw new Error(`Falsy value passed to setGroup`); this.group = group; const wasStaff = this.isStaff; - this.isStaff = Config.groups[this.group] && (Config.groups[this.group].lock || Config.groups[this.group].root); + const groupInfo = Config.groups[this.group]; + this.isStaff = !!(groupInfo && (groupInfo.lock || groupInfo.root)); if (!this.isStaff) { const staffRoom = Rooms.get('staff'); this.isStaff = !!(staffRoom?.auth?.[this.id]); @@ -1761,7 +1683,6 @@ export const Users = { isUsernameKnown, isTrusted, importUsergroups, - cacheGroupData, PLAYER_SYMBOL, HOST_SYMBOL, connections,