"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var teams_exports = {};
__export(teams_exports, {
TeamsHandler: () => TeamsHandler,
commands: () => commands,
destroy: () => destroy,
pages: () => pages
});
module.exports = __toCommonJS(teams_exports);
var import_lib = require("../../lib");
const MAX_TEAMS = 200;
const MAX_SEARCH = 3e3;
const ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz".split("");
function refresh(context) {
return ``;
}
const TeamsHandler = new class {
constructor() {
this.database = new import_lib.PostgresDatabase();
this.readyPromise = Config.usepostgres ? (async () => {
try {
await this.database.query("SELECT * FROM teams LIMIT 1");
} catch {
await this.database.query((0, import_lib.FS)(`databases/schemas/teams.sql`).readSync());
}
})() : null;
}
destroy() {
void this.database.destroy();
}
async search(search, user, count = 10, includePrivate = false) {
const args = [];
const where = [];
if (count > 500) {
throw new Chat.ErrorMessage("Cannot search more than 500 teams.");
}
if (search.format) {
where.push(`format = $${args.length + 1}`);
args.push(toID(search.format));
}
if (search.owner) {
where.push(`ownerid = $${args.length + 1}`);
args.push(toID(search.owner));
}
if (search.gen) {
where.push(`format LIKE 'gen${search.gen}%'`);
}
if (!includePrivate)
where.push("private IS NULL");
const result = await this.query(
`SELECT * FROM teams${where.length ? ` WHERE ${where.join(" AND ")}` : ""} ORDER BY date DESC LIMIT ${count}`,
args
);
return result.filter((row) => {
const team = Teams.unpack(row.team);
if (row.private && row.ownerid !== user.id) {
return false;
}
let match = true;
if (search.pokemon?.length) {
match = search.pokemon.some(
(pokemon) => team.some((set) => toID(set.species) === toID(pokemon))
);
}
if (!match)
return false;
if (search.moves?.length) {
match = search.moves.some(
(move) => team.some((set) => set.moves.some((m) => toID(m) === toID(move)))
);
}
if (!match)
return false;
if (search.abilities?.length) {
match = search.abilities.some(
(ability) => team.some((set) => toID(set.ability) === toID(ability))
);
}
return match;
});
}
async query(statement, values = []) {
if (this.readyPromise)
await this.readyPromise;
return this.database.query(statement, values);
}
async save(context, formatName, rawTeam, teamName = null, isPrivate, isUpdate) {
const connection = context.connection;
this.validateAccess(connection, true);
if (Monitor.countPrepBattle(connection.ip, connection)) {
return null;
}
const user = connection.user;
const format = Dex.formats.get(toID(formatName));
if (!format.exists || format.team) {
connection.popup("Invalid format:\n\n" + formatName);
return null;
}
let existing = null;
if (isUpdate) {
existing = await this.get(isUpdate);
if (!existing) {
connection.popup("You're trying to edit a team that doesn't exist.");
return null;
}
if (context.user.id !== existing.ownerid) {
connection.popup("This is not your team.");
return null;
}
}
const team = Teams.import(rawTeam, true);
if (!team) {
connection.popup("Invalid team:\n\n" + rawTeam);
return null;
}
if (team.length > 24) {
connection.popup("Your team has too many Pokemon.");
}
let unownWord = "";
for (const set of team) {
const namedSpecies = Dex.species.get(set.name);
if (!namedSpecies.exists) {
set.name = set.species;
}
if (!Dex.species.get(set.species).exists) {
connection.popup(`Invalid Pokemon ${set.species} in team.`);
return null;
}
const speciesid = toID(set.species);
if (speciesid.length <= 6 && speciesid.startsWith("unown")) {
unownWord += speciesid.charAt(5) || "a";
}
if (set.moves.length > 24) {
connection.popup("Only 24 moves are allowed per set.");
return null;
}
for (const m of set.moves) {
if (!Dex.moves.get(m).exists) {
connection.popup(`Invalid move ${m} on ${set.species}.`);
return null;
}
}
if (toID(set.ability) === "none") {
set.ability = "No Ability";
}
if (set.ability && !Dex.abilities.get(set.ability).exists) {
connection.popup(`Invalid ability ${set.ability} on ${set.species}.`);
return null;
}
if (set.item && !Dex.items.get(set.item).exists) {
connection.popup(`Invalid item ${set.item} on ${set.species}.`);
return null;
}
if (set.nature && !Dex.natures.get(set.nature).exists) {
connection.popup(`Invalid nature ${set.nature} on ${set.species}.`);
return null;
}
if (set.teraType && !Dex.types.get(set.teraType).exists) {
connection.popup(`Invalid Tera Type ${set.nature} on ${set.species}.`);
return null;
}
}
if (unownWord) {
const filtered = Chat.nicknamefilter(unownWord, user);
if (!filtered || filtered !== unownWord) {
connection.popup(
`Your team was rejected for the following reason:
- Your Unowns spell out a banned word: ${unownWord.toUpperCase()}`
);
return null;
}
}
if (teamName) {
if (teamName.length > 100) {
connection.popup("Your team's name is too long.");
return null;
}
const filtered = context.filter(teamName);
if (!filtered || filtered?.trim() !== teamName.trim()) {
connection.popup(`Your team's name has a filtered word.`);
return null;
}
}
const count = await this.count(user);
if (count >= MAX_TEAMS) {
connection.popup(`You have too many teams stored. If you wish to upload this team, delete some first.`);
return null;
}
rawTeam = Teams.pack(team);
if (isUpdate && existing) {
const differenceExists = existing.team !== rawTeam || teamName && teamName !== existing.title || format.id !== existing.format || existing.private !== isPrivate;
if (!differenceExists) {
connection.popup("Your team was not saved as no changes were made.");
return null;
}
await this.query(
"UPDATE teams SET team = $1, title = $2, private = $3, format = $4 WHERE teamid = $5",
[rawTeam, teamName, isPrivate, format.id, isUpdate]
);
return isUpdate;
} else {
const exists = await this.query("SELECT * FROM teams WHERE ownerid = $1 AND team = $2", [user.id, rawTeam]);
if (exists.length) {
connection.popup("You've already uploaded that team.");
return null;
}
const loaded = await this.query(
`INSERT INTO teams (ownerid, team, date, format, views, title, private) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING teamid`,
[user.id, rawTeam, new Date(), format.id, 0, teamName, isPrivate]
);
return loaded?.[0].teamid;
}
}
generatePassword(len = 20) {
let pw = "";
for (let i = 0; i < len; i++)
pw += ALPHABET[Math.floor(Math.random() * ALPHABET.length)];
return pw;
}
updateViews(teamid) {
return this.query(`UPDATE teams SET views = views + 1 WHERE teamid = $1`, [teamid]);
}
list(userid, count, publicOnly = false) {
let query = `SELECT * FROM teams WHERE ownerid = $1 `;
if (publicOnly) {
query += `AND private IS NULL `;
}
query += `ORDER BY date DESC LIMIT $2`;
return this.query(
query,
[userid, count]
);
}
preview(teamData, user, isFull = false) {
let buf = import_lib.Utils.html`${teamData.title || `Untitled ${teamData.teamid}`}`;
if (teamData.private)
buf += ` (Private)`;
buf += `
`;
buf += `Uploaded by: ${teamData.ownerid}
`;
buf += `Uploaded on: ${Chat.toTimestamp(teamData.date, { human: true })}
`;
buf += `Format: ${Dex.formats.get(teamData.format).name}
`;
buf += `Views: ${teamData.views === -1 ? 0 : teamData.views}`;
const team = Teams.unpack(teamData.team);
let link = `view-team-${teamData.teamid}`;
if (teamData.private) {
link += `-${teamData.private}`;
}
buf += `
`;
buf += team.map((set) => ``).join(" ");
buf += `
${isFull ? "View full team" : "Shareable link to team"}`;
buf += ` (or copy/paste <<${link}>> in chat to share!)`;
if (user && (teamData.ownerid === user.id || user.can("rangeban"))) {
buf += `
`;
buf += `Manage (edit/delete/etc)
`;
buf += `
`;
buf += `
`;
buf += ``;
buf += ` `;
}
return buf;
}
renderTeam(teamData, user) {
let buf = this.preview(teamData, user, true);
buf += `
`;
const team = Teams.unpack(teamData.team);
buf += team.map((set) => {
let teamBuf = Teams.exportSet(set).replace(/\n/g, "
");
if (set.name && set.name !== set.species) {
teamBuf = teamBuf.replace(set.name, import_lib.Utils.html`
${set.name}`);
} else {
teamBuf = teamBuf.replace(set.species, `
${set.species}`);
}
if (set.item) {
const tester = new RegExp(`${import_lib.Utils.escapeRegex(set.item)}\\b`);
teamBuf = teamBuf.replace(tester, `${set.item} `);
}
return teamBuf;
}).join("
");
return buf;
}
validateAccess(conn, popup = false) {
const user = conn.user;
if (!user)
throw new Chat.Interruption();
const err = (message) => {
if (popup) {
conn.popup(message);
throw new Chat.Interruption();
}
throw new Chat.ErrorMessage(message);
};
if (!Config.usepostgres || !Config.usepostgresteams) {
err(`The teams database is currently disabled.`);
}
if (!Users.globalAuth.atLeast(user, Config.usepostgresteams)) {
err("You cannot currently use the teams database.");
}
if (user.locked || user.semilocked)
err("You cannot use the teams database while locked.");
if (!user.autoconfirmed)
err("You must be autoconfirmed to use the teams database.");
}
async count(user) {
const id = toID(user);
const result = await this.query(`SELECT count(*) AS count FROM teams WHERE ownerid = $1`, [id]);
return result?.[0]?.count || 0;
}
async get(teamid) {
teamid = Number(teamid);
if (isNaN(teamid)) {
throw new Chat.ErrorMessage(`Invalid team ID.`);
}
const rows = await this.query(
`SELECT * FROM teams WHERE teamid = $1`,
[teamid]
);
if (!rows.length)
return null;
return rows[0];
}
async delete(id) {
id = Number(id);
if (isNaN(id)) {
throw new Chat.ErrorMessage("Invalid team ID");
}
await this.query(
`DELETE FROM teams WHERE teamid = $1`,
[id]
);
}
}();
const destroy = () => TeamsHandler.destroy();
const commands = {
teams: {
upload() {
return this.parse("/j view-teams-upload");
},
update: "save",
async save(target, room, user, connection, cmd) {
TeamsHandler.validateAccess(connection, true);
const targets = import_lib.Utils.splitFirst(target, ",", 5);
const isEdit = cmd === "update";
const rawTeamID = isEdit ? targets.shift() : void 0;
let [teamName, formatid, rawPrivacy, rawTeam] = targets;
const teamID = Number(rawTeamID);
if (isEdit && (!rawTeamID?.length || isNaN(teamID))) {
connection.popup("Invalid team ID provided.");
return null;
}
if (rawTeam.includes("\n")) {
rawTeam = Teams.pack(Teams.import(rawTeam, true));
}
if (!rawTeam) {
connection.popup("Invalid team.");
return null;
}
formatid = toID(formatid);
teamName = toID(teamName) ? teamName : null;
const privacy = toID(rawPrivacy) === "1" ? TeamsHandler.generatePassword() : null;
const id = await TeamsHandler.save(
this,
formatid,
rawTeam,
teamName,
privacy,
isEdit ? teamID : void 0
);
const page = isEdit ? "edit" : "upload";
if (id) {
connection.send(`|queryresponse|teamupload|` + JSON.stringify({ teamid: id, teamName }));
connection.send(`>view-teams-${page}
|deinit`);
this.parse(`/join view-teams-view-${id}-${id}`);
} else {
this.parse(`/join view-teams-${page}`);
return;
}
},
""(target) {
return this.parse("/teams user " + toID(target) || this.user.id);
},
latest() {
return this.parse(`/j view-teams-filtered-latest`);
},
views: "mostviews",
mostviews() {
return this.parse(`/j view-teams-filtered-views`);
},
user: "view",
for: "view",
view(target) {
const [name, rawNum] = target.split(",").map(toID);
const num = parseInt(rawNum);
if (rawNum && isNaN(num)) {
return this.popupReply(`Invalid count.`);
}
let page = "view";
switch (this.cmd) {
case "for":
case "user":
page = "all";
break;
}
return this.parse(`/j view-teams-${page}-${toID(name)}${num ? `-${num}` : ""}`);
},
async delete(target, room, user, connection) {
TeamsHandler.validateAccess(connection, true);
const teamid = Number(toID(target));
if (isNaN(teamid))
return this.popupReply(`Invalid team ID.`);
const teamData = await TeamsHandler.get(teamid);
if (!teamData)
return this.popupReply(`Team not found.`);
if (teamData.ownerid !== user.id && !user.can("rangeban")) {
return this.errorReply("You cannot delete teams you do not own.");
}
await TeamsHandler.delete(teamid);
this.popupReply(`Team ${teamid} deleted.`);
for (const page of connection.openPages || /* @__PURE__ */ new Set()) {
if (page.startsWith("teams-"))
this.refreshPage(page);
}
},
async setprivacy(target, room, user, connection) {
TeamsHandler.validateAccess(connection, true);
const [teamId, rawPrivacy] = target.split(",").map(toID);
let privacy;
if (!teamId.length) {
return this.popupReply("Invalid team ID.");
}
if (this.meansYes(rawPrivacy)) {
privacy = TeamsHandler.generatePassword();
} else if (this.meansNo(rawPrivacy)) {
privacy = null;
} else {
return this.popupReply(`Invalid privacy setting.`);
}
const team = await TeamsHandler.get(teamId);
if (!team)
return this.popupReply(`Team not found.`);
if (team.ownerid !== user.id && !user.can("rangeban")) {
return this.popupReply(`You cannot change privacy for a team you don't own.`);
}
await TeamsHandler.query(`UPDATE teams SET private = $1 WHERE teamid = $2`, [privacy, teamId]);
for (const pageid of this.connection.openPages || /* @__PURE__ */ new Set()) {
if (pageid.startsWith("teams-")) {
this.refreshPage(pageid);
}
}
return this.popupReply(privacy ? `Team set to private. Password: ${privacy}` : `Team set to public.`);
},
search(target, room, user) {
return this.parse(`/j view-teams-searchpersonal`);
},
browse(target, room, user) {
return this.parse(`/j view-teams-browse${target ? `-${target}` : ""}`);
},
help() {
return this.parse("/help teams");
}
},
teamshelp: [
`/teams OR /teams for [user]- View the (public) teams of the given [user].`,
`/teams upload - Open the page to upload a team.`,
`/teams setprivacy [team id], [privacy] - Set the privacy of the team matching the [teamid].`,
`/teams delete [team id] - Delete the team matching the [teamid].`,
`/teams search - Opens the page to search your teams`,
`/teams mostviews - Views public teams, sorted by most views.`,
`/teams view [team ID] - View the team matching the given [team ID]`,
`/teams browse - Opens a list of public teams uploaded by other users.`
]
};
const pages = {
// support view-team-${teamid}
team(query, user, connection) {
return pages.teams.view.call(this, query, user, connection);
},
teams: {
async all(query, user, connection) {
if (!user.named)
return Rooms.RETRY_AFTER_LOGIN;
TeamsHandler.validateAccess(connection);
const targetUserid = toID(query.shift()) || user.id;
let count = Number(query.shift()) || 10;
if (count > MAX_TEAMS)
count = MAX_TEAMS;
this.title = `[Teams] ${targetUserid}`;
const teams = await TeamsHandler.list(targetUserid, count, user.id !== targetUserid);
let buf = `${targetUserid}'s last ${Chat.count(count, "teams")}
`;
buf += refresh(this);
buf += `
Search your teams `;
buf += `
Browse public teams`;
if (targetUserid === user.id) {
buf += `
Upload new`;
}
buf += `
`;
for (const team of teams) {
buf += TeamsHandler.preview(team, user);
buf += `
`;
}
const total = await TeamsHandler.count(user.id);
if (total > count) {
buf += `
`;
}
return buf;
},
async filtered(query, user, connection) {
if (!user.named)
return Rooms.RETRY_AFTER_LOGIN;
const type = query.shift() || "";
TeamsHandler.validateAccess(connection);
let count = Number(query.shift()) || 50;
if (count > MAX_TEAMS)
count = MAX_TEAMS;
let teams = [], title = "";
const buttons = {
views: `
`,
latest: `
`
};
switch (type) {
case "views":
this.title = `[Most Viewed Teams]`;
teams = await TeamsHandler.query(
`SELECT * FROM teams WHERE private IS NULL ORDER BY views DESC LIMIT $1`,
[count]
);
title = `Most viewed teams:`;
delete buttons.views;
break;
default:
this.title = `[Latest Teams]`;
teams = await TeamsHandler.query(
`SELECT * FROM teams WHERE private IS NULL ORDER BY date DESC LIMIT $1`,
[count]
);
title = `Recently uploaded teams:`;
delete buttons.latest;
break;
}
let buf = `
${title}
${refresh(this)}`;
buf += Object.values(buttons).join("
");
buf += `
`;
buf += teams.map((team) => TeamsHandler.preview(team, user)).join("
");
buf += ``;
return buf;
},
async view(query, user, connection) {
if (!user.named)
return Rooms.RETRY_AFTER_LOGIN;
TeamsHandler.validateAccess(connection);
const rawTeamid = toID(query.shift() || "");
const password = toID(query.shift());
this.title = `[View Team]`;
const teamid = Number(rawTeamid);
if (isNaN(teamid)) {
throw new Chat.ErrorMessage(`Invalid team ID.`);
}
const team = await TeamsHandler.get(teamid);
if (!team) {
this.title = `[Invalid Team]`;
return this.errorReply(`No team with the ID ${teamid} was found.`);
}
if (team?.private && user.id !== team.ownerid && password !== team.private) {
this.title = `[Private Team]`;
return this.errorReply(`That team is private.`);
}
this.title = `[Team] ${team.teamid}`;
if (user.id !== team.ownerid && team.views >= 0) {
void TeamsHandler.updateViews(team.teamid);
}
return `
` + TeamsHandler.renderTeam(team, user) + "
";
},
upload(query, user, connection) {
if (!user.named)
return Rooms.RETRY_AFTER_LOGIN;
TeamsHandler.validateAccess(connection);
this.title = `[Upload Team]`;
let buf = `
Upload a team
${refresh(this)}
`;
buf += ``;
return buf;
},
async edit(query, user, connection) {
if (!user.named)
return Rooms.RETRY_AFTER_LOGIN;
TeamsHandler.validateAccess(connection);
const teamID = toID(query.shift() || "");
if (!teamID.length) {
return this.errorReply(`Invalid team ID.`);
}
this.title = `[Edit Team] ${teamID}`;
const data = await TeamsHandler.get(teamID);
if (!data) {
return this.errorReply(`Team ${teamID} not found.`);
}
let buf = `
Edit team ${teamID}
${refresh(this)}
`;
buf += ``;
return buf;
},
async searchpublic(query, user, connection) {
if (!user.named)
return Rooms.RETRY_AFTER_LOGIN;
TeamsHandler.validateAccess(connection, true);
this.title = "[Teams] Search";
let buf = '
';
buf += refresh(this);
buf += "
Search all teams";
const type = this.pageid.split("-")[2];
const isPersonal = type === "searchpersonal";
query = query.join("-").split("--");
if (!query.map(toID).filter(Boolean).length || isPersonal && query.length === 1) {
buf += `
`;
buf += `