diff --git a/config/config-example.js b/config/config-example.js index 89921d3594..051fa892cc 100644 --- a/config/config-example.js +++ b/config/config-example.js @@ -288,12 +288,12 @@ exports.restrictLinks = false; exports.chatmodchat = false; /** * battle modchat - default minimum group for speaking in battles; changeable with /modchat - * @type {false | string} + * @type {false | AuthLevel} */ exports.battlemodchat = false; /** - * pm modchat - minimum group for PMing other users, challenging other users - * @type {false | string} + * PM modchat - minimum group for sending private messages or challenges to other users + * @type {false | AuthLevel} */ exports.pmmodchat = false; /** diff --git a/server/chat-commands/core.ts b/server/chat-commands/core.ts index 2d5a8920ee..992538233f 100644 --- a/server/chat-commands/core.ts +++ b/server/chat-commands/core.ts @@ -572,8 +572,8 @@ export const commands: ChatCommands = { if (user.settings.blockPMs === (target || true)) { return this.errorReply(this.tr("You are already blocking private messages! To unblock, use /unblockpms")); } - if (target in Config.groups) { - user.settings.blockPMs = target as GroupSymbol; + if (Users.Auth.isAuthLevel(target)) { + user.settings.blockPMs = target; this.sendReply(this.tr `You are now blocking private messages, except from staff and ${target}.`); } else if (target === 'autoconfirmed' || target === 'trusted' || target === 'unlocked') { user.settings.blockPMs = target; @@ -728,7 +728,7 @@ export const commands: ChatCommands = { for (const setting in user.settings) { if (setting in raw) { if (setting === 'blockPMs' && - (raw[setting] in Config.groups || ['autoconfirmed', 'trusted', 'unlocked'].includes(raw[setting]))) { + Users.Auth.isAuthLevel(raw[setting])) { settings[setting] = raw[setting]; } else { settings[setting as keyof UserSettings] = !!raw[setting]; @@ -1202,12 +1202,10 @@ export const commands: ChatCommands = { async search(target, room, user, connection) { if (target) { - if (Config.laddermodchat) { - if (!Users.globalAuth.atLeast(user, Config.laddermodchat)) { - const groupName = Config.groups[Config.laddermodchat].name || Config.laddermodchat; - this.popupReply(`On this server, you must be of rank ${groupName} or higher to search for a battle.`); - return false; - } + if (Config.laddermodchat && !Users.globalAuth.atLeast(user, Config.laddermodchat)) { + const groupName = Config.groups[Config.laddermodchat].name || Config.laddermodchat; + this.popupReply(`This server requires you to be rank ${groupName} or higher to search for a battle.`); + return false; } const ladder = Ladders(target); if (!user.registered && Config.forceregisterelo && await ladder.getRating(user.id) >= Config.forceregisterelo) { @@ -1245,12 +1243,10 @@ export const commands: ChatCommands = { if (!user.named) { return this.popupReply(`You must choose a username before you challenge someone.`); } - if (Config.pmmodchat) { - if (Users.globalAuth.atLeast(user, 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; - } + if (Config.pmmodchat && !user.hasSysopAccess() && !Users.globalAuth.atLeast(user, Config.pmmodchat as GroupSymbol)) { + const groupName = Config.groups[Config.pmmodchat].name || Config.pmmodchat; + this.popupReply(`This server requires you to be rank ${groupName} or higher to challenge users.`); + return false; } return Ladders(target).makeChallenge(connection, targetUser); }, diff --git a/server/chat-commands/room-settings.ts b/server/chat-commands/room-settings.ts index f6a5fd9704..28f7f0ecd7 100644 --- a/server/chat-commands/room-settings.ts +++ b/server/chat-commands/room-settings.ts @@ -93,7 +93,7 @@ export const commands: ChatCommands = { target = Users.PLAYER_SYMBOL; /* falls through */ default: - if (!Config.groups[target]) { + if (!Users.Auth.isAuthLevel(target)) { this.errorReply(`The rank '${target}' was unrecognized as a modchat level.`); return this.parse('/help modchat'); } @@ -203,7 +203,7 @@ export const commands: ChatCommands = { this.add(`|raw|
Moderated join is set to autoconfirmed!
Users must be rank autoconfirmed or invited with /invite to join
`); this.addModAction(`${user.name} set modjoin to autoconfirmed.`); this.modlog('MODJOIN', null, 'autoconfirmed'); - } else if (target in Config.groups || target === 'trusted') { + } else if (Users.Auth.isAuthLevel(target)) { if (room.battle && !user.can('makeroom') && !'+%'.includes(target)) { return this.errorReply(`/modjoin - Access denied from setting modjoin past % in battles.`); } @@ -1563,7 +1563,7 @@ export const pages: PageTable = { this.title = `[Permissions]`; const room = this.extractRoom(); if (!room) return `

This room does not exist or does not support permissions.

`; - if (!user.authAtLeast('%', room)) return `

Access denied.

`; + if (!room.auth.atLeast(user, '%')) return `

Access denied.

`; const roomGroups = ['default', ...Config.groupsranking.slice(1)]; const permissions = room.settings.permissions || {}; @@ -1576,7 +1576,7 @@ export const pages: PageTable = { const requiredRank = permissions[permission]; atLeastOne = true; buf += `${permission}`; - if (user.authAtLeast('#', room)) { + if (room.auth.atLeast(user, '#')) { buf += roomGroups.map(group => ( requiredRank === group ? Utils.html`` : diff --git a/server/chat.ts b/server/chat.ts index 6b6056abd8..cf26fee8c7 100644 --- a/server/chat.ts +++ b/server/chat.ts @@ -954,7 +954,7 @@ export class CommandContext extends MessageContext { this.errorReply(this.tr(`You are muted and cannot talk in this room.`)); return null; } - if (room.settings.modchat && !user.authAtLeast(room.settings.modchat, room)) { + if (room.settings.modchat && !room.auth.atLeast(user, room.settings.modchat)) { if (room.settings.modchat === 'autoconfirmed') { this.errorReply( this.tr( @@ -996,14 +996,14 @@ export class CommandContext extends MessageContext { this.errorReply(`The user "${targetUser.name}" is locked and cannot be PMed.`); return null; } - if (Config.pmmodchat && !user.authAtLeast(Config.pmmodchat) && + if (Config.pmmodchat && !Users.globalAuth.atLeast(user, Config.pmmodchat) && !Users.Auth.hasPermission(targetUser, 'promote', Config.pmmodchat as GroupSymbol)) { const groupName = Config.groups[Config.pmmodchat] && Config.groups[Config.pmmodchat].name || Config.pmmodchat; this.errorReply(`On this server, you must be of rank ${groupName} or higher to PM users.`); return null; } if (targetUser.settings.blockPMs && - (targetUser.settings.blockPMs === true || !user.authAtLeast(targetUser.settings.blockPMs)) && + (targetUser.settings.blockPMs === true || !Users.globalAuth.atLeast(user, targetUser.settings.blockPMs)) && !user.can('lock')) { Chat.maybeNotifyBlocked('pm', targetUser, user); if (!targetUser.can('lock')) { @@ -1016,7 +1016,7 @@ export class CommandContext extends MessageContext { } } if (user.settings.blockPMs && (user.settings.blockPMs === true || - !targetUser.authAtLeast(user.settings.blockPMs)) && !targetUser.can('lock')) { + !Users.globalAuth.atLeast(targetUser, user.settings.blockPMs)) && !targetUser.can('lock')) { this.errorReply(`You are blocking private messages right now.`); return null; } @@ -1132,7 +1132,7 @@ export class CommandContext extends MessageContext { return false; } if (targetUser.settings.blockPMs && - (targetUser.settings.blockPMs === true || !this.user.authAtLeast(targetUser.settings.blockPMs)) && + (targetUser.settings.blockPMs === true || !Users.globalAuth.atLeast(this.user, targetUser.settings.blockPMs)) && !this.user.can('lock') ) { Chat.maybeNotifyBlocked('pm', targetUser, this.user); diff --git a/server/global-types.ts b/server/global-types.ts index 4b74572a3a..5b8441dfc7 100644 --- a/server/global-types.ts +++ b/server/global-types.ts @@ -1,6 +1,7 @@ type Config = typeof import('../config/config-example') & AnyObject; type GroupSymbol = import('./user-groups').GroupSymbol; +type AuthLevel = import('./user-groups').AuthLevel; /** not actually guaranteed to be one of these */ type PunishType = '#chatfilter' | '#hostfilter' | '#dnsbl' | '#ipban'; diff --git a/server/rooms.ts b/server/rooms.ts index b6e232cadb..0cebf5ff33 100644 --- a/server/rooms.ts +++ b/server/rooms.ts @@ -76,8 +76,8 @@ export interface RoomSettings { aliases?: string[]; banwords?: string[]; isPrivate?: boolean | 'hidden' | 'voice'; - modjoin?: string | true | null; - modchat?: string | null; + modjoin?: AuthLevel | true | null; + modchat?: AuthLevel | null; staffRoom?: boolean; language?: string | false; slowchat?: number | false; @@ -473,23 +473,13 @@ export abstract class BasicRoom { if (!this.settings.modjoin) return true; // users with a room rank can always join if (this.auth.has(user.id)) return true; - const userGroup = user.can('makeroom') ? user.group : this.auth.get(user.id); const modjoinSetting = this.settings.modjoin !== true ? this.settings.modjoin : this.settings.modchat; if (!modjoinSetting) return true; - let modjoinGroup = modjoinSetting; - - if (modjoinGroup === 'trusted') { - if (user.trusted) return true; - modjoinGroup = Config.groupsranking[1]; + if (!Users.Auth.isAuthLevel(modjoinSetting)) { + Monitor.error(`Invalid modjoin setting in ${this.roomid}: ${modjoinSetting}`); } - if (modjoinGroup === 'autoconfirmed') { - if (user.autoconfirmed) return true; - modjoinGroup = Config.groupsranking[1]; - } - if (!(userGroup in Config.groups)) return false; - if (!(modjoinGroup in Config.groups)) throw new Error(`Invalid modjoin setting in ${this.roomid}: ${modjoinGroup}`); - return Config.groups[userGroup].rank >= Config.groups[modjoinGroup].rank; + return Users.globalAuth.atLeast(user, modjoinSetting); } mute(user: User, setTime?: number) { const userid = user.id; @@ -596,7 +586,7 @@ export abstract class BasicRoom { this.destroy(); } reportJoin(type: 'j' | 'l' | 'n', entry: string, user: User) { - const canTalk = user.authAtLeast(this.settings.modchat ?? 'unlocked', this) && !this.isMuted(user); + const canTalk = this.auth.atLeast(user, this.settings.modchat ?? 'unlocked') && !this.isMuted(user); if (this.reportJoins && (canTalk || this.auth.has(user.id))) { this.add(`|${type}|${entry}`).update(); return; diff --git a/server/user-groups.ts b/server/user-groups.ts index d7aa4fe408..21b7ff9183 100644 --- a/server/user-groups.ts +++ b/server/user-groups.ts @@ -2,6 +2,7 @@ import {FS} from '../lib/fs'; export type GroupSymbol = '~' | '&' | '#' | '★' | '*' | '@' | '%' | '☆' | '+' | ' ' | '‽' | '!'; export type EffectiveGroupSymbol = GroupSymbol | 'whitelist'; +export type AuthLevel = EffectiveGroupSymbol | 'unlocked' | 'trusted' | 'autoconfirmed'; export const PLAYER_SYMBOL: GroupSymbol = '\u2606'; export const HOST_SYMBOL: GroupSymbol = '\u2605'; @@ -58,9 +59,16 @@ export abstract class Auth extends Map { isStaff(userid: ID) { return this.has(userid) && this.get(userid) !== '+'; } - atLeast(user: User, group: GroupSymbol, isPermissionCheck?: boolean) { - if (!Config.groups[group]) return false; + atLeast(user: User, group: AuthLevel, isPermissionCheck?: boolean) { + if (user.hasSysopAccess()) return true; + if (group === 'trusted' || group === 'autoconfirmed') { + if (user.trusted && group === 'trusted') return true; + if (user.autoconfirmed && group === 'autoconfirmed') return true; + group = Config.groupsranking[1]; + } if (user.locked || user.semilocked) return false; + if (group === 'unlocked') return true; + if (!Config.groups[group]) return false; if (this.get(user.id) === ' ' && group !== ' ') return false; if (Auth.atLeast(this.get(user.id, true), group)) { return true; @@ -109,7 +117,9 @@ export abstract class Auth extends Map { const symbol = auth.getEffectiveSymbol(user, useVisualGroup); let targetSymbol = (typeof target === 'string' || !target) ? target : auth.get(target); - if (targetSymbol === 'whitelist') targetSymbol = Auth.defaultSymbol(); + if (!targetSymbol || ['whitelist', 'trusted', 'autoconfirmed'].includes(targetSymbol)) { + targetSymbol = Auth.defaultSymbol(); + } const group = Auth.getGroup(symbol); if (group['root']) return true; @@ -129,7 +139,7 @@ export abstract class Auth extends Map { } } - return Auth.hasJurisdiction(symbol, jurisdiction, targetSymbol); + return Auth.hasJurisdiction(symbol, jurisdiction, targetSymbol as GroupSymbol); } static atLeast(symbol: EffectiveGroupSymbol, symbol2: EffectiveGroupSymbol) { return Auth.getGroup(symbol).rank >= Auth.getGroup(symbol2).rank; @@ -183,6 +193,10 @@ export abstract class Auth extends Map { if (symbol.length !== 1) return false; return !/[A-Za-z0-9|,]/.test(symbol); } + static isAuthLevel(level: string): level is AuthLevel { + if (this.isValidSymbol(level)) return true; + return ['unlocked', 'trusted', 'autoconfirmed', 'whitelist'].includes(level); + } static ROOM_PERMISSIONS = ROOM_PERMISSIONS; static GLOBAL_PERMISSIONS = GLOBAL_PERMISSIONS; } diff --git a/server/users.ts b/server/users.ts index 5d0cf30300..949cc9bc03 100644 --- a/server/users.ts +++ b/server/users.ts @@ -343,7 +343,7 @@ export class User extends Chat.MessageContext { settings: { blockChallenges: boolean, - blockPMs: boolean | GroupSymbol | 'autoconfirmed' | 'trusted' | 'unlocked', + blockPMs: boolean | AuthLevel, ignoreTickets: boolean, hideBattlesFromTrainerCard: boolean, }; @@ -531,20 +531,6 @@ export class User extends Chat.MessageContext { const status = statusMessage + (this.userMessage || ''); return status; } - authAtLeast(minAuth: string, room: BasicRoom | null = null) { - if (!minAuth || minAuth === ' ') return true; - if (this.locked || this.semilocked) return false; - if (minAuth === 'unlocked') return true; - if (minAuth === 'trusted' && this.trusted) return true; - if (minAuth === 'autoconfirmed' && this.autoconfirmed) return true; - - if (minAuth === 'trusted' || minAuth === 'autoconfirmed') { - minAuth = Config.groupsranking[1]; - } - if (!(minAuth in Config.groups)) return false; - const auth = (room && !this.can('makeroom') ? room.auth.get(this.id) : this.group); - return auth in Config.groups && Config.groups[auth].rank >= Config.groups[minAuth].rank; - } can(permission: RoomPermission, target: User | null, room: BasicRoom, cmd?: string, useVisualGroup?: boolean): boolean; can( permission: GlobalPermission, @@ -1360,12 +1346,12 @@ export class User extends Chat.MessageContext { chat(message: string, room: Room | null, connection: Connection) { const now = Date.now(); - if (message.startsWith('/cmd userdetails') || message.startsWith('>> ') || this.isSysop) { + if (message.startsWith('/cmd userdetails') || message.startsWith('>> ') || this.hasSysopAccess()) { // certain commands are exempt from the queue Monitor.activeIp = connection.ip; Chat.parse(message, room, this, connection); Monitor.activeIp = null; - if (this.isSysop) return; + if (this.hasSysopAccess()) return; return false; // but end the loop here }