mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-04-25 15:40:31 -05:00
Some checks failed
Node.js CI / build (18.x) (push) Has been cancelled
This minimizes side effects of import/require across the codebase, and lets the caller be responsible of initializing child processeses, as well as other async logic, such as restoring saved battles.
570 lines
20 KiB
TypeScript
570 lines
20 KiB
TypeScript
/**
|
|
* Wrapper to facilitate posting / interacting with Smogon.
|
|
* By Mia.
|
|
* @author mia-pi-git
|
|
*/
|
|
import { Net, FS, Utils } from '../../lib';
|
|
|
|
export interface Nomination {
|
|
by: ID;
|
|
ips: string[];
|
|
info: string;
|
|
date: number;
|
|
standing: string;
|
|
alts: string[];
|
|
primaryID: ID;
|
|
claimed?: ID;
|
|
post?: string;
|
|
}
|
|
|
|
interface IPData {
|
|
country: string;
|
|
isp: string;
|
|
city: string;
|
|
regionName: string;
|
|
lat: number;
|
|
lon: number;
|
|
}
|
|
|
|
export function getIPData(ip: string) {
|
|
try {
|
|
return Net("https://miapi.dev/api/ip/" + ip).get().then(JSON.parse) as Promise<IPData>;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export const Smogon = new class {
|
|
async post(threadNum: string, postText: string) {
|
|
if (!Config.smogon) return null;
|
|
try {
|
|
const raw = await Net(`https://www.smogon.com/forums/api/posts`).get({
|
|
method: 'POST',
|
|
body: new URLSearchParams({
|
|
thread_id: threadNum,
|
|
message: postText,
|
|
}).toString(),
|
|
headers: {
|
|
'XF-Api-Key': Config.smogon,
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
});
|
|
// todo return URL of post
|
|
const data = JSON.parse(raw);
|
|
if (data.errors?.length) {
|
|
const errData = data.errors.pop();
|
|
throw new Error(errData.message);
|
|
}
|
|
return data;
|
|
} catch (e: any) {
|
|
if (e.message.includes('Not Found')) {
|
|
// special case to be loud
|
|
throw new Error("WHO DELETED THE PERMA THREAD");
|
|
}
|
|
return { error: e.message };
|
|
}
|
|
}
|
|
};
|
|
|
|
export const Nominations = new class {
|
|
noms: Nomination[] = [];
|
|
icons: Record<string, string> = {};
|
|
constructor() {
|
|
this.load();
|
|
}
|
|
load() {
|
|
try {
|
|
let data = JSON.parse(FS('config/chat-plugins/permas.json').readSync());
|
|
if (Array.isArray(data)) {
|
|
data = { noms: data, icons: {} };
|
|
FS('config/chat-plugins/permas.json').writeSync(JSON.stringify(data));
|
|
}
|
|
this.noms = data.noms;
|
|
this.icons = data.icons;
|
|
} catch {}
|
|
}
|
|
fetchModlog(id: string) {
|
|
return Rooms.Modlog.search('global', {
|
|
user: [{ search: id, isExact: true }],
|
|
note: [],
|
|
ip: [],
|
|
action: [],
|
|
actionTaker: [],
|
|
}, undefined, true);
|
|
}
|
|
save() {
|
|
FS('config/chat-plugins/permas.json').writeUpdate(() => JSON.stringify({ noms: this.noms, icons: this.icons }));
|
|
}
|
|
notifyStaff() {
|
|
const usRoom = Rooms.get('upperstaff');
|
|
if (!usRoom) return;
|
|
usRoom.send(`|uhtml|permanoms|${this.getDisplayButton()}`);
|
|
Chat.refreshPageFor('permalocks', usRoom);
|
|
}
|
|
async add(target: string, connection: Connection) {
|
|
const user = connection.user;
|
|
const [primary, rawAlts, rawIps, type, details] = Utils.splitFirst(target, '|', 4).map(f => f.trim());
|
|
const primaryID = toID(primary);
|
|
const alts = rawAlts.split(',').map(toID).filter(Boolean);
|
|
const ips = rawIps.split(',').map(f => f.trim()).filter(Boolean);
|
|
for (const ip of ips) {
|
|
if (!IPTools.ipRegex.test(ip)) this.error(`Invalid IP: ${ip}`, connection);
|
|
}
|
|
const standings = this.getStandings();
|
|
if (!standings[type]) {
|
|
this.error(`Invalid standing: ${type}.`, connection);
|
|
}
|
|
if (!details) {
|
|
this.error("Details must be provided. Explain why this user should be permalocked.", connection);
|
|
}
|
|
if (!primaryID) {
|
|
this.error("A primary username must be provided. Use one of their alts if necessary.", connection);
|
|
}
|
|
for (const nom of this.noms) {
|
|
if (nom.primaryID === primaryID) {
|
|
this.error(`'${primaryID}' was already nominated for permalock by ${nom.by}.`, connection);
|
|
}
|
|
}
|
|
const ipTable = new Set<string>(ips);
|
|
const altTable = new Set<string>(alts);
|
|
for (const alt of [primaryID, ...alts]) {
|
|
const modlog = await this.fetchModlog(alt);
|
|
if (!modlog?.results.length) continue;
|
|
for (const entry of modlog.results) {
|
|
if (entry.ip) ipTable.add(entry.ip);
|
|
if (entry.autoconfirmedID) altTable.add(entry.autoconfirmedID);
|
|
if (entry.alts) {
|
|
for (const id of entry.alts) altTable.add(id);
|
|
}
|
|
}
|
|
}
|
|
altTable.delete(primaryID);
|
|
this.noms.push({
|
|
by: user.id,
|
|
alts: [...altTable],
|
|
ips: Utils.sortBy([...ipTable], z => -(IPTools.ipToNumber(z) || Infinity)),
|
|
info: details,
|
|
primaryID,
|
|
standing: type,
|
|
date: Date.now(),
|
|
});
|
|
Utils.sortBy(this.noms, nom => -nom.date);
|
|
this.save();
|
|
this.notifyStaff();
|
|
Rooms.get('staff')?.addByUser(user, `${user.name} submitted a perma nomination for ${primaryID}`).update();
|
|
}
|
|
find(id: string) {
|
|
return this.noms.find(f => f.primaryID === id);
|
|
}
|
|
error(message: string, conn: Connection): never {
|
|
conn.popup(message);
|
|
throw new Chat.Interruption();
|
|
}
|
|
close(target: string, context: Chat.CommandContext) {
|
|
const entry = this.find(target);
|
|
if (!entry) {
|
|
this.error(`There is no nomination pending for '${toID(target)}'.`, context.connection);
|
|
}
|
|
this.noms.splice(this.noms.findIndex(f => f.primaryID === entry.primaryID), 1);
|
|
this.save();
|
|
this.notifyStaff();
|
|
// todo fix when on good comp
|
|
return context.closePage(`permalocks-view-${entry.primaryID}`);
|
|
}
|
|
display(nom: Nomination, canEdit?: boolean) {
|
|
let buf = `<div class="infobox">`;
|
|
let title = nom.primaryID as string;
|
|
if (canEdit) {
|
|
title = `<a href="/view-permalocks-view-${nom.primaryID}" target="_replace">${nom.primaryID}</a>`;
|
|
}
|
|
buf += `<strong>${title}</strong> (submitted by ${nom.by})<br />`;
|
|
buf += `Submitted ${Chat.toTimestamp(new Date(nom.date), { human: true })}<br />`;
|
|
buf += `${Chat.count(nom.alts, 'alts')}, ${Chat.count(nom.ips, 'IPs')}`;
|
|
buf += `</div>`;
|
|
return buf;
|
|
}
|
|
displayModlog(results: import('../modlog').ModlogEntry[] | null) {
|
|
if (!results) return '';
|
|
let curDate = '';
|
|
return results.map(result => {
|
|
const date = new Date(result.time || Date.now());
|
|
const entryRoom = result.visualRoomID || result.roomID || 'global';
|
|
let [dateString, timestamp] = Chat.toTimestamp(date, { human: true }).split(' ');
|
|
let line = `<small>[${timestamp}] (${entryRoom})</small> ${result.action}`;
|
|
if (result.userid) {
|
|
line += `: [${result.userid}]`;
|
|
if (result.autoconfirmedID) line += ` ac: [${result.autoconfirmedID}]`;
|
|
if (result.alts.length) line += ` alts: [${result.alts.join('], [')}]`;
|
|
if (result.ip) line += ` [<a href="https://whatismyipaddress.com/ip/${result.ip}" target="_blank">${result.ip}</a>]`;
|
|
}
|
|
|
|
if (result.loggedBy) line += `: by ${result.loggedBy}`;
|
|
if (result.note) line += Utils.html`: ${result.note}`;
|
|
|
|
if (dateString !== curDate) {
|
|
curDate = dateString;
|
|
dateString = `</p><p>[${dateString}]<br />`;
|
|
} else {
|
|
dateString = ``;
|
|
}
|
|
const thisRoomID = entryRoom?.split(' ')[0];
|
|
if (thisRoomID.startsWith('battle-')) {
|
|
timestamp = `<a href="/${thisRoomID}">${timestamp}</a>`;
|
|
} else {
|
|
const [day, time] = Chat.toTimestamp(date).split(' ');
|
|
timestamp = `<a href="/view-chatlog-${thisRoomID}--${day}--time-${toID(time)}">${timestamp}</a>`;
|
|
}
|
|
return `${dateString}${line}`;
|
|
}).join(`<br />`);
|
|
}
|
|
async displayActionPage(nom: Nomination) {
|
|
let buf = `<div class="pad">`;
|
|
const standings = this.getStandings();
|
|
buf += `<button class="button" name="send" value="/perma viewnom ${nom.primaryID}" style="float:right">`;
|
|
buf += `<i class="fa fa-refresh"></i> Refresh</button>`;
|
|
buf += `<h3>Nomination: ${nom.primaryID}</h3><hr />`;
|
|
buf += `<strong>By:</strong> ${nom.by} (on ${Chat.toTimestamp(new Date(nom.date))})<br />`;
|
|
buf += `<strong>Recommended punishment:</strong> ${standings[nom.standing]}<br />`;
|
|
buf += `<details class="readmore"><summary><strong>Modlog</strong></summary>`;
|
|
buf += `<div class="infobox limited">`;
|
|
const modlog = await this.fetchModlog(nom.primaryID);
|
|
if (!modlog) {
|
|
buf += `None found.`;
|
|
} else {
|
|
buf += this.displayModlog(modlog.results);
|
|
}
|
|
buf += `</div></details>`;
|
|
if (nom.alts.length) {
|
|
buf += `<details class="readmore"><summary><strong>Listed alts</strong></summary>`;
|
|
for (const [i, alt] of nom.alts.entries()) {
|
|
buf += `- ${alt}: `;
|
|
buf += `<form data-submitsend="/perma standing ${alt},{standing},{reason}">`;
|
|
buf += this.standingDropdown("standing");
|
|
buf += ` <button class="button notifying" type="submit">Change standing</button>`;
|
|
buf += ` <input name="reason" placeholder="Reason" />`;
|
|
buf += `</form>`;
|
|
if (nom.alts[i + 1]) buf += `<br />`;
|
|
}
|
|
buf += `</details>`;
|
|
}
|
|
if (nom.ips.length) {
|
|
buf += `<details class="readmore"><summary><strong>Listed IPs</strong></summary>`;
|
|
for (const [i, ip] of nom.ips.entries()) {
|
|
const ipData = await getIPData(ip);
|
|
buf += `- <a href="https://whatismyipaddress.com/ip/${ip}">${ip}</a>`;
|
|
if (ipData) {
|
|
buf += `(ISP: ${ipData.isp}, loc: ${ipData.city}, ${ipData.regionName} in ${ipData.country})`;
|
|
}
|
|
buf += `: `;
|
|
buf += `<form data-submitsend="/perma ipstanding ${ip},{standing},{reason}">`;
|
|
buf += this.standingDropdown("standing");
|
|
buf += ` <button class="button notifying" type="submit">Change standing for all users on IP</button>`;
|
|
buf += ` <input name="reason" placeholder="Reason" />`;
|
|
buf += `</form>`;
|
|
if (nom.ips[i + 1]) buf += `<br />`;
|
|
}
|
|
buf += `</details>`;
|
|
}
|
|
const [matches] = await LoginServer.request('ipmatches', {
|
|
id: nom.primaryID,
|
|
});
|
|
if (matches?.results?.length) {
|
|
buf += `<details class="readmore"><summary><strong>Registration IP matches</strong></summary>`;
|
|
for (const [i, { userid, banstate }] of matches.results.entries()) {
|
|
buf += `- ${userid}: `;
|
|
buf += `<form data-submitsend="/perma standing ${userid},{standing}">`;
|
|
buf += this.standingDropdown("standing", `${banstate}`);
|
|
buf += ` <button class="button notifying" type="submit">Change standing</button></form>`;
|
|
if (matches.results[i + 1]) buf += `<br />`;
|
|
}
|
|
buf += `</details>`;
|
|
}
|
|
buf += `<p><strong>Staff notes:</strong></p>`;
|
|
buf += `<p><div class="infobox">${Chat.formatText(nom.info).replace(/\n/ig, '<br />')}</div></p>`;
|
|
buf += `<details class="readmore"><summary><strong>Act on primary:</strong></summary>`;
|
|
buf += `<form data-submitsend="/perma actmain ${nom.primaryID},{standing},{note}">`;
|
|
buf += `Standing: ${this.standingDropdown('standing')}`;
|
|
buf += `<br />Notes:<br />`;
|
|
buf += `<textarea name="note" style="width: 100%" cols="50" rows="10"></textarea><br />`;
|
|
buf += `<button class="button notifying" type="submit">Change standing and make post</button>`;
|
|
buf += `</form></details><br />`;
|
|
buf += `<button class="button notifying" name="send" value="/perma resolve ${nom.primaryID}">Mark resolved</button>`;
|
|
return buf;
|
|
}
|
|
standingDropdown(elemName: string, curStanding: string | null = null) {
|
|
let buf = `<select name="${elemName}">`;
|
|
const standings = this.getStandings();
|
|
for (const k in standings) {
|
|
buf += `<option ${curStanding === k ? "disabled" : ""} value="${k}">${standings[k]}</option>`;
|
|
}
|
|
buf += `</select>`;
|
|
return buf;
|
|
}
|
|
getStandings() {
|
|
if (Config.standings) return Config.standings;
|
|
Config.standings = {
|
|
'-20': "Confirmed",
|
|
'-10': "Autoconfirmed",
|
|
'0': "New",
|
|
"20": "Permalock",
|
|
"30": "Permaban",
|
|
"100": "Disabled",
|
|
};
|
|
return Config.standings;
|
|
}
|
|
displayAll(canEdit: boolean) {
|
|
let buf = `<div class="pad">`;
|
|
buf += `<button class="button" name="send" value="/perma noms" style="float:right"><i class="fa fa-refresh"></i> Refresh</button>`;
|
|
buf += `<h3>Pending perma nominations</h3><hr />`;
|
|
if (!this.noms.length) {
|
|
buf += `None found.`;
|
|
return buf;
|
|
}
|
|
for (const nom of this.noms) {
|
|
buf += this.display(nom, canEdit);
|
|
buf += `<br />`;
|
|
}
|
|
return buf;
|
|
}
|
|
displayNomPage() {
|
|
let buf = `<div class="pad"><h3>Make a nomination for a permanent punishment.</h3><hr />`;
|
|
// const [primary, rawAlts, rawIps, details] = Utils.splitFirst(target, '|', 3).map(f => f.trim());
|
|
buf += `<form data-submitsend="/perma submit {primary}|{alts}|{ips}|{type}|{details}">`;
|
|
buf += `<div class="infobox">`;
|
|
buf += `<strong>Primary userid:</strong> <input name="primary" /><br />`;
|
|
buf += `<strong>Alts:</strong><br /><textarea name="alts"></textarea><br /><small>(Separated by commas)</small><br />`;
|
|
buf += `<strong>Static IPs:</strong><br /><textarea name="ips"></textarea><br /><small>(Separated by commas)</small></div><br />`;
|
|
buf += `<strong>Punishment:</strong> `;
|
|
buf += `<select name="type"><option value="20">Permalock</option><option value="30">Permaban</option></select>`;
|
|
buf += `<div class="infobox">`;
|
|
buf += `<strong>Please explain why this user deserves a permanent punishment</strong><br />`;
|
|
buf += `<small>Note: Modlogs are automatically included in review and do not need to be added here.</small><br />`;
|
|
buf += `<textarea style="width: 100%" name="details" cols="50" rows="10"></textarea></div>`;
|
|
buf += `<button class="button notifying" type="submit">Submit nomination</button>`;
|
|
return buf;
|
|
}
|
|
getDisplayButton() {
|
|
const unclaimed = this.noms.filter(f => !f.claimed);
|
|
let buf = `<div class="infobox">`;
|
|
if (!this.noms.length) {
|
|
buf += `No permalock nominations active.`;
|
|
} else {
|
|
let className = 'button';
|
|
if (unclaimed.length) className += ' notifying';
|
|
buf += `<button class="${className}" name="send" value="/j view-permalocks-list">`;
|
|
buf += `${Chat.count(this.noms.length, 'nominations')}`;
|
|
if (unclaimed.length !== this.noms.length) {
|
|
buf += ` (${unclaimed.length} unclaimed)`;
|
|
}
|
|
buf += `</button>`;
|
|
}
|
|
buf += `</div>`;
|
|
return buf;
|
|
}
|
|
};
|
|
|
|
export const commands: Chat.ChatCommands = {
|
|
perma: {
|
|
''(target, room, user) {
|
|
this.checkCan('lock');
|
|
if (!user.can('rangeban')) {
|
|
return this.parse(`/j view-permalocks-submit`);
|
|
} else {
|
|
return this.parse(`/j view-permalocks-list`);
|
|
}
|
|
},
|
|
viewnom(target) {
|
|
this.checkCan('rangeban');
|
|
return this.parse(`/j view-permalocks-view-${toID(target)}`);
|
|
},
|
|
submit(target, room, user) {
|
|
this.checkCan('lock');
|
|
return Nominations.add(target, this.connection);
|
|
},
|
|
list() {
|
|
this.checkCan('lock');
|
|
return this.parse(`/j view-permalocks-list`);
|
|
},
|
|
nom() {
|
|
this.checkCan('lock');
|
|
return this.parse(`/j view-permalocks-submit`);
|
|
},
|
|
async actmain(target, room, user) {
|
|
this.checkCan('rangeban');
|
|
const [primaryName, standingName, postReason] = Utils.splitFirst(target, ',', 2).map(f => f.trim());
|
|
const primary = toID(primaryName);
|
|
if (!primary) return this.popupReply(`Invalid primary username.`);
|
|
const nom = Nominations.find(primary);
|
|
if (!nom) return this.popupReply(`No permalock nomination found for ${primary}.`);
|
|
const standing = parseInt(standingName);
|
|
const standings = Nominations.getStandings();
|
|
if (!standings[standing]) return this.popupReply(`Invalid standing.`);
|
|
if (!toID(postReason)) return this.popupReply(`A reason must be given.`);
|
|
// todo thread num
|
|
const threadNum = Config.permathread;
|
|
if (!threadNum) {
|
|
throw new Chat.ErrorMessage("The link to the perma has not been set - the post could not be made.");
|
|
}
|
|
let postBuf = `[b][url="https://${Config.routes.root}/users/${primary}"]${primary}[/url][/b]`;
|
|
const icon = Nominations.icons[user.id] ? `:${Nominations.icons[user.id]}: - ` : ``;
|
|
postBuf += ` was added to ${standings[standing]} by ${user.name} (${icon}${postReason}).\n`;
|
|
postBuf += `Nominated by ${nom.by}.\n[spoiler=Nomination notes]${nom.info}[/spoiler]\n`;
|
|
postBuf += `${nom.alts.length ? `[spoiler=Alts]${nom.alts.join(', ')}[/spoiler]` : ""}\n`;
|
|
if (nom.ips.length) {
|
|
postBuf += `[spoiler=IPs]`;
|
|
for (const ip of nom.ips) {
|
|
const ipData = await getIPData(ip);
|
|
postBuf += `- [url=https://whatismyipaddress.com/ip/${ip}]${ip}[/url]`;
|
|
if (ipData) {
|
|
postBuf += ` (ISP: ${ipData.isp}, loc: ${ipData.city}, ${ipData.regionName} in ${ipData.country})`;
|
|
}
|
|
postBuf += '\n';
|
|
}
|
|
postBuf += `[/spoiler]`;
|
|
}
|
|
|
|
const modlog = await Nominations.fetchModlog(nom.primaryID);
|
|
if (modlog?.results.length) {
|
|
let rawHTML = Nominations.displayModlog(modlog.results);
|
|
rawHTML = rawHTML.replace(/<br \/>/g, '\n');
|
|
rawHTML = Utils.stripHTML(rawHTML);
|
|
rawHTML = rawHTML.replace(///g, '/');
|
|
postBuf += `\n[spoiler=Modlog]${rawHTML}[/spoiler]`;
|
|
}
|
|
|
|
const res = await Smogon.post(
|
|
threadNum,
|
|
postBuf,
|
|
);
|
|
if (!res || res.error) {
|
|
return this.popupReply(`Error making post: ${res?.error}`);
|
|
}
|
|
const url = `https://smogon.com/forums/threads/${threadNum}/post-${res.post.post_id}`;
|
|
const result = await LoginServer.request('setstanding', {
|
|
user: primary,
|
|
standing,
|
|
reason: url,
|
|
actor: user.id,
|
|
});
|
|
if (result[1]) {
|
|
return this.popupReply(`Error changing standing: ${result[1].message}`);
|
|
}
|
|
nom.post = url;
|
|
this.popupReply(`|html|Standing successfully changed. Smogon post can be found <a href="${url}">at this link</a>.`);
|
|
},
|
|
async standing(target) {
|
|
this.checkCan('rangeban');
|
|
const [name, rawStanding, reason] = Utils.splitFirst(target, ',', 2).map(f => f.trim());
|
|
const id = toID(name);
|
|
if (!id || id.length > 18) {
|
|
return this.popupReply('Invalid username: ' + name);
|
|
}
|
|
const standingNum = parseInt(rawStanding);
|
|
if (!standingNum) {
|
|
return this.popupReply(`Invalid standing: ` + rawStanding);
|
|
}
|
|
if (!reason.length) {
|
|
return this.popupReply(`A reason must be given.`);
|
|
}
|
|
const res = await LoginServer.request('setstanding', {
|
|
user: id,
|
|
standing: standingNum,
|
|
reason,
|
|
actor: this.user.id,
|
|
});
|
|
if (res[1]) {
|
|
return this.popupReply(`Error in standing change: ` + res[1].message);
|
|
}
|
|
this.popupReply(`Standing successfully changed to ${standingNum} for ${id}.`);
|
|
// no need to modlog, is in usermodlog already
|
|
},
|
|
async ipstanding(target) {
|
|
this.checkCan('rangeban');
|
|
const [ip, standingName, reason] = Utils.splitFirst(target, ',', 2).map(f => f.trim());
|
|
if (!IPTools.ipToNumber(ip)) {
|
|
return this.popupReply(`Invalid IP: ${ip}`);
|
|
}
|
|
const standingNum = parseInt(standingName);
|
|
if (!Config.standings[`${standingNum}`]) {
|
|
return this.popupReply(`Invalid standing: ${standingName}.`);
|
|
}
|
|
if (!reason.length) {
|
|
return this.popupReply('Specify a reason.');
|
|
}
|
|
const res = await LoginServer.request('ipstanding', {
|
|
reason,
|
|
standing: standingNum,
|
|
ip,
|
|
actor: this.user.id,
|
|
});
|
|
if (res[1]) {
|
|
return this.popupReply(`Error changing standing: ${res[1].message}`);
|
|
}
|
|
this.popupReply(`All standings on the IP ${ip} changed successfully to ${standingNum}.`);
|
|
this.globalModlog(`IPSTANDING`, null, `${standingNum}${reason ? ` (${reason})` : ""}`, ip);
|
|
},
|
|
resolve(target) {
|
|
this.checkCan('rangeban');
|
|
Nominations.close(target, this);
|
|
},
|
|
seticon(target, room, user) {
|
|
this.checkCan('rangeban');
|
|
let [monName, targetId] = target.split(',');
|
|
if (!targetId) targetId = user.id;
|
|
const mon = Dex.species.get(monName);
|
|
if (!mon.exists) {
|
|
throw new Chat.ErrorMessage(`Species ${monName} does not exist.`);
|
|
}
|
|
Nominations.icons[targetId] = mon.name.toLowerCase();
|
|
Nominations.save();
|
|
this.sendReply(
|
|
`|html|Updated ${targetId === user.id ? 'your' : `${targetId}'s`} permalock post icon to ` +
|
|
`<psicon pokemon='${mon.name.toLowerCase()}' />`
|
|
);
|
|
},
|
|
deleteicon(target, room, user) {
|
|
this.checkCan('rangeban');
|
|
const targetID = toID(target);
|
|
if (!Nominations.icons[targetID]) {
|
|
throw new Chat.ErrorMessage(`${targetID} does not have an icon set.`);
|
|
}
|
|
delete Nominations.icons[targetID];
|
|
Nominations.save();
|
|
this.sendReply(`Removed ${targetID}'s permalock post icon.`);
|
|
},
|
|
help: [
|
|
'/perma nom OR /perma - Open the page to make a nomination for a permanent punishment. Requires: % @ ~',
|
|
'/perma list - View open nominations. Requires: % @ ~',
|
|
'/perma viewnom [userid] - View a nomination for the given [userid]. Requires: ~',
|
|
],
|
|
},
|
|
};
|
|
|
|
export const pages: Chat.PageTable = {
|
|
permalocks: {
|
|
list(query, user, conn) {
|
|
this.checkCan('lock');
|
|
this.title = '[Permalock Nominations]';
|
|
return Nominations.displayAll(user.can('rangeban'));
|
|
},
|
|
view(query, user) {
|
|
this.checkCan('rangeban');
|
|
const id = toID(query.shift());
|
|
if (!id) throw new Chat.ErrorMessage(`Invalid userid.`);
|
|
const nom = Nominations.find(id);
|
|
if (!nom) throw new Chat.ErrorMessage(`No nomination found for '${id}'.`);
|
|
this.title = `[Perma Nom] ${nom.primaryID}`;
|
|
return Nominations.displayActionPage(nom);
|
|
},
|
|
submit() {
|
|
this.checkCan('lock');
|
|
this.title = '[Perma Nom] Create';
|
|
return Nominations.displayNomPage();
|
|
},
|
|
},
|
|
};
|
|
|
|
export function start() {
|
|
Chat.multiLinePattern.register('/perma(noms?)? ');
|
|
}
|