844 lines
31 KiB
JavaScript
844 lines
31 KiB
JavaScript
"use strict";
|
|
var __create = Object.create;
|
|
var __defProp = Object.defineProperty;
|
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
var __getProtoOf = Object.getPrototypeOf;
|
|
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 __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
// If the importer is in node compatibility mode or this is not an ESM
|
|
// file that has been converted to a CommonJS file using a Babel-
|
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
mod
|
|
));
|
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
var helptickets_auto_exports = {};
|
|
__export(helptickets_auto_exports, {
|
|
actionHandlers: () => actionHandlers,
|
|
addModAction: () => addModAction,
|
|
checkers: () => checkers,
|
|
classifier: () => classifier,
|
|
commands: () => commands,
|
|
determinePunishment: () => determinePunishment,
|
|
getMessageAverages: () => getMessageAverages,
|
|
getModlog: () => getModlog,
|
|
globalModlog: () => globalModlog,
|
|
pages: () => pages,
|
|
punishmentsFor: () => punishmentsFor,
|
|
runPunishments: () => runPunishments,
|
|
settings: () => settings
|
|
});
|
|
module.exports = __toCommonJS(helptickets_auto_exports);
|
|
var import_lib = require("../../lib");
|
|
var import_helptickets = require("./helptickets");
|
|
var Artemis = __toESM(require("../artemis"));
|
|
const ORDERED_PUNISHMENTS = ["WARN", "FORCERENAME", "LOCK", "NAMELOCK", "WEEKLOCK", "WEEKNAMELOCK"];
|
|
const PMLOG_IGNORE_TIME = 24 * 60 * 60 * 1e3;
|
|
const WHITELIST = ["mia"];
|
|
const defaults = {
|
|
punishments: [{
|
|
ticketType: "inapname",
|
|
punishment: "forcerename",
|
|
severity: { type: ["sexual_explicit", "severe_toxicity", "identity_attack"], certainty: 0.4 }
|
|
}, {
|
|
ticketType: "pmharassment",
|
|
punishment: "warn",
|
|
severity: { type: ["sexual_explicit", "severe_toxicity", "identity_attack"], certainty: 0.15 }
|
|
}],
|
|
applyPunishments: false
|
|
};
|
|
const settings = (() => {
|
|
try {
|
|
return { ...defaults, ...JSON.parse((0, import_lib.FS)("config/chat-plugins/ht-auto.json").readSync()) };
|
|
} catch {
|
|
return defaults;
|
|
}
|
|
})();
|
|
function saveSettings() {
|
|
return (0, import_lib.FS)("config/chat-plugins/ht-auto.json").writeUpdate(() => JSON.stringify(settings));
|
|
}
|
|
function visualizePunishment(punishment) {
|
|
const buf = [`punishment: ${punishment.punishment?.toUpperCase()}`];
|
|
buf.push(`ticket type: ${punishment.ticketType}`);
|
|
if (punishment.severity) {
|
|
buf.push(`severity: ${punishment.severity.certainty} (for ${punishment.severity.type.join(", ")})`);
|
|
}
|
|
if (punishment.modlogCount) {
|
|
buf.push(`required modlog: ${punishment.modlogCount}`);
|
|
}
|
|
if (punishment.isSingleMessage) {
|
|
buf.push(`for single messages only`);
|
|
}
|
|
return buf.join(", ");
|
|
}
|
|
function checkAccess(context) {
|
|
if (!WHITELIST.includes(context.user.id))
|
|
context.checkCan("bypassall");
|
|
}
|
|
function punishmentsFor(type) {
|
|
return settings.punishments.filter((t) => t.ticketType === type);
|
|
}
|
|
function supersedes(p1, p2) {
|
|
return ORDERED_PUNISHMENTS.indexOf(p1) > ORDERED_PUNISHMENTS.indexOf(p2);
|
|
}
|
|
function determinePunishment(ticketType, results, modlog, isSingleMessage = false) {
|
|
const punishments = punishmentsFor(ticketType);
|
|
let action = null;
|
|
const types = [];
|
|
import_lib.Utils.sortBy(punishments, (p) => -ORDERED_PUNISHMENTS.indexOf(p.punishment));
|
|
for (const punishment of punishments) {
|
|
if (isSingleMessage && !punishment.isSingleMessage)
|
|
continue;
|
|
if (punishment.modlogCount && modlog.length < punishment.modlogCount)
|
|
continue;
|
|
if (punishment.severity) {
|
|
let hit = false;
|
|
for (const type of punishment.severity.type) {
|
|
if (results[type] < punishment.severity.certainty)
|
|
continue;
|
|
hit = true;
|
|
types.push(type);
|
|
break;
|
|
}
|
|
if (!hit)
|
|
continue;
|
|
}
|
|
if (!action || supersedes(punishment.punishment, action)) {
|
|
action = punishment.punishment;
|
|
}
|
|
}
|
|
return { action, types };
|
|
}
|
|
function globalModlog(action, user, note, roomid) {
|
|
user = Users.get(user) || user;
|
|
void Rooms.Modlog.write(roomid || "global", {
|
|
action,
|
|
ip: user && typeof user === "object" ? user.latestIp : void 0,
|
|
userid: toID(user) || void 0,
|
|
loggedBy: "artemis",
|
|
note
|
|
});
|
|
}
|
|
function addModAction(message) {
|
|
Rooms.get("staff")?.add(`|c|&|/log ${message}`).update();
|
|
}
|
|
async function getModlog(params) {
|
|
const search = {
|
|
note: [],
|
|
user: [],
|
|
ip: [],
|
|
action: [],
|
|
actionTaker: []
|
|
};
|
|
if (params.user)
|
|
search.user = [{ search: params.user, isExact: true }];
|
|
if (params.ip)
|
|
search.ip = [{ search: params.ip }];
|
|
if (params.actions)
|
|
search.action = params.actions.map((s) => ({ search: s }));
|
|
const res = await Rooms.Modlog.search("global", search);
|
|
return res?.results || [];
|
|
}
|
|
function closeTicket(ticket, msg) {
|
|
if (!ticket.open)
|
|
return;
|
|
ticket.open = false;
|
|
ticket.active = false;
|
|
ticket.resolved = {
|
|
time: Date.now(),
|
|
by: "the Artemis AI",
|
|
// we want it to be clear to end users that it was not a human
|
|
seen: false,
|
|
staffReason: "",
|
|
result: msg || "",
|
|
note: `Want to learn more about the AI? <a href="https://www.smogon.com/forums/threads/3570628/#post-9056769">Visit the information thread</a>.`
|
|
};
|
|
(0, import_helptickets.writeTickets)();
|
|
(0, import_helptickets.notifyStaff)();
|
|
const tarUser = Users.get(ticket.userid);
|
|
if (tarUser) {
|
|
import_helptickets.HelpTicket.notifyResolved(tarUser, ticket, ticket.userid);
|
|
}
|
|
(0, import_helptickets.writeStats)(`${ticket.type} ${Date.now() - ticket.created} 0 0 resolved valid artemis`);
|
|
}
|
|
async function lock(user, result, ticket, isWeek, isName) {
|
|
const id = toID(user);
|
|
let desc, type;
|
|
const expireTime = isWeek ? Date.now() + 7 * 24 * 60 * 60 * 1e3 : null;
|
|
if (isName) {
|
|
if (typeof user === "object")
|
|
user.resetName();
|
|
desc = "locked your username and prevented you from changing names";
|
|
type = `locked from talking${isWeek ? ` for a week` : ""}`;
|
|
await Punishments.namelock(id, expireTime, null, false, result.reason || "Automatically locked due to a user report");
|
|
} else {
|
|
type = isWeek ? "weeknamelocked" : "namelocked";
|
|
desc = "locked you from talking in chats, battles, and PMing regular users";
|
|
await Punishments.lock(id, expireTime, null, false, result.reason || "Automatically locked due to a user report");
|
|
}
|
|
if (typeof user !== "string") {
|
|
let message = `|popup||html|${user.name} has ${desc} for ${isWeek ? "7" : "2"} days.`;
|
|
if (result.reason)
|
|
message += `
|
|
|
|
Reason: ${result.reason}`;
|
|
let appeal = "";
|
|
if (Chat.pages.help) {
|
|
appeal += `<a href="view-help-request--appeal"><button class="button"><strong>Appeal your punishment</strong></button></a>`;
|
|
} else if (Config.appealurl) {
|
|
appeal += `appeal: <a href="${Config.appealurl}">${Config.appealurl}</a>`;
|
|
}
|
|
if (appeal)
|
|
message += `
|
|
|
|
If you feel that your lock was unjustified, you can ${appeal}.`;
|
|
message += `
|
|
|
|
Your lock will expire in a few days.`;
|
|
user.send(message);
|
|
}
|
|
addModAction(`${id} was ${type} by Artemis. (${result.reason || `report from ${ticket.creator}`})`);
|
|
globalModlog(
|
|
`${isWeek ? "WEEK" : ""}${isName ? "NAME" : ""}LOCK`,
|
|
id,
|
|
(result.reason || `report from ${ticket.creator}`) + (result.proof ? ` PROOF: ${result.proof}` : "")
|
|
);
|
|
}
|
|
const actionHandlers = {
|
|
forcerename(user, result, ticket) {
|
|
if (typeof user === "string")
|
|
return;
|
|
const id = toID(user);
|
|
user.resetName();
|
|
user.trackRename = id;
|
|
Monitor.forceRenames.set(id, true);
|
|
user.send(
|
|
`|nametaken|Your name was detected to be breaking our name rules. ${result.reason ? `Reason: ${result.reason}. ` : ""}Please change it, or submit a help ticket by typing /ht in chat to appeal this action.`
|
|
);
|
|
Rooms.get("staff")?.add(
|
|
`|html|<span class="username">${id}</span> was automatically forced to choose a new name by Artemis (report from ${ticket.userid}).`
|
|
).update();
|
|
globalModlog(
|
|
"FORCERENAME",
|
|
id,
|
|
`username determined to be inappropriate due to a report by ${ticket.creator}`,
|
|
result.roomid
|
|
);
|
|
return `${id} was automatically forcerenamed. Thank you for reporting.`;
|
|
},
|
|
async namelock(user, result, ticket) {
|
|
await lock(user, result, ticket, false, true);
|
|
return `${toID(user)} was automatically namelocked. Thank you for reporting.`;
|
|
},
|
|
async weeknamelock(user, result, ticket) {
|
|
await lock(user, result, ticket, true, true);
|
|
return `${toID(user)} was automatically weeknamelocked. Thank you for reporting.`;
|
|
},
|
|
async lock(user, result, ticket) {
|
|
await lock(user, result, ticket);
|
|
return `${toID(user)} was automatically locked. Thank you for reporting.`;
|
|
},
|
|
async weeklock(user, result, ticket) {
|
|
await lock(user, result, ticket, true);
|
|
return `${toID(user)} was automatically weeklocked. Thank you for reporting.`;
|
|
},
|
|
warn(user, result, ticket) {
|
|
user = toID(user);
|
|
user = Users.get(user) || user;
|
|
if (typeof user === "object") {
|
|
user.send(`|c|~|/warn ${result.reason || ""}`);
|
|
} else {
|
|
Punishments.offlineWarns.set(user, result.reason);
|
|
}
|
|
addModAction(
|
|
`${user} was warned by Artemis. ${typeof user === "string" ? "while offline " : ""}(${result.reason || `report from ${ticket.creator}`})`
|
|
);
|
|
globalModlog(
|
|
"WARN",
|
|
user,
|
|
result.reason || `report from ${ticket.creator}`
|
|
);
|
|
return `${user} was automatically warned. Thank you for reporting.`;
|
|
}
|
|
};
|
|
function shouldNotProcess(message) {
|
|
return message.startsWith("/") && !message.startsWith("//") || // broadcasted chat command
|
|
message.startsWith("!");
|
|
}
|
|
async function getMessageAverages(messages) {
|
|
const counts = {};
|
|
const classified = [];
|
|
for (const message of messages) {
|
|
if (shouldNotProcess(message))
|
|
continue;
|
|
const res = await classifier.classify(message);
|
|
if (!res)
|
|
continue;
|
|
classified.push(res);
|
|
for (const k in res) {
|
|
if (!counts[k])
|
|
counts[k] = { count: 0, raw: 0 };
|
|
counts[k].count++;
|
|
counts[k].raw += res[k];
|
|
}
|
|
}
|
|
const averages = {};
|
|
for (const k in counts) {
|
|
averages[k] = counts[k].raw / counts[k].count;
|
|
}
|
|
return { averages, classified };
|
|
}
|
|
const checkers = {
|
|
async inapname(ticket) {
|
|
const id = toID(ticket.text[0]);
|
|
const user = Users.getExact(id);
|
|
if (user && !user.trusted) {
|
|
const result = await classifier.classify(user.name);
|
|
if (!result)
|
|
return;
|
|
const keys = ["identity_attack", "sexual_explicit", "severe_toxicity"];
|
|
const matched = keys.some((k) => result[k] >= 0.4);
|
|
if (matched) {
|
|
const modlog = await getModlog({
|
|
ip: user.latestIp,
|
|
actions: ["FORCERENAME", "NAMELOCK", "WEEKNAMELOCK"]
|
|
});
|
|
let { action } = determinePunishment("inapname", result, modlog);
|
|
if (!action)
|
|
action = "forcerename";
|
|
return /* @__PURE__ */ new Map([[user.id, {
|
|
action,
|
|
user,
|
|
result,
|
|
reason: "Username detected to be breaking username rules"
|
|
}]]);
|
|
}
|
|
}
|
|
},
|
|
async inappokemon(ticket) {
|
|
const actions = /* @__PURE__ */ new Map();
|
|
const links = [...(0, import_helptickets.getBattleLinks)(ticket.text[0]), ...(0, import_helptickets.getBattleLinks)(ticket.text[1])];
|
|
for (const link of links) {
|
|
const log = await (0, import_helptickets.getBattleLog)(link);
|
|
if (!log)
|
|
continue;
|
|
for (const [user, pokemon] of Object.entries(log.pokemon)) {
|
|
const userid = toID(user);
|
|
let result = null;
|
|
for (const set of pokemon) {
|
|
if (!set.name)
|
|
continue;
|
|
const results = await classifier.classify(set.name);
|
|
if (!results)
|
|
continue;
|
|
const curAction = determinePunishment("inappokemon", results, []).action;
|
|
if (curAction && (!result || supersedes(curAction, result.action))) {
|
|
result = { action: curAction, name: set.name, result: results, replay: link };
|
|
}
|
|
}
|
|
if (result) {
|
|
actions.set(user, {
|
|
action: result.action,
|
|
user: userid,
|
|
result: result.result,
|
|
reason: `Pokemon name detected to be breaking rules - '${result.name}'`,
|
|
roomid: link
|
|
});
|
|
}
|
|
}
|
|
}
|
|
if (actions.size)
|
|
return actions;
|
|
},
|
|
async battleharassment(ticket) {
|
|
const urls = (0, import_helptickets.getBattleLinks)(ticket.text[0]);
|
|
const actions = /* @__PURE__ */ new Map();
|
|
for (const url of urls) {
|
|
const log = await (0, import_helptickets.getBattleLog)(url);
|
|
if (!log)
|
|
continue;
|
|
const messages = {};
|
|
for (const message of log.log) {
|
|
const [username, text] = import_lib.Utils.splitFirst(message.slice(3), "|").map((f) => f.trim());
|
|
const id = toID(username);
|
|
if (!id)
|
|
continue;
|
|
if (!messages[id])
|
|
messages[id] = [];
|
|
messages[id].push(text);
|
|
}
|
|
for (const [id, messageList] of Object.entries(messages)) {
|
|
const { averages, classified } = await getMessageAverages(messageList);
|
|
const { action } = determinePunishment("battleharassment", averages, []);
|
|
if (action) {
|
|
const existingPunishment = actions.get(id);
|
|
if (!existingPunishment || supersedes(action, existingPunishment.action)) {
|
|
actions.set(id, {
|
|
action,
|
|
user: toID(id),
|
|
result: averages,
|
|
reason: `Not following rules in battles (https://${Config.routes.client}/${url})`,
|
|
proof: urls.join(", ")
|
|
});
|
|
}
|
|
}
|
|
for (const result of classified) {
|
|
const curPunishment = determinePunishment("battleharassment", result, [], true).action;
|
|
if (!curPunishment)
|
|
continue;
|
|
const exists = actions.get(id);
|
|
if (!exists || supersedes(curPunishment, exists.action)) {
|
|
actions.set(id, {
|
|
action: curPunishment,
|
|
user: toID(id),
|
|
result: averages,
|
|
reason: `Not following rules in battles (https://${Config.routes.client}/${url})`,
|
|
proof: urls.join(", ")
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
const creatorWasPunished = actions.get(ticket.userid);
|
|
if (creatorWasPunished) {
|
|
let displayReason = "You were punished for your behavior.";
|
|
if (actions.size !== 1) {
|
|
displayReason += ` ${actions.size - 1} other(s) were also punished.`;
|
|
}
|
|
creatorWasPunished.displayReason = displayReason;
|
|
}
|
|
if (actions.size)
|
|
return actions;
|
|
},
|
|
async pmharassment(ticket) {
|
|
const actions = /* @__PURE__ */ new Map();
|
|
const targetId = toID(ticket.text[0]);
|
|
const creator = ticket.userid;
|
|
if (!Config.getpmlog)
|
|
return;
|
|
const pmLog = await Config.getpmlog(targetId, creator);
|
|
const messages = {};
|
|
const ids = /* @__PURE__ */ new Set();
|
|
for (const { from, message, timestamp } of pmLog) {
|
|
if (Date.now() - new Date(timestamp).getTime() > PMLOG_IGNORE_TIME)
|
|
continue;
|
|
const id = toID(from);
|
|
ids.add(id);
|
|
if (!messages[id])
|
|
messages[id] = [];
|
|
messages[id].push(message);
|
|
}
|
|
for (const id of ids) {
|
|
let punishment;
|
|
const { averages, classified } = await getMessageAverages(messages[id]);
|
|
const curPunishment = determinePunishment("pmharassment", averages, []).action;
|
|
if (curPunishment) {
|
|
if (!punishment || supersedes(curPunishment, punishment)) {
|
|
punishment = curPunishment;
|
|
}
|
|
if (punishment) {
|
|
actions.set(id, {
|
|
action: punishment,
|
|
user: id,
|
|
result: {},
|
|
reason: `PM harassment (against ${ticket.userid === id ? targetId : ticket.userid})`
|
|
});
|
|
}
|
|
}
|
|
for (const result of classified) {
|
|
const { action } = determinePunishment("pmharassment", result, [], true);
|
|
if (!action)
|
|
continue;
|
|
const exists = actions.get(id);
|
|
if (!exists || supersedes(action, exists.action)) {
|
|
actions.set(id, {
|
|
action,
|
|
user: id,
|
|
result: {},
|
|
reason: `PM harassment (against ${ticket.userid === id ? targetId : ticket.userid})`
|
|
});
|
|
}
|
|
}
|
|
}
|
|
const creatorWasPunished = actions.get(ticket.userid);
|
|
if (creatorWasPunished) {
|
|
let displayReason = `You were punished for your behavior. `;
|
|
if (actions.has(targetId) && targetId !== ticket.userid) {
|
|
displayReason += ` The person you reported was also punished.`;
|
|
}
|
|
creatorWasPunished.displayReason = displayReason;
|
|
}
|
|
if (actions.size)
|
|
return actions;
|
|
}
|
|
};
|
|
const classifier = new Artemis.LocalClassifier();
|
|
async function runPunishments(ticket, typeId) {
|
|
let result = null;
|
|
if (checkers[typeId]) {
|
|
result = await checkers[typeId](ticket) || null;
|
|
}
|
|
if (result) {
|
|
if (settings.applyPunishments) {
|
|
const responses = [];
|
|
for (const res of result.values()) {
|
|
const curResult = await actionHandlers[res.action.toLowerCase()](res.user, res, ticket);
|
|
if (curResult)
|
|
responses.push([res.action, res.displayReason || curResult]);
|
|
if (toID(res.user) === ticket.creator) {
|
|
closeTicket(ticket, res.displayReason);
|
|
}
|
|
}
|
|
if (responses.length) {
|
|
import_lib.Utils.sortBy(responses, (r) => -ORDERED_PUNISHMENTS.indexOf(r[0]));
|
|
closeTicket(ticket, responses[0][1]);
|
|
} else {
|
|
closeTicket(ticket);
|
|
}
|
|
} else {
|
|
ticket.recommended = [];
|
|
for (const res of result.values()) {
|
|
Rooms.get("abuselog")?.add(
|
|
`|c|&|/log [${ticket.type} Monitor] Recommended: ${res.action}: for ${res.user} (${res.reason})`
|
|
).update();
|
|
ticket.recommended.push(`${res.action}: for ${res.user} (${res.reason})`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
const commands = {
|
|
aht: "autohelpticket",
|
|
autohelpticket: {
|
|
""() {
|
|
return this.parse(`/help autohelpticket`);
|
|
},
|
|
async test(target) {
|
|
checkAccess(this);
|
|
target = target.trim();
|
|
const response = await classifier.classify(target) || {};
|
|
let buf = import_lib.Utils.html`<strong>Results for "${target}":</strong><br />`;
|
|
buf += `<strong>Score breakdown:</strong><br />`;
|
|
for (const k in response) {
|
|
buf += `• ${k}: ${response[k]}<br />`;
|
|
}
|
|
this.runBroadcast();
|
|
this.sendReplyBox(buf);
|
|
},
|
|
ap: "addpunishment",
|
|
add: "addpunishment",
|
|
addpunishment(target, room, user) {
|
|
checkAccess(this);
|
|
if (!toID(target))
|
|
return this.parse(`/help autohelpticket`);
|
|
const args = Chat.parseArguments(target);
|
|
const punishment = {};
|
|
for (const [k, list] of Object.entries(args)) {
|
|
if (k !== "type" && list.length > 1)
|
|
throw new Chat.ErrorMessage(`More than one ${k} param provided.`);
|
|
const val = list[0];
|
|
switch (k) {
|
|
case "type":
|
|
case "t":
|
|
const types = list.map((f) => f.toLowerCase().replace(/\s/g, "_"));
|
|
for (const type2 of types) {
|
|
if (!Artemis.LocalClassifier.ATTRIBUTES[type2]) {
|
|
return this.errorReply(
|
|
`Invalid classifier type '${type2}'. Valid types are ` + Object.keys(Artemis.LocalClassifier.ATTRIBUTES).join(", ")
|
|
);
|
|
}
|
|
}
|
|
if (!punishment.severity) {
|
|
punishment.severity = { certainty: 0, type: [] };
|
|
}
|
|
punishment.severity.type.push(...types);
|
|
break;
|
|
case "certainty":
|
|
case "c":
|
|
const num = parseFloat(val);
|
|
if (isNaN(num) || num < 0 || num > 1) {
|
|
return this.errorReply(`Certainty must be a number below 1 and above 0.`);
|
|
}
|
|
if (!punishment.severity) {
|
|
punishment.severity = { certainty: 0, type: [] };
|
|
}
|
|
punishment.severity.certainty = num;
|
|
break;
|
|
case "modlog":
|
|
case "m":
|
|
const count = parseInt(val);
|
|
if (isNaN(count) || count < 0) {
|
|
return this.errorReply(`Modlog count must be a number above 0.`);
|
|
}
|
|
punishment.modlogCount = count;
|
|
break;
|
|
case "ticket":
|
|
case "tt":
|
|
case "tickettype":
|
|
const type = toID(val);
|
|
if (!(type in checkers)) {
|
|
return this.errorReply(
|
|
`The ticket type '${type}' does not exist or is not supported. Supported types are ${Object.keys(checkers).join(", ")}.`
|
|
);
|
|
}
|
|
punishment.ticketType = type;
|
|
break;
|
|
case "p":
|
|
case "punishment":
|
|
const name = toID(val).toUpperCase();
|
|
if (!ORDERED_PUNISHMENTS.includes(name)) {
|
|
return this.errorReply(
|
|
`Punishment '${name}' not supported. Supported punishments: ${ORDERED_PUNISHMENTS.join(", ")}`
|
|
);
|
|
}
|
|
punishment.punishment = name;
|
|
break;
|
|
case "single":
|
|
case "s":
|
|
if (!this.meansYes(toID(val))) {
|
|
return this.errorReply(
|
|
`The 'single' value must always be 'on'. If you don't want it enabled, just do not use this argument type.`
|
|
);
|
|
}
|
|
punishment.isSingleMessage = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!punishment.ticketType) {
|
|
return this.errorReply(`Must specify a ticket type to handle.`);
|
|
}
|
|
if (!punishment.punishment) {
|
|
return this.errorReply(`Must specify a punishment to apply.`);
|
|
}
|
|
if (!(punishment.severity?.certainty && punishment.severity?.type.length)) {
|
|
return this.errorReply(`A severity to monitor for must be specified (certainty).`);
|
|
}
|
|
for (const curP of settings.punishments) {
|
|
let matches = 0;
|
|
for (const k in curP) {
|
|
if (punishment[k] === curP[k]) {
|
|
matches++;
|
|
}
|
|
}
|
|
if (matches === Object.keys(punishment).length) {
|
|
return this.errorReply(`That punishment is already added.`);
|
|
}
|
|
}
|
|
settings.punishments.push(punishment);
|
|
saveSettings();
|
|
this.privateGlobalModAction(
|
|
`${user.name} added a ${punishment.punishment} punishment to the Artemis helpticket handler.`
|
|
);
|
|
this.globalModlog(`AUTOHELPTICKET ADDPUNISHMENT`, null, visualizePunishment(punishment));
|
|
},
|
|
dp: "deletepunishment",
|
|
delete: "deletepunishment",
|
|
deletepunishment(target, room, user) {
|
|
checkAccess(this);
|
|
const num = parseInt(target) - 1;
|
|
if (isNaN(num))
|
|
return this.parse(`/h autohelpticket`);
|
|
const punishment = settings.punishments[num];
|
|
if (!punishment)
|
|
return this.errorReply(`There is no punishment at index ${num + 1}.`);
|
|
settings.punishments.splice(num, 1);
|
|
this.privateGlobalModAction(
|
|
`${user.name} removed the Artemis helpticket ${punishment.punishment} punishment indexed at ${num + 1}`
|
|
);
|
|
this.globalModlog(`AUTOHELPTICKET REMOVE`, null, visualizePunishment(punishment));
|
|
},
|
|
vp: "viewpunishments",
|
|
view: "viewpunishments",
|
|
viewpunishments() {
|
|
checkAccess(this);
|
|
let buf = `<strong>Artemis helpticket punishments</strong><hr />`;
|
|
if (!settings.punishments.length) {
|
|
buf += `None.`;
|
|
return this.sendReplyBox(buf);
|
|
}
|
|
buf += settings.punishments.map(
|
|
(curP, i) => `<strong>${i + 1}:</strong> ${visualizePunishment(curP)}`
|
|
).join("<br />");
|
|
return this.sendReplyBox(buf);
|
|
},
|
|
togglepunishments(target, room, user) {
|
|
checkAccess(this);
|
|
let message;
|
|
if (this.meansYes(target)) {
|
|
if (settings.applyPunishments) {
|
|
return this.errorReply(`Automatic punishments are already enabled.`);
|
|
}
|
|
settings.applyPunishments = true;
|
|
message = `${user.name} enabled automatic punishments for the Artemis ticket handler`;
|
|
} else if (this.meansNo(target)) {
|
|
if (!settings.applyPunishments) {
|
|
return this.errorReply(`Automatic punishments are already disabled.`);
|
|
}
|
|
settings.applyPunishments = false;
|
|
message = `${user.name} disabled automatic punishments for the Artemis ticket handler`;
|
|
} else {
|
|
return this.errorReply(`Invalid setting. Must be 'on' or 'off'.`);
|
|
}
|
|
this.privateGlobalModAction(message);
|
|
this.globalModlog(`AUTOHELPTICKET TOGGLE`, null, settings.applyPunishments ? "on" : "off");
|
|
saveSettings();
|
|
},
|
|
stats(target) {
|
|
if (!target)
|
|
target = Chat.toTimestamp(new Date()).split(" ")[0];
|
|
return this.parse(`/j view-autohelpticket-stats-${target}`);
|
|
},
|
|
logs(target) {
|
|
if (!target)
|
|
target = Chat.toTimestamp(new Date()).split(" ")[0];
|
|
return this.parse(`/j view-autohelpticket-logs-${target}`);
|
|
},
|
|
resolve(target, room, user) {
|
|
this.checkCan("lock");
|
|
const [ticketId, result] = import_lib.Utils.splitFirst(target, ",").map(toID);
|
|
const ticket = import_helptickets.tickets[ticketId];
|
|
if (!ticket?.open) {
|
|
return this.popupReply(`The user '${ticketId}' does not have a ticket open at present.`);
|
|
}
|
|
if (!["success", "failure"].includes(result)) {
|
|
return this.popupReply(`The result must be 'success' or 'failure'.`);
|
|
}
|
|
(ticket.state || (ticket.state = {})).recommendResult = result;
|
|
(0, import_helptickets.writeTickets)();
|
|
Chat.refreshPageFor(`help-text-${ticketId}`, "staff");
|
|
}
|
|
},
|
|
autohelptickethelp: [
|
|
`/aht addpunishment [args] - Adds a punishment with the given [args]. Requires: whitelist &`,
|
|
`/aht deletepunishment [index] - Deletes the automatic helpticket punishment at [index]. Requires: whitelist &`,
|
|
`/aht viewpunishments - View automatic helpticket punishments. Requires: whitelist &`,
|
|
`/aht togglepunishments [on | off] - Turn [on | off] automatic helpticket punishments. Requires: whitelist &`,
|
|
`/aht stats - View success rates of the Artemis ticket handler. Requires: whitelist &`
|
|
]
|
|
};
|
|
const pages = {
|
|
autohelpticket: {
|
|
async stats(query, user) {
|
|
checkAccess(this);
|
|
let month;
|
|
if (query.length) {
|
|
month = /[0-9]{4}-[0-9]{2}/.exec(query.join("-"))?.[0];
|
|
} else {
|
|
month = Chat.toTimestamp(new Date()).split(" ")[0].slice(0, -3);
|
|
}
|
|
if (!month) {
|
|
return this.errorReply(`Invalid month. Must be in YYYY-MM format.`);
|
|
}
|
|
this.title = `[Artemis Ticket Stats] ${month}`;
|
|
this.setHTML(`<div class="pad"><h3>Artemis ticket stats</h3><hr />Searching...`);
|
|
const found = await import_helptickets.HelpTicket.getTextLogs(["recommendResult"], month);
|
|
const percent = (numerator, denom) => Math.floor(numerator / denom * 100);
|
|
let buf = `<div class="pad">`;
|
|
buf += `<button style="float:right;" class="button" name="send" value="/join ${this.pageid}">`;
|
|
buf += `<i class="fa fa-refresh"></i> Refresh</button>`;
|
|
buf += `<h3>Artemis ticket stats</h3><hr />`;
|
|
const dayStats = {};
|
|
const total = { successes: 0, failures: 0, total: 0 };
|
|
const failed = [];
|
|
for (const ticket of found) {
|
|
const day = Chat.toTimestamp(new Date(ticket.created)).split(" ")[0];
|
|
if (!dayStats[day])
|
|
dayStats[day] = { successes: 0, failures: 0, total: 0 };
|
|
dayStats[day].total++;
|
|
total.total++;
|
|
switch (ticket.state.recommendResult) {
|
|
case "success":
|
|
dayStats[day].successes++;
|
|
total.successes++;
|
|
break;
|
|
case "failure":
|
|
dayStats[day].failures++;
|
|
total.failures++;
|
|
failed.push([ticket.userid, ticket.type]);
|
|
break;
|
|
}
|
|
}
|
|
buf += `<strong>Total:</strong> ${total.total}<br />`;
|
|
buf += `<strong>Success rate:</strong> ${percent(total.successes, total.total)}% (${total.successes})<br />`;
|
|
buf += `<strong>Failure rate:</strong> ${percent(total.failures, total.total)}% (${total.failures})<br />`;
|
|
buf += `<strong>Day stats:</strong><br />`;
|
|
buf += `<div class="ladder pad"><table>`;
|
|
let header = "";
|
|
let data = "";
|
|
const sortedDays = import_lib.Utils.sortBy(Object.keys(dayStats), (d) => new Date(d).getTime());
|
|
for (const [i, day] of sortedDays.entries()) {
|
|
const cur = dayStats[day];
|
|
if (!cur.total)
|
|
continue;
|
|
header += `<th>${day.split("-")[2]} (${cur.total})</th>`;
|
|
data += `<td><small>${cur.successes} (${percent(cur.successes, cur.total)}%)`;
|
|
if (cur.failures) {
|
|
data += ` | ${cur.failures} (${percent(cur.failures, cur.total)}%)`;
|
|
} else {
|
|
data += " | 0 (0%)";
|
|
}
|
|
data += "</small></td>";
|
|
if ((i + 1) % 5 === 0 && sortedDays[i + 1]) {
|
|
buf += `<tr>${header}</tr><tr>${data}</tr>`;
|
|
buf += `</div></table>`;
|
|
buf += `<div class="ladder pad"><table>`;
|
|
header = "";
|
|
data = "";
|
|
}
|
|
}
|
|
buf += `<tr>${header}</tr><tr>${data}</tr>`;
|
|
buf += `</div></table>`;
|
|
buf += `<br />`;
|
|
if (failed.length) {
|
|
buf += `<details class="readmore"><summary>Marked as inaccurate</summary>`;
|
|
buf += failed.map(([userid, type]) => `<a href="/view-help-text-${userid}">${userid}</a> (${type})`).join("<br />");
|
|
buf += `</details>`;
|
|
}
|
|
return buf;
|
|
},
|
|
async logs(query, user) {
|
|
checkAccess(this);
|
|
let month;
|
|
if (query.length) {
|
|
month = /[0-9]{4}-[0-9]{2}/.exec(query.join("-"))?.[0];
|
|
} else {
|
|
month = Chat.toTimestamp(new Date()).split(" ")[0].slice(0, -3);
|
|
}
|
|
if (!month) {
|
|
return this.errorReply(`Invalid month. Must be in YYYY-MM format.`);
|
|
}
|
|
this.title = `[Artemis Ticket Logs]`;
|
|
let buf = `<div class="pad"><h3>Artemis ticket logs</h3><hr />`;
|
|
const allHits = await import_helptickets.HelpTicket.getTextLogs(["recommended"], month);
|
|
import_lib.Utils.sortBy(allHits, (h) => -h.created);
|
|
if (allHits.length) {
|
|
buf += `<strong>All hits:</strong><hr />`;
|
|
for (const hit of allHits) {
|
|
if (!hit.recommended)
|
|
continue;
|
|
buf += `<a href="/view-help-text-${hit.userid}">${hit.userid}</a> (${hit.type}) `;
|
|
buf += `[${Chat.toTimestamp(new Date(hit.created))}]<br />`;
|
|
buf += import_lib.Utils.html`• <code><small>${hit.recommended.join(", ")}</small></code><hr />`;
|
|
}
|
|
} else {
|
|
buf += `<div class="message-error">No hits found.</div>`;
|
|
}
|
|
return buf;
|
|
}
|
|
}
|
|
};
|
|
//# sourceMappingURL=helptickets-auto.js.map
|