Add room sections (#8205)

This commit is contained in:
Kris Johnson 2021-04-23 23:39:56 -06:00 committed by GitHub
parent 909d4657f4
commit a1bdafbfe8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 233 additions and 54 deletions

View File

@ -650,6 +650,14 @@ exports.grouplist = [
modchat: true,
hiderank: true,
},
{
symbol: '\u25B8',
id: "sectionleader",
name: "Section Leader",
inherit: '+',
roomonly: true,
},
{
// Bots are ranked below Driver/Mod so that Global Bots can be kept out
// of modjoin % rooms (namely, Staff).

View File

@ -9,7 +9,7 @@ for (const row of usergroupData) {
if (!toID(row)) continue;
const cells = row.split(',');
if (cells.length !== 2) throw new Error(`Invalid entry when parsing usergroups.csv`);
if (cells.length > 3) throw new Error(`Invalid entry when parsing usergroups.csv`);
usergroups[toID(cells[0])] = cells[1].trim() || ' ';
}

View File

@ -1704,12 +1704,14 @@ export const commands: ChatCommands = {
let group = targetUser.tempGroup;
if (targetUser.locked) group = Config.punishgroups?.locked?.symbol ?? '\u203d';
if (targetUser.namelocked) group = Config.punishgroups?.namelocked?.symbol ?? '✖';
const sectionleader = Users.globalAuth.sectionLeaders.has(targetUser.id);
const userdetails: AnyObject = {
id: target,
userid: targetUser.id,
name: targetUser.name,
avatar: targetUser.avatar,
group: group,
customgroup: sectionleader ? "Section Leader" : undefined,
autoconfirmed: !!targetUser.autoconfirmed,
status: targetUser.getStatus(),
rooms: roomList,

View File

@ -11,6 +11,7 @@
import * as net from 'net';
import {YoutubeInterface} from '../chat-plugins/youtube';
import {Net, Utils} from '../../lib';
import {RoomSections} from './room-settings';
const ONLINE_SYMBOL = ` \u25C9 `;
const OFFLINE_SYMBOL = ` \u25CC `;
@ -102,6 +103,9 @@ export const commands: ChatCommands = {
if (Config.groups[targetUser.tempGroup]?.name) {
buf += Utils.html`<br />Global ${Config.groups[targetUser.tempGroup].name} (${targetUser.tempGroup})`;
}
if (Users.globalAuth.sectionLeaders.has(targetUser.id)) {
buf += Utils.html`<br />Section Leader (${RoomSections.sectionNames[Users.globalAuth.sectionLeaders.get(targetUser.id)!]})`;
}
if (targetUser.isSysop) {
buf += `<br />(Pok&eacute;mon Showdown System Operator)`;
}
@ -315,6 +319,9 @@ export const commands: ChatCommands = {
if (Config.groups[group]?.name) {
buf += `<br />Global ${Config.groups[group].name} (${group})`;
}
if (Users.globalAuth.sectionLeaders.has(userid)) {
buf += `<br />Section Leader (${RoomSections.sectionNames[Users.globalAuth.sectionLeaders.get(userid)!]})`;
}
buf += `<br /><br />`;
let atLeastOne = false;

View File

@ -9,6 +9,7 @@
* @license MIT
*/
import {Utils} from '../../lib';
import {RoomSection, RoomSections} from './room-settings';
/* eslint no-else-return: "error" */
@ -101,6 +102,11 @@ export function runPromote(
export function runCrisisDemote(userid: ID) {
const from = [];
const section = Users.globalAuth.sectionLeaders.get(userid);
if (section) {
from.push(`Section Leader (${RoomSections.sectionNames[section] || section})`);
Users.globalAuth.deleteSection(userid);
}
const globalGroup = Users.globalAuth.get(userid);
if (globalGroup && globalGroup !== ' ') {
from.push(globalGroup);
@ -121,7 +127,6 @@ export function runCrisisDemote(userid: ID) {
}
export const commands: ChatCommands = {
roomowner(target, room, user) {
room = this.requireRoom();
if (!room.persist) {
@ -374,6 +379,10 @@ export const commands: ChatCommands = {
if (group !== ' ' || Users.isTrusted(targetId)) {
buffer.push(`Global auth: ${group === ' ' ? 'trusted' : group}`);
}
const sectionLeader = Users.globalAuth.sectionLeaders.get(targetId);
if (sectionLeader) {
buffer.push(`Section leader: ${RoomSections.sectionNames[sectionLeader]}`);
}
for (const curRoom of Rooms.rooms.values()) {
if (curRoom.settings.isPrivate) continue;
if (!curRoom.auth.has(targetId)) continue;
@ -414,6 +423,25 @@ export const commands: ChatCommands = {
connection.popup(buffer.join("\n\n"));
},
sectionleaders(target, room, user, connection) {
const usernames = Users.globalAuth.usernames;
const buffer = [];
const sections: {[k in RoomSection]: Set<string>} = Object.create(null);
for (const [id, username] of usernames) {
const sectionid = Users.globalAuth.sectionLeaders.get(id);
if (!sectionid) continue;
if (!sections[sectionid]) sections[sectionid] = new Set();
sections[sectionid].add(username);
}
let sectionid: RoomSection;
for (sectionid in sections) {
if (!sections[sectionid].size) continue;
buffer.push(`**${RoomSections.sectionNames[sectionid]}**:\n` + [...sections[sectionid]].join(', '));
}
if (!buffer.length) throw new Chat.ErrorMessage(`There are no Section Leaders currently.`);
connection.popup(buffer.join(`\n\n`));
},
async autojoin(target, room, user, connection) {
const targets = target.split(',');
if (targets.length > 16 || connection.inRooms.size > 1) {
@ -1363,6 +1391,57 @@ export const commands: ChatCommands = {
`/untrustuser [username] - Removes the trusted user status from the user. Requires: &`,
],
desectionleader: 'sectionleader',
sectionleader(target, room, user, connection, cmd) {
this.checkCan('gdeclare');
room = this.requireRoom();
const demoting = cmd === 'desectionleader';
if (!target || (target.split(',').length < 2 && !demoting)) return this.parse(`/help sectionleader`);
const [targetStr, sectionid] = this.splitOne(target);
this.splitTarget(targetStr);
const targetUser = this.targetUser;
const userid = toID(this.targetUsername);
const section = demoting ? Users.globalAuth.sectionLeaders.get(userid)! : room.validateSection(sectionid);
const name = targetUser ? targetUser.name : this.targetUsername;
if (Users.globalAuth.sectionLeaders.has(targetUser?.id || userid) && !demoting) {
throw new Chat.ErrorMessage(`${name} is already a Section Leader of ${RoomSections.sectionNames[section]}.`);
} else if (!Users.globalAuth.sectionLeaders.has(targetUser?.id || userid) && demoting) {
throw new Chat.ErrorMessage(`${name} is not a Section Leader.`);
}
const staffRoom = Rooms.get('staff');
if (!demoting) {
Users.globalAuth.setSection(userid, section);
this.addGlobalModAction(`${name} was appointed Section Leader of ${RoomSections.sectionNames[section]} by ${user.name}.`);
this.globalModlog(`SECTION LEADER`, userid, section);
if (!staffRoom?.auth.has(userid)) this.parse(`/msgroom staff,/forceroompromote ${userid},▸`);
targetUser?.popup(`You were appointed Section Leader of ${RoomSections.sectionNames[section]} by ${user.name}.`);
} else {
const group = Users.globalAuth.get(userid);
Users.globalAuth.deleteSection(userid);
this.privateGlobalModAction(`${name} was demoted from Section Leader of ${RoomSections.sectionNames[section]} by ${user.name}.`);
if (group === ' ') this.sendReply(`They are also no longer manually trusted. If they should be, use '/trustuser'.`);
this.globalModlog(`DESECTION LEADER`, userid, section);
if (staffRoom?.auth.getDirect(userid) as any === '\u25B8') this.parse(`/msgroom staff,/roomdeauth ${userid}`);
targetUser?.popup(`You were demoted from Section Leader of ${RoomSections.sectionNames[section]} by ${user.name}.`);
}
if (targetUser) {
targetUser.updateIdentity();
Rooms.global.checkAutojoin(targetUser);
if (targetUser.trusted && !Users.isTrusted(targetUser.id)) {
targetUser.trusted = '';
}
}
},
sectionleaderhelp: [
`/sectionleader [target user], [sectionid] - Appoints [target user] Section Leader.`,
`/desectionleader [target user] - Demotes [target user] from Section Leader.`,
`Valid sections: ${RoomSections.sections.join(', ')}`,
`If you want to change which section someone leads, demote them and then re-promote them in the desired section.`,
`Requires: &`,
],
globaldemote: 'demote',
demote(target) {
if (!target) return this.parse('/help demote');

View File

@ -15,6 +15,25 @@ const SLOWCHAT_MINIMUM = 2;
const SLOWCHAT_MAXIMUM = 60;
const SLOWCHAT_USER_REQUIREMENT = 10;
export const sections = [
'official', 'battleformats', 'languages', 'entertainment', 'gaming', 'lifehobbies', 'onsitegames',
] as const;
export type RoomSection = typeof sections[number];
export const RoomSections: {sectionNames: {[k in RoomSection]: string}, sections: readonly RoomSection[]} = {
sectionNames: {
official: 'Official',
battleformats: 'Battle formats',
languages: 'Languages',
entertainment: 'Entertainment',
gaming: 'Gaming',
lifehobbies: 'Life & hobbies',
onsitegames: 'On-site games',
},
sections,
};
export const commands: ChatCommands = {
roomsetting: 'roomsettings',
roomsettings(target, room, user, connection) {
@ -1085,6 +1104,7 @@ export const commands: ChatCommands = {
}
this.addModAction(`${user.name} made this room ${settingName}.`);
this.modlog(`${settingName.toUpperCase()}ROOM`);
if (!room.settings.isPersonal && !room.battle) room.setSection();
room.setPrivate(setting);
room.privacySetter = new Set([user.id]);
}
@ -1114,48 +1134,27 @@ export const commands: ChatCommands = {
],
officialchatroom: 'officialroom',
officialroom(target, room, user) {
room = this.requireRoom();
this.checkCan('makeroom');
if (!room.persist) {
return this.errorReply(`/officialroom - This room can't be made official`);
}
if (this.meansNo(target)) {
if (!room.settings.isOfficial) return this.errorReply(`This chat room is already unofficial.`);
delete room.settings.isOfficial;
this.addModAction(`${user.name} made this chat room unofficial.`);
this.modlog('UNOFFICIALROOM');
delete room.settings.isOfficial;
room.saveSettings();
} else {
if (room.settings.isOfficial) return this.errorReply(`This chat room is already official.`);
room.settings.isOfficial = true;
this.addModAction(`${user.name} made this chat room official.`);
this.modlog('OFFICIALROOM');
room.settings.isOfficial = true;
room.saveSettings();
}
officialroom() {
this.parse(`/setroomsection official`);
},
psplwinnerroom(target, room, user) {
this.checkCan('makeroom');
room = this.requireRoom();
if (!room.persist) {
return this.errorReply(`/psplwinnerroom - This room can't be marked as a PSPL Winner room`);
return this.errorReply(`/psplwinnerroom - This room can't be marked as a PSPL Winner room.`);
}
if (this.meansNo(target)) {
if (!room.settings.pspl) return this.errorReply(`This chat room is already not a PSPL Winner room.`);
delete room.settings.pspl;
this.addModAction(`${user.name} made this chat room no longer a PSPL Winner room.`);
this.modlog('PSPLROOM');
this.globalModlog('UNPSPLROOM');
delete room.settings.pspl;
room.saveSettings();
} else {
if (room.settings.pspl) return this.errorReply("This chat room is already a PSPL Winner room.");
room.settings.pspl = true;
this.addModAction(`${user.name} made this chat room a PSPL Winner room.`);
this.modlog('UNPSPLROOM');
room.settings.pspl = true;
this.globalModlog('PSPLROOM');
room.settings.pspl = "PSPL Winner";
room.saveSettings();
}
},
@ -1486,6 +1485,27 @@ export const commands: ChatCommands = {
`/roomtierdisplay [option] - changes the current room's tier display. Valid options are: tiers, doubles tiers, numbers. Requires: # &`,
`/resettierdisplay - resets the current room's tier display. Requires: # &`,
],
setroomsection: 'roomsection',
roomsection(target, room, user) {
room = this.requireRoom();
const sectionNames = RoomSections.sectionNames;
if (!target) {
if (!this.runBroadcast()) return;
this.sendReplyBox(Utils.html`This room is ${room.settings.section ? `in the ${sectionNames[room.settings.section]} section` : `not in a section`}.`);
return;
}
this.checkCan('gdeclare');
const section = room.setSection(target);
this.sendReply(`The room section is now: ${section ? sectionNames[section] : 'none'}`);
this.privateGlobalModAction(`${user.name} changed the room section of ${room.title} to ${section ? sectionNames[section] : 'none'}.`);
this.globalModlog('ROOMSECTION', null, section || 'none');
},
roomsectionhelp: [
`/roomsection [section] - Sets the room this is used in to the specified [section]. Requires: &`,
`Valid sections: ${sections.join(', ')}`,
],
};
export const roomSettings: SettingsHandler[] = [
@ -1586,7 +1606,7 @@ export const pages: PageTable = {
atLeastOne = true;
buf += `<tr><td><strong>${permission}</strong></td><td>`;
if (room.auth.atLeast(user, '#')) {
buf += roomGroups.map(group => (
buf += roomGroups.filter(group => group !== Users.SECTIONLEADER_SYMBOL).map(group => (
requiredRank === group ?
Utils.html`<button class="button disabled" style="font-weight:bold;color:#575757;background:#d3d3d3">${group}</button>` :
Utils.html`<button class="button" name="send" value="/msgroom ${room.roomid},/permissions set ${permission}, ${group}">${group}</button>`

View File

@ -109,7 +109,7 @@ export const LogReader = new class {
}
} else if (!room) {
if (opts === 'all' || opts === 'deleted') deleted.push(roomid);
} else if (room.settings.isOfficial) {
} else if (room.settings.section === 'official') {
official.push(roomid);
} else if (!room.settings.isPrivate) {
normal.push(roomid);

View File

@ -29,6 +29,7 @@ const LAST_BATTLE_WRITE_THROTTLE = 10;
const RETRY_AFTER_LOGIN = null;
import {FS, Utils, Streams} from '../lib';
import {RoomSection, RoomSections} from './chat-commands/room-settings';
import {GTSGiveaway, LotteryGiveaway, QuestionGiveaway} from './chat-plugins/wifi';
import {QueuedHunt} from './chat-plugins/scavengers';
import {ScavengerGameTemplate} from './chat-plugins/scavenger-games';
@ -56,7 +57,9 @@ interface ChatRoomTable {
title: string;
desc: string;
userCount: number;
section?: string;
subRooms?: string[];
spotlight?: string;
}
interface ShowRequest {
@ -80,6 +83,7 @@ export interface RoomSettings {
title: string;
auth: {[userid: string]: GroupSymbol};
creationTime: number;
section?: RoomSection;
readonly autojoin?: boolean;
aliases?: string[];
@ -101,8 +105,7 @@ export interface RoomSettings {
hangmanDisabled?: boolean;
gameNumber?: number;
highTraffic?: boolean;
isOfficial?: boolean;
pspl?: boolean;
pspl?: string;
parentid?: string | null;
desc?: string | null;
introMessage?: string | null;
@ -697,7 +700,7 @@ export abstract class BasicRoom {
}
message += `</div>`;
if (this.settings.introMessage) {
message += `\n|raw|<div class="infobox infobox-roomintro"><div ${(!this.settings.isOfficial ? 'class="infobox-limited"' : '')}>` +
message += `\n|raw|<div class="infobox infobox-roomintro"><div ${(this.settings.section !== 'official' ? 'class="infobox-limited"' : '')}>` +
this.settings.introMessage.replace(/\n/g, '') +
`</div></div>`;
}
@ -824,6 +827,34 @@ export abstract class BasicRoom {
}
}
}
validateSection(section: string) {
const target = toID(section);
if (!RoomSections.sections.includes(target as any)) {
throw new Chat.ErrorMessage(`"${target}" is not a valid room section. Valid categories include: ${RoomSections.sections.join(', ')}`);
}
return target as RoomSection;
}
setSection(section?: string) {
if (!this.persist) {
throw new Chat.ErrorMessage(`You cannot change the section of temporary rooms.`);
}
if (section) {
const validatedSection = this.validateSection(section);
if (this.settings.isPrivate && [true, 'hidden'].includes(this.settings.isPrivate)) {
throw new Chat.ErrorMessage(`Only public rooms can change their section.`);
}
const oldSection = this.settings.section;
if (oldSection === section) {
throw new Chat.ErrorMessage(`${this.title}'s room section is already set to "${RoomSections.sectionNames[oldSection]}".`);
}
this.settings.section = validatedSection;
this.saveSettings();
return validatedSection;
}
delete this.settings.section;
this.saveSettings();
return undefined;
}
/**
* Displays a warning popup to all non-staff users users in the room.
@ -1123,8 +1154,8 @@ export class GlobalRoomState {
title: 'Lobby',
auth: {},
creationTime: Date.now(),
isOfficial: true,
autojoin: true,
section: 'official',
}, {
title: 'Staff',
auth: {},
@ -1353,11 +1384,13 @@ export class GlobalRoomState {
}
getRooms(user: User) {
const roomsData: {
official: ChatRoomTable[], pspl: ChatRoomTable[], chat: ChatRoomTable[], userCount: number, battleCount: number,
chat: ChatRoomTable[],
sectionTitles: string[],
userCount: number,
battleCount: number,
} = {
official: [],
pspl: [],
chat: [],
sectionTitles: Object.values(RoomSections.sectionNames),
userCount: Users.onlineCount,
battleCount: this.battleCount,
};
@ -1369,18 +1402,14 @@ export class GlobalRoomState {
title: room.title,
desc: room.settings.desc || '',
userCount: room.userCount,
section: room.settings.section ?
(RoomSections.sectionNames[room.settings.section] || room.settings.section) : undefined,
};
const subrooms = room.getSubRooms().map(r => r.title);
if (subrooms.length) roomData.subRooms = subrooms;
if (room.settings.pspl) roomData.spotlight = room.settings.pspl;
if (room.settings.isOfficial) {
roomsData.official.push(roomData);
// @ts-ignore
} else if (room.pspl) {
roomsData.pspl.push(roomData);
} else {
roomsData.chat.push(roomData);
}
roomsData.chat.push(roomData);
}
return roomsData;
}
@ -1853,7 +1882,7 @@ export const Rooms = {
Rooms.rooms.set(roomid, room);
return room;
},
createChatRoom(roomid: RoomID, title: string, options: AnyObject) {
createChatRoom(roomid: RoomID, title: string, options: Partial<RoomSettings>) {
if (Rooms.rooms.has(roomid)) throw new Error(`Room ${roomid} already exists`);
const room: ChatRoom = new (BasicRoom as any)(roomid, title, options);
Rooms.rooms.set(roomid, room);

View File

@ -1,14 +1,16 @@
import {FS} from '../lib/fs';
import type {RoomSection} from './chat-commands/room-settings';
export type GroupSymbol = '~' | '&' | '#' | '★' | '*' | '@' | '%' | '☆' | '+' | ' ' | '‽' | '!';
export type GroupSymbol = '~' | '&' | '#' | '★' | '*' | '@' | '%' | '☆' | '▸' | '+' | '^' | ' ' | '‽' | '!';
export type EffectiveGroupSymbol = GroupSymbol | 'whitelist';
export type AuthLevel = EffectiveGroupSymbol | 'unlocked' | 'trusted' | 'autoconfirmed';
export const SECTIONLEADER_SYMBOL: GroupSymbol = '\u25B8';
export const PLAYER_SYMBOL: GroupSymbol = '\u2606';
export const HOST_SYMBOL: GroupSymbol = '\u2605';
export const ROOM_PERMISSIONS = [
'addhtml', 'announce', 'ban', 'bypassafktimer', 'declare', 'editprivacy', 'editroom', 'exportinputlog', 'game', 'gamemanagement', 'gamemoderation', 'joinbattle', 'kick', 'minigame', 'modchat', 'modlog', 'mute', 'nooverride', 'receiveauthmessages', 'roombot', 'roomdriver', 'roommod', 'roomowner', 'roomvoice', 'roomprizewinner', 'show', 'showmedia', 'timer', 'tournaments', 'warn',
'addhtml', 'announce', 'ban', 'bypassafktimer', 'declare', 'editprivacy', 'editroom', 'exportinputlog', 'game', 'gamemanagement', 'gamemoderation', 'joinbattle', 'kick', 'minigame', 'modchat', 'modlog', 'mute', 'nooverride', 'receiveauthmessages', 'roombot', 'roomdriver', 'roommod', 'roomowner', 'roomsectionleader', 'roomvoice', 'roomprizewinner', 'show', 'showmedia', 'timer', 'tournaments', 'warn',
] as const;
export const GLOBAL_PERMISSIONS = [
@ -307,6 +309,7 @@ export class RoomAuth extends Auth {
export class GlobalAuth extends Auth {
usernames = new Map<ID, string>();
sectionLeaders = new Map<ID, RoomSection>();
constructor() {
super();
this.load();
@ -315,7 +318,7 @@ export class GlobalAuth extends Auth {
FS('config/usergroups.csv').writeUpdate(() => {
let buffer = '';
for (const [userid, groupSymbol] of this) {
buffer += `${this.usernames.get(userid) || userid},${groupSymbol}\n`;
buffer += `${this.usernames.get(userid) || userid},${groupSymbol},${this.sectionLeaders.get(userid) || ''}\n`;
}
return buffer;
});
@ -324,9 +327,10 @@ export class GlobalAuth extends Auth {
const data = FS('config/usergroups.csv').readIfExistsSync();
for (const row of data.split("\n")) {
if (!row) continue;
const [name, symbol] = row.split(",");
const [name, symbol, sectionid] = row.split(",");
const id = toID(name);
this.usernames.set(id, name);
if (sectionid) this.sectionLeaders.set(id, sectionid as RoomSection);
super.set(id, symbol.charAt(0) as GroupSymbol);
}
}
@ -355,4 +359,31 @@ export class GlobalAuth extends Auth {
this.save();
return true;
}
setSection(id: ID, sectionid: RoomSection, username?: string) {
if (!username) username = id;
const user = Users.get(id);
if (user) {
user.updateIdentity();
username = user.name;
Rooms.global.checkAutojoin(user);
}
if (!super.has(id)) this.set(id, ' ', username);
this.sectionLeaders.set(id, sectionid);
void this.save();
return this;
}
deleteSection(id: ID) {
if (!this.sectionLeaders.has(id)) return false;
this.sectionLeaders.delete(id);
if (super.get(id) === ' ') {
return this.delete(id);
}
const user = Users.get(id);
if (user) {
user.updateIdentity();
Rooms.global.checkAutojoin(user);
}
this.save();
return true;
}
}

View File

@ -44,7 +44,9 @@ const PERMALOCK_CACHE_TIME = 30 * 24 * 60 * 60 * 1000; // 30 days
const DEFAULT_TRAINER_SPRITES = [1, 2, 101, 102, 169, 170, 265, 266];
import {FS, Utils, ProcessManager} from '../lib';
import {Auth, GlobalAuth, PLAYER_SYMBOL, HOST_SYMBOL, RoomPermission, GlobalPermission} from './user-groups';
import {
Auth, GlobalAuth, SECTIONLEADER_SYMBOL, PLAYER_SYMBOL, HOST_SYMBOL, RoomPermission, GlobalPermission,
} from './user-groups';
const MINUTES = 60 * 1000;
const IDLE_TIMER = 60 * MINUTES;
@ -1703,6 +1705,7 @@ export const Users = {
globalAuth,
isUsernameKnown,
isTrusted,
SECTIONLEADER_SYMBOL,
PLAYER_SYMBOL,
HOST_SYMBOL,
connections,

View File

@ -67,7 +67,7 @@ describe('Rooms features', function () {
});
it('should copy auth from tournament', function () {
parent = Rooms.createChatRoom('parentroom', '', {});
parent = Rooms.createChatRoom('parentroom');
parent.auth.get = () => '%';
const p1 = makeUser();
const p2 = makeUser();
@ -86,7 +86,7 @@ describe('Rooms features', function () {
});
it('should prevent overriding tournament room auth by a tournament player', function () {
parent = Rooms.createChatRoom('parentroom2', '', {});
parent = Rooms.createChatRoom('parentroom2');
parent.auth.get = () => '%';
const p1 = makeUser();
const p2 = makeUser();