mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-05-15 17:00:23 -05:00
* Lint arrow-body-style * Lint prefer-object-spread Object spread is faster _and_ more readable. This also fixes a few unnecessary object clones. * Enable no-parameter-properties This isn't currently used, but this makes clear that it shouldn't be. * Refactor more Promises to async/await * Remove unnecessary code from getDataMoveHTML etc * Lint prefer-string-starts-ends-with * Stop using no-undef According to the typescript-eslint FAQ, this is redundant with TypeScript, and they're not wrong. This will save us from needing to specify globals in two different places which will be nice.
666 lines
26 KiB
TypeScript
666 lines
26 KiB
TypeScript
/**
|
|
* PS Help room auto-response plugin.
|
|
* Uses Regex to match room frequently asked question (RFAQ) entries,
|
|
* and replies if a match is found.
|
|
* Supports configuration.
|
|
* Written by Mia.
|
|
* @author mia-pi-git
|
|
*/
|
|
|
|
import {FS} from '../../lib/fs';
|
|
import {Utils} from '../../lib/utils';
|
|
import {LogViewer} from './chatlog';
|
|
import {roomFaqs} from './room-faqs';
|
|
|
|
const PATH = 'config/chat-plugins/help.json';
|
|
// 4: filters out conveniently short aliases
|
|
const MINIMUM_LENGTH = 4;
|
|
|
|
const BAN_DURATION = 2 * 24 * 60 * 60 * 1000;
|
|
/**
|
|
* Terms commonly used in helping that should be ignored
|
|
* within question parsing (the Help#find function).
|
|
*/
|
|
const COMMON_TERMS = [
|
|
'<<(.+)>>', '(do|use|type)( |)(``|)(/|!|//)(.+)(``|)', 'go to', '/rfaq (.+)', 'you can', Chat.linkRegex, 'click',
|
|
'need to',
|
|
].map(item => new RegExp(item, "i"));
|
|
|
|
Punishments.roomPunishmentTypes.set('HELPSUGGESTIONBAN', "Banned from submitting suggestions to the Help filter");
|
|
|
|
export let helpData: PluginData;
|
|
|
|
try {
|
|
helpData = JSON.parse(FS(PATH).readSync());
|
|
} catch (e) {
|
|
if (e.code !== 'ENOENT') throw e;
|
|
helpData = {
|
|
stats: {},
|
|
pairs: {},
|
|
settings: {
|
|
filterDisabled: false,
|
|
queueDisabled: false,
|
|
},
|
|
queue: [],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* A message caught by the Help filter.
|
|
*/
|
|
interface LoggedMessage {
|
|
/** Message that's matched by the Help filter. */
|
|
message: string;
|
|
/** The FAQ that it's matched to. */
|
|
faqName: string;
|
|
/** The regex that it's matched to. */
|
|
regex: string;
|
|
}
|
|
/** Object of stats for that day. */
|
|
interface DayStats {
|
|
matches?: LoggedMessage[];
|
|
total?: number;
|
|
}
|
|
|
|
interface QueueEntry {
|
|
/** User who submitted. */
|
|
userid: ID;
|
|
/** Regex string submitted */
|
|
regexString: string;
|
|
}
|
|
|
|
interface FilterSettings {
|
|
/** Whether or not the filter is disabled. */
|
|
filterDisabled?: boolean;
|
|
queueDisabled?: boolean;
|
|
}
|
|
|
|
interface PluginData {
|
|
/** Stats - filter match and faq that was matched - done day by day. */
|
|
stats?: {[k: string]: DayStats};
|
|
/** Word pairs that have been marked as a match for a specific FAQ. */
|
|
pairs: {[k: string]: string[]};
|
|
/** Filter settings (are they enabled or disabled.) */
|
|
settings: FilterSettings;
|
|
/** Queue of suggested regex. */
|
|
queue?: QueueEntry[];
|
|
}
|
|
|
|
export class HelpResponder {
|
|
queue: QueueEntry[];
|
|
data: PluginData;
|
|
settings: FilterSettings;
|
|
constructor(data: PluginData) {
|
|
this.data = data;
|
|
this.queue = data.queue || [];
|
|
this.settings = data.settings || {queueDisabled: false, filterDisabled: false};
|
|
}
|
|
getRoom() {
|
|
if (!Config.helpFilterRoom) return null;
|
|
const room = Rooms.get(Config.helpFilterRoom);
|
|
if (!room) {
|
|
throw new Error(`The Help filter is configured for room '${Config.helpFilterRoom}', but that room does not exist.`);
|
|
}
|
|
return room;
|
|
}
|
|
static roomNotFound(): never {
|
|
throw new Chat.ErrorMessage(`There is no room configured to use the Help filter.`);
|
|
}
|
|
find(question: string, user?: User) {
|
|
// sanity slice, APPARENTLY people are dumb.
|
|
question = question.slice(0, 300);
|
|
const room = this.getRoom();
|
|
if (!room) return;
|
|
const helpFaqs = roomFaqs[room.roomid];
|
|
const faqs = Object.keys((helpFaqs || '{}'))
|
|
.filter(item => item.length >= MINIMUM_LENGTH && !helpFaqs[item].startsWith('>'));
|
|
if (COMMON_TERMS.some(t => t.test(question))) return null;
|
|
for (const faq of faqs) {
|
|
const match = this.match(question, faq);
|
|
if (match) {
|
|
if (user) {
|
|
const timestamp = Chat.toTimestamp(new Date()).split(' ')[1];
|
|
const log = `${timestamp} |c| ${user.name}|${question}`;
|
|
this.log(log, faq, match.regex);
|
|
}
|
|
return helpFaqs[match.faq];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
visualize(question: string, hideButton?: boolean, user?: User) {
|
|
const response = this.find(question, user);
|
|
if (response) {
|
|
let buf = '';
|
|
buf += Utils.html`<strong>You said:</strong> ${question}<br />`;
|
|
buf += `<strong>Our automated reply:</strong> ${Chat.formatText(response)}`;
|
|
if (!hideButton) {
|
|
buf += Utils.html`<hr /><button class="button" name="send" value="A: ${question}">`;
|
|
buf += `Send to the Help room if you weren't answered correctly or wanted to help </button>`;
|
|
}
|
|
return buf;
|
|
}
|
|
return null;
|
|
}
|
|
getFaqID(faq?: string) {
|
|
if (!faq) throw new Chat.ErrorMessage(`Your input must be in the format [input] => [faq].`);
|
|
faq = faq.trim();
|
|
if (!faq) throw new Chat.ErrorMessage(`Your FAQ ID can't be empty.`);
|
|
const room = this.getRoom();
|
|
if (!room) throw new Chat.ErrorMessage(`The Help filter room is not configured, and so cannot be used.`);
|
|
const entry: string = roomFaqs[room.roomid][faq];
|
|
if (!entry) throw new Chat.ErrorMessage(`FAQ ID "${faq}" not found.`);
|
|
|
|
if (!entry.startsWith('>')) return faq; // not an alias
|
|
return entry.slice(1);
|
|
}
|
|
/**
|
|
* Checks if the FAQ exists. If not, deletes all references to it.
|
|
*/
|
|
updateFaqData(faq: string) {
|
|
// testing purposes
|
|
if (Config.nofswriting) return true;
|
|
const room = this.getRoom();
|
|
if (!room) return;
|
|
if (roomFaqs[room.roomid][faq]) return true;
|
|
if (this.data.pairs[faq]) delete this.data.pairs[faq];
|
|
for (const item of this.queue) {
|
|
const [, targetFaq] = item.regexString.split('=>');
|
|
if (toID(targetFaq).includes(toID(faq))) {
|
|
this.queue.splice(this.queue.indexOf(item), 1);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
stringRegex(str: string, raw?: boolean) {
|
|
[str] = Utils.splitFirst(str, '=>');
|
|
const args = str.split(',').map(item => item.trim());
|
|
if (!raw && args.length > 10) {
|
|
throw new Chat.ErrorMessage(`Too many arguments.`);
|
|
}
|
|
if (str.length > 300 && !raw) throw new Chat.ErrorMessage("Your given string is too long.");
|
|
return args.map(item => {
|
|
const split = item.split('&').map(string => {
|
|
// allow raw regex for admins and users with staff in Dev and Help
|
|
if (raw) return string;
|
|
// escape
|
|
return string.replace(/[\\^$.*+?()[\]{}]/g, '\\$&').trim();
|
|
});
|
|
return split.map(term => {
|
|
if (term.length > 100 && !raw) {
|
|
throw new Chat.ErrorMessage(`One or more of your arguments is too long. Use less than 100 characters.`);
|
|
}
|
|
if (item.startsWith('|') || item.endsWith('|')) {
|
|
throw new Chat.ErrorMessage(`Invalid use of |. Make sure you have an option on either side.`);
|
|
}
|
|
if (term.startsWith('!')) {
|
|
return `^(?!.*${term.slice(1)})`;
|
|
}
|
|
if (!term.trim()) return null;
|
|
return `(?=.*?(${term.trim()}))`;
|
|
}).filter(Boolean).join('');
|
|
}).filter(Boolean).join('');
|
|
}
|
|
match(question: string, faq: string) {
|
|
if (!this.data.pairs[faq]) this.data.pairs[faq] = [];
|
|
const regexes = this.data.pairs[faq].map(item => new RegExp(item, "i"));
|
|
if (!regexes) return;
|
|
for (const regex of regexes) {
|
|
if (regex.test(Chat.normalize(question))) return {faq, regex: regex.toString()};
|
|
}
|
|
return;
|
|
}
|
|
log(entry: string, faq: string, expression: string) {
|
|
if (!this.data.stats) this.data.stats = {};
|
|
const [day] = Utils.splitFirst(Chat.toTimestamp(new Date), ' ');
|
|
if (!this.data.stats[day]) this.data.stats[day] = {};
|
|
const today = this.data.stats[day];
|
|
const log: LoggedMessage = {
|
|
message: entry,
|
|
faqName: faq,
|
|
regex: expression,
|
|
};
|
|
const stats = {
|
|
matches: today.matches || [],
|
|
total: today.matches ? today.matches.length : 0,
|
|
};
|
|
const dayLog = Object.assign(this.data.stats[day], stats);
|
|
dayLog.matches.push(log);
|
|
dayLog.total++;
|
|
return this.writeState();
|
|
}
|
|
writeState() {
|
|
this.data.queue = this.queue;
|
|
for (const faq in this.data.pairs) {
|
|
// while writing, clear old data. In the meantime, the rest of the data is inaccessible
|
|
// so this is the best place to clear the data
|
|
this.updateFaqData(faq);
|
|
}
|
|
this.data.queue = this.queue;
|
|
return FS(PATH).writeUpdate(() => JSON.stringify(this.data));
|
|
}
|
|
tryAddRegex(inputString: string, raw?: boolean) {
|
|
let [args, faq] = inputString.split('=>').map(item => item.trim()) as [string, string | undefined];
|
|
faq = this.getFaqID(toID(faq));
|
|
if (!this.data.pairs) this.data.pairs = {};
|
|
if (!this.data.pairs[faq]) this.data.pairs[faq] = [];
|
|
const regex = raw ? args.trim() : this.stringRegex(args, raw);
|
|
if (this.data.pairs[faq].includes(regex)) {
|
|
throw new Chat.ErrorMessage(`That regex is already stored.`);
|
|
}
|
|
Chat.validateRegex(regex);
|
|
this.data.pairs[faq].push(regex);
|
|
return this.writeState();
|
|
}
|
|
tryRemoveRegex(faq: string, index: number) {
|
|
faq = this.getFaqID(faq);
|
|
if (!this.data.pairs) this.data.pairs = {};
|
|
if (!this.data.pairs[faq]) throw new Chat.ErrorMessage(`There are no regexes for ${faq}.`);
|
|
if (!this.data.pairs[faq][index]) throw new Chat.ErrorMessage("Your provided index is invalid.");
|
|
this.data.pairs[faq].splice(index, 1);
|
|
this.writeState();
|
|
return true;
|
|
}
|
|
ban(userid: string, reason = '') {
|
|
const room = this.getRoom();
|
|
if (!room) return;
|
|
const user = Users.get(userid)?.id || toID(userid);
|
|
const punishment: [string, ID, number, string] = ['HELPSUGGESTIONBAN', toID(user), Date.now() + BAN_DURATION, reason];
|
|
for (const entry of this.queue) {
|
|
const index = this.queue.indexOf(entry);
|
|
if (entry.userid === user) {
|
|
this.queue.splice(index, 1);
|
|
}
|
|
}
|
|
this.writeState();
|
|
return Punishments.roomPunish(room.roomid, user, punishment);
|
|
}
|
|
isBanned(user: User | string) {
|
|
const room = this.getRoom();
|
|
if (!room) return;
|
|
return Punishments.getRoomPunishType(room, toID(user)) === 'HELPSUGGESTIONBAN';
|
|
}
|
|
static canOverride(user: User) {
|
|
const devAuth = Rooms.get('development')?.auth;
|
|
const room = Answerer.getRoom();
|
|
if (!room) HelpResponder.roomNotFound();
|
|
return (
|
|
devAuth?.atLeast(user, '%') && devAuth?.has(user.id) &&
|
|
room.auth.has(user.id) && room.auth.atLeast(user, '@') ||
|
|
user.can('rangeban')
|
|
);
|
|
}
|
|
}
|
|
|
|
export const Answerer = new HelpResponder(helpData);
|
|
|
|
export const chatfilter: ChatFilter = function (message, user, room) {
|
|
const helpRoom = Answerer.getRoom();
|
|
if (!helpRoom) return message;
|
|
if (room?.roomid === helpRoom.roomid && helpRoom.auth.get(user.id) === ' ' && !Answerer.settings.filterDisabled) {
|
|
if (message.startsWith('a:') || message.startsWith('A:')) return message.replace(/(a|A):/, '');
|
|
const reply = Answerer.visualize(message, false, user);
|
|
if (message.startsWith('/') || message.startsWith('!')) return message;
|
|
if (!reply) {
|
|
return message;
|
|
} else {
|
|
user.sendTo(room.roomid, `|uhtml|askhelp-${user}-${toID(message)}|<div class="infobox">${reply}</div>`);
|
|
const trimmedMessage = `<div class="infobox">${Answerer.visualize(message, true)}</div>`;
|
|
setTimeout(() => {
|
|
user.sendTo(
|
|
room.roomid,
|
|
`|c| ${user.name}|/uhtmlchange askhelp-${user}-${toID(message)}, ${trimmedMessage}`
|
|
);
|
|
}, 10 * 1000);
|
|
return false;
|
|
}
|
|
}
|
|
};
|
|
|
|
export const commands: ChatCommands = {
|
|
question(target, room, user) {
|
|
if (!Answerer.getRoom()) HelpResponder.roomNotFound();
|
|
if (!target) return this.parse("/help question");
|
|
const reply = Answerer.visualize(target, true);
|
|
if (!reply) return this.sendReplyBox(`No answer found.`);
|
|
this.runBroadcast();
|
|
this.sendReplyBox(reply);
|
|
},
|
|
questionhelp: ["/question [question] - Asks the Help room auto-response plugin a question."],
|
|
|
|
hf: 'helpfilter',
|
|
helpfilter: {
|
|
''(target) {
|
|
if (!Answerer.getRoom()) HelpResponder.roomNotFound();
|
|
if (!target) {
|
|
this.parse('/help helpfilter');
|
|
return this.sendReply(
|
|
`The Help auto-response filter is currently set to: ${Answerer.settings.filterDisabled ? 'OFF' : "ON"}`
|
|
);
|
|
}
|
|
return this.parse(`/j view-helpfilter-${target}`);
|
|
},
|
|
view(target, room, user) {
|
|
return this.parse(`/join view-helpfilter-${target}`);
|
|
},
|
|
toggle(target, room, user) {
|
|
room = this.requireRoom();
|
|
const helpRoom = Answerer.getRoom();
|
|
if (!helpRoom) HelpResponder.roomNotFound();
|
|
if (room.roomid !== helpRoom.roomid) return this.errorReply(`This command is only available in the Help room.`);
|
|
if (!target) {
|
|
return this.sendReply(
|
|
`The Help auto-response filter is currently set to: ${Answerer.settings.filterDisabled ? 'OFF' : "ON"}`
|
|
);
|
|
}
|
|
this.checkCan('ban', null, room);
|
|
if (this.meansYes(target)) {
|
|
if (!Answerer.settings.filterDisabled) return this.errorReply(`The Help auto-response filter is already enabled.`);
|
|
Answerer.settings.filterDisabled = false;
|
|
}
|
|
if (this.meansNo(target)) {
|
|
if (Answerer.settings.filterDisabled) return this.errorReply(`The Help auto-response filter is already disabled.`);
|
|
Answerer.settings.filterDisabled = true;
|
|
}
|
|
Answerer.writeState();
|
|
this.privateModAction(`${user.name} ${Answerer.settings.filterDisabled ? 'disabled' : 'enabled'} the Help auto-response filter.`);
|
|
this.modlog(`HELPFILTER`, null, Answerer.settings.filterDisabled ? 'OFF' : 'ON');
|
|
},
|
|
forceadd: 'add',
|
|
add(target, room, user, connection, cmd) {
|
|
room = this.requireRoom();
|
|
const helpRoom = Answerer.getRoom();
|
|
if (!helpRoom) HelpResponder.roomNotFound();
|
|
if (room.roomid !== helpRoom.roomid) return this.errorReply(`This command is only available in the Help room.`);
|
|
const force = cmd === 'forceadd';
|
|
if (force && !HelpResponder.canOverride(user)) {
|
|
return this.errorReply(`You cannot use raw regex - use /helpfilter add instead.`);
|
|
}
|
|
this.checkCan('ban', null, helpRoom);
|
|
Answerer.tryAddRegex(target, force);
|
|
this.privateModAction(`${user.name} added regex for "${target.split('=>')[0]}" to the filter.`);
|
|
this.modlog(`HELPFILTER ADD`, null, target);
|
|
},
|
|
remove(target, room, user) {
|
|
const helpRoom = Answerer.getRoom();
|
|
if (!helpRoom) return this.errorReply(`There is no room configured for use of this filter.`);
|
|
this.checkCan('ban', null, helpRoom);
|
|
const [faq, index] = target.split(',');
|
|
// intended for use mainly within the page, so supports being used in all rooms
|
|
this.room = helpRoom;
|
|
const num = parseInt(index);
|
|
if (isNaN(num)) return this.errorReply("Invalid index.");
|
|
Answerer.tryRemoveRegex(faq, num - 1);
|
|
this.privateModAction(`${user.name} removed regex ${num} from the usable regexes for ${faq}.`);
|
|
this.modlog('HELPFILTER REMOVE', null, index);
|
|
},
|
|
suggest(target, room, user) {
|
|
room = this.requireRoom();
|
|
const helpRoom = Answerer.getRoom();
|
|
if (!helpRoom) HelpResponder.roomNotFound();
|
|
if (room.roomid !== helpRoom.roomid) return this.errorReply(`This command is only available in the Help room.`);
|
|
if (!target) return this.errorReply(`Specify regex.`);
|
|
if (!user.autoconfirmed) {
|
|
return this.errorReply(`You must be autoconfirmed to suggest regexes to the Help filter.`);
|
|
}
|
|
|
|
// validate
|
|
Answerer.getFaqID(target.split('=>')[1]);
|
|
|
|
if (this.filter(this.message) !== this.message) {
|
|
return this.errorReply(`Invalid suggestion.`);
|
|
}
|
|
if (Answerer.settings.queueDisabled) {
|
|
return this.errorReply(`The Help filter suggestion queue is disabled.`);
|
|
}
|
|
if (Answerer.isBanned(user)) {
|
|
return this.errorReply(`You are banned from making suggestions to the Help filter.`);
|
|
}
|
|
const regex = Answerer.stringRegex(target);
|
|
const entry = {
|
|
regexString: target,
|
|
userid: user.id,
|
|
};
|
|
if (Object.values(Answerer.queue).filter(item => {
|
|
const {regexString} = item;
|
|
return regexString === target;
|
|
}).length) {
|
|
return this.errorReply(`That regex string is already in queue.`);
|
|
}
|
|
Chat.validateRegex(regex);
|
|
Answerer.queue.push(entry);
|
|
Answerer.writeState();
|
|
return this.sendReply(`Added "${target}" to the regex suggestion queue.`);
|
|
},
|
|
approve(target, room, user) {
|
|
const helpRoom = Answerer.getRoom();
|
|
if (!helpRoom) HelpResponder.roomNotFound();
|
|
this.checkCan('ban', null, helpRoom);
|
|
// intended for use mainly within the page, so supports being used in all rooms
|
|
this.room = helpRoom;
|
|
const index = parseInt(target) - 1;
|
|
if (isNaN(index)) return this.errorReply(`Invalid queue index.`);
|
|
if (!Answerer.queue[index]) {
|
|
return this.errorReply(`There is no item in queue with index ${target}.`);
|
|
}
|
|
const {regexString, userid} = Answerer.queue[index];
|
|
const regex = Answerer.stringRegex(regexString);
|
|
|
|
// validated on submission but FAQs may have changed since then
|
|
const faq = Answerer.getFaqID(regexString.split('=>')[1].trim());
|
|
|
|
if (!Answerer.data.pairs[faq]) helpData.pairs[faq] = [];
|
|
Answerer.data.pairs[faq].push(regex);
|
|
Answerer.queue.splice(index, 1);
|
|
Answerer.writeState();
|
|
this.parse(`/hf view queue`);
|
|
|
|
this.privateModAction(`${user.name} approved regex for use with queue number ${target} (suggested by ${userid})`);
|
|
this.modlog(`HELPFILTER APPROVE`, null, `${target}: ${regexString} (from ${userid})`);
|
|
},
|
|
deny(target, room, user) {
|
|
const helpRoom = Answerer.getRoom();
|
|
if (!helpRoom) HelpResponder.roomNotFound();
|
|
this.checkCan('ban', null, helpRoom);
|
|
// intended for use mainly within the page, so supports being used in all rooms
|
|
this.room = helpRoom;
|
|
target = target.trim();
|
|
const index = parseInt(target) - 1;
|
|
if (isNaN(index)) return this.errorReply(`Invalid queue index.`);
|
|
if (!Answerer.queue[index]) throw new Chat.ErrorMessage(`Item does not exist in queue.`);
|
|
Answerer.queue.splice(index, 1);
|
|
Answerer.writeState();
|
|
this.parse(`/hf view queue`);
|
|
this.privateModAction(`${user.name} denied regex with queue number ${target}`);
|
|
this.modlog(`HELPFILTER DENY`, null, `${target}`);
|
|
},
|
|
unban: 'ban',
|
|
ban(target, room, user, connection, cmd) {
|
|
this.room = Answerer.getRoom();
|
|
if (!this.room) HelpResponder.roomNotFound();
|
|
target = target.trim();
|
|
if (!target) return this.parse('/help helpfilter');
|
|
let [userid, reason] = target.split(',').map(item => item.trim());
|
|
userid = toID(userid);
|
|
const targetUser = Users.get(userid);
|
|
this.checkCan('ban', targetUser, this.room);
|
|
const unban = cmd === 'unban';
|
|
const isBanned = Answerer.isBanned(userid);
|
|
if (unban) {
|
|
if (!isBanned) return this.errorReply(`${userid} is not banned from making suggestions.`);
|
|
Punishments.roomUnpunish(this.room.roomid, userid, 'HELPSUGGESTIONBAN');
|
|
this.privateModAction(`${user.name} allowed ${userid} to make suggestions to the Help filter again.`);
|
|
} else {
|
|
if (isBanned) return this.errorReply(`${userid} is already banned from making suggestions.`);
|
|
Answerer.ban(userid, reason);
|
|
if (room?.roomid !== this.room.roomid) {
|
|
this.sendReply(`You banned ${userid} from making suggestions to the Help filter.`);
|
|
}
|
|
this.privateModAction(`${user.name} banned ${userid} from making suggestions to the Help filter.`);
|
|
}
|
|
if (!room || room.roomid !== this.room.roomid) {
|
|
this.sendReply(
|
|
`You ${unban ? ` allowed ${userid} to make ` : ` banned ${userid} from making `} suggestions to the Help filter.`
|
|
);
|
|
}
|
|
return this.modlog(`HELPFILTER ${unban ? 'UN' : ''}SUGGESTIONBAN`, userid, reason);
|
|
},
|
|
queue(target, room, user) {
|
|
if (!room) return this.requireRoom();
|
|
const helpRoom = Answerer.getRoom();
|
|
if (!helpRoom) HelpResponder.roomNotFound();
|
|
if (room.roomid !== helpRoom.roomid) return this.errorReply(`Must be used in the Help room.`);
|
|
target = target.trim();
|
|
this.checkCan('ban', null, room);
|
|
if (!target) {
|
|
return this.sendReply(`The Help suggestion queue is currently ${Answerer.settings.queueDisabled ? 'OFF' : 'ON'}.`);
|
|
}
|
|
if (this.meansYes(target)) {
|
|
if (!Answerer.settings.queueDisabled) return this.errorReply(`The queue is already enabled.`);
|
|
Answerer.settings.queueDisabled = false;
|
|
} else if (this.meansNo(target)) {
|
|
if (Answerer.settings.queueDisabled) return this.errorReply(`The queue is already disabled.`);
|
|
Answerer.settings.queueDisabled = true;
|
|
} else {
|
|
return this.errorReply(`Unrecognized setting.`);
|
|
}
|
|
Answerer.writeState();
|
|
this.privateModAction(`${user.name} ${Answerer.settings.queueDisabled ? 'disabled' : 'enabled'} the Help suggestion queue.`);
|
|
},
|
|
clearqueue: 'emptyqueue',
|
|
emptyqueue(target, room, user) {
|
|
const helpRoom = Answerer.getRoom();
|
|
if (!helpRoom) HelpResponder.roomNotFound();
|
|
if (!room || room.roomid !== helpRoom.roomid) return this.errorReply(`Must be used in the Help room.`);
|
|
if (!HelpResponder.canOverride(user)) return this.errorReply(`/helpfilter ${this.cmd} - Access denied.`);
|
|
Answerer.queue = [];
|
|
Answerer.writeState();
|
|
this.privateModAction(`${user.name} cleared the Help suggestion queue.`);
|
|
this.modlog(`HELPFILTER CLEARQUEUE`);
|
|
},
|
|
},
|
|
helpfilterhelp() {
|
|
const help = [
|
|
`<code>/helpfilter view [page]</code> - Views the Help filter page [page]. (options: keys, stats, queue.)`,
|
|
`<code>/helpfilter toggle [on | off]</code> - Enables or disables the Help filter. Requires: @ # &`,
|
|
`<code>/helpfilter add [input] => [faq]</code> - Adds regex made from the input string to the Help filter, to respond with [faq] to matches.`,
|
|
`<code>/helpfilter remove [faq], [regex index]</code> - removes the regex matching the [index] from the Help filter's responses for [faq].`,
|
|
`<code>/helpfilter suggest [regex] => [faq]</code> - Adds [regex] for [faq] to the queue for Help staff to review.`,
|
|
`<code>/helpfilter [ban|unban] [username], [reason]</code> - Bans or unbans a user from making suggestions to the Help filter.`,
|
|
`<code>/helpfilter approve [index]</code> - Approves the regex at position [index] in the queue for use in the Help filter.`,
|
|
`<code>/helpfilter deny [index]</code> - Denies the regex at position [index] in the Help filter queue.`,
|
|
`Indexes can be found in /helpfilter keys.`,
|
|
`Requires: @ # &`,
|
|
];
|
|
return this.sendReplyBox(help.join('<br/ >'));
|
|
},
|
|
};
|
|
|
|
export const pages: PageTable = {
|
|
helpfilter(args, user) {
|
|
const helpRoom = Answerer.getRoom();
|
|
if (!helpRoom) HelpResponder.roomNotFound();
|
|
const canChange = helpRoom.auth.atLeast(user, '@');
|
|
let buf = '';
|
|
const refresh = (type: string, extra?: string[]) => {
|
|
let button = `<button class="button" name="send" value="/join view-helpfilter-${type}`;
|
|
button += `${extra ? `-${extra.join('-')}` : ''}" style="float: right">`;
|
|
button += `<i class="fa fa-refresh"></i> Refresh</button><br />`;
|
|
return button;
|
|
};
|
|
const back = `<br /><a roomid="view-helpfilter-">Back to all</a>`;
|
|
switch (args[0]) {
|
|
case 'stats':
|
|
args.shift();
|
|
this.checkCan('mute', null, helpRoom);
|
|
const date = args.join('-') || '';
|
|
if (!!date && isNaN(new Date(date).getTime())) {
|
|
return `<h2>Invalid date.</h2>`;
|
|
}
|
|
buf = `<div class="pad"><strong>Stats for the Help auto-response filter${date ? ` on ${date}` : ''}.</strong>`;
|
|
buf += `${back}${refresh('stats', [date])}<hr />`;
|
|
const stats = helpData.stats;
|
|
if (!stats) return `<h2>No stats.</h2>`;
|
|
this.title = `[Help Stats] ${date ? date : ''}`;
|
|
if (date) {
|
|
if (!stats[date]) return `<h2>No stats for ${date}.</h2>`;
|
|
buf += `<strong>Total messages answered: ${stats[date].total}</strong><hr />`;
|
|
buf += `<details><summary>All messages and the corresponding answers (FAQs):</summary>`;
|
|
if (!stats[date].matches) return `<h2>No logs.</h2>`;
|
|
for (const entry of stats[date].matches!) {
|
|
buf += `<small>Message:</small>${LogViewer.renderLine(entry.message)}`;
|
|
buf += `<small>FAQ: ${entry.faqName}</small><br />`;
|
|
buf += `<small>Regex: <code>${entry.regex}</code></small> <hr />`;
|
|
}
|
|
return LogViewer.linkify(buf);
|
|
}
|
|
buf += `<strong> No date specified.<br />`;
|
|
buf += `Dates with stats:</strong><br />`;
|
|
for (const key in stats) {
|
|
buf += `- <a roomid="view-helpfilter-stats-${key}">${key}</a> (${stats[key].total})<br />`;
|
|
}
|
|
break;
|
|
case 'pairs':
|
|
case 'keys':
|
|
this.title = '[Help Regexes]';
|
|
this.checkCan('show', null, helpRoom);
|
|
buf = `<div class="pad"><h2>Help filter regexes and responses:</h2>${back}${refresh('keys')}<hr />`;
|
|
buf += Object.keys(helpData.pairs).map(item => {
|
|
const regexes = helpData.pairs[item];
|
|
if (regexes.length < 1) return null;
|
|
let buffer = `<details><summary>${item}</summary>`;
|
|
buffer += `<div class="ladder pad"><table><tr><th>Index</th><th>Regex</th>`;
|
|
if (canChange) buffer += `<th>Options</th>`;
|
|
buffer += `</tr>`;
|
|
for (const regex of regexes) {
|
|
const index = regexes.indexOf(regex) + 1;
|
|
const button = `<button class="button" name="send"value="/hf remove ${item}, ${index}">Remove</button>`;
|
|
buffer += `<tr><td>${index}</td><td><code>${regex}</code></td>`;
|
|
if (canChange) buffer += `<td>${button}</td></tr>`;
|
|
}
|
|
buffer += `</details>`;
|
|
return buffer;
|
|
}).filter(Boolean).join('<hr />');
|
|
break;
|
|
case 'queue':
|
|
this.title = `[Help Queue]`;
|
|
const canViewAll = user.can('show', null, helpRoom);
|
|
buf = `<div class="pad"><h2>`;
|
|
buf += `${Answerer.queue.length > 0 ? 'R' : 'No r'}egexes queued for review.`;
|
|
if (!canViewAll) buf += ` (your submissions only)`;
|
|
buf += `</h2>${back}${refresh('queue')}`;
|
|
buf += `<div class="ladder pad"><table><tr>`;
|
|
buf += `<th>User</th><th>Input</th><th>Full Regex</th>`;
|
|
if (canChange) buf += `<th>Options</th>`;
|
|
buf += `</tr>`;
|
|
if (!helpData.queue) helpData.queue = [];
|
|
for (const request of helpData.queue) {
|
|
const {regexString, userid} = request;
|
|
if (!canViewAll && userid !== user.id) continue;
|
|
const submitter = Users.get(userid) ? Users.get(userid)?.name : userid;
|
|
buf += `<tr><td><div class="username">${submitter}</div></td>`;
|
|
buf += `<td>${regexString}</td>`;
|
|
buf += `<td><code>${Answerer.stringRegex(regexString)}</td>`;
|
|
const index = helpData.queue.indexOf(request) + 1;
|
|
if (canChange) {
|
|
buf += `<td><button class="button" name="send"value="/hf approve ${index}">Approve</button>`;
|
|
buf += `<button class="button" name="send"value="/hf deny ${index}">Deny</button>`;
|
|
buf += `<button class="button" name="send"value="/hf ban ${userid}">Ban from submitting</button>`;
|
|
}
|
|
buf += `</td></tr>`;
|
|
}
|
|
buf += '</table></div>';
|
|
break;
|
|
default:
|
|
this.title = '[Help Filter]';
|
|
buf = `<div class="pad"><h2>Specify a filter page to view.</h2>`;
|
|
buf += `<hr /><strong>Options:</strong><hr />`;
|
|
buf += `<a roomid="view-helpfilter-stats">Stats</a><hr />`;
|
|
buf += `<a roomid="view-helpfilter-keys">Regex keys</a><hr/>`;
|
|
buf += `<a roomid="view-helpfilter-queue">Queue</a><hr/>`;
|
|
buf += `</div>`;
|
|
}
|
|
return LogViewer.linkify(buf);
|
|
},
|
|
};
|