mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-04-26 02:39:38 -05:00
1397 lines
60 KiB
TypeScript
1397 lines
60 KiB
TypeScript
import {FS} from '../../lib/fs';
|
|
import {Utils} from '../../lib/utils';
|
|
import {getCommonBattles} from '../chat-commands/info';
|
|
import type {Punishment} from '../punishments';
|
|
|
|
const TICKET_FILE = 'config/tickets.json';
|
|
const TICKET_CACHE_TIME = 24 * 60 * 60 * 1000; // 24 hours
|
|
const TICKET_BAN_DURATION = 48 * 60 * 60 * 1000; // 48 hours
|
|
|
|
Punishments.roomPunishmentTypes.set(`TICKETBAN`, 'banned from creating help tickets');
|
|
|
|
interface TicketState {
|
|
creator: string;
|
|
userid: ID;
|
|
open: boolean;
|
|
active: boolean;
|
|
type: string;
|
|
created: number;
|
|
claimed: string | null;
|
|
ip: string;
|
|
}
|
|
type TicketResult = 'approved' | 'valid' | 'assisted' | 'denied' | 'invalid' | 'unassisted' | 'ticketban' | 'deleted';
|
|
|
|
const tickets: {[k: string]: TicketState} = {};
|
|
|
|
try {
|
|
const ticketData = JSON.parse(FS(TICKET_FILE).readSync());
|
|
for (const t in ticketData) {
|
|
const ticket = ticketData[t];
|
|
if (ticket.banned) {
|
|
if (ticket.expires && ticket.expires <= Date.now()) continue;
|
|
Punishments.roomPunish(`staff`, ticket.userid, ['TICKETBAN', ticket.userid, ticket.expires, ticket.reason]);
|
|
delete ticketData[t]; // delete the old format
|
|
} else {
|
|
if (ticket.created + TICKET_CACHE_TIME <= Date.now()) {
|
|
// Tickets that have been open for 24+ hours will be automatically closed.
|
|
const ticketRoom = Rooms.get(`help-${ticket.userid}`) as ChatRoom | null;
|
|
if (ticketRoom) {
|
|
const ticketGame = ticketRoom.game as HelpTicket;
|
|
ticketGame.writeStats(false);
|
|
ticketRoom.expire();
|
|
}
|
|
continue;
|
|
}
|
|
// Close open tickets after a restart
|
|
if (ticket.open && !Chat.oldPlugins.helptickets) ticket.open = false;
|
|
tickets[t] = ticket;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
if (e.code !== 'ENOENT') throw e;
|
|
}
|
|
|
|
function writeTickets() {
|
|
FS(TICKET_FILE).writeUpdate(() => (
|
|
JSON.stringify(Object.assign({}, tickets))
|
|
));
|
|
}
|
|
|
|
function writeStats(line: string) {
|
|
// ticketType\ttotalTime\ttimeToFirstClaim\tinactiveTime\tresolution\tresult\tstaff,userids,seperated,with,commas
|
|
const date = new Date();
|
|
const month = Chat.toTimestamp(date).split(' ')[0].split('-', 2).join('-');
|
|
try {
|
|
FS(`logs/tickets/${month}.tsv`).appendSync(line + '\n');
|
|
} catch (e) {
|
|
if (e.code !== 'ENOENT') throw e;
|
|
}
|
|
}
|
|
|
|
export class HelpTicket extends Rooms.RoomGame {
|
|
room: ChatRoom;
|
|
ticket: TicketState;
|
|
claimQueue: string[];
|
|
involvedStaff: Set<ID>;
|
|
createTime: number;
|
|
activationTime: number;
|
|
emptyRoom: boolean;
|
|
firstClaimTime: number;
|
|
unclaimedTime: number;
|
|
lastUnclaimedStart: number;
|
|
closeTime: number;
|
|
resolution: 'unknown' | 'dead' | 'unresolved' | 'resolved';
|
|
result: TicketResult | null;
|
|
|
|
constructor(room: ChatRoom, ticket: TicketState) {
|
|
super(room);
|
|
this.room = room;
|
|
this.room.settings.language = Users.get(ticket.creator)?.language || 'english' as ID;
|
|
this.title = `Help Ticket - ${ticket.type}`;
|
|
this.gameid = "helpticket" as ID;
|
|
this.allowRenames = true;
|
|
this.ticket = ticket;
|
|
this.claimQueue = [];
|
|
|
|
/* Stats */
|
|
this.involvedStaff = new Set();
|
|
this.createTime = Date.now();
|
|
this.activationTime = (ticket.active ? this.createTime : 0);
|
|
this.emptyRoom = false;
|
|
this.firstClaimTime = 0;
|
|
this.unclaimedTime = 0;
|
|
this.lastUnclaimedStart = (ticket.active ? this.createTime : 0);
|
|
this.closeTime = 0;
|
|
this.resolution = 'unknown';
|
|
this.result = null;
|
|
}
|
|
|
|
onJoin(user: User, connection: Connection) {
|
|
if (!this.ticket.open) return false;
|
|
if (!user.isStaff || user.id === this.ticket.userid) {
|
|
if (this.emptyRoom) this.emptyRoom = false;
|
|
this.addPlayer(user);
|
|
return false;
|
|
}
|
|
if (!this.ticket.claimed) {
|
|
this.ticket.claimed = user.name;
|
|
if (!this.firstClaimTime) {
|
|
this.firstClaimTime = Date.now();
|
|
// I'd use the player list for this, but it dosen't track DCs so were checking the userlist
|
|
// Non-staff users in the room currently (+ the ticket creator even if they are staff)
|
|
const users = Object.entries(this.room.users).filter(u => {
|
|
return !((u[1].isStaff && u[1].id !== this.ticket.userid) || !u[1].named);
|
|
});
|
|
if (!users.length) this.emptyRoom = true;
|
|
}
|
|
if (this.ticket.active) {
|
|
this.unclaimedTime += Date.now() - this.lastUnclaimedStart;
|
|
this.lastUnclaimedStart = 0; // Set back to 0 so we know that it was active when closed
|
|
}
|
|
tickets[this.ticket.userid] = this.ticket;
|
|
writeTickets();
|
|
this.room.modlog({action: 'TICKETCLAIM', isGlobal: true, loggedBy: user.id});
|
|
this.addText(`${user.name} claimed this ticket.`, user);
|
|
notifyStaff();
|
|
} else {
|
|
this.claimQueue.push(user.name);
|
|
}
|
|
}
|
|
|
|
onLeave(user: User, oldUserid: ID) {
|
|
const player = this.playerTable[oldUserid || user.id];
|
|
if (player) {
|
|
this.removePlayer(player);
|
|
return;
|
|
}
|
|
if (!this.ticket.open) return;
|
|
if (toID(this.ticket.claimed) === user.id) {
|
|
if (this.claimQueue.length) {
|
|
this.ticket.claimed = this.claimQueue.shift() || null;
|
|
this.room.modlog({action: 'TICKETCLAIM', isGlobal: true, loggedBy: toID(this.ticket.claimed)});
|
|
this.addText(`This ticket is now claimed by ${this.ticket.claimed}.`, user);
|
|
} else {
|
|
const oldClaimed = this.ticket.claimed;
|
|
this.ticket.claimed = null;
|
|
this.lastUnclaimedStart = Date.now();
|
|
this.room.modlog({action: 'TICKETUNCLAIM', isGlobal: true, loggedBy: toID(oldClaimed)});
|
|
this.addText(`This ticket is no longer claimed.`, user);
|
|
notifyStaff();
|
|
}
|
|
tickets[this.ticket.userid] = this.ticket;
|
|
writeTickets();
|
|
} else {
|
|
const index = this.claimQueue.map(toID).indexOf(user.id);
|
|
if (index > -1) this.claimQueue.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
onLogMessage(message: string, user: User) {
|
|
if (!this.ticket.open) return;
|
|
if (user.isStaff && this.ticket.userid !== user.id) this.involvedStaff.add(user.id);
|
|
if (this.ticket.active) return;
|
|
const blockedMessages = [
|
|
'hi', 'hello', 'hullo', 'hey', 'yo', 'ok',
|
|
'hesrude', 'shesrude', 'hesinappropriate', 'shesinappropriate', 'heswore', 'sheswore',
|
|
'help', 'yes',
|
|
];
|
|
if ((!user.isStaff || this.ticket.userid === user.id) && blockedMessages.includes(toID(message))) {
|
|
this.room.add(`|c|&Staff|${this.room.tr`Hello! The global staff team would be happy to help you, but you need to explain what's going on first.`}`);
|
|
this.room.add(`|c|&Staff|${this.room.tr`Please post the information I requested above so a global staff member can come to help.`}`);
|
|
this.room.update();
|
|
return false;
|
|
}
|
|
if ((!user.isStaff || this.ticket.userid === user.id) && !this.ticket.active) {
|
|
this.ticket.active = true;
|
|
this.activationTime = Date.now();
|
|
if (!this.ticket.claimed) this.lastUnclaimedStart = Date.now();
|
|
notifyStaff();
|
|
this.room.add(`|c|&Staff|${this.room.tr`Thank you for the information, global staff will be here shortly. Please stay in the room.`}`).update();
|
|
}
|
|
}
|
|
|
|
forfeit(user: User) {
|
|
if (!(user.id in this.playerTable)) return;
|
|
this.removePlayer(user);
|
|
if (!this.ticket.open) return;
|
|
this.room.modlog({action: 'TICKETABANDON', isGlobal: true, loggedBy: user.id});
|
|
this.addText(`${user.name} is no longer interested in this ticket.`, user);
|
|
if (this.playerCount - 1 > 0) return; // There are still users in the ticket room, dont close the ticket
|
|
this.close(!!(this.firstClaimTime), user);
|
|
return true;
|
|
}
|
|
|
|
addText(text: string, user?: User) {
|
|
if (user) {
|
|
this.room.addByUser(user, text);
|
|
} else {
|
|
this.room.add(text);
|
|
}
|
|
this.room.update();
|
|
}
|
|
|
|
getButton() {
|
|
const notifying = this.ticket.claimed ? `` : `notifying`;
|
|
const creator = (
|
|
this.ticket.claimed ? Utils.html`${this.ticket.creator}` : Utils.html`<strong>${this.ticket.creator}</strong>`
|
|
);
|
|
return (
|
|
`<a class="button ${notifying}" href="/help-${this.ticket.userid}"` +
|
|
` ${this.getPreview()}>Help ${creator}: ${this.ticket.type}</a> `
|
|
);
|
|
}
|
|
|
|
getPreview() {
|
|
if (!this.ticket.active) return `title="The ticket creator has not spoken yet."`;
|
|
const hoverText = [];
|
|
for (let i = this.room.log.log.length - 1; i >= 0; i--) {
|
|
// Don't show anything after the first linebreak for multiline messages
|
|
const entry = this.room.log.log[i].split('\n')[0].split('|');
|
|
entry.shift(); // Remove empty string
|
|
if (!/c:?/.test(entry[0])) continue;
|
|
if (entry[0] === 'c:') entry.shift(); // c: includes a timestamp and needs an extra shift
|
|
entry.shift();
|
|
const user = entry.shift();
|
|
let message = entry.join('|');
|
|
message = message.startsWith('/log ') ? message.slice(5) : `${user}: ${message}`;
|
|
hoverText.push(Utils.html`${message}`);
|
|
if (hoverText.length >= 3) break;
|
|
}
|
|
if (!hoverText.length) return `title="The ticket creator has not spoken yet."`;
|
|
return `title="${hoverText.reverse().join(` `)}"`;
|
|
}
|
|
|
|
close(result: boolean | 'ticketban' | 'deleted', staff?: User) {
|
|
this.ticket.open = false;
|
|
tickets[this.ticket.userid] = this.ticket;
|
|
writeTickets();
|
|
this.room.modlog({action: 'TICKETCLOSE', isGlobal: true, loggedBy: staff?.id || 'unknown' as ID});
|
|
this.addText(staff ? `${staff.name} closed this ticket.` : `This ticket was closed.`, staff);
|
|
notifyStaff();
|
|
this.room.pokeExpireTimer();
|
|
for (const ticketGameUser of Object.values(this.playerTable)) {
|
|
this.removePlayer(ticketGameUser);
|
|
const user = Users.get(ticketGameUser.id);
|
|
if (user) user.updateSearch();
|
|
}
|
|
if (!this.involvedStaff.size) {
|
|
if (staff?.isStaff && staff.id !== this.ticket.userid) {
|
|
this.involvedStaff.add(staff.id);
|
|
} else {
|
|
this.involvedStaff.add(toID(this.ticket.claimed));
|
|
}
|
|
}
|
|
this.writeStats(result);
|
|
}
|
|
|
|
writeStats(result: boolean | 'ticketban' | 'deleted') {
|
|
// Only run when a ticket is closed/banned/deleted
|
|
this.closeTime = Date.now();
|
|
if (this.lastUnclaimedStart) this.unclaimedTime += this.closeTime - this.lastUnclaimedStart;
|
|
if (!this.ticket.active) {
|
|
this.resolution = "dead";
|
|
} else if (!this.firstClaimTime || this.emptyRoom) {
|
|
this.resolution = "unresolved";
|
|
} else {
|
|
this.resolution = "resolved";
|
|
}
|
|
if (typeof result === 'boolean') {
|
|
switch (this.ticket.type) {
|
|
case 'Appeal':
|
|
case 'IP-Appeal':
|
|
case 'ISP-Appeal':
|
|
this.result = (result ? 'approved' : 'denied');
|
|
break;
|
|
case 'PM Harassment':
|
|
case 'Battle Harassment':
|
|
case 'Inappropriate Username':
|
|
case 'Inappropriate Pokemon Nicknames':
|
|
this.result = (result ? 'valid' : 'invalid');
|
|
break;
|
|
case 'Public Room Assistance Request':
|
|
case 'Other':
|
|
default:
|
|
this.result = (result ? 'assisted' : 'unassisted');
|
|
break;
|
|
}
|
|
} else {
|
|
this.result = result;
|
|
}
|
|
let firstClaimWait = 0;
|
|
let involvedStaff = '';
|
|
if (this.activationTime) {
|
|
firstClaimWait = (this.firstClaimTime ? this.firstClaimTime : this.closeTime) - this.activationTime;
|
|
involvedStaff = Array.from(this.involvedStaff.entries()).map(s => s[0]).join(',');
|
|
}
|
|
// Write to TSV
|
|
// ticketType\ttotalTime\ttimeToFirstClaim\tinactiveTime\tresolution\tresult\tstaff,userids,seperated,with,commas
|
|
const line = `${this.ticket.type}\t${(this.closeTime - this.createTime)}\t${firstClaimWait}\t${this.unclaimedTime}\t${this.resolution}\t${this.result}\t${involvedStaff}`;
|
|
writeStats(line);
|
|
}
|
|
|
|
deleteTicket(staff: User) {
|
|
this.close('deleted', staff);
|
|
this.room.modlog({action: 'TICKETDELETE', isGlobal: true, loggedBy: staff.id});
|
|
this.addText(`${staff.name} deleted this ticket.`, staff);
|
|
delete tickets[this.ticket.userid];
|
|
writeTickets();
|
|
notifyStaff();
|
|
this.room.destroy();
|
|
}
|
|
|
|
// Modified version of RoomGame.destory
|
|
destroy() {
|
|
if (tickets[this.ticket.userid] && this.ticket.open) {
|
|
// Ticket was not deleted - deleted tickets already have this done to them - and was not closed.
|
|
// Write stats and change flags as appropriate prior to deletion.
|
|
this.ticket.open = false;
|
|
tickets[this.ticket.userid] = this.ticket;
|
|
notifyStaff();
|
|
writeTickets();
|
|
this.writeStats(false);
|
|
}
|
|
|
|
this.room.game = null;
|
|
// @ts-ignore
|
|
this.room = null;
|
|
for (const player of this.players) {
|
|
player.destroy();
|
|
}
|
|
// @ts-ignore
|
|
this.players = null;
|
|
// @ts-ignore
|
|
this.playerTable = null;
|
|
}
|
|
static ban(user: string, reason = '') {
|
|
const userid = toID(user);
|
|
const punishment: Punishment = ['TICKETBAN', userid, Date.now() + TICKET_BAN_DURATION, reason];
|
|
return Punishments.roomPunish('staff', userid, punishment);
|
|
}
|
|
static unban(user: string) {
|
|
user = toID(user);
|
|
return Punishments.roomUnpunish('staff', user, 'TICKETBAN');
|
|
}
|
|
static checkBanned(user: User) {
|
|
const staffRoom = Rooms.get('staff');
|
|
if (!staffRoom) return;
|
|
const punishment = Punishments.getRoomPunishType(staffRoom, user.id);
|
|
if (punishment === 'TICKETBAN') {
|
|
return `You are banned from creating tickets.`;
|
|
}
|
|
// skip if the user is autoconfirmed and on a shared ip
|
|
if (Punishments.sharedIps.has(user.latestIp) && user.autoconfirmed) return false;
|
|
|
|
for (const ip of user.ips) {
|
|
const curPunishment = Punishments.roomIps.get('staff')?.get(ip);
|
|
if (curPunishment && curPunishment[0] === 'TICKETBAN') {
|
|
const [, userid,, reason] = curPunishment;
|
|
return (
|
|
`You are banned from creating help tickets` +
|
|
`${userid !== user.id ? `, because you have the same IP as ${userid}` : ''}. ${reason ? `Reason: ${reason}` : ''}`
|
|
);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const NOTIFY_ALL_TIMEOUT = 5 * 60 * 1000;
|
|
const NOTIFY_ASSIST_TIMEOUT = 60 * 1000;
|
|
const unclaimedTicketTimer: {[k: string]: NodeJS.Timer | null} = {upperstaff: null, staff: null};
|
|
const timerEnds: {[k: string]: number} = {upperstaff: 0, staff: 0};
|
|
function pokeUnclaimedTicketTimer(hasUnclaimed: boolean, hasAssistRequest: boolean) {
|
|
const room = Rooms.get('staff');
|
|
if (!room) return;
|
|
if (hasUnclaimed && !unclaimedTicketTimer[room.roomid]) {
|
|
unclaimedTicketTimer[room.roomid] = setTimeout(
|
|
() =>
|
|
notifyUnclaimedTicket(hasAssistRequest), hasAssistRequest ? NOTIFY_ASSIST_TIMEOUT : NOTIFY_ALL_TIMEOUT
|
|
);
|
|
timerEnds[room.roomid] = Date.now() + (hasAssistRequest ? NOTIFY_ASSIST_TIMEOUT : NOTIFY_ALL_TIMEOUT);
|
|
} else if (
|
|
hasAssistRequest &&
|
|
(timerEnds[room.roomid] - NOTIFY_ASSIST_TIMEOUT) > NOTIFY_ASSIST_TIMEOUT &&
|
|
unclaimedTicketTimer[room.roomid]
|
|
) {
|
|
// Shorten timer
|
|
clearTimeout(unclaimedTicketTimer[room.roomid]!);
|
|
unclaimedTicketTimer[room.roomid] = setTimeout(() => notifyUnclaimedTicket(hasAssistRequest), NOTIFY_ASSIST_TIMEOUT);
|
|
timerEnds[room.roomid] = Date.now() + NOTIFY_ASSIST_TIMEOUT;
|
|
} else if (!hasUnclaimed && unclaimedTicketTimer[room.roomid]) {
|
|
clearTimeout(unclaimedTicketTimer[room.roomid]!);
|
|
unclaimedTicketTimer[room.roomid] = null;
|
|
timerEnds[room.roomid] = 0;
|
|
}
|
|
}
|
|
function notifyUnclaimedTicket(hasAssistRequest: boolean) {
|
|
const room = Rooms.get('staff');
|
|
if (!room) return;
|
|
clearTimeout(unclaimedTicketTimer[room.roomid]!);
|
|
unclaimedTicketTimer[room.roomid] = null;
|
|
timerEnds[room.roomid] = 0;
|
|
for (const i in room.users) {
|
|
const user: User = room.users[i];
|
|
if (user.can('mute', null, room) && !user.settings.ignoreTickets) {
|
|
user.sendTo(
|
|
room,
|
|
`|tempnotify|helptickets|Unclaimed help tickets!|${hasAssistRequest ? 'Public Room Staff need help' : 'There are unclaimed Help tickets'}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function notifyStaff() {
|
|
const room = Rooms.get('staff');
|
|
if (!room) return;
|
|
let buf = ``;
|
|
const keys = Object.keys(tickets).sort((aKey, bKey) => {
|
|
const a = tickets[aKey];
|
|
const b = tickets[bKey];
|
|
if (a.open !== b.open) {
|
|
return (a.open ? -1 : 1);
|
|
} else if (a.open && b.open) {
|
|
if (a.active !== b.active) {
|
|
return (a.active ? -1 : 1);
|
|
}
|
|
if (!!a.claimed !== !!b.claimed) {
|
|
return (a.claimed ? 1 : -1);
|
|
}
|
|
return a.created - b.created;
|
|
}
|
|
return 0;
|
|
});
|
|
let count = 0;
|
|
let hiddenTicketUnclaimedCount = 0;
|
|
let hiddenTicketCount = 0;
|
|
let hasUnclaimed = false;
|
|
let fourthTicketIndex = 0;
|
|
let hasAssistRequest = false;
|
|
for (const key of keys) {
|
|
const ticket = tickets[key];
|
|
if (!ticket.open) continue;
|
|
if (!ticket.active) continue;
|
|
if (count >= 3) {
|
|
hiddenTicketCount++;
|
|
if (!ticket.claimed) hiddenTicketUnclaimedCount++;
|
|
if (hiddenTicketCount === 1) {
|
|
fourthTicketIndex = buf.length;
|
|
} else {
|
|
continue;
|
|
}
|
|
}
|
|
// should always exist
|
|
const ticketRoom = Rooms.get(`help-${ticket.userid}`) as ChatRoom;
|
|
const ticketGame = ticketRoom.getGame(HelpTicket)!;
|
|
if (!ticket.claimed) {
|
|
hasUnclaimed = true;
|
|
if (ticket.type === 'Public Room Assistance Request') hasAssistRequest = true;
|
|
}
|
|
buf += ticketGame.getButton();
|
|
count++;
|
|
}
|
|
if (hiddenTicketCount > 1) {
|
|
const notifying = hiddenTicketUnclaimedCount > 0 ? ` notifying` : ``;
|
|
if (hiddenTicketUnclaimedCount > 0) hasUnclaimed = true;
|
|
buf = buf.slice(0, fourthTicketIndex) +
|
|
`<a class="button${notifying}" href="/view-help-tickets">and ${hiddenTicketCount} more Help ticket${Chat.plural(hiddenTicketCount)} (${hiddenTicketUnclaimedCount} unclaimed)</a>`;
|
|
}
|
|
buf = `|${hasUnclaimed ? 'uhtml' : 'uhtmlchange'}|latest-tickets|<div class="infobox" style="padding: 6px 4px">${buf}${count === 0 ? `There were open Help tickets, but they've all been closed now.` : ``}</div>`;
|
|
room.send(buf);
|
|
|
|
if (hasUnclaimed) {
|
|
buf = `|tempnotify|helptickets|Unclaimed help tickets!|${hasAssistRequest ? 'Public Room Staff need help' : 'There are unclaimed Help tickets'}`;
|
|
} else {
|
|
buf = `|tempnotifyoff|helptickets`;
|
|
}
|
|
|
|
if (hasUnclaimed) {
|
|
// only notify for people highlighting
|
|
buf = `${buf}|${hasAssistRequest ? 'Public Room Staff need help' : 'There are unclaimed Help tickets'}`;
|
|
}
|
|
for (const user of Object.values(room.users)) {
|
|
if (user.can('lock') && !user.settings.ignoreTickets) user.sendTo(room, buf);
|
|
}
|
|
pokeUnclaimedTicketTimer(hasUnclaimed, hasAssistRequest);
|
|
}
|
|
|
|
function checkIp(ip: string) {
|
|
for (const t in tickets) {
|
|
if (tickets[t].ip === ip && tickets[t].open && !Punishments.sharedIps.has(ip)) {
|
|
return tickets[t];
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Prevent a desynchronization issue when hotpatching
|
|
for (const room of Rooms.rooms.values()) {
|
|
if (!room.settings.isHelp || !room.game) continue;
|
|
const game = room.getGame(HelpTicket)!;
|
|
if (game.ticket) game.ticket = tickets[game.ticket.userid];
|
|
}
|
|
|
|
const ticketTitles: {[k: string]: string} = Object.assign(Object.create(null), {
|
|
pmharassment: `PM Harassment`,
|
|
battleharassment: `Battle Harassment`,
|
|
inapname: `Inappropriate Username`,
|
|
inappokemon: `Inappropriate Pokemon Nicknames`,
|
|
appeal: `Appeal`,
|
|
ipappeal: `IP-Appeal`,
|
|
appealsemi: `ISP-Appeal`,
|
|
roomhelp: `Public Room Assistance Request`,
|
|
other: `Other`,
|
|
});
|
|
const ticketPages: {[k: string]: string} = Object.assign(Object.create(null), {
|
|
report: `I want to report someone`,
|
|
pmharassment: `Someone is harassing me in PMs`,
|
|
battleharassment: `Someone is harassing me in a battle`,
|
|
inapname: `Someone is using an offensive username`,
|
|
inappokemon: `Someone is using offensive Pokemon nicknames`,
|
|
|
|
appeal: `I want to appeal a punishment`,
|
|
permalock: `I want to appeal my permalock`,
|
|
lock: `I want to appeal my lock`,
|
|
ip: `I'm locked because I have the same IP as someone I don't recognize`,
|
|
semilock: `I can't talk in chat because of my ISP`,
|
|
hostfilter: `I'm locked because of a proxy or VPN`,
|
|
hasautoconfirmed: `Yes, I have an autoconfirmed account`,
|
|
lacksautoconfirmed: `No, I don't have an autoconfirmed account`,
|
|
appealother: `I want to appeal a mute/roomban/blacklist`,
|
|
|
|
misc: `Something else`,
|
|
password: `I lost my password`,
|
|
roomhelp: `I need global staff to help watch a public room`,
|
|
other: `Other`,
|
|
|
|
confirmpmharassment: `Report harassment in a private message (PM)`,
|
|
confirmbattleharassment: `Report harassment in a battle`,
|
|
confirminapname: `Report an inappropriate username`,
|
|
confirminappokemon: `Report inappropriate Pokemon nicknames`,
|
|
confirmappeal: `Appeal your lock`,
|
|
confirmipappeal: `Appeal IP lock`,
|
|
confirmappealsemi: `Appeal ISP lock`,
|
|
confirmroomhelp: `Call a Global Staff member to help`,
|
|
confirmother: `Call a Global Staff member`,
|
|
});
|
|
|
|
export const pages: PageTable = {
|
|
help: {
|
|
request(query, user, connection) {
|
|
if (!user.named) {
|
|
const buf = `>view-help-request${query.length ? '-' + query.join('-') : ''}\n` +
|
|
`|init|html\n` +
|
|
`|title|Request Help\n` +
|
|
`|pagehtml|<div class="pad"><h2>${this.tr`Request help from global staff`}</h2><p>${this.tr`Please <button name="login" class="button">Log In</button> to request help.`}</p></div>`;
|
|
connection.send(buf);
|
|
return Rooms.RETRY_AFTER_LOGIN;
|
|
}
|
|
this.title = this.tr`Request Help`;
|
|
let buf = `<div class="pad"><h2>${this.tr`Request help from global staff`}</h2>`;
|
|
|
|
const banMsg = HelpTicket.checkBanned(user);
|
|
if (banMsg) return connection.popup(banMsg);
|
|
let ticket = tickets[user.id];
|
|
const ipTicket = checkIp(user.latestIp);
|
|
if (ticket?.open || ipTicket) {
|
|
if (!ticket && ipTicket) ticket = ipTicket;
|
|
const helpRoom = Rooms.get(`help-${ticket.userid}`);
|
|
if (!helpRoom) {
|
|
// Should never happen
|
|
tickets[ticket.userid].open = false;
|
|
writeTickets();
|
|
} else {
|
|
if (!helpRoom.auth.has(user.id)) helpRoom.auth.set(user.id, '+');
|
|
connection.popup(this.tr`You already have a Help ticket.`);
|
|
user.joinRoom(`help-${ticket.userid}` as RoomID);
|
|
return this.close();
|
|
}
|
|
}
|
|
|
|
const isStaff = user.can('lock');
|
|
// room / user being reported
|
|
let meta = '';
|
|
const targetTypeIndex = Math.max(query.indexOf('user'), query.indexOf('room'));
|
|
if (targetTypeIndex >= 0) meta = '-' + query.splice(targetTypeIndex).join('-');
|
|
if (!query.length) query = [''];
|
|
for (const [i, page] of query.entries()) {
|
|
const isLast = (i === query.length - 1);
|
|
const isFirst = i === 1;
|
|
if (page && page in ticketPages && !page.startsWith('confirm')) {
|
|
let prevPageLink = query.slice(0, i).join('-');
|
|
if (prevPageLink) prevPageLink = `-${prevPageLink}`;
|
|
buf += `<p><a href="/view-help-request${prevPageLink}${!isFirst ? meta : ''}" target="replace"><button class="button">${this.tr`Back`}</button></a> <button class="button disabled" disabled>${this.tr(ticketPages[page])}</button></p>`;
|
|
}
|
|
switch (page) {
|
|
case '':
|
|
buf += `<p><b>${this.tr`What's going on?`}</b></p>`;
|
|
if (isStaff) {
|
|
buf += `<p class="message-error">${this.tr`Global staff cannot make Help requests. This form is only for reference.`}</p>`;
|
|
} else {
|
|
buf += `<p class="message-error">${this.tr`Abuse of Help requests can result in punishments.`}</p>`;
|
|
}
|
|
if (!isLast) break;
|
|
buf += `<p><Button>report</Button></p>`;
|
|
buf += `<p><Button>appeal</Button></p>`;
|
|
buf += `<p><Button>misc</Button></p>`;
|
|
break;
|
|
case 'report':
|
|
buf += `<p><b>${this.tr`What do you want to report someone for?`}</b></p>`;
|
|
if (!isLast) break;
|
|
buf += `<p><Button>pmharassment</Button></p>`;
|
|
buf += `<p><Button>battleharassment</Button></p>`;
|
|
buf += `<p><Button>inapname</Button></p>`;
|
|
buf += `<p><Button>inappokemon</Button></p>`;
|
|
break;
|
|
case 'pmharassment':
|
|
buf += `<p>${this.tr`If someone is harassing you in private messages (PMs), click the button below and a global staff member will take a look. If you are being harassed in a chatroom, please ask a room staff member to handle it. If it's a minor issue, consider using <code>/ignore [username]</code> instead.`}</p>`;
|
|
if (!isLast) break;
|
|
buf += `<p><Button>confirmpmharassment</Button></p>`;
|
|
break;
|
|
case 'battleharassment':
|
|
buf += `<p>${this.tr`If someone is harassing you in a battle, click the button below and a global staff member will take a look. If you are being harassed in a chatroom, please ask a room staff member to handle it. If it's a minor issue, consider using <code>/ignore [username]</code> instead.`}</p>`;
|
|
buf += `<p>${this.tr`Please save a replay of the battle if it has ended, or provide a link to the battle if it is still ongoing.`}</p>`;
|
|
if (!isLast) break;
|
|
buf += `<p><Button>confirmbattleharassment</Button></p>`;
|
|
break;
|
|
case 'inapname':
|
|
buf += `<p>${this.tr`If a user has an inappropriate name, click the button below and a global staff member will take a look.`}</p>`;
|
|
if (!isLast) break;
|
|
buf += `<p><Button>confirminapname</Button></p>`;
|
|
break;
|
|
case 'inappokemon':
|
|
buf += `<p>${this.tr`If a user has inappropriate Pokemon nicknames, click the button below and a global staff member will take a look.`}</p>`;
|
|
buf += `<p>${this.tr`Please save a replay of the battle if it has ended, or provide a link to the battle if it is still ongoing.`}</p>`;
|
|
if (!isLast) break;
|
|
buf += `<p><Button>confirminappokemon</Button></p>`;
|
|
break;
|
|
case 'appeal':
|
|
buf += `<p><b>${this.tr`What would you like to appeal?`}</b></p>`;
|
|
if (!isLast) break;
|
|
if (user.locked || isStaff) {
|
|
const namelocked = user.named && user.id.startsWith('guest');
|
|
if (user.locked === user.id || namelocked || isStaff) {
|
|
if (user.permalocked || isStaff) {
|
|
buf += `<p><Button>permalock</Button></p>`;
|
|
}
|
|
if (!user.permalocked || isStaff) {
|
|
buf += `<p><Button>lock</Button></p>`;
|
|
}
|
|
}
|
|
if (user.locked === '#hostfilter' || (user.latestHostType === 'proxy' && user.locked !== user.id) || isStaff) {
|
|
buf += `<p><Button>hostfilter</Button></p>`;
|
|
}
|
|
if ((user.locked !== '#hostfilter' && user.latestHostType !== 'proxy' && user.locked !== user.id) || isStaff) {
|
|
buf += `<p><Button>ip</Button></p>`;
|
|
}
|
|
}
|
|
if (user.semilocked || isStaff) {
|
|
buf += `<p><Button>semilock</Button></p>`;
|
|
}
|
|
buf += `<p><Button>appealother</Button></p>`;
|
|
buf += `<p><Button>other</Button></p>`;
|
|
break;
|
|
case 'permalock':
|
|
buf += `<p>${this.tr`Permalocks are usually for repeated incidents of poor behavior over an extended period of time, and rarely for a single severe infraction. Please keep this in mind when appealing a permalock.`}</p>`;
|
|
buf += `<p>${this.tr`Please visit the <a href="https://www.smogon.com/forums/threads/discipline-appeal-rules.3583479/">Discipline Appeals</a> page to appeal your permalock.`}</p>`;
|
|
break;
|
|
case 'lock':
|
|
buf += `<p>${this.tr`If you want to appeal your lock or namelock, click the button below and a global staff member will be with you shortly.`}</p>`;
|
|
if (!isLast) break;
|
|
buf += `<p><Button>confirmappeal</Button></p>`;
|
|
break;
|
|
case 'ip':
|
|
buf += `<p>${this.tr`If you are locked or namelocked under a name you don't recognize, click the button below to call a global staff member so we can check.`}</p>`;
|
|
if (!isLast) break;
|
|
buf += `<p><Button>confirmipappeal</Button></p>`;
|
|
break;
|
|
case 'hostfilter':
|
|
buf += `<p>${this.tr`We automatically lock proxies and VPNs to prevent evasion of punishments and other attacks on our server. To get unlocked, you need to disable your proxy or VPN.`}</p>`;
|
|
break;
|
|
case 'semilock':
|
|
buf += `<p>${this.tr`Do you have an autoconfirmed account? An account is autoconfirmed when it has won at least one rated battle and has been registered for one week or longer.`}</p>`;
|
|
if (!isLast) break;
|
|
buf += `<p><Button>hasautoconfirmed</Button> <Button>lacksautoconfirmed</Button></p>`;
|
|
break;
|
|
case 'hasautoconfirmed':
|
|
buf += `<p>${this.tr`Login to your autoconfirmed account by using the <code>/nick</code> command in any chatroom, and the semilock will automatically be removed. Afterwords, you can use the <code>/nick</code> command to switch back to your current username without being semilocked again.`}</p>`;
|
|
buf += `<p>${this.tr`If the semilock does not go away, you can try asking a global staff member for help. Click the button below to call a global staff member.`}</p>`;
|
|
if (!isLast) break;
|
|
buf += `<p><Button>confirmappealsemi</Button></p>`;
|
|
break;
|
|
case 'lacksautoconfirmed':
|
|
buf += `<p>${this.tr`If you don't have an autoconfirmed account, you will need to contact a global staff member to appeal your semilock. Click the button below to call a global staff member.`}</p>`;
|
|
if (!isLast) break;
|
|
buf += `<p><Button>confirmappealsemi</Button></p>`;
|
|
break;
|
|
case 'appealother':
|
|
buf += `<p>${this.tr`Please PM the staff member who punished you. If you don't know who punished you, ask another room staff member; they will redirect you to the correct user. If you are banned or blacklisted from the room, use <code>/roomauth [name of room]</code> to get a list of room staff members. Bold names are online.`}</p>`;
|
|
buf += `<p><strong>${this.tr`Do not PM staff if you are locked (signified by the symbol <code>‽</code> in front of your username). Locks are a different type of punishment; to appeal a lock, make a help ticket by clicking the Back button and then selecting the most relevant option.`}</strong></p>`;
|
|
break;
|
|
case 'misc':
|
|
buf += `<p><b>${this.tr`Maybe one of these options will be helpful?`}</b></p>`;
|
|
if (!isLast) break;
|
|
buf += `<p><Button>password</Button></p>`;
|
|
if (user.trusted || isStaff) buf += `<p><Button>roomhelp</Button></p>`;
|
|
buf += `<p><Button>other</Button></p>`;
|
|
break;
|
|
case 'password':
|
|
buf += `<p>${this.tr`If you lost your password, click the button below to request a password reset. We will need to clarify a few pieces of information before resetting the account. Please note that password resets are low priority and may take a while; we recommend using a new account while waiting.`}</p>`;
|
|
buf += `<p><a class="button" href="https://www.smogon.com/forums/password-reset-form/">${this.tr`Request a password reset`}</a></p>`;
|
|
break;
|
|
case 'roomhelp':
|
|
buf += `<p>${this.tr`If you are a room driver or up in a public room, and you need help watching the chat, one or more global staff members would be happy to assist you!`}</p>`;
|
|
buf += `<p><Button>confirmroomhelp</Button></p>`;
|
|
break;
|
|
case 'other':
|
|
buf += `<p>${this.tr`If your issue is not handled above, click the button below to talk to a global staff member. Please be ready to explain the situation.`}</p>`;
|
|
if (!isLast) break;
|
|
buf += `<p><Button>confirmother</Button></p>`;
|
|
break;
|
|
default:
|
|
if (!page.startsWith('confirm') || !ticketTitles[page.slice(7)]) {
|
|
buf += `<p>${this.tr`Malformed help request.`}</p>`;
|
|
buf += `<a href="/view-help-request" target="replace"><button class="button">${this.tr`Back`}</button></a>`;
|
|
break;
|
|
}
|
|
const type = this.tr(ticketTitles[page.slice(7)]);
|
|
buf += `<p><b>${this.tr`Are you sure you want to submit a ticket for ${type}?`}</b></p>`;
|
|
const submitMeta = Utils.splitFirst(meta, '-', 2).join('|'); // change the delimiter as some ticket titles include -
|
|
buf += `<p><button class="button notifying" name="send" value="/helpticket submit ${ticketTitles[page.slice(7)]} ${submitMeta}">${this.tr`Yes, contact global staff`}</button> <a href="/view-help-request-${query.slice(0, i).join('-')}${meta}" target="replace"><button class="button">${this.tr`No, cancel`}</button></a></p>`;
|
|
break;
|
|
}
|
|
}
|
|
buf += '</div>';
|
|
const curPageLink = query.length ? '-' + query.join('-') : '';
|
|
buf = buf.replace(
|
|
/<Button>([a-z]+)<\/Button>/g,
|
|
(match, id) => (
|
|
`<a class="button" href="/view-help-request${curPageLink}-${id}${meta}" target="replace">${this.tr(ticketPages[id])}</a>`
|
|
)
|
|
);
|
|
return buf;
|
|
},
|
|
tickets(query, user, connection) {
|
|
if (!user.named) return Rooms.RETRY_AFTER_LOGIN;
|
|
this.title = this.tr`Ticket List`;
|
|
this.checkCan('lock');
|
|
let buf = `<div class="pad ladder"><button class="button" name="send" value="/helpticket list" style="float:left"><i class="fa fa-refresh"></i> ${this.tr`Refresh`}</button> <button class="button" name="send" value="/helpticket stats" style="float: right"><i class="fa fa-th-list"></i> ${this.tr`Help Ticket Stats`}</button><br /><br />`;
|
|
buf += `<table style="margin-left: auto; margin-right: auto"><tbody><tr><th colspan="5"><h2 style="margin: 5px auto">${this.tr`Help tickets`}</h1></th></tr>`;
|
|
buf += `<tr><th>${this.tr`Status`}</th><th>${this.tr`Creator`}</th><th>${this.tr`Ticket Type`}</th><th>${this.tr`Claimed by`}</th><th>${this.tr`Action`}</th></tr>`;
|
|
|
|
const keys = Object.keys(tickets).sort((aKey, bKey) => {
|
|
const a = tickets[aKey];
|
|
const b = tickets[bKey];
|
|
if (a.open !== b.open) {
|
|
return (a.open ? -1 : 1);
|
|
}
|
|
if (a.open) {
|
|
if (a.active !== b.active) {
|
|
return (a.active ? -1 : 1);
|
|
}
|
|
return a.created - b.created;
|
|
}
|
|
return b.created - a.created;
|
|
});
|
|
let count = 0;
|
|
for (const key of keys) {
|
|
if (count >= 100 && query[0] !== 'all') {
|
|
buf += `<tr><td colspan="5">${this.tr`And ${keys.length - count} more tickets.`} <a class="button" href="/view-help-tickets-all" target="replace">${this.tr`View all tickets`}</a></td></tr>`;
|
|
break;
|
|
}
|
|
const ticket = tickets[key];
|
|
let icon = `<span style="color:gray"><i class="fa fa-check-circle-o"></i> ${this.tr`Closed`}</span>`;
|
|
if (ticket.open) {
|
|
if (!ticket.active) {
|
|
icon = `<span style="color:gray"><i class="fa fa-circle-o"></i> ${this.tr`Inactive`}</span>`;
|
|
} else if (ticket.claimed) {
|
|
icon = `<span style="color:green"><i class="fa fa-circle-o"></i> ${this.tr`Claimed`}</span>`;
|
|
} else {
|
|
icon = `<span style="color:orange"><i class="fa fa-circle-o"></i> <strong>${this.tr`Unclaimed`}</strong></span>`;
|
|
}
|
|
}
|
|
buf += `<tr><td>${icon}</td>`;
|
|
buf += Utils.html`<td>${ticket.creator}</td>`;
|
|
buf += `<td>${ticket.type}</td>`;
|
|
buf += Utils.html`<td>${ticket.claimed ? ticket.claimed : `-`}</td>`;
|
|
buf += `<td>`;
|
|
const roomid = 'help-' + ticket.userid;
|
|
let logUrl = '';
|
|
if (Config.modloglink) {
|
|
logUrl = Config.modloglink(new Date(ticket.created), roomid);
|
|
}
|
|
const room = Rooms.get(roomid);
|
|
if (room) {
|
|
const ticketGame = room.getGame(HelpTicket)!;
|
|
buf += `<a href="/${roomid}"><button class="button" ${ticketGame.getPreview()}>${this.tr(!ticket.claimed && ticket.open ? 'Claim' : 'View')}</button></a> `;
|
|
}
|
|
if (logUrl) {
|
|
buf += `<a href="${logUrl}"><button class="button">${this.tr`Log`}</button></a>`;
|
|
}
|
|
buf += '</td></tr>';
|
|
count++;
|
|
}
|
|
buf += `<tr><th colspan="5"><h2 style="margin: 5px auto">${this.tr`Ticket Bans`} <i class="fa fa-ban"></i></h1></th></tr>`;
|
|
buf += `<tr><th>Userids</th><th>IPs</th><th>Expires</th><th>Reason</th></tr>`;
|
|
const ticketBans = Array.from(Punishments.getPunishments('staff'))
|
|
.sort((a, b) => a[1].expireTime - b[1].expireTime)
|
|
.filter(item => item[1].punishType === 'TICKETBAN');
|
|
for (const [userid, entry] of ticketBans) {
|
|
let ids = [userid];
|
|
if (entry.userids) ids = ids.concat(entry.userids);
|
|
buf += `<tr><td>${ids.map(Utils.escapeHTML).join(', ')}</td>`;
|
|
buf += `<td>${entry.ips.join(', ')}</td>`;
|
|
buf += `<td>${Chat.toDurationString(entry.expireTime - Date.now(), {precision: 1})}</td>`;
|
|
buf += `<td>${entry.reason || ''}</td></tr>`;
|
|
}
|
|
buf += `</tbody></table></div>`;
|
|
return buf;
|
|
},
|
|
stats(query, user, connection) {
|
|
// view-help-stats-TABLE-YYYY-MM-COL
|
|
if (!user.named) return Rooms.RETRY_AFTER_LOGIN;
|
|
this.title = this.tr`Ticket Stats`;
|
|
this.checkCan('lock');
|
|
|
|
let [table, yearString, monthString, col] = query;
|
|
if (!['staff', 'tickets'].includes(table)) table = 'tickets';
|
|
const year = parseInt(yearString);
|
|
const month = parseInt(monthString) - 1;
|
|
let date = null;
|
|
if (isNaN(year) || isNaN(month) || month < 0 || month > 11 || year < 2010) {
|
|
// year/month not provided or is invalid, use current date
|
|
date = new Date();
|
|
} else {
|
|
date = new Date(year, month);
|
|
}
|
|
const dateUrl = Chat.toTimestamp(date).split(' ')[0].split('-', 2).join('-');
|
|
|
|
const rawTicketStats = FS(`logs/tickets/${dateUrl}.tsv`).readIfExistsSync();
|
|
if (!rawTicketStats) return `<div class="pad"><br />${this.tr`No ticket stats found.`}</div>`;
|
|
|
|
// Calculate next/previous month for stats and validate stats exist for the month
|
|
|
|
// date.getMonth() returns 0-11, we need 1-12 +/-1 for this
|
|
const prevDate = new Date(
|
|
date.getMonth() === 0 ?
|
|
date.getFullYear() - 1 :
|
|
date.getFullYear(),
|
|
date.getMonth() === 0 ?
|
|
11 :
|
|
date.getMonth() - 1
|
|
);
|
|
const nextDate = new Date(
|
|
date.getMonth() === 11 ?
|
|
date.getFullYear() + 1 :
|
|
date.getFullYear(),
|
|
date.getMonth() === 11 ?
|
|
0 :
|
|
date.getMonth() + 1
|
|
);
|
|
const prevString = Chat.toTimestamp(prevDate).split(' ')[0].split('-', 2).join('-');
|
|
const nextString = Chat.toTimestamp(nextDate).split(' ')[0].split('-', 2).join('-');
|
|
|
|
let buttonBar = '';
|
|
if (FS(`logs/tickets/${prevString}.tsv`).readIfExistsSync()) {
|
|
buttonBar += `<a class="button" href="/view-help-stats-${table}-${prevString}" target="replace" style="float: left">< ${this.tr`Previous Month`}</a>`;
|
|
} else {
|
|
buttonBar += `<a class="button disabled" style="float: left">< ${this.tr`Previous Month`}Month</a>`;
|
|
}
|
|
buttonBar += `<a class="button${table === 'tickets' ? ' disabled"' : `" href="/view-help-stats-tickets-${dateUrl}" target="replace"`}>${this.tr`Ticket Stats`}</a> <a class="button ${table === 'staff' ? ' disabled"' : `" href="/view-help-stats-staff-${dateUrl}" target="replace"`}>${this.tr`Staff Stats`}</a>`;
|
|
if (FS(`logs/tickets/${nextString}.tsv`).readIfExistsSync()) {
|
|
buttonBar += `<a class="button" href="/view-help-stats-${table}-${nextString}" target="replace" style="float: right">${this.tr`Next Month`} ></a>`;
|
|
} else {
|
|
buttonBar += `<a class="button disabled" style="float: right">${this.tr`Next Month`} ></a>`;
|
|
}
|
|
|
|
let buf = `<div class="pad ladder"><div style="text-align: center">${buttonBar}</div><br />`;
|
|
buf += `<table style="margin-left: auto; margin-right: auto"><tbody><tr><th colspan="${table === 'tickets' ? 7 : 3}"><h2 style="margin: 5px auto">${this.tr`Help Ticket Stats`} - ${date.toLocaleString('en-us', {month: 'long', year: 'numeric'})}</h1></th></tr>`;
|
|
if (table === 'tickets') {
|
|
if (!['type', 'totaltickets', 'total', 'initwait', 'wait', 'resolution', 'result'].includes(col)) col = 'type';
|
|
buf += `<tr><th><Button>type</Button></th><th><Button>totaltickets</Button></th><th><Button>total</Button></th><th><Button>initwait</Button></th><th><Button>wait</Button></th><th><Button>resolution</Button></th><th><Button>result</Button></th></tr>`;
|
|
} else {
|
|
if (!['staff', 'num', 'time'].includes(col)) col = 'num';
|
|
buf += `<tr><th><Button>staff</Button></th><th><Button>num</Button></th><th><Button>time</Button></th></tr>`;
|
|
}
|
|
|
|
const ticketStats: {[k: string]: string}[] = rawTicketStats.split('\n').filter(
|
|
(line: string) => line
|
|
).map(
|
|
(line: string) => {
|
|
const splitLine = line.split('\t');
|
|
return {
|
|
type: splitLine[0],
|
|
total: splitLine[1],
|
|
initwait: splitLine[2],
|
|
wait: splitLine[3],
|
|
resolution: splitLine[4],
|
|
result: splitLine[5],
|
|
staff: splitLine[6],
|
|
};
|
|
}
|
|
);
|
|
if (table === 'tickets') {
|
|
const typeStats: {[key: string]: {[key: string]: number}} = {};
|
|
for (const stats of ticketStats) {
|
|
if (!typeStats[stats.type]) {
|
|
typeStats[stats.type] = {
|
|
total: 0,
|
|
initwait: 0,
|
|
wait: 0,
|
|
dead: 0,
|
|
unresolved: 0,
|
|
resolved: 0,
|
|
result: 0,
|
|
totaltickets: 0,
|
|
};
|
|
}
|
|
const type = typeStats[stats.type];
|
|
type.totaltickets++;
|
|
type.total += parseInt(stats.total);
|
|
type.initwait += parseInt(stats.initwait);
|
|
type.wait += parseInt(stats.wait);
|
|
if (['approved', 'valid', 'assisted'].includes(stats.result.toString())) type.result++;
|
|
if (['dead', 'unresolved', 'resolved'].includes(stats.resolution.toString())) {
|
|
type[stats.resolution.toString()]++;
|
|
}
|
|
}
|
|
|
|
// Calculate averages/percentages
|
|
for (const t in typeStats) {
|
|
const type = typeStats[t];
|
|
// Averages
|
|
for (const key of ['total', 'initwait', 'wait']) {
|
|
type[key] = Math.round(type[key] / type.totaltickets);
|
|
}
|
|
// Percentages
|
|
for (const key of ['result', 'dead', 'unresolved', 'resolved']) {
|
|
type[key] = Math.round((type[key] / type.totaltickets) * 100);
|
|
}
|
|
}
|
|
|
|
const sortedStats = Object.keys(typeStats).sort((a, b) => {
|
|
if (col === 'type') {
|
|
// Alphabetize strings
|
|
return a.localeCompare(b, 'en');
|
|
} else if (col === 'resolution') {
|
|
return (typeStats[b].resolved || 0) - (typeStats[a].resolved || 0);
|
|
}
|
|
return typeStats[b][col] - typeStats[a][col];
|
|
});
|
|
|
|
for (const type of sortedStats) {
|
|
const resolution = `${this.tr`Resolved`}: ${typeStats[type].resolved}%<br/>${this.tr`Unresolved`}: ${typeStats[type].unresolved}%<br/>${this.tr`Dead`}: ${typeStats[type].dead}%`;
|
|
buf += `<tr><td>${type}</td><td>${typeStats[type].totaltickets}</td><td>${Chat.toDurationString(typeStats[type].total, {hhmmss: true})}</td><td>${Chat.toDurationString(typeStats[type].initwait, {hhmmss: true}) || '-'}</td><td>${Chat.toDurationString(typeStats[type].wait, {hhmmss: true}) || '-'}</td><td>${resolution}</td><td>${typeStats[type].result}%</td></tr>`;
|
|
}
|
|
} else {
|
|
const staffStats: {[key: string]: {[key: string]: number}} = {};
|
|
for (const stats of ticketStats) {
|
|
const staffArray = (typeof stats.staff === 'string' ? stats.staff.split(',') : []);
|
|
for (const staff of staffArray) {
|
|
if (!staff) continue;
|
|
if (!staffStats[staff]) staffStats[staff] = {num: 0, time: 0};
|
|
staffStats[staff].num++;
|
|
staffStats[staff].time += (parseInt(stats.total) - parseInt(stats.initwait));
|
|
}
|
|
}
|
|
for (const staff in staffStats) {
|
|
staffStats[staff].time = Math.round(staffStats[staff].time / staffStats[staff].num);
|
|
}
|
|
const sortedStaff = Object.keys(staffStats).sort((a, b) => {
|
|
if (col === 'staff') {
|
|
// Alphabetize strings
|
|
return a.localeCompare(b, 'en');
|
|
}
|
|
return staffStats[b][col] - staffStats[a][col];
|
|
});
|
|
for (const staff of sortedStaff) {
|
|
buf += `<tr><td>${staff}</td><td>${staffStats[staff].num}</td><td>${Chat.toDurationString(staffStats[staff].time, {precision: 1})}</td></tr>`;
|
|
}
|
|
}
|
|
buf += `</tbody></table></div>`;
|
|
const headerTitles: {[id: string]: string} = {
|
|
type: 'Type',
|
|
totaltickets: 'Total Tickets',
|
|
total: 'Average Total Time',
|
|
initwait: 'Average Initial Wait',
|
|
wait: 'Average Total Wait',
|
|
resolution: 'Resolutions',
|
|
result: 'Positive Result',
|
|
staff: 'Staff ID',
|
|
num: 'Number of Tickets',
|
|
time: 'Average Time Per Ticket',
|
|
};
|
|
buf = buf.replace(/<Button>([a-z]+)<\/Button>/g, (match, id) => {
|
|
if (col === id) return this.tr(headerTitles[id]);
|
|
return `<a class="button" href="/view-help-stats-${table}-${dateUrl}-${id}" target="replace">${this.tr(headerTitles[id])}</a>`;
|
|
});
|
|
return buf;
|
|
},
|
|
},
|
|
};
|
|
|
|
export const commands: ChatCommands = {
|
|
report(target, room, user) {
|
|
if (!this.runBroadcast()) return;
|
|
const meta = this.pmTarget ? `-user-${this.pmTarget.id}` : this.room ? `-room-${this.room.roomid}` : '';
|
|
if (this.broadcasting) {
|
|
if (room?.battle) return this.errorReply(this.tr`This command cannot be broadcast in battles.`);
|
|
return this.sendReplyBox(`<button name="joinRoom" value="view-help-request--report${meta}" class="button"><strong>${this.tr`Report someone`}</strong></button>`);
|
|
}
|
|
|
|
return this.parse(`/join view-help-request--report${meta}`);
|
|
},
|
|
|
|
appeal(target, room, user) {
|
|
if (!this.runBroadcast()) return;
|
|
const meta = this.pmTarget ? `-user-${this.pmTarget.id}` : this.room ? `-room-${this.room.roomid}` : '';
|
|
if (this.broadcasting) {
|
|
if (room?.battle) return this.errorReply(this.tr`This command cannot be broadcast in battles.`);
|
|
return this.sendReplyBox(`<button name="joinRoom" value="view-help-request--appeal${meta}" class="button"><strong>${this.tr`Appeal a punishment`}</strong></button>`);
|
|
}
|
|
|
|
return this.parse(`/join view-help-request--appeal${meta}`);
|
|
},
|
|
|
|
requesthelp: 'helpticket',
|
|
helprequest: 'helpticket',
|
|
ht: 'helpticket',
|
|
helpticket: {
|
|
'': 'create',
|
|
create(target, room, user) {
|
|
if (!this.runBroadcast()) return;
|
|
const meta = this.pmTarget ? `-user-${this.pmTarget.id}` : this.room ? `-room-${this.room.roomid}` : '';
|
|
if (this.broadcasting) {
|
|
return this.sendReplyBox(`<button name="joinRoom" value="view-help-request${meta}" class="button"><strong>${this.tr`Request help`}</strong></button>`);
|
|
}
|
|
if (user.can('lock')) {
|
|
return this.parse('/join view-help-request'); // Globals automatically get the form for reference.
|
|
}
|
|
if (!user.named) return this.errorReply(this.tr`You need to choose a username before doing this.`);
|
|
return this.parse(`/join view-help-request${meta}`);
|
|
},
|
|
createhelp: [`/helpticket create - Creates a new ticket requesting help from global staff.`],
|
|
|
|
submit(target, room, user, connection) {
|
|
if (user.can('lock') && !user.can('bypassall')) {
|
|
return this.popupReply(this.tr`Global staff can't make tickets. They can only use the form for reference.`);
|
|
}
|
|
if (!user.named) return this.popupReply(this.tr`You need to choose a username before doing this.`);
|
|
const banMsg = HelpTicket.checkBanned(user);
|
|
if (banMsg) return this.popupReply(banMsg);
|
|
let ticket = tickets[user.id];
|
|
const ipTicket = checkIp(user.latestIp);
|
|
if (ticket?.open || ipTicket) {
|
|
if (!ticket && ipTicket) ticket = ipTicket;
|
|
const helpRoom = Rooms.get(`help-${ticket.userid}`);
|
|
if (!helpRoom) {
|
|
// Should never happen
|
|
tickets[ticket.userid].open = false;
|
|
writeTickets();
|
|
} else {
|
|
if (!helpRoom.auth.has(user.id)) helpRoom.auth.set(user.id, '+');
|
|
this.parse(`/join help-${ticket.userid}`);
|
|
return this.popupReply(this.tr`You already have an open ticket; please wait for global staff to respond.`);
|
|
}
|
|
}
|
|
if (Monitor.countTickets(user.latestIp)) {
|
|
const maxTickets = Punishments.sharedIps.has(user.latestIp) ? `50` : `5`;
|
|
return this.popupReply(this.tr`Due to high load, you are limited to creating ${maxTickets} tickets every hour.`);
|
|
}
|
|
let [ticketType, reportTargetType, reportTarget] = Utils.splitFirst(target, '|', 2).map(s => s.trim());
|
|
reportTarget = Utils.escapeHTML(reportTarget);
|
|
if (!Object.values(ticketTitles).includes(ticketType)) return this.parse('/helpticket');
|
|
const contexts: {[k: string]: string} = {
|
|
'PM Harassment': `Hi! Who was harassing you in private messages?`,
|
|
'Battle Harassment': `Hi! Who was harassing you, and in which battle did it happen? Please post a link to the battle or a replay of the battle.`,
|
|
'Inappropriate Username': `Hi! Tell us the username that is inappropriate.`,
|
|
'Inappropriate Pokemon Nicknames': `Hi! Which user has Pokemon with inappropriate nicknames, and in which battle? Please post a link to the battle or a replay of the battle.`,
|
|
Appeal: `Hi! Can you please explain why you feel your punishment is undeserved?`,
|
|
'IP-Appeal': `Hi! How are you connecting to Showdown right now? At home, at school, on a phone using mobile data, or some other way?`,
|
|
'Public Room Assistance Request': `Hi! Which room(s) do you need us to help you watch?`,
|
|
Other: `Hi! What seems to be the problem? Tell us about any people involved,` +
|
|
` and if this happened in a specific place on the site.`,
|
|
};
|
|
const staffContexts: {[k: string]: string} = {
|
|
'IP-Appeal': `<p><strong>${user.name}'s IP Addresses</strong>: ${user.ips.map(ip => `<a href="https://whatismyipaddress.com/ip/${ip}" target="_blank">${ip}</a>`).join(', ')}</p>`,
|
|
};
|
|
ticket = {
|
|
creator: user.name,
|
|
userid: user.id,
|
|
open: true,
|
|
active: !contexts[ticketType],
|
|
type: ticketType,
|
|
created: Date.now(),
|
|
claimed: null,
|
|
ip: user.latestIp,
|
|
};
|
|
let closeButtons = ``;
|
|
switch (ticket.type) {
|
|
case 'Appeal':
|
|
case 'IP-Appeal':
|
|
case 'ISP-Appeal':
|
|
closeButtons = `<button class="button" style="margin: 5px 0" name="send" value="/helpticket close ${user.id}">Close Ticket as Appeal Granted</button> <button class="button" style="margin: 5px 0" name="send" value="/helpticket close ${user.id}, false">Close Ticket as Appeal Denied</button>`;
|
|
break;
|
|
case 'PM Harassment':
|
|
case 'Battle Harassment':
|
|
case 'Inappropriate Pokemon Nicknames':
|
|
case 'Inappropriate Username':
|
|
closeButtons = `<button class="button" style="margin: 5px 0" name="send" value="/helpticket close ${user.id}">Close Ticket as Valid Report</button> <button class="button" style="margin: 5px 0" name="send" value="/helpticket close ${user.id}, false">Close Ticket as Invalid Report</button>`;
|
|
break;
|
|
case 'Public Room Assistance Request':
|
|
case 'Other':
|
|
default:
|
|
closeButtons = `<button class="button" style="margin: 5px 0" name="send" value="/helpticket close ${user.id}">Close Ticket as Assisted</button> <button class="button" style="margin: 5px 0" name="send" value="/helpticket close ${user.id}, false">Close Ticket as Unable to Assist</button>`;
|
|
}
|
|
let staffIntroButtons = '';
|
|
let pmRequestButton = '';
|
|
if (reportTargetType === 'user' && reportTarget) {
|
|
switch (ticket.type) {
|
|
case 'PM Harassment':
|
|
if (!Config.pmLogButton) break;
|
|
pmRequestButton = Config.pmLogButton(user.id, toID(reportTarget));
|
|
contexts['PM Harassment'] = this.tr`Hi! Please click the button below to give global staff permission to check PMs.` +
|
|
this.tr` Or if ${reportTarget} is not the user you want to report, please tell us the name of the user who you want to report.`;
|
|
break;
|
|
case 'Inappropriate Username':
|
|
staffIntroButtons = `<button class="button" name="send" value="/forcerename ${reportTarget}">Force-rename ${reportTarget}</button> `;
|
|
break;
|
|
}
|
|
staffIntroButtons += `<button class="button" name="send" value="/modlog global, ${reportTarget}">Global Modlog for ${reportTarget}</button> <button class="button" name="send" value="/sharedbattles ${user.id}, ${toID(reportTarget)}">Shared battles</button> `;
|
|
}
|
|
if (ticket.type === 'Appeal') {
|
|
staffIntroButtons += `<button class="button" name="send" value="/modlog global, ${user.name}">Global Modlog for ${user.name}</button>`;
|
|
}
|
|
const introMsg = Utils.html`<h2 style="margin-top:0">${this.tr`Help Ticket`} - ${user.name}</h2>` +
|
|
`<p><b>${this.tr`Issue`}</b>: ${ticket.type}<br />${this.tr`A Global Staff member will be with you shortly.`}</p>`;
|
|
const staffMessage = `<p>${closeButtons} <details><summary class="button">More Options</summary> ${staffIntroButtons}<button class="button" name="send" value="/helpticket ban ${user.id}"><small>Ticketban</small></button></details></p>`;
|
|
const staffHint = staffContexts[ticketType] || '';
|
|
let reportTargetInfo = '';
|
|
if (reportTargetType === 'room') {
|
|
reportTargetInfo = `Reported in room: <a href="/${reportTarget}">${reportTarget}</a>`;
|
|
const reportRoom = Rooms.get(reportTarget);
|
|
if (reportRoom && (reportRoom as GameRoom).uploadReplay) {
|
|
void (reportRoom as GameRoom).uploadReplay(user, connection, 'forpunishment');
|
|
}
|
|
} else if (reportTargetType === 'user') {
|
|
reportTargetInfo = `Reported user: <strong class="username">${reportTarget}</strong>`;
|
|
if (ticket.type === 'Battle Harassment') {
|
|
const commonBattles = getCommonBattles(
|
|
toID(reportTarget), Users.get(reportTarget),
|
|
ticket.userid, Users.get(ticket.userid)
|
|
);
|
|
|
|
reportTargetInfo += `<p></p>`;
|
|
if (!commonBattles.length) {
|
|
reportTargetInfo += `There are no common battles between '${reportTarget}' and '${ticket.creator}'.`;
|
|
} else {
|
|
reportTargetInfo += `Showing ${commonBattles.length} common battle(s) between '${reportTarget}' and '${ticket.creator}': `;
|
|
reportTargetInfo += commonBattles.map(roomid => Utils.html`<a href=/${roomid}>${roomid.replace(/^battle-/, '')}`);
|
|
}
|
|
}
|
|
}
|
|
let helpRoom = Rooms.get(`help-${user.id}`) as ChatRoom | null;
|
|
if (!helpRoom) {
|
|
helpRoom = Rooms.createChatRoom(`help-${user.id}` as RoomID, `[H] ${user.name}`, {
|
|
isPersonal: true,
|
|
isHelp: true,
|
|
isPrivate: 'hidden',
|
|
modjoin: '%',
|
|
auth: {[user.id]: '+'},
|
|
introMessage: introMsg,
|
|
staffMessage: staffMessage + staffHint + reportTargetInfo,
|
|
});
|
|
helpRoom.game = new HelpTicket(helpRoom, ticket);
|
|
} else {
|
|
helpRoom.pokeExpireTimer();
|
|
helpRoom.settings.introMessage = introMsg;
|
|
helpRoom.settings.staffMessage = staffMessage + staffHint + reportTargetInfo;
|
|
if (helpRoom.game) helpRoom.game.destroy();
|
|
helpRoom.game = new HelpTicket(helpRoom, ticket);
|
|
}
|
|
const ticketGame = helpRoom.getGame(HelpTicket)!;
|
|
helpRoom.modlog({action: 'TICKETOPEN', isGlobal: true, loggedBy: user.id, note: ticket.type});
|
|
ticketGame.addText(`${user.name} opened a new ticket. Issue: ${ticket.type}`, user);
|
|
this.parse(`/join help-${user.id}`);
|
|
if (!(user.id in ticketGame.playerTable)) {
|
|
// User was already in the room, manually add them to the "game" so they get a popup if they try to leave
|
|
ticketGame.addPlayer(user);
|
|
}
|
|
if (contexts[ticket.type]) {
|
|
helpRoom.add(`|c|&Staff|${this.tr(contexts[ticket.type])}`);
|
|
helpRoom.update();
|
|
}
|
|
if (pmRequestButton) {
|
|
helpRoom.add(pmRequestButton);
|
|
helpRoom.update();
|
|
}
|
|
tickets[user.id] = ticket;
|
|
writeTickets();
|
|
notifyStaff();
|
|
connection.send(`>view-help-request\n|deinit`);
|
|
},
|
|
|
|
list(target, room, user) {
|
|
this.checkCan('lock');
|
|
this.parse('/join view-help-tickets');
|
|
},
|
|
listhelp: [`/helpticket list - Lists all tickets. Requires: % @ &`],
|
|
|
|
stats(target, room, user) {
|
|
this.checkCan('lock');
|
|
this.parse('/join view-help-stats');
|
|
},
|
|
statshelp: [`/helpticket stats - List the stats for help tickets. Requires: % @ &`],
|
|
|
|
close(target, room, user) {
|
|
if (!target) return this.parse(`/help helpticket close`);
|
|
let result = !(this.splitTarget(target) === 'false');
|
|
const ticket = tickets[toID(this.inputUsername)];
|
|
if (!ticket || !ticket.open || (ticket.userid !== user.id && !user.can('lock'))) {
|
|
return this.errorReply(this.tr`${this.inputUsername} does not have an open ticket.`);
|
|
}
|
|
const helpRoom = Rooms.get(`help-${ticket.userid}`) as ChatRoom | null;
|
|
if (helpRoom) {
|
|
const ticketGame = helpRoom.getGame(HelpTicket)!;
|
|
if (ticket.userid === user.id && !user.isStaff) {
|
|
result = !!(ticketGame.firstClaimTime);
|
|
}
|
|
ticketGame.close(result, user);
|
|
} else {
|
|
ticket.open = false;
|
|
notifyStaff();
|
|
writeTickets();
|
|
}
|
|
ticket.claimed = user.name;
|
|
this.sendReply(`You closed ${ticket.creator}'s ticket.`);
|
|
},
|
|
closehelp: [`/helpticket close [user] - Closes an open ticket. Requires: % @ &`],
|
|
|
|
ban(target, room, user) {
|
|
if (!target) return this.parse('/help helpticket ban');
|
|
target = this.splitTarget(target, true);
|
|
const targetUser = this.targetUser;
|
|
this.checkCan('lock', targetUser);
|
|
|
|
const punishment = Punishments.roomUserids.nestedGet('staff', toID(this.targetUsername));
|
|
if (!targetUser && !Punishments.search(toID(this.targetUsername)).length) {
|
|
return this.errorReply(this.tr`User '${this.targetUsername}' not found.`);
|
|
}
|
|
if (target.length > 300) {
|
|
return this.errorReply(this.tr`The reason is too long. It cannot exceed 300 characters.`);
|
|
}
|
|
|
|
let username;
|
|
let userid;
|
|
|
|
if (targetUser) {
|
|
username = targetUser.getLastName();
|
|
userid = targetUser.getLastId();
|
|
if (punishment) {
|
|
return this.privateModAction(`${username} would be ticket banned by ${user.name} but was already ticket banned.`);
|
|
}
|
|
if (targetUser.trusted) {
|
|
Monitor.log(`[CrisisMonitor] Trusted user ${targetUser.name}${(targetUser.trusted !== targetUser.id ? ` (${targetUser.trusted})` : ``)} was ticket banned by ${user.name}, and should probably be demoted.`);
|
|
}
|
|
} else {
|
|
username = this.targetUsername;
|
|
userid = toID(this.targetUsername);
|
|
if (punishment) {
|
|
return this.privateModAction(`${username} would be ticket banned by ${user.name} but was already ticket banned.`);
|
|
}
|
|
}
|
|
|
|
if (targetUser) {
|
|
targetUser.popup(`|modal|${user.name} has banned you from creating help tickets.${(target ? `\n\nReason: ${target}` : ``)}\n\nYour ban will expire in a few days.`);
|
|
}
|
|
|
|
const affected = HelpTicket.ban(userid, target);
|
|
this.addModAction(`${username} was ticket banned by ${user.name}.${target ? ` (${target})` : ``}`);
|
|
const acAccount = (targetUser && targetUser.autoconfirmed !== userid && targetUser.autoconfirmed);
|
|
let displayMessage = '';
|
|
if (affected.length > 1) {
|
|
displayMessage = `${username}'s ${acAccount ? ` ac account: ${acAccount}, ` : ""}ticket banned alts: ${affected.slice(1).map(userObj => userObj.getLastName()).join(", ")}`;
|
|
this.privateModAction(displayMessage);
|
|
} else if (acAccount) {
|
|
displayMessage = `${username}'s ac account: ${acAccount}`;
|
|
this.privateModAction(displayMessage);
|
|
}
|
|
|
|
this.globalModlog(`TICKETBAN`, targetUser || userid, target);
|
|
for (const userObj of affected) {
|
|
const userObjID = (typeof userObj !== 'string' ? userObj.getLastId() : toID(userObj));
|
|
const targetTicket = tickets[userObjID];
|
|
if (targetTicket?.open) targetTicket.open = false;
|
|
const helpRoom = Rooms.get(`help-${userObjID}`);
|
|
if (helpRoom) {
|
|
const ticketGame = helpRoom.getGame(HelpTicket)!;
|
|
ticketGame.writeStats('ticketban');
|
|
helpRoom.destroy();
|
|
}
|
|
}
|
|
writeTickets();
|
|
notifyStaff();
|
|
return true;
|
|
},
|
|
banhelp: [`/helpticket ban [user], (reason) - Bans a user from creating tickets for 2 days. Requires: % @ &`],
|
|
|
|
unban(target, room, user) {
|
|
if (!target) return this.parse('/help helpticket unban');
|
|
|
|
this.checkCan('lock');
|
|
const targetUser = Users.get(target, true);
|
|
if (!targetUser) return this.errorReply(`User not found.`);
|
|
const banned = HelpTicket.checkBanned(targetUser);
|
|
if (!banned) {
|
|
return this.errorReply(this.tr`${targetUser ? targetUser.name : target} is not ticket banned.`);
|
|
}
|
|
|
|
const affected = HelpTicket.unban(target);
|
|
this.addModAction(`${affected} was ticket unbanned by ${user.name}.`);
|
|
this.globalModlog("UNTICKETBAN", toID(target));
|
|
if (targetUser) targetUser.popup(`${user.name} has ticket unbanned you.`);
|
|
},
|
|
unbanhelp: [`/helpticket unban [user] - Ticket unbans a user. Requires: % @ &`],
|
|
|
|
ignore(target, room, user) {
|
|
this.checkCan('lock');
|
|
if (user.settings.ignoreTickets) {
|
|
return this.errorReply(this.tr`You are already ignoring help ticket notifications. Use /helpticket unignore to receive notifications again.`);
|
|
}
|
|
user.settings.ignoreTickets = true;
|
|
user.update();
|
|
this.sendReply(this.tr`You are now ignoring help ticket notifications.`);
|
|
},
|
|
ignorehelp: [`/helpticket ignore - Ignore notifications for unclaimed help tickets. Requires: % @ &`],
|
|
|
|
unignore(target, room, user) {
|
|
this.checkCan('lock');
|
|
if (!user.settings.ignoreTickets) {
|
|
return this.errorReply(this.tr`You are not ignoring help ticket notifications. Use /helpticket ignore to stop receiving notifications.`);
|
|
}
|
|
user.settings.ignoreTickets = false;
|
|
user.update();
|
|
this.sendReply(this.tr`You will now receive help ticket notifications.`);
|
|
},
|
|
unignorehelp: [`/helpticket unignore - Stop ignoring notifications for help tickets. Requires: % @ &`],
|
|
|
|
delete(target, room, user) {
|
|
// This is a utility only to be used if something goes wrong
|
|
this.checkCan('makeroom');
|
|
if (!target) return this.parse(`/help helpticket delete`);
|
|
const ticket = tickets[toID(target)];
|
|
if (!ticket) return this.errorReply(this.tr`${target} does not have a ticket.`);
|
|
const targetRoom = Rooms.get(`help-${ticket.userid}`);
|
|
if (targetRoom) {
|
|
targetRoom.getGame(HelpTicket)!.deleteTicket(user);
|
|
} else {
|
|
delete tickets[ticket.userid];
|
|
writeTickets();
|
|
notifyStaff();
|
|
}
|
|
this.sendReply(this.tr`You deleted ${target}'s ticket.`);
|
|
},
|
|
deletehelp: [`/helpticket delete [user] - Deletes a user's ticket. Requires: &`],
|
|
|
|
},
|
|
helptickethelp: [
|
|
`/helpticket create - Creates a new ticket, requesting help from global staff.`,
|
|
`/helpticket list - Lists all tickets. Requires: % @ &`,
|
|
`/helpticket close [user] - Closes an open ticket. Requires: % @ &`,
|
|
`/helpticket ban [user], (reason) - Bans a user from creating tickets for 2 days. Requires: % @ &`,
|
|
`/helpticket unban [user] - Ticket unbans a user. Requires: % @ &`,
|
|
`/helpticket ignore - Ignore notifications for unclaimed help tickets. Requires: % @ &`,
|
|
`/helpticket unignore - Stop ignoring notifications for help tickets. Requires: % @ &`,
|
|
`/helpticket delete [user] - Deletes a user's ticket. Requires: &`,
|
|
],
|
|
};
|
|
|
|
export const punishmentfilter: Chat.PunishmentFilter = (user, punishment) => {
|
|
if (punishment[0] !== 'BAN') return;
|
|
|
|
const helpRoom = Rooms.get(`help-${toID(user)}`);
|
|
if (helpRoom?.game?.gameid !== 'helpticket') return;
|
|
|
|
const ticket = helpRoom.game as HelpTicket;
|
|
ticket.close('ticketban');
|
|
};
|