Repeats: Support non-time-based repeats (#7518)

This commit is contained in:
Annika 2020-12-28 03:39:52 -08:00 committed by GitHub
parent 2cca8bd48e
commit 6d92ac8547
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 92 additions and 35 deletions

View File

@ -5,6 +5,7 @@
*/
import {roomFaqs, getAlias} from './room-faqs';
import type {MessageHandler} from '../rooms';
export interface RepeatedPhrase {
/** Identifier for deleting */
@ -13,13 +14,14 @@ export interface RepeatedPhrase {
/** interval in milliseconds */
interval: number;
faq?: boolean;
isByMessages?: boolean;
isHTML?: boolean;
}
export const Repeats = new class {
// keying to Room rather than RoomID will help us correctly handle room renames
/** room:identifier:phrase:timeout map */
repeats = new Map<BasicRoom, Map<ID, Map<string, NodeJS.Timeout>>>();
repeats = new Map<BasicRoom, Map<ID, Map<string, NodeJS.Timeout | MessageHandler>>>();
constructor() {
for (const room of Rooms.rooms.values()) {
@ -30,6 +32,14 @@ export const Repeats = new class {
}
}
removeRepeatHandler(room: BasicRoom, handler?: NodeJS.Timeout | MessageHandler) {
if (typeof handler === 'function') {
room.nthMessageHandlers.delete(handler);
} else if (typeof handler === 'object') {
clearInterval(handler);
}
}
hasRepeat(room: BasicRoom, id: ID) {
return !!this.repeats.get(room)?.get(id);
}
@ -51,7 +61,7 @@ export const Repeats = new class {
const roomRepeats = this.repeats.get(room);
if (!roomRepeats) return;
const oldInterval = roomRepeats.get(id)?.get(phrase!);
if (oldInterval) clearInterval(oldInterval);
this.removeRepeatHandler(room, oldInterval);
roomRepeats.delete(id);
}
@ -60,7 +70,7 @@ export const Repeats = new class {
if (!roomRepeats) return;
for (const ids of roomRepeats.values()) {
for (const interval of ids.values()) {
if (interval) clearInterval(interval);
this.removeRepeatHandler(room, interval);
}
}
this.repeats.delete(room);
@ -77,25 +87,31 @@ export const Repeats = new class {
if (roomRepeats.has(id)) {
throw new Error(`Repeat already exists`);
}
roomRepeats.set(id, new Map().set(phrase, setInterval(() => {
if (room !== Rooms.get(room.roomid)) {
const repeater = (targetRoom: BasicRoom) => {
if (targetRoom !== Rooms.get(targetRoom.roomid)) {
// room was deleted
this.clearRepeats(room);
this.clearRepeats(targetRoom);
return;
}
const formattedText = repeat.faq ? Chat.formatText(roomFaqs[room.roomid][repeat.id], true) :
const formattedText = repeat.faq ? Chat.formatText(roomFaqs[targetRoom.roomid][repeat.id], true) :
repeat.isHTML ? repeat.phrase : Chat.formatText(repeat.phrase, false, true);
room.add(`|html|<div class="infobox">${formattedText}</div>`);
room.update();
}, interval)));
targetRoom.add(`|html|<div class="infobox">${formattedText}</div>`);
targetRoom.update();
};
if (repeat.isByMessages) {
room.nthMessageHandlers.set(repeater, interval);
roomRepeats.set(id, new Map().set(phrase, repeater));
} else {
roomRepeats.set(id, new Map().set(phrase, setInterval(repeater, interval, room)));
}
}
destroy() {
for (const roomRepeats of this.repeats.values()) {
for (const [room, roomRepeats] of this.repeats) {
for (const ids of roomRepeats.values()) {
for (const interval of ids.values()) {
if (interval) clearInterval(interval);
this.removeRepeatHandler(room, interval);
}
}
}
@ -119,14 +135,14 @@ export const pages: PageTable = {
html += `<h2>${this.tr`Repeated phrases in ${room.title}`}</h2>`;
html += `<table><tr><th>${this.tr`Identifier`}</th><th>${this.tr`Phrase`}</th><th>${this.tr`Raw text`}</th><th>${this.tr`Interval`}</th><th>${this.tr`Action`}</th>`;
for (const repeat of room.settings.repeats) {
const minutes = repeat.interval / (60 * 1000);
const minutes = repeat.interval / (repeat.isByMessages ? 1 : 60 * 1000);
if (!repeat.faq) {
const phrase = repeat.isHTML ? repeat.phrase : Chat.formatText(repeat.phrase, false, true);
html += `<tr><td>${repeat.id}</td><td>${phrase}</td><td>${Chat.getReadmoreCodeBlock(repeat.phrase)}</td><td>${this.tr`every ${minutes} minute(s)`}</td>`;
html += `<tr><td>${repeat.id}</td><td>${phrase}</td><td>${Chat.getReadmoreCodeBlock(repeat.phrase)}</td><td>${repeat.isHTML ? this.tr`every ${minutes} chat message(s)` : this.tr`every ${minutes} minute(s)`}</td>`;
html += `<td><button class="button" name="send" value="/removerepeat ${repeat.id},${room.roomid}">${this.tr`Remove`}</button></td>`;
} else {
const phrase = Chat.formatText(roomFaqs[room.roomid][repeat.id], true);
html += `<tr><td>${repeat.id}</td><td>${phrase}</td><td>${Chat.getReadmoreCodeBlock(roomFaqs[room.roomid][repeat.id])}</td><td>${this.tr`every ${minutes} minute(s)`}</td>`;
html += `<tr><td>${repeat.id}</td><td>${phrase}</td><td>${Chat.getReadmoreCodeBlock(roomFaqs[room.roomid][repeat.id])}</td><td>${repeat.isHTML ? this.tr`every ${minutes} chat message(s)` : this.tr`every ${minutes} minute(s)`}</td>`;
html += `<td><button class="button" name="send" value="/removerepeat ${repeat.id},${room.roomid}">${this.tr`Remove`}</button></td>`;
}
}
@ -140,9 +156,12 @@ export const pages: PageTable = {
};
export const commands: ChatCommands = {
repeatbymessages: 'repeat',
repeathtmlbymessages: 'repeat',
repeathtml: 'repeat',
repeat(target, room, user, connection, cmd) {
const isHTML = cmd === 'repeathtml';
const isHTML = cmd.includes('html');
const isByMessages = cmd.includes('bymessages');
room = this.requireRoom();
this.checkCan(isHTML ? 'addhtml' : 'mute', null, room);
const [intervalString, name, ...messageArray] = target.split(',');
@ -150,7 +169,7 @@ export const commands: ChatCommands = {
const phrase = messageArray.join(',').trim();
const interval = parseInt(intervalString);
if (isNaN(interval) || !/[0-9]{1,}/.test(intervalString) || interval < 1 || interval > 24 * 60) {
throw new Chat.ErrorMessage(this.tr`You must specify an interval as a number of minutes between 1 and 1440.`);
throw new Chat.ErrorMessage(this.tr`You must specify an interval as a number of minutes or chat messages between 1 and 1440.`);
}
if (Repeats.hasRepeat(room, id)) {
@ -162,34 +181,42 @@ export const commands: ChatCommands = {
Repeats.addRepeat(room, {
id,
phrase,
interval: interval * 60 * 1000, // convert to milliseconds
// convert to milliseconds for time-based repeats
interval: interval * (isByMessages ? 1 : 60 * 1000),
isHTML,
isByMessages,
});
this.modlog('REPEATPHRASE', null, `every ${interval} minute${Chat.plural(interval)}: "${phrase.replace(/\n/g, ' ')}"`);
this.modlog('REPEATPHRASE', null, `every ${interval} ${isByMessages ? `chat messages` : `minute`}${Chat.plural(interval)}: "${phrase.replace(/\n/g, ' ')}"`);
this.privateModAction(
room.tr`${user.name} set the phrase labeled with "${id}" to be repeated every ${interval} minute(s).`
isByMessages ?
room.tr`${user.name} set the phrase labeled with "${id}" to be repeated every ${interval} chat message(s).` :
room.tr`${user.name} set the phrase labeled with "${id}" to be repeated every ${interval} minute(s).`
);
},
repeathelp() {
this.runBroadcast();
this.sendReplyBox(
`<code>/repeat [minutes], [id], [phrase]</code>: repeats a given phrase every [minutes] minutes.<br />` +
`<code>/repeat [minutes], [id], [phrase]</code>: repeats a given phrase every [minutes] minutes. Requires: % @ # &<br />` +
`<code>/repeathtml [minutes], [id], [phrase]</code>: repeats a given phrase containing HTML every [minutes] minutes. Requires: # &<br />` +
`<code>/repeatfaq [minutes], [FAQ name/alias]</code>: repeats a given Room FAQ every [minutes] minutes.<br />` +
`<code>/removerepeat [id]</code>: removes a repeated phrase.<br />` +
`<code>/viewrepeats [optional room]</code>: Displays all repeated phrases in a room.<br />` +
`Phrases for <code>/repeat</code> can include normal chat formatting, but not commands. Requires: % @ # &`
`<code>/repeatfaq [minutes], [FAQ name/alias]</code>: repeats a given Room FAQ every [minutes] minutes. Requires: % @ # &<br />` +
`<code>/removerepeat [id]</code>: removes a repeated phrase. Requires: % @ # &<br />` +
`<code>/viewrepeats [optional room]</code>: Displays all repeated phrases in a room. Requires: % @ # &<br />` +
`You can append <code>bymessages</code> to a <code>/repeat</code> command to repeat a phrase based on how many messages have been sent in chat. For example, <code>/repeatfaqbymessages ...</code><br />` +
`Phrases for <code>/repeat</code> can include normal chat formatting, but not commands.`
);
},
repeatfaq(target, room, user) {
repeatfaqbymessages: 'repeatfaq',
repeatfaq(target, room, user, connection, cmd) {
room = this.requireRoom();
this.checkCan('mute', null, room);
const isByMessages = cmd.includes('bymessages');
let [intervalString, topic] = target.split(',');
const interval = parseInt(intervalString);
if (isNaN(interval) || !/[0-9]{1,}/.test(intervalString) || interval < 1 || interval > 24 * 60) {
throw new Chat.ErrorMessage(this.tr`You must specify an interval as a number of minutes between 1 and 1440.`);
throw new Chat.ErrorMessage(this.tr`You must specify an interval as a number of minutes or chat messages between 1 and 1440.`);
}
if (!roomFaqs[room.roomid]) {
throw new Chat.ErrorMessage(`This room has no FAQs.`);
@ -206,13 +233,16 @@ export const commands: ChatCommands = {
Repeats.addRepeat(room, {
id: topic as ID,
phrase: roomFaqs[room.roomid][topic],
interval: interval * 60 * 1000,
interval: interval * (isByMessages ? 1 : 60 * 1000),
faq: true,
isByMessages,
});
this.modlog('REPEATPHRASE', null, `every ${interval} minute${Chat.plural(interval)}: the Room FAQ for "${topic}"`);
this.modlog('REPEATPHRASE', null, `every ${interval} ${isByMessages ? 'chat message' : 'minute'}${Chat.plural(interval)}: the Room FAQ for "${topic}"`);
this.privateModAction(
room.tr`${user.name} set the Room FAQ "${topic}" to be repeated every ${interval} minute(s).`
isByMessages ?
room.tr`${user.name} set the Room FAQ "${topic}" to be repeated every ${interval} minute(s).` :
room.tr`${user.name} set the Room FAQ "${topic}" to be repeated every ${interval} chat message(s).`
);
},

View File

@ -1599,9 +1599,19 @@ export const Chat = new class {
*/
parse(message: string, room: Room | null | undefined, user: User, connection: Connection) {
Chat.loadPlugins();
const context = new CommandContext({message, room, user, connection});
return context.parse();
const initialRoomlogLength = room?.log.log.length;
const context = new CommandContext({message, room, user, connection});
const result = context.parse();
if (room && room.log.log.length !== initialRoomlogLength) {
room.messagesSent++;
for (const [handler, numMessages] of room.nthMessageHandlers) {
if (room.messagesSent % numMessages === 0) handler(room, message);
}
}
return result;
}
sendPM(message: string, user: User, pmTarget: User, onlyRecipient: User | null = null) {
const buf = `|pm|${user.getIdentity()}|${pmTarget.getIdentity()}|${message}`;

View File

@ -128,7 +128,10 @@ export interface RoomSettings {
noAutoTruncate?: boolean;
isMultichannel?: boolean;
}
export type MessageHandler = (room: BasicRoom, message: string) => void;
export type Room = GameRoom | ChatRoom;
import type {Announcement, AnnouncementData} from './chat-plugins/announcements';
import type {Poll, PollData} from './chat-plugins/poll';
import type {AutoResponder} from './chat-plugins/responder';
@ -205,6 +208,13 @@ export abstract class BasicRoom {
userList: string;
pendingApprovals: Map<string, ShowRequest> | null;
messagesSent: number;
/**
* These handlers will be invoked every n messages.
* handler:number-of-messages map
*/
nthMessageHandlers: Map<MessageHandler, number>;
constructor(roomid: RoomID, title?: string, options: Partial<RoomSettings> = {}) {
this.users = Object.create(null);
this.type = 'chat';
@ -289,6 +299,8 @@ export abstract class BasicRoom {
this.userList = this.getUserList();
}
this.pendingApprovals = null;
this.messagesSent = 0;
this.nthMessageHandlers = new Map();
this.tour = null;
this.game = null;
this.battle = null;

View File

@ -12,10 +12,12 @@ export const translations: Translations = {
"Raw text": "",
"Remove": "",
"Remove all repeats": "",
"You must specify an interval as a number of minutes between 1 and 1440.": "",
"You must specify an interval as a number of minutes or chat messages between 1 and 1440.": "",
'The phrase labeled with "${id}" is already being repeated in this room.': "",
'${user.name} set the phrase labeled with "${id}" to be repeated every ${interval} minute(s).': "",
'${user.name} set the phrase labeled with "${id}" to be repeated every ${interval} chat message(s).': "",
'${user.name} set the Room FAQ "${topic}" to be repeated every ${interval} minute(s).': "",
'${user.name} set the Room FAQ "${topic}" to be repeated every ${interval} chat message(s).': "",
'The phrase labeled with "${id}" is not being repeated in this room.': "",
'The text for the Room FAQ "${topic}" is already being repeated.': "",
'${user.name} removed the repeated phrase labeled with "${id}".': "",

View File

@ -10,12 +10,15 @@ export const translations: Translations = {
"Interval": "Zeitspanne",
"every ${minutes} minute(s)": "jede ${minutes} Minute(n)",
"Raw text": "Rohtext",
"every ${messages} chat message(s)": "",
"Remove": "Entfernen",
"Remove all repeats": "Entferne alle Wiederholungen",
"You must specify an interval as a number of minutes between 1 and 1440.": "Du musst eine Zeitspanne als eine Zahl zwischen 1 und 1440 angeben.",
"You must specify an interval as a number of minutes or chat messages between 1 and 1440.": "Du musst eine Zeitspanne (oder Chat-Nachrichten) als eine Zahl zwischen 1 und 1440 angeben.",
'The phrase labeled with "${id}" is already being repeated in this room.': 'Der Ausdruck "${id}" wird bereits im Raum wiederholt.',
'${user.name} set the phrase labeled with "${id}" to be repeated every ${interval} minute(s).': '${user.name} hat eingestellt, dass der Ausdruck "${id}" jede ${interval} Minute(n) wiederholt wird.',
'${user.name} set the phrase labeled with "${id}" to be repeated every ${interval} chat message(s).': '${user.name} hat eingestellt, dass der Ausdruck "${id}" jede ${interval} Chat-Nachrichte(n) wiederholt wird.',
'${user.name} set the Room FAQ "${topic}" to be repeated every ${interval} minute(s).': '${user.name} hat eingestellt, dass der Raum-FAQ "${topic}" jede ${interval} Minute(n) wiederholt wird.',
'${user.name} set the Room FAQ "${topic}" to be repeated every ${interval} chat message(s).': '${user.name} hat eingestellt, dass der Raum-FAQ "${topic}" jede ${interval} Chat-Nachrichte(n) wiederholt wird.',
'The phrase labeled with "${id}" is not being repeated in this room.': 'Der Ausdruck "${id}" wird gerade nicht in diesem Raum wiederholt.',
'The text for the Room FAQ "${topic}" is already being repeated.': 'Der Text für den Raum-FAQ "${topic}" wird bereits wiederholt.',
'${user.name} removed the repeated phrase labeled with "${id}".': '${user.name} hat den sich wiederholenden Ausdruck "${id}" entfernt.',