/** * A sample teams plugin * * @author Kris */ import {FS, Utils} from "../../lib"; const SAMPLE_TEAMS = 'config/chat-plugins/sample-teams.json'; interface SampleTeamsData { whitelist: {[formatid: string]: RoomID[]}; /** Teams are stored in the packed format */ teams: { [formatid: string]: { uncategorized: {[k: string]: string}, [category: string]: {[teamName: string]: string}, }, }; } export const teamData: SampleTeamsData = (() => { try { return JSON.parse(FS(SAMPLE_TEAMS).readIfExistsSync()); } catch { return { whitelist: {}, teams: {}, }; } })(); function save() { FS(SAMPLE_TEAMS).writeUpdate(() => JSON.stringify(teamData)); } for (const formatid in teamData.teams) { if (!teamData.teams[formatid].uncategorized) teamData.teams[formatid].uncategorized = {}; } save(); export const SampleTeams = new class SampleTeams { private isRoomStaff(user: User, roomids: RoomID[]) { let matched = false; if (!roomids?.length) return false; for (const roomid of roomids) { const room = Rooms.search(roomid); // Malformed entry from botched room rename if (!room) continue; matched = room.auth.isStaff(user.id); if (matched) break; } return matched; } isDevMod(user: User) { return !!Rooms.get('development')?.auth.atLeast(user, '@'); } checkPermissions(user: User, roomids: RoomID[]) { // Give Development room mods access to help fix crashes return this.isRoomStaff(user, roomids) || user.can('bypassall') || this.isDevMod(user); } whitelistedRooms(formatid: string, names = false) { formatid = this.sanitizeFormat(formatid); if (!teamData.whitelist[formatid]?.length) return null; return Utils.sortBy(teamData.whitelist[formatid], (x) => { if (!names) return x; const room = Rooms.search(x); if (!room) return x; return room.title; }); } whitelistRooms(formatids: string[], roomids: string[]) { for (const unsanitizedFormatid of formatids) { const formatid = this.sanitizeFormat(unsanitizedFormatid); if (!teamData.whitelist[formatid]) teamData.whitelist[formatid] = []; for (const roomid of roomids) { const targetRoom = Rooms.search(roomid); if (!targetRoom?.persist) { throw new Chat.ErrorMessage(`Room ${roomid} not found. Check spelling?`); } if (teamData.whitelist[formatid].includes(targetRoom.roomid)) { throw new Chat.ErrorMessage(`Room ${targetRoom.title} is already added.`); } teamData.whitelist[formatid].push(targetRoom.roomid); save(); } } } unwhitelistRoom(formatid: string, roomid: string) { formatid = this.sanitizeFormat(formatid, false); const targetRoom = Rooms.search(roomid); if (!targetRoom?.persist) throw new Chat.ErrorMessage(`Room ${roomid} not found. Check spelling?`); if (!teamData.whitelist[formatid]?.length) throw new Chat.ErrorMessage(`No rooms are whitelisted for ${formatid}.`); if (!teamData.whitelist[formatid].includes(targetRoom.roomid)) { throw new Chat.ErrorMessage(`Room ${targetRoom.title} isn't whitelisted.`); } const index = teamData.whitelist[formatid].indexOf(targetRoom.roomid); teamData.whitelist[formatid].splice(index, 1); if (!teamData.whitelist[formatid].length) delete teamData.whitelist[formatid]; save(); } sanitizeFormat(formatid: string, checkExists = false) { const format = Dex.formats.get(formatid); if (checkExists && !format.exists) { throw new Chat.ErrorMessage(`Format "${formatid.trim()}" not found. Check spelling?`); } if (format.team) { throw new Chat.ErrorMessage(`Formats with computer-generated teams can't have team storage.`); } return format.id; } initializeFormat(formatid: string) { if (!teamData.teams[formatid]) { teamData.teams[formatid] = {uncategorized: {}}; save(); } } addCategory(user: User, formatid: string, category: string) { formatid = this.sanitizeFormat(formatid); if (!this.checkPermissions(user, teamData.whitelist[formatid])) { let rankNeeded = `a global administrator`; if (teamData.whitelist[formatid]) { rankNeeded = `staff in ${Chat.toListString(teamData.whitelist[formatid], "or")}`; } throw new Chat.ErrorMessage(`Access denied. You need to be ${rankNeeded} to add teams for ${formatid}`); } category = category.trim(); this.initializeFormat(formatid); if (this.findCategory(formatid, category)) { throw new Chat.ErrorMessage(`The category named ${category} already exists.`); } teamData.teams[formatid][category] = {}; save(); } removeCategory(user: User, formatid: string, category: string) { formatid = this.sanitizeFormat(formatid, false); if (!this.checkPermissions(user, teamData.whitelist[formatid])) { let rankNeeded = `a global administrator`; if (teamData.whitelist[formatid]) { rankNeeded = `staff in ${Chat.toListString(teamData.whitelist[formatid], "or")}`; } throw new Chat.ErrorMessage(`Access denied. You need to be ${rankNeeded} to add teams for ${formatid}`); } const categoryName = this.findCategory(formatid, category); if (!categoryName) { throw new Chat.ErrorMessage(`There's no category named "${category.trim()}" for the format ${formatid}.`); } delete teamData.teams[formatid][categoryName]; save(); } /** * @param user * @param formatid * @param teamName * @param team - Can be a team in the packed, JSON, or exported format * @param category - Category the team will go in, defaults to uncategorized */ addTeam(user: User, formatid: string, teamName: string, team: string, category = "uncategorized") { formatid = this.sanitizeFormat(formatid); if (!this.checkPermissions(user, teamData.whitelist[formatid])) { let rankNeeded = `a global administrator`; if (teamData.whitelist[formatid]?.length) { rankNeeded = `staff in ${Chat.toListString(teamData.whitelist[formatid], "or")}`; } throw new Chat.ErrorMessage(`Access denied. You need to be ${rankNeeded} to add teams for ${formatid}`); } teamName = teamName.trim(); category = category.trim(); this.initializeFormat(formatid); if (this.findTeamName(formatid, category, teamName)) { throw new Chat.ErrorMessage(`There is already a team for ${formatid} with the name ${teamName} in the ${category} category.`); } if (!teamData.teams[formatid][category]) this.addCategory(user, formatid, category); teamData.teams[formatid][category][teamName] = Teams.pack(Teams.import(team.trim())); save(); return teamData.teams[formatid][category][teamName]; } removeTeam(user: User, formatid: string, teamid: string, category: string) { formatid = this.sanitizeFormat(formatid, false); category = category.trim(); if (!this.checkPermissions(user, teamData.whitelist[formatid])) { let required = `an administrator`; if (teamData.whitelist[formatid]) { required = `staff in ${Chat.toListString(teamData.whitelist[formatid], "or")}`; } throw new Chat.ErrorMessage(`Access denied. You need to be ${required} to add teams for ${formatid}`); } const categoryName = this.findCategory(formatid, category); if (!categoryName) { throw new Chat.ErrorMessage(`There are no teams for ${formatid} under the category ${category.trim()}. Check spelling?`); } const teamName = this.findTeamName(formatid, category, teamid); if (!teamName) { throw new Chat.ErrorMessage(`There is no team for ${formatid} with the name of "${teamid}". Check spelling?`); } const oldTeam = teamData.teams[formatid][categoryName][teamName]; delete teamData.teams[formatid][categoryName][teamName]; if (!Object.keys(teamData.teams[formatid][categoryName]).length) delete teamData.teams[formatid][categoryName]; if (!Object.keys(teamData.teams[formatid]).filter(x => x !== 'uncategorized').length) delete teamData.teams[formatid]; save(); return oldTeam; } formatTeam(teamName: string, teamStr: string, broadcasting = false) { const team = Teams.unpack(teamStr); if (!team) return `Team is not correctly formatted. PM room staff to fix the formatting.`; let buf = ``; if (!broadcasting) { buf += `
${team.map(x => ``).join('')}`; buf += `
${Utils.escapeHTML(teamName.toUpperCase())}
`; buf += Chat.getReadmoreCodeBlock(Teams.export(team).trim()); } else { buf += `
${team.map(x => ``).join('')} – ${Utils.escapeHTML(teamName)}`; buf += `${Teams.export(team).trim().replace(/\n/g, '
')}
`; } return buf; } modlog(context: Chat.CommandContext, formatid: string, action: string, note: string, log: string) { formatid = this.sanitizeFormat(formatid); const whitelistedRooms = this.whitelistedRooms(formatid); if (whitelistedRooms?.length) { for (const roomid of whitelistedRooms) { const room = Rooms.get(roomid); if (!room) continue; context.room = room; context.modlog(action, null, `${formatid}: ${note}`); context.privateModAction(log); } } else { context.room = Rooms.get('staff') || null; context.globalModlog(action, null, `${formatid}: ${note}`); context.privateGlobalModAction(log); } } /** * Returns the category name of the provided category ID if there is one. */ findCategory(formatid: string, categoryid: string) { formatid = toID(formatid); categoryid = toID(categoryid); let match: string | null = null; for (const categoryName in teamData.teams[formatid] || {}) { if (toID(categoryName) === categoryid) { match = categoryName; break; } } return match; } findTeamName(formatid: string, categoryid: string, teamid: string) { const categoryName = this.findCategory(formatid, categoryid); if (!categoryName) return null; let match: string | null = null; for (const teamName in teamData.teams[formatid][categoryName] || {}) { if (toID(teamName) === teamid) { match = teamName; break; } } return match; } getFormatName(formatid: string) { return Dex.formats.get(formatid).exists ? Dex.formats.get(formatid).name : formatid; } destroy() { for (const formatid in teamData.whitelist) { for (const [i, roomid] of teamData.whitelist[formatid].entries()) { const room = Rooms.search(roomid); if (room) continue; teamData.whitelist[formatid].splice(i, 1); if (!teamData.whitelist[formatid].length) delete teamData.whitelist[formatid]; save(); } } } }; export const destroy = SampleTeams.destroy; export const handlers: Chat.Handlers = { onRenameRoom(oldID, newID) { for (const formatid in teamData.whitelist) { if (!SampleTeams.whitelistedRooms(formatid)?.includes(oldID)) continue; SampleTeams.unwhitelistRoom(formatid, oldID); SampleTeams.whitelistRooms([formatid], [newID]); } }, }; export const commands: Chat.ChatCommands = { sampleteams: { ''(target, room, user) { this.runBroadcast(); let [formatid, category] = target.split(','); if (!this.broadcasting) { if (!formatid) return this.parse(`/j view-sampleteams-view`); formatid = SampleTeams.sanitizeFormat(formatid); if (!category) return this.parse(`/j view-sampleteams-view-${formatid}`); const categoryName = SampleTeams.findCategory(formatid, category); return this.parse(`/j view-sampleteams-view-${formatid}${categoryName ? '-' + toID(categoryName) : ''}`); } if (!formatid) return this.parse(`/help sampleteams`); formatid = SampleTeams.sanitizeFormat(formatid); if (!teamData.teams[formatid]) { throw new Chat.ErrorMessage(`No teams for ${SampleTeams.getFormatName(formatid)} found. Check spelling?`); } let buf = ``; if (!category) { for (const categoryName in teamData.teams[formatid]) { if (!Object.keys(teamData.teams[formatid][categoryName]).length) continue; if (buf) buf += `
`; buf += `${categoryName.toUpperCase()}`; for (const [i, teamName] of Object.keys(teamData.teams[formatid][categoryName]).entries()) { if (i) buf += `
`; buf += SampleTeams.formatTeam(teamName, teamData.teams[formatid][categoryName][teamName], true); } buf += ``; } if (!buf) { throw new Chat.ErrorMessage(`No teams for ${SampleTeams.getFormatName(formatid)} found. Check spelling?`); } else { buf = `

Sample Teams for ${SampleTeams.getFormatName(formatid)}


${buf}`; } } else { const categoryName = SampleTeams.findCategory(formatid, category); if (!categoryName) { throw new Chat.ErrorMessage(`No teams for ${SampleTeams.getFormatName(formatid)} in the ${category} category found. Check spelling?`); } for (const teamName in teamData.teams[formatid][categoryName]) { buf += SampleTeams.formatTeam(teamName, teamData.teams[formatid][categoryName][teamName], true); } } this.sendReplyBox(buf); }, addcategory(target, room, user) { let [formatid, categoryName] = target.split(','); if (!(formatid && categoryName)) return this.parse(`/help sampleteams`); if (!categoryName.trim()) categoryName = "uncategorized"; SampleTeams.addCategory(user, formatid, categoryName.trim()); SampleTeams.modlog( this, formatid, 'ADDTEAMCATEGORY', categoryName.trim(), `${user.name} added ${categoryName.trim()} as a category for ${formatid}.` ); this.sendReply(`Added ${categoryName.trim()} as a category for ${formatid}.`); }, removecategory(target, room, user) { const [formatid, categoryName] = target.split(','); if (!(formatid && categoryName)) return this.parse(`/help sampleteams`); if (toID(categoryName) === 'uncategorized') { throw new Chat.ErrorMessage(`The uncategorized category cannot be removed.`); } SampleTeams.removeCategory(user, formatid, categoryName); SampleTeams.modlog( this, formatid, 'REMOVETEAMCATEGORY', categoryName.trim(), `${user.name} removed ${categoryName.trim()} as a category for ${formatid}.` ); this.sendReply(`Removed ${categoryName.trim()} as a category for ${formatid}.`); }, add(target, room, user) { const [formatid, category, teamName, team] = target.split(','); if (!(formatid && category?.trim() && teamName && team)) return this.parse('/j view-sampleteams-add'); const packedTeam = SampleTeams.addTeam(user, formatid, teamName, team, category); SampleTeams.modlog( this, formatid, 'ADDTEAM', `${category}: ${teamName}: ${packedTeam}`, `${user.name} added a team for ${formatid} in the ${category} category.` ); this.sendReply(`Added a team for ${formatid} in the ${category} category.`); }, remove(target, room, user) { const [formatid, category, teamName] = target.split(',').map(x => x.trim()); if (!(formatid && category && teamName)) return this.parse(`/help sampleteams`); const team = SampleTeams.removeTeam(user, formatid, toID(teamName), category); SampleTeams.modlog( this, formatid, 'REMOVETEAM', `${category}: ${teamName}: ${team}`, `${user.name} removed a team from ${formatid} in the ${category} category.` ); this.sendReply(`Removed a team from ${formatid} in the ${category} category.`); }, whitelist: { add(target, room, user) { // Allow development roommods to whitelist to help debug if (!SampleTeams.isDevMod(user)) { this.checkCan('bypassall'); } const [formatids, roomids] = target.split('|').map(x => x.split(',')); if (!(formatids?.length && roomids?.length)) return this.parse(`/help sampleteams`); SampleTeams.whitelistRooms(formatids, roomids); this.privateGlobalModAction(`${user.name} whitelisted ${Chat.toListString(roomids.map(x => Rooms.search(x)!.title))} to handle sample teams for ${Chat.toListString(formatids)}.`); this.globalModlog(`SAMPLETEAMS WHITELIST`, null, roomids.join(', ')); this.sendReply(`Whitelisted ${Chat.toListString(roomids)} to handle sample teams for ${Chat.toListString(formatids)}.`); }, remove(target, room, user) { // Allow development roommods to whitelist to help debug if (!SampleTeams.isDevMod(user)) { this.checkCan('bypassall'); } const [formatid, roomid] = target.split(','); if (!(formatid && roomid)) return this.parse(`/help sampleteams`); SampleTeams.unwhitelistRoom(formatid, roomid); this.refreshPage('sampleteams-whitelist'); }, '': 'view', view(target, room, user) { // Allow development roommods to whitelist to help debug if (!SampleTeams.isDevMod(user)) { this.checkCan('bypassall'); } this.parse(`/j view-sampleteams-whitelist`); }, }, }, sampleteamshelp: [ `/sampleteams [format] - Lists the sample teams for [format], if there are any.`, `/sampleteams addcategory/removecategory [format], [category] - Adds/removes categories for [format].`, `/sampleteams add - Pulls up the interface to add sample teams. Requires: Room staff in dedicated tier room, &`, `/sampleteams remove [format], [category], [team name] - Removes a sample team for [format] in [category].`, `/sampleteams whitelist add [formatid], [formatid] | [roomid], [roomid], ... - Whitelists room staff for the provided roomids to add sample teams. Requires: &`, `/sampleteams whitelist remove [formatid], [roomid] - Unwhitelists room staff for the provided room to add sample teams. Requires: &`, ], }; function formatFakeButton(url: string, text: string) { return `${text}`; } export const pages: Chat.PageTable = { sampleteams: { whitelist(query, user, connection) { this.title = `Sample Teams Whitelist`; if (!(SampleTeams.isDevMod(user) || user.can('bypassall'))) { return `

Access denied.

`; } const staffRoomAccess = Rooms.get('staff')?.checkModjoin(user); let buf = `

Sample Teams Rooms Whitelist

`; if ((!teamData.whitelist || !Object.keys(teamData.whitelist).length) && !query.length) { buf += `

No rooms are whitelisted for any formats.

`; } if (query[0] === 'add') { buf += `
`; buf += `

`; buf += `
`; buf += `
`; buf += `
${formatFakeButton("view-sampleteams-whitelist", "Return to list")}`; } else { buf += `
`; for (const formatid of Object.keys(teamData.whitelist).sort().reverse()) { if (!teamData.whitelist[formatid]?.length) continue; buf += `
${SampleTeams.getFormatName(formatid)}
`; for (const roomid of teamData.whitelist[formatid]) { buf += `
${Rooms.get(roomid)?.title || roomid}`; buf += `
`; } } buf += `
`; } buf += `${query.length ? '' : formatFakeButton("view-sampleteams-whitelist-add", "Whitelist room")}
`; return buf; }, view(query, user, connection) { this.title = `Sample Teams`; let buf = `
`; if (query.slice(0, query.length - 1).join('-')) { buf += `${formatFakeButton(`view-sampleteams-view-${query.slice(0, query.length - 1).join('-')}`, "« Back")}`; } else { buf += ``; } buf += ``; buf += `

Sample Teams


`; const q0Teams = teamData.teams[query[0]]; const q0TeamKeys = q0Teams ? Object.keys(q0Teams) : []; if (!query[0]) { const formats = Object.keys(teamData.teams); if (!formats.length) return `${buf}

No teams found.

`; buf += `

Pick a format

`; } else if (!q0Teams || !q0TeamKeys.length || (!Object.keys(q0Teams.uncategorized).length && !q0TeamKeys.filter(x => x !== 'uncategorized').length)) { const name = Dex.formats.get(query[0]).exists ? Dex.formats.get(query[0]).name : query[0]; return `${buf}

No teams for ${name} were found.

`; } else if (!query[1] || (!SampleTeams.findCategory(query[0], query[1]) && query[1] !== 'allteams')) { buf += `

Pick a category

`; } else if (query[1] === 'allteams') { buf += `

All teams for ${SampleTeams.getFormatName(query[0])}

`; for (const categoryName in teamData.teams[query[0]]) { const category = teamData.teams[query[0]][categoryName]; if (!Object.keys(category).length) continue; buf += `

${categoryName}

`; for (const teamName in category) { const team = category[teamName]; if (SampleTeams.checkPermissions(user, teamData.whitelist[query[0]] || [])) { buf += ``; } buf += SampleTeams.formatTeam(teamName, team); } buf += `
`; const index = Object.keys(teamData.teams[query[0]]).indexOf(categoryName); if (index !== Object.keys(teamData.teams[query[0]]).length - 1) buf += `
`; } } else if (SampleTeams.findCategory(query[0], query[1])) { const categoryName = SampleTeams.findCategory(query[0], query[1])!; buf += `

Sample teams for ${SampleTeams.getFormatName(query[0])} in the ${categoryName} category

`; for (const teamName in teamData.teams[query[0]][categoryName]) { const team = teamData.teams[query[0]][categoryName][teamName]; buf += SampleTeams.formatTeam(teamName, team); } } buf += ``; return buf; }, add(query, user, connection) { this.title = `Sample Teams`; const trimFormatName = (name: string) => (name.includes('(') ? name.slice(0, name.indexOf('(')) : name).trim(); const formatsSet = new Set(Dex.formats.all().map(format => trimFormatName(format.name))); const formatsArr = Array.from(formatsSet); const formats = formatsArr.map(formatName => Dex.formats.get(formatName)) .filter(format => ( !format.name.includes('Custom') && format.effectType === 'Format' && !format.team && SampleTeams.checkPermissions(user, SampleTeams.whitelistedRooms(format.id) || []) )); if (!formats.length) return `

Access denied.

`; let buf = `
`; if (query.slice(0, query.length - 1).join('-')) { buf += `${formatFakeButton(`view-sampleteams-add-${query.slice(0, query.length - 1).join('-')}`, "« Back")}`; } else { buf += ``; } let buttonTitle = 'Refresh'; if (query[2] === 'submit') buttonTitle = 'Add another team'; buf += ``; buf += `

Add a sample team

`; if (!query[0] || !Dex.formats.get(query[0]).exists) { buf += `

Pick a format

    `; for (const format of formats) { buf += `
  • ${formatFakeButton(`view-sampleteams-add-${format.id}`, format.name)}
  • `; } buf += `
`; } else if (!query[1] || !SampleTeams.findCategory(query[0], query[1]) || query[1] === 'addnewcategory') { const name = SampleTeams.getFormatName(query[0]); if (query[1] === 'addnewcategory') { buf += `

Add a category for ${name}

`; buf += `
`; buf += ``; buf += `
`; } else { buf += `

Pick a category for ${name}

    `; for (const category of Object.keys(teamData.teams[query[0]] || {}) || []) { buf += `
  • ${formatFakeButton(`view-sampleteams-add-${query[0]}-${toID(category)}-submit`, category)}
  • `; } buf += `
  • ${formatFakeButton(`view-sampleteams-add-${query[0]}-addnewcategory`, `Add new category`)}
`; } } const categoryName = SampleTeams.findCategory(query[0], query[1]); if (categoryName) { buf += `
`; buf += `

Enter a team name

`; buf += `

Enter team importable


`; buf += `
`; } buf += `
`; return buf; }, }, }; Chat.multiLinePattern.register(`/sampleteams add `);