"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var responder_exports = {};
__export(responder_exports, {
AutoResponder: () => AutoResponder,
answererData: () => answererData,
chatfilter: () => chatfilter,
commands: () => commands,
handlers: () => handlers,
pages: () => pages
});
module.exports = __toCommonJS(responder_exports);
var import_lib = require("../../lib");
var import_chatlog = require("./chatlog");
var import_room_faqs = require("./room-faqs");
const DATA_PATH = "config/chat-plugins/responder.json";
const LOG_PATH = Monitor.logPath("responder.jsonl").path;
let answererData = {};
try {
answererData = JSON.parse((0, import_lib.FS)(DATA_PATH).readSync());
} catch {
}
const _AutoResponder = class {
constructor(room, data) {
this.room = room;
this.data = data || { pairs: {}, ignore: [] };
_AutoResponder.migrateStats(this.data, this);
}
static migrateStats(data, responder) {
if (!data.stats)
return data;
for (const date in data.stats) {
for (const entry of data.stats[date].matches) {
void this.logMessage(responder.room.roomid, { ...entry, date });
}
}
delete data.stats;
responder.data = data;
responder.writeState();
return data;
}
static logMessage(roomid, entry) {
return this.logStream.writeLine(JSON.stringify({
...entry,
room: roomid,
regex: entry.regex.toString()
}));
}
find(question, user) {
question = question.slice(0, 300);
const room = this.room;
const helpFaqs = import_room_faqs.roomFaqs[room.roomid];
if (!helpFaqs)
return null;
const normalized = Chat.normalize(question);
if (this.data.ignore) {
if (this.data.ignore.some((t) => new RegExp(t, "i").test(normalized))) {
return null;
}
}
const faqs = Object.keys(helpFaqs).filter((item) => !helpFaqs[item].alias);
for (const faq of faqs) {
const match = this.test(normalized, 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, hideButton, user) {
const response = this.find(question, user);
if (response) {
let buf = "";
buf += import_lib.Utils.html`You said: ${question}
`;
buf += `Our automated reply: ${Chat.collapseLineBreaksHTML((0, import_room_faqs.visualizeFaq)(response))}`;
if (!hideButton) {
buf += import_lib.Utils.html`
`;
}
return buf;
}
return null;
}
getFaqID(faq) {
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.room;
const entry = import_room_faqs.roomFaqs[room.roomid][faq];
if (!entry)
throw new Chat.ErrorMessage(`FAQ ID "${faq}" not found.`);
if (!entry.alias)
return faq;
return entry.source;
}
async getStatsFor(date) {
const stream = (0, import_lib.FS)(LOG_PATH).createReadStream();
const buf = [];
for await (const raw of stream.byLine()) {
try {
const data = JSON.parse(raw);
if (data.date !== date || data.room !== this.room.roomid)
continue;
buf.push(data);
} catch {
}
}
return buf;
}
async listDays() {
const stream = (0, import_lib.FS)(LOG_PATH).createReadStream();
const buf = new import_lib.Utils.Multiset();
for await (const raw of stream.byLine()) {
try {
const data = JSON.parse(raw);
if (!data.date || data.room !== this.room.roomid)
continue;
buf.add(data.date);
} catch {
}
}
return buf;
}
/**
* Checks if the FAQ exists. If not, deletes all references to it.
*/
updateFaqData(faq) {
if (Config.nofswriting)
return true;
const room = this.room;
if (!room)
return;
if (import_room_faqs.roomFaqs[room.roomid][faq])
return true;
if (this.data.pairs[faq])
delete this.data.pairs[faq];
return false;
}
stringRegex(str, raw) {
[str] = import_lib.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) => {
if (raw)
return string;
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("");
}
test(question, faq) {
if (!this.data.pairs[faq])
this.data.pairs[faq] = [];
const regexes = this.data.pairs[faq].map((item) => new RegExp(item, "i"));
if (!regexes.length)
return;
for (const regex of regexes) {
if (regex.test(question))
return { faq, regex: regex.toString() };
}
return null;
}
log(entry, faq, expression) {
const [day] = import_lib.Utils.splitFirst(Chat.toTimestamp(new Date()), " ");
void _AutoResponder.logMessage(this.room.roomid, {
message: entry,
faqName: faq,
regex: expression,
date: day
});
}
writeState() {
for (const faq in this.data.pairs) {
this.updateFaqData(faq);
}
answererData[this.room.roomid] = this.data;
return (0, import_lib.FS)(DATA_PATH).writeUpdate(() => JSON.stringify(answererData));
}
tryAddRegex(inputString, raw) {
let [args, faq] = inputString.split("=>").map((item) => item.trim());
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, index) {
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;
}
static canOverride(user, room) {
const devAuth = Rooms.get("development")?.auth;
return devAuth?.atLeast(user, "%") && devAuth?.has(user.id) && room.auth.atLeast(user, "@") || user.can("rangeban");
}
destroy() {
this.writeState();
this.room.responder = null;
this.room = null;
}
ignore(terms, context) {
const filtered = terms.map((t) => context.filter(t)).filter(Boolean);
if (filtered.length !== terms.length) {
throw new Chat.ErrorMessage(`Invalid terms.`);
}
if (terms.some((t) => t.length > 300)) {
throw new Chat.ErrorMessage(`One of your terms is too long.`);
}
if (!this.data.ignore)
this.data.ignore = [];
this.data.ignore.push(...terms);
this.writeState();
return terms;
}
unignore(terms) {
if (!this.data.ignore) {
throw new Chat.ErrorMessage(`The autoresponse filter in this room has no ignored terms.`);
}
this.data.ignore = this.data.ignore.filter((item) => !terms.includes(item));
this.writeState();
return true;
}
};
let AutoResponder = _AutoResponder;
AutoResponder.logStream = (0, import_lib.FS)(LOG_PATH).createAppendStream();
for (const room of Rooms.rooms.values()) {
room.responder?.destroy();
if (answererData[room.roomid]) {
room.responder = new AutoResponder(room, answererData[room.roomid]);
}
}
const BYPASS_TERMS = ["a:", "A:", "!", "/"];
const chatfilter = function(message, user, room) {
if (BYPASS_TERMS.some((t) => message.startsWith(t))) {
return;
}
if (room?.responder && room.auth.get(user.id) === " ") {
const responder = room.responder;
const reply = responder.visualize(message, false, user);
if (!reply) {
return message;
} else {
this.sendReply(`|uhtml|askhelp-${user}-${toID(message)}|${reply}
`);
const trimmedMessage = `${responder.visualize(message, true)}
`;
setTimeout(() => {
this.sendReply(`|uhtmlchange|askhelp-${user}-${toID(message)}|${trimmedMessage}`);
}, 10 * 1e3);
return false;
}
}
};
const commands = {
question(target, room, user) {
room = this.requireRoom();
const responder = room.responder;
if (!responder)
return this.errorReply(`This room does not have an autoresponder configured.`);
if (!target)
return this.parse("/help question");
const reply = responder.visualize(target, true);
if (!reply)
return this.sendReplyBox(`No answer found.`);
this.runBroadcast();
this.sendReplyBox(reply);
},
questionhelp: ["/question [question] - Asks the current room's auto-response filter a question."],
ar: "autoresponder",
autoresponder: {
""(target, room) {
room = this.requireRoom();
const responder = room.responder;
if (!responder) {
return this.errorReply(`This room has not configured an autoresponder.`);
}
if (!target) {
return this.parse("/help autoresponder");
}
return this.parse(`/j view-autoresponder-${room.roomid}-${target}`);
},
view(target, room, user) {
room = this.requireRoom();
return this.parse(`/join view-autoresponder-${room.roomid}-${target}`);
},
toggle(target, room, user) {
room = this.requireRoom();
if (!target) {
return this.sendReply(
`The Help auto-response filter is currently set to: ${room.responder ? "ON" : "OFF"}`
);
}
this.checkCan("ban", null, room);
if (room.settings.isPrivate === true) {
return this.errorReply(`Secret rooms cannot enable an autoresponder.`);
}
if (this.meansYes(target)) {
if (room.responder)
return this.errorReply(`The Autoresponder for this room is already enabled.`);
room.responder = new AutoResponder(room, answererData[room.roomid]);
room.responder.writeState();
}
if (this.meansNo(target)) {
if (!room.responder)
return this.errorReply(`The Autoresponder for this room is already disabled.`);
room.responder.destroy();
}
this.privateModAction(`${user.name} ${!room.responder ? "disabled" : "enabled"} the auto-response filter.`);
this.modlog(`AUTOFILTER`, null, !room.responder ? "OFF" : "ON");
},
forceadd: "add",
add(target, room, user, connection, cmd) {
room = this.requireRoom();
if (!room.responder) {
return this.errorReply(`This room has not configured an auto-response filter.`);
}
const force = cmd === "forceadd";
if (force && !AutoResponder.canOverride(user, room)) {
return this.errorReply(`You cannot use raw regex - use /autoresponder add instead.`);
}
this.checkCan("ban", null, room);
room.responder.tryAddRegex(target, force);
this.privateModAction(`${user.name} added regex for "${target.split("=>")[0]}" to the autoresponder.`);
this.modlog(`AUTOFILTER ADD`, null, target);
},
remove(target, room, user) {
const [faq, index] = target.split(",");
room = this.requireRoom();
if (!room.responder) {
return this.errorReply(`${room.title} has not configured an auto-response filter.`);
}
this.checkCan("ban", null, room);
const num = parseInt(index);
if (isNaN(num))
return this.errorReply("Invalid index.");
room.responder.tryRemoveRegex(faq, num - 1);
this.privateModAction(`${user.name} removed regex ${num} from the usable regexes for ${faq}.`);
this.modlog("AUTOFILTER REMOVE", null, `removed regex ${index} for FAQ ${faq}`);
const pages2 = [`keys`, `pairs`];
for (const p of pages2) {
this.refreshPage(`autofilter-${room.roomid}-${p}`);
}
},
ignore(target, room, user) {
room = this.requireRoom();
if (!room.responder) {
return this.errorReply(`This room has not configured an auto-response filter.`);
}
this.checkCan("ban", null, room);
if (!toID(target)) {
return this.parse(`/help autoresponder`);
}
const targets = target.split(",");
room.responder.ignore(targets, this);
this.privateModAction(
`${user.name} added ${Chat.count(targets.length, "terms")} to the autoresponder ignore list.`
);
this.modlog(`AUTOFILTER IGNORE`, null, target);
},
unignore(target, room, user) {
room = this.requireRoom();
if (!room.responder) {
return this.errorReply(`${room.title} has not configured an auto-response filter.`);
}
this.checkCan("ban", null, room);
if (!toID(target)) {
return this.parse(`/help autoresponder`);
}
const targets = target.split(",");
room.responder.unignore(targets);
this.privateModAction(`${user.name} removed ${Chat.count(targets.length, "terms")} from the autoresponder ignore list.`);
this.modlog(`AUTOFILTER UNIGNORE`, null, target);
if (this.connection.openPages?.has(`autoresponder-${room.roomid}-ignore`)) {
return this.parse(`/join view-autoresponder-${room.roomid}-ignore`);
}
}
},
autoresponderhelp() {
const help = [
`/autoresponder view [page]
- Views the Autoresponder page [page]. (options: keys, stats)`,
`/autoresponder toggle [on | off]
- Enables or disables the Autoresponder for the current room. Requires: @ # ~`,
`/autoresponder add [input] => [faq]
- Adds regex made from the input string to the current room's Autoresponder, to respond with [faq] to matches.`,
`/autoresponder remove [faq], [regex index]
- removes the regex matching the [index] from the current room's responses for [faq].`,
`Indexes can be found in /autoresponder keys.`,
`Requires: @ # ~`
];
return this.sendReplyBox(help.join("
"));
}
};
const pages = {
async autoresponder(args, user) {
const room = this.requireRoom();
if (!room.responder) {
return this.errorReply(`${room.title} does not have a configured autoresponder.`);
}
args.shift();
const roomData = answererData[room.roomid];
const canChange = user.can("ban", null, room);
let buf = "";
const refresh = (type, extra) => {
if (extra)
extra = extra.filter(Boolean);
let button = `
`;
return button;
};
const back = `
Back to all`;
switch (args[0]) {
case "stats":
args.shift();
this.checkCan("mute", null, room);
const date = args.join("-") || "";
if (!!date && isNaN(new Date(date).getTime())) {
return `Invalid date.
`;
}
buf = `Stats for the ${room.title} auto-response filter${date ? ` on ${date}` : ""}.`;
buf += `${back}${refresh("stats", [date])}
`;
if (date) {
const stats = await room.responder.getStatsFor(date);
if (!stats)
return `
No stats.
`;
this.title = `[Autoresponder Stats] ${date ? date : ""}`;
if (!stats.length)
return `
No stats for ${date}.
`;
buf += `
Total messages answered: ${stats.length}
`;
buf += `
All messages and the corresponding answers (FAQs):
`;
for (const entry of stats) {
buf += `Message:${import_chatlog.LogViewer.renderLine(entry.message)}`;
buf += `FAQ: ${entry.faqName}
`;
buf += `Regex: ${entry.regex}
`;
}
return import_chatlog.LogViewer.linkify(buf);
}
buf += ` No date specified.
`;
const days = [];
let totalCount = 0;
const dayKeys = await room.responder.listDays();
for (const [dateKey, total] of dayKeys) {
totalCount += total;
days.push(`- ${dateKey} (${total})`);
}
buf += `Dates with stats:(total matches: ${totalCount})
`;
buf += days.join("
");
break;
case "pairs":
case "keys":
this.title = "[Autoresponder Regexes]";
this.checkCan("show", null, room);
buf = `${room.title} responder regexes and responses:
${back}${refresh("keys")}
`;
buf += Object.entries(roomData.pairs).map(([item, regexes]) => {
if (regexes.length < 1)
return null;
let buffer = `
${item}
`;
buffer += `Index | Regex | `;
if (canChange)
buffer += `Options | `;
buffer += `
`;
for (const regex of regexes) {
const index = regexes.indexOf(regex) + 1;
const button = ``;
buffer += `${index} | ${regex} | `;
if (canChange)
buffer += `${button} |
`;
}
buffer += ``;
return buffer;
}).filter(Boolean).join("
");
break;
case "ignore":
this.title = `[${room.title} Autoresponder ignore list]`;
buf = `${room.title} responder terms to ignore:
${back}${refresh("ignore")}
`;
if (!roomData.ignore) {
return this.errorReply(`No terms on ignore list.`);
}
for (const term of roomData.ignore) {
buf += `- ${term}
`;
}
buf += ``;
break;
default:
this.title = `[${room.title} Autoresponder]`;
buf = ``;
}
return import_chatlog.LogViewer.linkify(buf);
}
};
const handlers = {
onRenameRoom(oldID, newID) {
if (answererData[oldID]) {
if (!answererData[newID])
answererData[newID] = { pairs: {} };
Object.assign(answererData[newID], answererData[oldID]);
delete answererData[oldID];
(0, import_lib.FS)(DATA_PATH).writeUpdate(() => JSON.stringify(answererData));
}
}
};
//# sourceMappingURL=responder.js.map