"Remove" the global room

I couldn't completely remove the global room in one commit, but this
solves basically every problem with it by making it no longer a `Room`.

In particular, this means:

- It's no longer of type `Room`
- It's no longer in the `Rooms.rooms` table
- Its class name is now `GlobalRoomState` rather than `GlobalRoom`
- It no longer tracks its own user list (online user count is now
  provided by `Users.onlineCount`)
- It's no longer a socket channel (there's new syntax for "send this
  message to every user")
This commit is contained in:
Guangcong Luo 2020-06-20 01:13:03 -07:00
parent bf21d8fd0a
commit 702fe9f532
28 changed files with 214 additions and 256 deletions

View File

@ -71,7 +71,7 @@ export const commands: ChatCommands = {
html += Utils.html`<div style="float:right;color:#888;font-size:8pt">[${user.name}]</div><div style="clear:both"></div>`;
}
this.room.sendRankedUsers(`|html|<div class="infobox">${html}</div>`, rank as GroupSymbol);
room.sendRankedUsers(`|html|<div class="infobox">${html}</div>`, rank as GroupSymbol);
},
addrankhtmlboxhelp: [
`/addrankhtmlbox [rank], [message] - Shows everyone with the specified rank or higher a message, parsing HTML code contained. Requires: * # &`,
@ -121,7 +121,7 @@ export const commands: ChatCommands = {
}
html = `|uhtml${(cmd === 'changerankuhtml' ? 'change' : '')}|${name}|${html}`;
this.room.sendRankedUsers(html, rank as GroupSymbol);
room.sendRankedUsers(html, rank as GroupSymbol);
},
addrankuhtmlhelp: [
`/addrankuhtml [rank], [name], [message] - Shows everyone with the specified rank or higher a message that can change, parsing HTML code contained. Requires: * # &`,
@ -324,7 +324,7 @@ export const commands: ChatCommands = {
// respawn simulator processes
void Rooms.PM.respawn();
// broadcast the new formats list to clients
Rooms.global.send(Rooms.global.formatListText);
Rooms.global.sendAll(Rooms.global.formatListText);
this.sendReply("Formats have been hot-patched.");
} else if (target === 'loginserver') {
@ -787,7 +787,7 @@ export const commands: ChatCommands = {
refreshpage(target, room, user) {
if (!this.can('lockdown')) return false;
Rooms.global.send('|refresh|');
Rooms.global.sendAll('|refresh|');
const logRoom = Rooms.get('staff') || room;
logRoom.roomlog(`${user.name} used /refreshpage`);
},

View File

@ -507,8 +507,8 @@ export const commands: ChatCommands = {
target = this.splitTarget(target);
const targetUser = this.targetUser;
if (this.targetUsername === '~') {
this.room = Rooms.global;
this.pmTarget = null;
this.room = null;
} else if (!targetUser) {
let error = `User ${this.targetUsername} not found. Did you misspell their name?`;
error = `|pm|${this.user.getIdentity()}| ${this.targetUsername}|/error ${error}`;
@ -516,7 +516,7 @@ export const commands: ChatCommands = {
return;
} else {
this.pmTarget = targetUser;
this.room = null!;
this.room = null;
}
if (targetUser && !targetUser.connected) {
@ -547,7 +547,7 @@ export const commands: ChatCommands = {
const targetUser = this.pmTarget!; // not room means it's a PM
if (!targetRoom || targetRoom === Rooms.global) {
if (!targetRoom) {
return this.errorReply(`The room "${target}" was not found.`);
}
if (!targetRoom.checkModjoin(targetUser)) {
@ -1393,7 +1393,7 @@ export const commands: ChatCommands = {
}
const targetRoom = Rooms.get(target);
if (!targetRoom || targetRoom === Rooms.global || (
if (!targetRoom || (
targetRoom.settings.isPrivate && !user.inRooms.has(targetRoom.roomid) && !user.games.has(targetRoom.roomid)
)) {
const roominfo = {id: target, error: 'not found or access denied'};

View File

@ -19,10 +19,8 @@ export const commands: ChatCommands = {
alt: 'whois',
alts: 'whois',
whoare: 'whois',
whois(target, room, user, connection, cmd) {
let usedRoom: ChatRoom | GameRoom | GlobalRoom = room;
if (usedRoom?.roomid === 'staff' && !this.runBroadcast()) return;
if (!usedRoom) usedRoom = Rooms.global;
whois(target, room: Room | null, user, connection, cmd) {
if (room?.roomid === 'staff' && !this.runBroadcast()) return;
const targetUser = this.targetUserOrSelf(target, user.group === ' ');
const showAll = (cmd === 'ip' || cmd === 'whoare' || cmd === 'alt' || cmd === 'alts');
if (!targetUser) {
@ -43,8 +41,8 @@ export const commands: ChatCommands = {
buf += ` <small style="color:gray">(trusted${targetUser.id === trusted ? `` : `: <span class="username">${trusted}</span>`})</small>`;
}
if (!targetUser.connected) buf += ` <em style="color:gray">(offline)</em>`;
const roomauth = usedRoom.auth.getDirect(targetUser.id);
if (Config.groups[roomauth]?.name) {
const roomauth = room?.auth.getDirect(targetUser.id);
if (roomauth && Config.groups[roomauth]?.name) {
buf += Utils.html`<br />${Config.groups[roomauth].name} (${roomauth})`;
}
if (Config.groups[targetUser.group]?.name) {
@ -85,7 +83,7 @@ export const commands: ChatCommands = {
}
const canViewAlts = (user === targetUser || user.can('alts', targetUser));
const canViewPunishments = canViewAlts ||
(usedRoom.settings.isPrivate !== true && user.can('mute', targetUser, usedRoom) && targetUser.id in usedRoom.users);
(room && room.settings.isPrivate !== true && user.can('mute', targetUser, room) && targetUser.id in room.users);
const canViewSecretRooms = user === targetUser || (canViewAlts && targetUser.locked) || user.can('makeroom');
buf += `<br />`;
@ -2631,13 +2629,15 @@ export const commands: ChatCommands = {
export const pages: PageTable = {
punishments(query, user) {
this.title = 'Punishments';
const room = this.extractRoom();
if (!room) return;
let buf = "";
this.extractRoom();
if (!user.named) return Rooms.RETRY_AFTER_LOGIN;
if (!this.room.persist) return;
if (!this.can('mute', null, this.room)) return;
if (!room.persist) return;
if (!this.can('mute', null, room)) return;
// Ascending order
const sortedPunishments = Array.from(Punishments.getPunishments(this.room.roomid))
const sortedPunishments = Array.from(Punishments.getPunishments(room.roomid))
.sort((a, b) => a[1].expireTime - b[1].expireTime);
const sP = new Map();
for (const punishment of sortedPunishments) {

View File

@ -227,7 +227,7 @@ export const commands: ChatCommands = {
let userLookup = '';
if (cmd === 'roomauth1') userLookup = `\n\nTo look up auth for a user, use /userauth ${target}`;
let targetRoom = room;
if (target) targetRoom = Rooms.search(target) as ChatRoom | GameRoom;
if (target) targetRoom = Rooms.search(target)!;
if (!targetRoom || targetRoom.roomid === 'global' || !targetRoom.checkModjoin(user)) {
return this.errorReply(`The room "${target}" does not exist.`);
}
@ -262,7 +262,7 @@ export const commands: ChatCommands = {
const also = buffer.length === 0 ? `` : ` also`;
buffer.push(`${curRoom.title} is a ${roomType}subroom of ${curRoom.parent.title}, so ${curRoom.parent.title} users${inheritedUserType}${also} have authority in this room.`);
}
curRoom = curRoom.parent as ChatRoom | GameRoom;
curRoom = curRoom.parent;
}
if (!buffer.length) {
connection.popup(`The room '${targetRoom.title}' has no auth. ${userLookup}`);
@ -375,7 +375,7 @@ export const commands: ChatCommands = {
leave: 'part',
part(target, room, user, connection) {
const targetRoom = target ? Rooms.search(target) : room;
if (!targetRoom || targetRoom === Rooms.global) {
if (!targetRoom) {
if (target.startsWith('view-')) return;
return this.errorReply(`The room '${target}' does not exist.`);
}
@ -1829,7 +1829,7 @@ export const commands: ChatCommands = {
blacklists: 'showblacklist',
showbl: 'showblacklist',
showblacklist(target, room, user, connection, cmd) {
if (target) room = Rooms.search(target) as ChatRoom | GameRoom;
if (target) room = Rooms.search(target)!;
if (!room) return this.errorReply(`The room "${target}" was not found.`);
if (!this.can('mute', null, room)) return false;
const SOON_EXPIRING_TIME = 3 * 30 * 24 * 60 * 60 * 1000; // 3 months

View File

@ -7,11 +7,11 @@ import {Utils} from '../../lib/utils';
export class Announcement {
readonly activityId: 'announcement';
announcementNumber: number;
room: ChatRoom | GameRoom;
room: Room;
source: string;
timeout: NodeJS.Timer | null;
timeoutMins: number;
constructor(room: ChatRoom | GameRoom, source: string) {
constructor(room: Room, source: string) {
this.activityId = 'announcement';
this.announcementNumber = room.nextGameNumber();
this.room = room;

View File

@ -18,7 +18,7 @@ type Symbols = '♥' | '♦' | '♣' | '♠';
type SymbolName = 'Hearts' | 'Diamonds' | 'Clubs' | 'Spades';
export class Blackjack extends Rooms.RoomGame {
room: ChatRoom | GameRoom;
room: Room;
blackjack: boolean;
@ -56,7 +56,7 @@ export class Blackjack extends Rooms.RoomGame {
playerTable: {[k: string]: BlackjackPlayer};
gameNumber: number;
constructor(room: ChatRoom | GameRoom, user: User, autostartMinutes = 0) {
constructor(room: Room, user: User, autostartMinutes = 0) {
super(room);
this.gameNumber = room.nextGameNumber();
this.room = room;

View File

@ -6,7 +6,7 @@ type FilterWord = [RegExp, string, string, string | null, number];
type MonitorHandler = (
this: CommandContext,
line: FilterWord,
room: ChatRoom | GameRoom | null,
room: Room | null,
user: User,
message: string,
lcMessage: string,

View File

@ -52,18 +52,20 @@ export const destroy = () => {
export const pages: PageTable = {
async spotlights(query, user, connection) {
this.title = 'Daily Spotlights';
this.extractRoom();
const room = this.extractRoom();
if (!room) return;
let buf = `<div class="pad ladder"><h2>Daily Spotlights</h2>`;
if (!spotlights[this.room.roomid]) {
if (!spotlights[room.roomid]) {
buf += `<p>This room has no daily spotlights.</p></div>`;
} else {
for (const key in spotlights[this.room.roomid]) {
for (const key in spotlights[room.roomid]) {
buf += `<table style="margin-bottom:30px;"><th colspan="2"><h3>${key}:</h3></th>`;
for (const [i, spotlight] of spotlights[this.room.roomid][key].entries()) {
for (const [i, spotlight] of spotlights[room.roomid][key].entries()) {
const html = await renderSpotlight(spotlight.description, spotlight.image);
buf += `<tr><td>${i ? i : 'Current'}</td><td>${html}</td></tr>`;
// @ts-ignore room is definitely a proper room here.
if (!user.can('announce', null, this.room)) break;
if (!user.can('announce', null, room)) break;
}
buf += '</table>';
}

View File

@ -18,7 +18,7 @@ export class Hangman extends Rooms.RoomGame {
lastGuesser: string;
wordSoFar: string[];
constructor(room: ChatRoom | GameRoom, user: User, word: string, hint = '') {
constructor(room: Room, user: User, word: string, hint = '') {
super(room);
this.gameNumber = room.nextGameNumber();

View File

@ -40,7 +40,7 @@ export class Jeopardy extends Rooms.RoomGame {
finals: boolean;
gameNumber: number;
constructor(room: ChatRoom | GameRoom, user: User, categoryCount: number, questionCount: number) {
constructor(room: Room, user: User, categoryCount: number, questionCount: number) {
super(room);
this.gameNumber = room.nextGameNumber();
this.playerTable = Object.create(null);

View File

@ -264,19 +264,21 @@ export const commands: ChatCommands = {
export const pages: PageTable = {
lottery(query, user) {
this.extractRoom();
this.title = 'Lottery';
const room = this.extractRoom();
if (!room) return;
let buf = '<div class="pad">';
const lottery = lotteries[this.room.roomid];
const lottery = lotteries[room.roomid];
if (!lottery) {
buf += `<h2>There is no lottery running in ${this.room.title}</h2></div>`;
buf += `<h2>There is no lottery running in ${room.title}</h2></div>`;
return buf;
}
buf += `<h2 style="text-align: center">${lottery.name}</h2>${lottery.markup}<br />`;
if (lottery.running) {
const userSignedUp = lottery.participants[user.latestIp] ||
Object.values(lottery.participants).map(toID).includes(user.id);
buf += `<button class="button" name="send" style=" display: block; margin: 0 auto" value="/lottery ${userSignedUp ? 'leave' : 'join'} ${this.room.roomid}">${userSignedUp ? "Leave the " : "Sign up for the"} lottery</button>`;
buf += `<button class="button" name="send" style=" display: block; margin: 0 auto" value="/lottery ${userSignedUp ? 'leave' : 'join'} ${room.roomid}">${userSignedUp ? "Leave the " : "Sign up for the"} lottery</button>`;
} else {
buf += '<p style="text-align: center"><b>This lottery has already ended. The winners are:</b></p>';
buf += '<ul style="display: table; margin: 0px auto">';

View File

@ -569,7 +569,7 @@ export const commands: ChatCommands = {
pl: 'modlog',
timedmodlog: 'modlog',
modlog(target, room, user, connection, cmd) {
if (!room) room = Rooms.get('global') as ChatRoom | GameRoom;
if (!room) (room as Room | undefined) = Rooms.get('global');
let roomid: RoomID = (room.roomid === 'staff' ? 'global' : room.roomid);
if (target.includes(',')) {

View File

@ -14,7 +14,7 @@ interface Option {
export class Poll {
readonly activityId: 'poll';
pollNumber: number;
room: ChatRoom | GameRoom;
room: Room;
question: string;
supportHTML: boolean;
multiPoll: boolean;
@ -26,7 +26,7 @@ export class Poll {
timeoutMins: number;
isQuiz: boolean;
options: Map<number, Option>;
constructor(room: ChatRoom | GameRoom, questionData: QuestionData, options: string[], multi: boolean) {
constructor(room: Room, questionData: QuestionData, options: string[], multi: boolean) {
this.activityId = 'poll';
this.pollNumber = room.nextGameNumber();
this.room = room;

View File

@ -663,12 +663,12 @@ const MODES: {[k: string]: GameMode | string} = {
};
export class ScavengerGameTemplate {
room: ChatRoom | GameRoom;
room: Room;
playerlist: null | string[];
timer: NodeJS.Timer | null;
[k: string]: any;
constructor(room: GameRoom | ChatRoom) {
constructor(room: Room) {
this.room = room;
this.playerlist = null;
this.timer = null;
@ -694,7 +694,7 @@ export class ScavengerGameTemplate {
}
}
const LoadGame = function (room: ChatRoom | GameRoom, gameid: string) {
const LoadGame = function (room: Room, gameid: string) {
let game = MODES[gameid];
if (!game) return false; // invalid id
if (typeof game === 'string') game = MODES[game];

View File

@ -202,7 +202,7 @@ if (LeaderboardRoom) {
LeaderboardRoom.scavLeaderboard.scavsPlayerLeaderboard = PlayerLeaderboard;
}
function formatQueue(queue: QueuedHunt[] | undefined, viewer: User, room: ChatRoom | GameRoom, broadcasting?: boolean) {
function formatQueue(queue: QueuedHunt[] | undefined, viewer: User, room: Room, broadcasting?: boolean) {
const showStaff = viewer.can('mute', null, room) && !broadcasting;
const queueDisabled = room.settings.scavSettings?.scavQueueDisabled;
const timerDuration = room.settings.scavSettings?.defaultScavTimer || DEFAULT_TIMER_DURATION;
@ -353,7 +353,7 @@ export class ScavengerHunt extends Rooms.RoomGame {
[k: string]: any; // for purposes of adding new temporary properties for the purpose of twists.
constructor(
room: ChatRoom | GameRoom,
room: Room,
staffHost: User | FakeUser,
hosts: FakeUser[],
gameType: GameTypes,
@ -952,7 +952,7 @@ export class ScavengerHunt extends Rooms.RoomGame {
return ips.filter((ip, index) => ips.indexOf(ip) === index).length;
}
static parseHosts(hostArray: string[], room: ChatRoom | GameRoom, allowOffline?: boolean) {
static parseHosts(hostArray: string[], room: Room, allowOffline?: boolean) {
const hosts = [];
for (const u of hostArray) {
const id = toID(u);
@ -2345,11 +2345,13 @@ const ScavengerCommands: ChatCommands = {
export const pages: PageTable = {
recycledHunts(query, user, connection) {
this.title = 'Recycled Hunts';
const room = this.extractRoom();
if (!room) return;
let buf = "";
this.extractRoom();
if (!user.named) return Rooms.RETRY_AFTER_LOGIN;
if (!this.room.persist) return;
if (!this.can('mute', null, this.room)) return;
if (!room.persist) return;
if (!this.can('mute', null, room)) return;
buf += `<div class="pad"><h2>List of recycled Scavenger hunts</h2>`;
buf += `<ol style="width: 90%;">`;
for (const hunt of scavengersData.recycledHunts) {

View File

@ -680,7 +680,7 @@ export const otdCommands: ChatCommands = {
return handler.generateWinnerDisplay().then(text => {
if (!text) return this.errorReply("There is no winner yet.");
this.sendReplyBox(text);
this.room.update();
room.update();
});
},
};

View File

@ -320,7 +320,7 @@ class Trivia extends Rooms.RoomGame {
curAnswers: string[];
askedAt: number[];
constructor(
room: GameRoom | ChatRoom, mode: string, category: string,
room: Room, mode: string, category: string,
length: string, questions: TriviaQuestion[], isRandomMode = false
) {
super(room);

View File

@ -100,7 +100,7 @@ export class UnoGame extends Rooms.RoomGame {
isPlusFour: boolean;
gameNumber: number;
constructor(room: ChatRoom | GameRoom, cap: number, suppressMessages: boolean) {
constructor(room: Room, cap: number, suppressMessages: boolean) {
super(room);
this.playerTable = Object.create(null);

View File

@ -34,7 +34,7 @@ class Giveaway {
gaNumber: number;
host: User;
giver: User;
room: ChatRoom | GameRoom;
room: Room;
ot: string;
tid: string;
prize: string;
@ -45,7 +45,7 @@ class Giveaway {
sprite: string;
constructor(
host: User, giver: User, room: ChatRoom | GameRoom,
host: User, giver: User, room: Room,
ot: string, tid: string, prize: string
) {
this.gaNumber = room.nextGameNumber();
@ -111,15 +111,15 @@ class Giveaway {
return false;
}
static checkBanned(room: ChatRoom | GameRoom, user: User) {
static checkBanned(room: Room, user: User) {
return Punishments.getRoomPunishType(room, toID(user)) === 'GIVEAWAYBAN';
}
static ban(room: ChatRoom | GameRoom, user: User, reason: string) {
static ban(room: Room, user: User, reason: string) {
Punishments.roomPunish(room, user, ['GIVEAWAYBAN', toID(user), Date.now() + BAN_DURATION, reason]);
}
static unban(room: ChatRoom | GameRoom, user: User) {
static unban(room: Room, user: User) {
Punishments.roomUnpunish(room, toID(user), 'GIVEAWAYBAN', false);
}
@ -219,7 +219,7 @@ export class QuestionGiveaway extends Giveaway {
winner: User | null;
constructor(
host: User, giver: User, room: ChatRoom | GameRoom, ot: string, tid: string,
host: User, giver: User, room: Room, ot: string, tid: string,
prize: string, question: string, answers: string[]
) {
super(host, giver, room, ot, tid, prize);
@ -359,7 +359,7 @@ export class LotteryGiveaway extends Giveaway {
maxWinners: number;
constructor(
host: User, giver: User, room: ChatRoom | GameRoom, ot: string,
host: User, giver: User, room: Room, ot: string,
tid: string, prize: string, winners: number
) {
super(host, giver, room, ot, tid, prize);
@ -484,7 +484,7 @@ export class LotteryGiveaway extends Giveaway {
export class GTSGiveaway {
gtsNumber: number;
room: ChatRoom | GameRoom;
room: Room;
giver: User;
left: number;
summary: string;
@ -497,7 +497,7 @@ export class GTSGiveaway {
timer: NodeJS.Timer | null;
constructor(
room: ChatRoom | GameRoom, giver: User, amount: number,
room: Room, giver: User, amount: number,
summary: string, deposit: string, lookfor: string
) {
this.gtsNumber = room.nextGameNumber();

View File

@ -35,7 +35,7 @@ export interface PageTable {
export type ChatHandler = (
this: CommandContext,
target: string,
room: ChatRoom | GameRoom,
room: Room,
user: User,
connection: Connection,
cmd: string,
@ -66,7 +66,7 @@ export type ChatFilter = (
this: CommandContext,
message: string,
user: User,
room: ChatRoom | GameRoom | null,
room: Room | null,
connection: Connection,
targetUser: User | null,
originalMessage: string
@ -203,7 +203,7 @@ class MessageContext {
export class PageContext extends MessageContext {
readonly connection: Connection;
room: Room;
room: Room | null;
pageid: string;
initialized: boolean;
title: string;
@ -211,7 +211,7 @@ export class PageContext extends MessageContext {
super(options.user, options.language);
this.connection = options.connection;
this.room = Rooms.get('global')!;
this.room = null;
this.pageid = options.pageid;
this.initialized = false;
@ -237,16 +237,16 @@ export class PageContext extends MessageContext {
const room = Rooms.get(parts[2]);
if (!room) {
this.send(`<h2>Invalid room.</h2>`);
return false;
return null;
}
this.room = room;
return room.roomid;
return room;
}
send(content: string) {
if (!content.startsWith('|deinit')) {
const roomid = this.room !== Rooms.global ? `[${this.room.roomid}] ` : '';
const roomid = this.room ? `[${this.room.roomid}] ` : '';
if (!this.initialized) {
content = `|init|html\n|title|${roomid}${this.title}\n|pagehtml|${content}`;
this.initialized = true;
@ -295,10 +295,18 @@ export class PageContext extends MessageContext {
}
}
/**
* This is a message sent in a PM or to a chat/battle room.
*
* There are three cases to be aware of:
* - PM to user: `context.pmTarget` will exist and `context.room` will be `null`
* - message to room: `context.room` will exist and `context.pmTarget` will be `null`
* - console command (PM to `~`): `context.pmTarget` and `context.room` will both be `null`
*/
export class CommandContext extends MessageContext {
message: string;
pmTarget: User | null;
room: Room;
room: Room | null;
connection: Connection;
cmd: string;
cmdToken: string;
@ -312,8 +320,8 @@ export class CommandContext extends MessageContext {
inputUsername: string;
constructor(
options:
{message: string, room: Room, user: User, connection: Connection} &
Partial<{pmTarget: User | null, cmd: string, cmdToken: string, target: string, fullCmd: string}>
{message: string, user: User, connection: Connection} &
Partial<{room: Room | null, pmTarget: User | null, cmd: string, cmdToken: string, target: string, fullCmd: string}>
) {
super(
options.user, options.room && options.room.settings.language ?
@ -365,7 +373,7 @@ export class CommandContext extends MessageContext {
message = this.run(commandHandler);
} else {
if (commandHandler === '!') {
if (this.room === Rooms.global) {
if (!this.room) {
return this.popupReply(`You tried use "${message}" as a global command, but it is not a global command.`);
} else if (this.room) {
return this.popupReply(`You tried to send "${message}" to the room "${this.room.roomid}" but it failed because you were not in that room.`);
@ -394,16 +402,14 @@ export class CommandContext extends MessageContext {
// Output the message
if (message && typeof message.then === 'function') {
message.then(() => this.update());
} else if (message && message !== true) {
if (this.pmTarget) {
Chat.sendPM(message, this.user, this.pmTarget);
} else {
this.room.add(`|c|${this.user.getIdentity(this.room.roomid)}|${message}`);
if (this.room && this.room.game && this.room.game.onLogMessage) {
this.room.game.onLogMessage(message, this.user);
void (message as Promise<any>).then(resolvedMessage => {
if (resolvedMessage && resolvedMessage !== true) {
this.sendChatMessage(resolvedMessage);
}
}
this.update();
});
} else if (message && message !== true) {
this.sendChatMessage(message);
}
this.update();
@ -411,6 +417,19 @@ export class CommandContext extends MessageContext {
return message;
}
sendChatMessage(message: string) {
if (this.pmTarget) {
Chat.sendPM(message, this.user, this.pmTarget);
} else if (this.room) {
this.room.add(`|c|${this.user.getIdentity(this.room.roomid)}|${message}`);
if (this.room.game && this.room.game.onLogMessage) {
this.room.game.onLogMessage(message, this.user);
}
} else {
this.connection.popup(`Your message could not be sent:\n\n${message}\n\nIt needs to be sent to a user or room.`);
}
}
splitCommand(message = this.message, recursing = false): '!' | undefined | ChatHandler {
this.cmd = '';
this.cmdToken = '';
@ -510,11 +529,7 @@ export class CommandContext extends MessageContext {
this.target = target;
this.fullCmd = fullCmd;
const requireGlobalCommand = (
this.pmTarget ||
this.room === Rooms.global ||
(this.room && !(this.user.id in this.room.users))
);
const requireGlobalCommand = !(this.room && this.user.id in this.room.users);
if (typeof commandHandler === 'function' && requireGlobalCommand) {
const baseCmd = typeof curCommands[cmd] === 'string' ? curCommands[cmd] : cmd;
@ -604,7 +619,7 @@ export class CommandContext extends MessageContext {
return this.room.game.onChatMessage(this.message, this.user);
}
pmTransform(originalMessage: string) {
if (!this.pmTarget && this.room !== Rooms.global) throw new Error(`Not a PM`);
if (this.room) throw new Error(`Not a PM`);
const targetIdentity = this.pmTarget ? this.pmTarget.getIdentity() : '~';
const prefix = `|pm|${this.user.getIdentity()}|${targetIdentity}|`;
return originalMessage.split('\n').map(message => {
@ -630,7 +645,7 @@ export class CommandContext extends MessageContext {
this.add(data);
} else {
// not broadcasting
if (this.pmTarget || this.room === Rooms.global) {
if (!this.room) {
data = this.pmTransform(data);
this.connection.send(data);
} else {
@ -651,36 +666,33 @@ export class CommandContext extends MessageContext {
this.connection.popup(message);
}
add(data: string) {
if (this.pmTarget) {
if (!this.room) {
data = this.pmTransform(data);
this.user.send(data);
if (this.pmTarget !== this.user) this.pmTarget.send(data);
if (this.pmTarget !== this.user) this.pmTarget!.send(data);
return;
}
this.room.add(data);
}
send(data: string) {
if (this.pmTarget) {
if (!this.room) {
data = this.pmTransform(data);
this.user.send(data);
if (this.pmTarget !== this.user) this.pmTarget.send(data);
if (this.pmTarget !== this.user) this.pmTarget!.send(data);
return;
}
this.room.send(data);
}
sendModCommand(data: string) {
this.room.sendModsByUser(this.user, data);
this.room!.sendModsByUser(this.user, data);
}
privateModCommand() {
throw new Error(`this.privateModCommand has been renamed to this.privateModAction, which no longer writes to modlog.`);
}
privateModAction(msg: string) {
this.room.sendMods(msg);
this.room!.sendMods(msg);
this.roomlog(msg);
}
globalModlog(action: string, user: string | User | null, note: string) {
let buf = `(${this.room.roomid}) ${action}: `;
let buf = `(${this.room ? this.room.roomid : 'global'}) ${action}: `;
if (user) {
if (typeof user === 'string') {
buf += `[${user}]`;
@ -696,7 +708,7 @@ export class CommandContext extends MessageContext {
buf += note.replace(/\n/gm, ' ');
Rooms.global.modlog(buf);
if (this.room !== Rooms.global) this.room.modlog(buf);
if (this.room) this.room.modlog(buf);
}
modlog(
action: string,
@ -704,7 +716,7 @@ export class CommandContext extends MessageContext {
note: string | null = null,
options: Partial<{noalts: any, noip: any}> = {}
) {
let buf = `(${this.room.roomid}) ${action}: `;
let buf = `(${this.room ? this.room.roomid : 'global'}) ${action}: `;
if (user) {
if (typeof user === 'string') {
buf += `[${toID(user)}]`;
@ -722,27 +734,20 @@ export class CommandContext extends MessageContext {
buf += ` by ${this.user.id}`;
if (note) buf += `: ${note.replace(/\n/gm, ' ')}`;
this.room.modlog(buf);
(this.room || Rooms.global).modlog(buf);
}
roomlog(data: string) {
if (this.pmTarget) return;
this.room.roomlog(data);
}
logEntry() {
throw new Error(`this.logEntry has been renamed to this.roomlog.`);
}
addModCommand() {
throw new Error(`this.addModCommand has been renamed to this.addModAction, which no longer writes to modlog.`);
if (this.room) this.room.roomlog(data);
}
addModAction(msg: string) {
this.room.addByUser(this.user, msg);
this.room!.addByUser(this.user, msg);
}
update() {
if (this.room) this.room.update();
}
filter(message: string, targetUser: User | null = null) {
if (!this.room || this.room.roomid === 'global') return null;
return Chat.filter(this, message, this.user, this.room as GameRoom | ChatRoom, this.connection, targetUser);
if (!this.room) return null;
return Chat.filter(this, message, this.user, this.room, this.connection, targetUser);
}
statusfilter(status: string) {
return Chat.statusfilter(status, this.user);
@ -771,14 +776,14 @@ export class CommandContext extends MessageContext {
return true;
}
if (this.room instanceof Rooms.GlobalRoom) {
this.errorReply(`You have no one to broadcast this to.`);
if (this.room && !this.user.can('broadcast', null, this.room)) {
this.errorReply(`You need to be voiced to broadcast this command's information.`);
this.errorReply(`To see it for yourself, use: /${this.message.slice(1)}`);
return false;
}
if (!this.pmTarget && !this.user.can('broadcast', null, this.room)) {
this.errorReply(`You need to be voiced to broadcast this command's information.`);
if (!this.room && !this.pmTarget) {
this.errorReply(`Broadcasting a command with "!" in a PM or chatroom will show it that user or room.`);
this.errorReply(`To see it for yourself, use: /${this.message.slice(1)}`);
return false;
}
@ -820,9 +825,9 @@ export class CommandContext extends MessageContext {
if (this.pmTarget) {
this.sendReply('|c~|' + (suppressMessage || this.message));
} else {
this.sendReply('|c|' + this.user.getIdentity(this.room.roomid) + '|' + (suppressMessage || this.message));
this.sendReply('|c|' + this.user.getIdentity(this.room ? this.room.roomid : '') + '|' + (suppressMessage || this.message));
}
if (!ignoreCooldown && !this.pmTarget) {
if (!ignoreCooldown && this.room) {
this.room.lastBroadcast = this.broadcastMessage;
this.room.lastBroadcastTime = Date.now();
}
@ -831,19 +836,15 @@ export class CommandContext extends MessageContext {
}
/* The sucrase transformation of optional chaining is too expensive to be used in a hot function like this. */
/* eslint-disable @typescript-eslint/prefer-optional-chain */
canTalk(message: string, room?: GameRoom | ChatRoom | null, targetUser?: User | null): string | null;
canTalk(message?: null, room?: GameRoom | ChatRoom | null, targetUser?: User | null): true | null;
canTalk(message: string | null = null, room: GameRoom | ChatRoom | null = null, targetUser: User | null = null) {
canTalk(message: string, room?: Room | null, targetUser?: User | null): string | null;
canTalk(message?: null, room?: Room | null, targetUser?: User | null): true | null;
canTalk(message: string | null = null, room: Room | null = null, targetUser: User | null = null) {
if (!targetUser && this.pmTarget) {
targetUser = this.pmTarget;
}
if (targetUser) {
room = null;
} else if (!room) {
if (this.room.roomid === 'global') {
this.connection.popup(`Your message could not be sent:\n\n${message}\n\nIt needs to be sent to a user or room.`);
return null;
}
// @ts-ignore excludes GlobalRoom above
room = this.room;
}
@ -1136,9 +1137,9 @@ export class CommandContext extends MessageContext {
}
if (tagName === 'img') {
if (this.room.settings.isPersonal && !this.user.can('lock')) {
if (!this.room || (this.room.settings.isPersonal && !this.user.can('lock'))) {
this.errorReply(`This tag is not allowed: <${tagContent}>`);
this.errorReply(`Images are not allowed in personal rooms.`);
this.errorReply(`Images are not allowed outside of chatrooms.`);
return null;
}
if (!/width ?= ?(?:[0-9]+|"[0-9]+")/i.test(tagContent) || !/height ?= ?(?:[0-9]+|"[0-9]+")/i.test(tagContent)) {
@ -1160,12 +1161,13 @@ export class CommandContext extends MessageContext {
}
}
if (tagName === 'button') {
if ((this.room.settings.isPersonal || this.room.settings.isPrivate === true) && !this.user.can('lock')) {
if ((!this.room || this.room.settings.isPersonal || this.room.settings.isPrivate === true) && !this.user.can('lock')) {
const buttonName = / name ?= ?"([^"]*)"/i.exec(tagContent)?.[1];
const buttonValue = / value ?= ?"([^"]*)"/i.exec(tagContent)?.[1];
if (buttonName === 'send' && buttonValue?.startsWith('/msg ')) {
const [pmTarget] = buttonValue.slice(5).split(',');
if (this.room.auth.get(toID(pmTarget)) !== '*') {
const auth = this.room ? this.room.auth : Users.globalAuth;
if (auth.get(toID(pmTarget)) !== '*') {
this.errorReply(`This button is not allowed: <${tagContent}>`);
this.errorReply(`Your scripted button can't send PMs to ${pmTarget}, because that user is not a Room Bot.`);
return null;
@ -1248,7 +1250,7 @@ export const Chat = new class {
context: CommandContext,
message: string,
user: User,
room: GameRoom | ChatRoom | null,
room: Room | null,
connection: Connection,
targetUser: User | null = null
) {
@ -1469,7 +1471,7 @@ export const Chat = new class {
* @param user - the user that sent the message
* @param connection - the connection the user sent the message from
*/
parse(message: string, room: Room, user: User, connection: Connection) {
parse(message: string, room: Room | null | undefined, user: User, connection: Connection) {
Chat.loadPlugins();
const context = new CommandContext({message, room, user, connection});
@ -1915,7 +1917,7 @@ export type FilterWord = [RegExp, string, string, string | null, number];
export type MonitorHandler = (
this: CommandContext,
line: FilterWord,
room: ChatRoom | GameRoom | null,
room: Room | null,
user: User,
message: string,
lcMessage: string,

View File

@ -29,7 +29,7 @@ namespace Chat {
}
// Rooms
type GlobalRoom = Rooms.GlobalRoom;
type GlobalRoomState = Rooms.GlobalRoomState;
type ChatRoom = Rooms.ChatRoom;
type GameRoom = Rooms.GameRoom;
type BasicRoom = Rooms.BasicRoom;
@ -40,7 +40,7 @@ type Roomlog = Rooms.Roomlog;
type Room = Rooms.Room;
type RoomID = "" | "global" | "lobby" | "staff" | "upperstaff" | "development" | "battle" | string & {__isRoomID: true};
namespace Rooms {
export type GlobalRoom = import('./rooms').GlobalRoom;
export type GlobalRoomState = import('./rooms').GlobalRoomState;
export type ChatRoom = import('./rooms').ChatRoom;
export type GameRoom = import('./rooms').GameRoom;
export type BasicRoom = import('./rooms').BasicRoom;

View File

@ -81,7 +81,7 @@ export class RoomGame {
* The room this roomgame is in. Rooms can only have one RoomGame at a time,
* which are available as `this.room.game === this`.
*/
room: ChatRoom | GameRoom;
room: Room;
gameid: ID;
title: string;
allowRenames: boolean;
@ -100,7 +100,7 @@ export class RoomGame {
* to be later. The /timer command is written to be resilient to this.
*/
timer?: {timerRequesters?: Set<ID>, start: (force?: User) => void, stop: (force?: User) => void} | NodeJS.Timer | null;
constructor(room: ChatRoom | GameRoom) {
constructor(room: Room) {
this.roomid = room.roomid;
this.room = room;
this.gameid = 'game' as ID;

View File

@ -114,7 +114,7 @@ export interface RoomSettings {
noAutoTruncate?: boolean;
isMultichannel?: boolean;
}
export type Room = GlobalRoom | GameRoom | ChatRoom;
export type Room = GameRoom | ChatRoom;
type Poll = import('./chat-plugins/poll').Poll;
type Announcement = import('./chat-plugins/announcements').Announcement;
type RoomEvent = import('./chat-plugins/room-events').RoomEvent;
@ -125,7 +125,7 @@ type Tournament = import('./tournaments/index').Tournament;
export abstract class BasicRoom {
roomid: RoomID;
title: string;
readonly type: 'chat' | 'battle' | 'global';
readonly type: 'chat' | 'battle';
readonly users: UserTable;
/**
* Scrollback log. This is the log that's sent to users when
@ -261,11 +261,9 @@ export abstract class BasicRoom {
* for everyone, and appears in the scrollback for new users who
* join.
*/
add(message: string): this { throw new Error(`should be implemented by subclass`); }
roomlog(message: string) { throw new Error(`should be implemented by subclass`); }
modlog(message: string) { throw new Error(`should be implemented by subclass`); }
logEntry() { throw new Error(`room.logEntry has been renamed room.roomlog`); }
addLogMessage() { throw new Error(`room.addLogMessage has been renamed room.addByUser`); }
abstract add(message: string): this;
abstract roomlog(message: string): void;
abstract modlog(message: string): void;
/**
* Inserts (sanitized) HTML into the room log.
*/
@ -474,9 +472,7 @@ export abstract class BasicRoom {
destroy() {}
}
export class GlobalRoom extends BasicRoom {
readonly type: 'global';
readonly active: false;
export class GlobalRoomState {
readonly settingsList: RoomSettings[];
readonly chatRooms: ChatRoom[];
/**
@ -488,7 +484,6 @@ export class GlobalRoom extends BasicRoom {
*/
readonly staffAutojoinList: RoomID[];
readonly ladderIpLog: WriteStream;
readonly users: UserTable;
readonly reportUserStatsInterval: NodeJS.Timeout;
readonly modlogStream: WriteStream;
lockdown: boolean | 'pre' | 'ddos';
@ -496,21 +491,11 @@ export class GlobalRoom extends BasicRoom {
lastReportedCrash: number;
lastBattle: number;
lastWrittenBattle: number;
userCount: number;
maxUsers: number;
maxUsersDate: number;
formatList: string;
readonly game: null;
readonly battle: null;
readonly tour: null;
constructor(roomid: RoomID) {
if (roomid !== 'global') throw new Error(`The global room's room ID must be 'global'`);
super(roomid);
this.type = 'global';
this.active = false;
constructor() {
this.settingsList = [];
try {
this.settingsList = require('../config/chatrooms.json');
@ -580,8 +565,6 @@ export class GlobalRoom extends BasicRoom {
);
// init users
this.users = Object.create(null);
this.userCount = 0; // cache of `size(this.users)`
this.maxUsers = 0;
this.maxUsersDate = 0;
this.lockdown = false;
@ -637,7 +620,7 @@ export class GlobalRoom extends BasicRoom {
}
void LoginServer.request('updateuserstats', {
date: Date.now(),
users: this.userCount,
users: Users.onlineCount,
});
}
@ -753,7 +736,7 @@ export class GlobalRoom extends BasicRoom {
official: [],
pspl: [],
chat: [],
userCount: this.userCount,
userCount: Users.onlineCount,
battleCount: this.battleCount,
};
for (const room of this.chatRooms) {
@ -779,18 +762,8 @@ export class GlobalRoom extends BasicRoom {
}
return roomsData;
}
checkModjoin(user: User) {
return true;
}
isMuted(user: User) {
return undefined;
}
send(message: string) {
Sockets.roomBroadcast(this.roomid, message);
}
add(message: string) {
// TODO: make sure this never happens
return this;
sendAll(message: string) {
Sockets.roomBroadcast('', message);
}
addChatRoom(title: string) {
const id = toID(title) as RoomID;
@ -896,7 +869,7 @@ export class GlobalRoom extends BasicRoom {
checkAutojoin(user: User, connection?: Connection) {
if (!user.named) return;
for (let [i, staffAutojoin] of this.staffAutojoinList.entries()) {
const room = Rooms.get(staffAutojoin) as ChatRoom | GameRoom;
const room = Rooms.get(staffAutojoin);
if (!room) {
this.staffAutojoinList.splice(i, 1);
i--;
@ -921,34 +894,12 @@ export class GlobalRoom extends BasicRoom {
}
}
}
onConnect(user: User, connection: Connection) {
handleConnect(user: User, connection: Connection) {
connection.send(user.getUpdateuserText() + '\n' + this.configRankList + this.formatListText);
}
onJoin(user: User, connection: Connection) {
if (!user) return false; // ???
if (this.users[user.id]) return user;
this.users[user.id] = user;
if (++this.userCount > this.maxUsers) {
this.maxUsers = this.userCount;
if (Users.users.size > this.maxUsers) {
this.maxUsers = Users.users.size;
this.maxUsersDate = Date.now();
}
return user;
}
onRename(user: User, oldid: ID, joining: boolean) {
delete this.users[oldid];
this.users[user.id] = user;
return user;
}
onLeave(user: User) {
if (!user) return; // ...
if (!(user.id in this.users)) {
Monitor.crashlog(new Error(`user ${user.id} already left`));
return;
}
delete this.users[user.id];
this.userCount--;
}
startLockdown(err: Error | null = null, slow = false) {
if (this.lockdown && err) return;
@ -1681,8 +1632,8 @@ export class GameRoom extends BasicChatRoom {
}
function getRoom(roomid?: string | Room) {
if (roomid && (roomid as Room).roomid) return roomid as Room;
return Rooms.rooms.get(roomid as RoomID);
if (typeof roomid === 'string') return Rooms.rooms.get(roomid as RoomID);
return roomid as Room;
}
export const Rooms = {
@ -1813,11 +1764,11 @@ export const Rooms = {
battleModlogStream: FS('logs/modlog/modlog_battle.txt').createAppendStream(),
groupchatModlogStream: FS('logs/modlog/modlog_groupchat.txt').createAppendStream(),
global: null! as GlobalRoom,
global: null! as GlobalRoomState,
lobby: null as ChatRoom | null,
BasicRoom,
GlobalRoom,
GlobalRoomState,
GameRoom,
ChatRoom: BasicChatRoom,
@ -1836,8 +1787,4 @@ export const Rooms = {
// initialize
Monitor.notice("NEW GLOBAL: global");
Rooms.global = new GlobalRoom('global');
Rooms.rooms.set('global', Rooms.global);
Rooms.global = new GlobalRoomState();

View File

@ -458,9 +458,11 @@ export class ServerStream extends Streams.ObjectReadWriteStream<string> {
case '#':
// #roomid, message
// message to all connections in room
// #, message
// message to all connections
nlLoc = data.indexOf('\n');
roomid = data.substr(1, nlLoc - 1) as RoomID;
room = this.rooms.get(roomid);
room = roomid ? this.rooms.get(roomid) : this.sockets;
if (!room) return;
message = data.substr(nlLoc + 1);
for (const curSocket of room.values()) curSocket.write(message);

View File

@ -1121,7 +1121,7 @@ function createTournamentGenerator(
return new TourGenerator(modifier || '');
}
function createTournament(
room: ChatRoom | GameRoom, formatId: string | undefined, generator: string | undefined, playerCap: string | undefined,
room: Room, formatId: string | undefined, generator: string | undefined, playerCap: string | undefined,
isRated: boolean, generatorMod: string | undefined, name: string | undefined, output: CommandContext
) {
if (room.type !== 'chat') {
@ -1241,10 +1241,10 @@ const tourCommands: {basic: TourCommands, creation: TourCommands, moderation: To
if (Config.tourdefaultplayercap && tournament.playerCap > Config.tourdefaultplayercap) {
Monitor.log(`[TourMonitor] Room ${tournament.room.roomid} starting a tour over default cap (${tournament.playerCap})`);
}
this.room.send(`|tournament|update|{"playerCap": "${playerCap}"}`);
this.room!.send(`|tournament|update|{"playerCap": "${playerCap}"}`);
} else if (tournament.playerCap && !playerCap) {
tournament.playerCap = 0;
this.room.send(`|tournament|update|{"playerCap": "${playerCap}"}`);
this.room!.send(`|tournament|update|{"playerCap": "${playerCap}"}`);
}
const capNote = (tournament.playerCap ? ' with a player cap of ' + tournament.playerCap : '');
this.privateModAction(`(${user.name} set tournament type to ${generator.name + capNote}.)`);
@ -1292,7 +1292,7 @@ const tourCommands: {basic: TourCommands, creation: TourCommands, moderation: To
this.modlog('TOUR PLAYERCAP', null, tournament.playerCap.toString());
this.sendReply(`Tournament cap set to ${tournament.playerCap}.`);
}
this.room.send(`|tournament|update|{"playerCap": "${tournament.playerCap}"}`);
this.room!.send(`|tournament|update|{"playerCap": "${tournament.playerCap}"}`);
},
end: 'delete',
stop: 'delete',
@ -1317,7 +1317,9 @@ const tourCommands: {basic: TourCommands, creation: TourCommands, moderation: To
return this.errorReply("The custom rules cannot be changed once the tournament has started.");
}
if (tournament.setCustomRules(params, this)) {
this.room.addRaw(`<div class='infobox infobox-limited'>This tournament includes:<br />${tournament.getCustomRules()}</div>`);
this.room!.addRaw(
`<div class='infobox infobox-limited'>This tournament includes:<br />${tournament.getCustomRules()}</div>`
);
this.privateModAction(`(${user.name} updated the tournament's custom rules.)`);
this.modlog('TOUR RULES', null, tournament.customRules.join(', '));
}
@ -1336,10 +1338,10 @@ const tourCommands: {basic: TourCommands, creation: TourCommands, moderation: To
tournament.fullFormat = tournament.baseFormat;
if (tournament.name === tournament.getDefaultCustomName()) {
tournament.name = tournament.baseFormat;
this.room.send(`|tournament|update|${JSON.stringify({format: tournament.name})}`);
this.room!.send(`|tournament|update|${JSON.stringify({format: tournament.name})}`);
tournament.update();
}
this.room.addRaw(`<b>The tournament's custom rules were cleared.</b>`);
this.room!.addRaw(`<b>The tournament's custom rules were cleared.</b>`);
this.privateModAction(`(${user.name} cleared the tournament's custom rules.)`);
this.modlog('TOUR CLEARRULES');
},
@ -1357,7 +1359,7 @@ const tourCommands: {basic: TourCommands, creation: TourCommands, moderation: To
}
if (name.includes('|')) return this.errorReply("The tournament's name cannot include the | symbol.");
tournament.name = name;
this.room.send(`|tournament|update|${JSON.stringify({format: tournament.name})}`);
this.room!.send(`|tournament|update|${JSON.stringify({format: tournament.name})}`);
this.privateModAction(`(${user.name} set the tournament's name to ${tournament.name}.)`);
this.modlog('TOUR NAME', null, tournament.name);
tournament.update();
@ -1366,7 +1368,7 @@ const tourCommands: {basic: TourCommands, creation: TourCommands, moderation: To
clearname(tournament, user) {
if (tournament.name === tournament.baseFormat) return this.errorReply("The tournament does not have a name.");
tournament.name = tournament.baseFormat;
this.room.send(`|tournament|update|${JSON.stringify({format: tournament.name})}`);
this.room!.send(`|tournament|update|${JSON.stringify({format: tournament.name})}`);
this.privateModAction(`(${user.name} cleared the tournament's name.)`);
this.modlog('TOUR CLEARNAME');
tournament.update();
@ -1376,7 +1378,7 @@ const tourCommands: {basic: TourCommands, creation: TourCommands, moderation: To
begin: 'start',
start(tournament, user) {
if (tournament.startTournament(this)) {
this.room.sendMods(`(${user.name} started the tournament.)`);
this.room!.sendMods(`(${user.name} started the tournament.)`);
}
},
dq: 'disqualify',
@ -1423,7 +1425,7 @@ const tourCommands: {basic: TourCommands, creation: TourCommands, moderation: To
return this.errorReply("The tournament is already set to autostart when the player cap is reached.");
}
tournament.autostartcap = true;
this.room.add(`The tournament will start once ${tournament.playerCap} players have joined.`);
this.room!.add(`The tournament will start once ${tournament.playerCap} players have joined.`);
this.privateModAction(`(The tournament was set to autostart when the player cap is reached by ${user.name})`);
this.modlog('TOUR AUTOSTART', null, 'when playercap is reached');
}
@ -1485,14 +1487,14 @@ const tourCommands: {basic: TourCommands, creation: TourCommands, moderation: To
if (tournament.scouting) return this.errorReply("Scouting for this tournament is already set to allowed.");
tournament.scouting = true;
tournament.modjoin = false;
this.room.add('|tournament|scouting|allow');
this.room!.add('|tournament|scouting|allow');
this.privateModAction(`(The tournament was set to allow scouting by ${user.name})`);
this.modlog('TOUR SCOUT', null, 'allow');
} else if (this.meansNo(option) || option === 'disallow' || option === 'disallowed') {
if (!tournament.scouting) return this.errorReply("Scouting for this tournament is already disabled.");
tournament.scouting = false;
tournament.modjoin = true;
this.room.add('|tournament|scouting|disallow');
this.room!.add('|tournament|scouting|disallow');
this.privateModAction(`(The tournament was set to disallow scouting by ${user.name})`);
this.modlog('TOUR SCOUT', null, 'disallow');
} else {
@ -1513,13 +1515,13 @@ const tourCommands: {basic: TourCommands, creation: TourCommands, moderation: To
if (this.meansYes(option) || option === 'allow' || option === 'allowed') {
if (tournament.modjoin) return this.errorReply("Modjoining is already allowed for this tournament.");
tournament.modjoin = true;
this.room.add("Modjoining is now allowed (Players can modjoin their tournament battles).");
this.room!.add("Modjoining is now allowed (Players can modjoin their tournament battles).");
this.privateModAction(`(The tournament was set to allow modjoin by ${user.name})`);
this.modlog('TOUR MODJOIN', null, option);
} else if (this.meansNo(option) || option === 'disallow' || option === 'disallowed') {
if (!tournament.modjoin) return this.errorReply("Modjoining is already not allowed for this tournament.");
tournament.modjoin = false;
this.room.add("Modjoining is now banned (Players cannot modjoin their tournament battles).");
this.room!.add("Modjoining is now banned (Players cannot modjoin their tournament battles).");
this.privateModAction(`(The tournament was set to disallow modjoin by ${user.name})`);
this.modlog('TOUR MODJOIN', null, option);
} else {
@ -1530,12 +1532,12 @@ const tourCommands: {basic: TourCommands, creation: TourCommands, moderation: To
const option = params[0] || 'on';
if (this.meansYes(option)) {
tournament.forcePublic = true;
this.room.add('Tournament battles forced public: ON');
this.room!.add('Tournament battles forced public: ON');
this.privateModAction(`(Tournament public battles were turned ON by ${user.name})`);
this.modlog('TOUR FORCEPUBLIC', null, 'ON');
} else if (this.meansNo(option) || option === 'stop') {
tournament.forcePublic = false;
this.room.add('Tournament battles forced public: OFF');
this.room!.add('Tournament battles forced public: OFF');
this.privateModAction(`(Tournament public battles were turned OFF by ${user.name})`);
this.modlog('TOUR FORCEPUBLIC', null, 'OFF');
} else {
@ -1546,12 +1548,12 @@ const tourCommands: {basic: TourCommands, creation: TourCommands, moderation: To
const option = params.length ? params[0].toLowerCase() : 'on';
if (this.meansYes(option)) {
tournament.forceTimer = true;
this.room.add('Forcetimer is now on for the tournament.');
this.room!.add('Forcetimer is now on for the tournament.');
this.privateModAction(`(The timer was turned on for the tournament by ${user.name})`);
this.modlog('TOUR FORCETIMER', null, 'ON');
} else if (this.meansNo(option) || option === 'stop') {
tournament.forceTimer = false;
this.room.add('Forcetimer is now off for the tournament.');
this.room!.add('Forcetimer is now off for the tournament.');
this.privateModAction(`(The timer was turned off for the tournament by ${user.name})`);
this.modlog('TOUR FORCETIMER', null, 'OFF');
} else {
@ -1714,9 +1716,9 @@ export const commands: ChatCommands = {
const punishment: [string, ID, number, string] =
['TOURBAN', targetUserid, Date.now() + TOURBAN_DURATION, reason];
if (targetUser) {
Punishments.roomPunish(this.room, targetUser, punishment);
Punishments.roomPunish(this.room!, targetUser, punishment);
} else {
Punishments.roomPunishName(this.room, targetUserid, punishment);
Punishments.roomPunishName(this.room!, targetUserid, punishment);
}
room.getGame(Tournament)?.removeBannedUser(targetUserid);
@ -1734,7 +1736,7 @@ export const commands: ChatCommands = {
if (!Tournament.checkBanned(room, targetUserid)) return this.errorReply("This user isn't banned from tournaments.");
if (targetUser) { Punishments.roomUnpunish(this.room, targetUserid, 'TOURBAN', false); }
if (targetUser) { Punishments.roomUnpunish(this.room!, targetUserid, 'TOURBAN', false); }
this.privateModAction(`${targetUser ? targetUser.name : targetUserid} was unbanned from joining tournaments by ${user.name}.`);
this.modlog('TOUR UNBAN', targetUser, null, {noip: 1, noalts: 1});
} else {

View File

@ -391,6 +391,7 @@ export class User extends Chat.MessageContext {
this.avatar = DEFAULT_TRAINER_SPRITES[Math.floor(Math.random() * DEFAULT_TRAINER_SPRITES.length)];
this.connected = true;
Users.onlineCount++;
if (connection.user) connection.user = this;
this.connections = [connection];
@ -457,7 +458,7 @@ export class User extends Chat.MessageContext {
sendTo(roomid: RoomID | BasicRoom | null, data: string) {
if (roomid && typeof roomid !== 'string') roomid = (roomid as BasicRoom).roomid;
if (roomid && roomid !== 'global' && roomid !== 'lobby') data = `>${roomid}\n${data}`;
if (roomid && roomid !== 'lobby') data = `>${roomid}\n${data}`;
for (const connection of this.connections) {
if (roomid && !connection.inRooms.has(roomid)) continue;
connection.send(data);
@ -479,7 +480,7 @@ export class User extends Chat.MessageContext {
const lockedSymbol = (punishgroups.locked && punishgroups.locked.symbol || '\u203d');
return lockedSymbol + this.name;
}
if (roomid && roomid !== 'global') {
if (roomid) {
const room = Rooms.get(roomid);
if (!room) {
throw new Error(`Room doesn't exist: ${roomid}`);
@ -946,7 +947,10 @@ export class User extends Chat.MessageContext {
mergeConnection(connection: Connection) {
// the connection has changed name to this user's username, and so is
// being merged into this account
this.connected = true;
if (!this.connected) {
this.connected = true;
Users.onlineCount++;
}
if (connection.connectedAt > this.lastConnected) {
this.lastConnected = connection.connectedAt;
}
@ -1079,7 +1083,9 @@ export class User extends Chat.MessageContext {
return removed;
}
markDisconnected() {
if (!this.connected) return;
this.connected = false;
Users.onlineCount--;
this.lastDisconnected = Date.now();
if (!this.registered) {
// for "safety"
@ -1217,12 +1223,7 @@ export class User extends Chat.MessageContext {
if (!room) throw new Error(`Room not found: ${roomid}`);
if (!connection) {
for (const curConnection of this.connections) {
// only join full clients, not pop-out single-room
// clients
// (...no, pop-out rooms haven't been implemented yet)
if (curConnection.inRooms.has('global')) {
this.joinRoom(room, curConnection);
}
this.joinRoom(room, curConnection);
}
return;
}
@ -1295,7 +1296,7 @@ export class User extends Chat.MessageContext {
* The user says message in room.
* Returns false if the rest of the user's messages should be discarded.
*/
chat(message: string, room: Room, connection: Connection) {
chat(message: string, room: Room | null, connection: Connection) {
const now = Date.now();
if (message.startsWith('/cmd userdetails') || message.startsWith('>> ') || this.isSysop) {
@ -1318,10 +1319,10 @@ export class User extends Chat.MessageContext {
);
return false;
} else {
this.chatQueue.push([message, room.roomid, connection]);
this.chatQueue.push([message, room ? room.roomid : '', connection]);
}
} else if (now < this.lastChatMessage + throttleDelay) {
this.chatQueue = [[message, room.roomid, connection]];
this.chatQueue = [[message, room ? room.roomid : '', connection]];
this.startChatQueue(throttleDelay - (now - this.lastChatMessage));
} else {
this.lastChatMessage = now;
@ -1366,12 +1367,12 @@ export class User extends Chat.MessageContext {
this.lastChatMessage = new Date().getTime();
const room = Rooms.get(roomid);
if (room) {
if (room || !roomid) {
Monitor.activeIp = connection.ip;
Chat.parse(message, room, this, connection);
Monitor.activeIp = null;
} else {
// room is expired, do nothing
// room no longer exists; do nothing
}
const throttleDelay = this.trusted ? THROTTLE_DELAY_TRUSTED : THROTTLE_DELAY;
@ -1516,7 +1517,7 @@ function socketConnect(
}
});
user.joinRoom('global', connection);
Rooms.global.handleConnect(user, connection);
}
function socketDisconnect(worker: StreamWorker, workerid: number, socketid: string) {
const id = '' + workerid + '-' + socketid;
@ -1558,11 +1559,10 @@ function socketReceive(worker: StreamWorker, workerid: number, socketid: string,
if (!user) return;
// The client obviates the room id when sending messages to Lobby by default
const roomId = message.substr(0, pipeIndex) || (Rooms.lobby || Rooms.global).roomid;
const roomId = message.slice(0, pipeIndex) || (Rooms.lobby && 'lobby') || '';
message = message.slice(pipeIndex + 1);
const room = Rooms.get(roomId);
if (!room) return;
const room = Rooms.get(roomId) || null;
const multilineMessage = Chat.multiLinePattern.test(message);
if (multilineMessage) {
user.chat(multilineMessage, room, connection);
@ -1571,7 +1571,8 @@ function socketReceive(worker: StreamWorker, workerid: number, socketid: string,
const lines = message.split('\n');
if (!lines[lines.length - 1]) lines.pop();
const maxLineCount = (user.isStaff || room.auth.isStaff(user.id)) ?
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
const maxLineCount = (user.isStaff || (room && room.auth.isStaff(user.id))) ?
THROTTLE_MULTILINE_WARN_STAFF : THROTTLE_MULTILINE_WARN;
if (lines.length > maxLineCount) {
connection.popup(`You're sending too many lines at once. Try using a paste service like [[Pastebin]].`);
@ -1603,6 +1604,7 @@ export const Users = {
merge,
users,
prevUsers,
onlineCount: 0,
get: getUser,
getExact: getExactUser,
findUsers,

View File

@ -14,9 +14,7 @@ let NumberModeTrivia;
function makeUser(name, connection) {
const user = new User(connection);
user.forceRename(name, true);
user.connected = true;
Users.users.set(user.id, user);
user.joinRoom('global', connection);
user.joinRoom('trivia', connection);
return user;
}

View File

@ -164,7 +164,6 @@ function createUser(connection) {
if (!connection) connection = createConnection();
const user = new Users.User(connection);
user.joinRoom('global', connection);
connection.user = user;
return user;