269 lines
7.3 KiB
TypeScript
269 lines
7.3 KiB
TypeScript
import {
|
|
BaseEntity,
|
|
Brackets,
|
|
Column,
|
|
CreateDateColumn,
|
|
Entity,
|
|
JoinColumn,
|
|
ManyToOne,
|
|
PrimaryGeneratedColumn,
|
|
UpdateDateColumn,
|
|
} from "typeorm";
|
|
import { infoEmbed } from "../bot/Util";
|
|
import { CDN_URL, getDate } from "../config/Constants";
|
|
import { Guild } from "./Guild";
|
|
|
|
@Entity({ name: "appeals" })
|
|
export class Appeal extends BaseEntity {
|
|
@PrimaryGeneratedColumn("uuid")
|
|
id: string;
|
|
|
|
// user data
|
|
@Column()
|
|
userTag: string;
|
|
@Column({ nullable: true })
|
|
userAvatar?: string;
|
|
@Column()
|
|
userID: string;
|
|
|
|
@ManyToOne(() => Guild, (guild) => guild.id, {
|
|
eager: true,
|
|
})
|
|
@JoinColumn()
|
|
guild: Guild;
|
|
|
|
@Column()
|
|
punishmentDate: string;
|
|
|
|
@Column()
|
|
banReason: string;
|
|
|
|
@Column()
|
|
appealReason: string;
|
|
|
|
@Column()
|
|
additionalInfo: string;
|
|
|
|
// Processing -> in_questioning -> questioning_rejoined -> accepted OR rejected
|
|
// Processing -> rejected
|
|
// Processing -> accepted
|
|
@Column()
|
|
status:
|
|
| "processing"
|
|
| "in_questioning"
|
|
| "questioning_rejoined"
|
|
| "accepted"
|
|
| "rejected";
|
|
|
|
@Column()
|
|
message: string;
|
|
|
|
@Column({ nullable: true })
|
|
rejectionExpiry?: Date; // If the user is rejected, the member is unable to apply again until this date passes.
|
|
|
|
@Column({ nullable: true })
|
|
decisionReason?: string;
|
|
|
|
@Column({ nullable: true })
|
|
decisionBy?: string;
|
|
|
|
@CreateDateColumn()
|
|
created: Date;
|
|
|
|
@UpdateDateColumn()
|
|
lastUpdate: Date;
|
|
|
|
constructor(
|
|
userID?: string,
|
|
userTag?: string,
|
|
userAvatar?: string,
|
|
guild?: Guild,
|
|
punishDate?: string,
|
|
banReason?: string,
|
|
appealReason?: string,
|
|
additionalInfo?: string
|
|
) {
|
|
super();
|
|
|
|
if (!(userID && userTag && punishDate && banReason && appealReason)) {
|
|
return;
|
|
}
|
|
|
|
this.userID = userID;
|
|
this.userTag = userTag;
|
|
this.userAvatar = userAvatar;
|
|
this.guild = guild;
|
|
this.punishmentDate = punishDate.trim();
|
|
this.banReason = banReason.trim();
|
|
this.appealReason = appealReason.trim();
|
|
this.additionalInfo = additionalInfo?.trim();
|
|
this.status = "processing";
|
|
}
|
|
|
|
accept(by: string, reason?: string) {
|
|
this.decisionBy = by;
|
|
this.status = "accepted";
|
|
this.decisionReason = reason;
|
|
this.save();
|
|
}
|
|
|
|
reject(by: string, reason?: string) {
|
|
this.decisionBy = by;
|
|
this.status = "rejected";
|
|
this.decisionReason = reason;
|
|
|
|
if (this.guild.defaultExpiry) {
|
|
this.rejectionExpiry = getDate(this.guild.defaultExpiry);
|
|
}
|
|
|
|
this.save();
|
|
}
|
|
|
|
validationMessage(): string {
|
|
if (this.punishmentDate === "") return "Punishment date required.";
|
|
if (this.banReason === "") return "Ban Reason Required.";
|
|
if (this.appealReason === "") return "Appeal Reason required.";
|
|
|
|
if (this.punishmentDate.length > 128)
|
|
return "Punishment date length must be < 128.";
|
|
if (this.banReason.length > 256)
|
|
return "Ban reason length must be < 256 characters.";
|
|
if (this.appealReason.length > 1000)
|
|
return "Appeal reason length must be < 1000.";
|
|
if (this.additionalInfo.length > 1000)
|
|
return "Additional info length must be < 1000.";
|
|
|
|
return null;
|
|
}
|
|
|
|
embed(full?: boolean) {
|
|
const embed = infoEmbed(`Appeal - ${this.status}`)
|
|
.setThumbnail(this.avatar())
|
|
.addField("\u200B", "\u200B")
|
|
.setFooter("Appeal ID: " + this.id);
|
|
|
|
if (this.lastUpdate) {
|
|
embed.addField("Last Updated", this.lastUpdate.toLocaleString());
|
|
}
|
|
|
|
if (full) {
|
|
embed
|
|
.addField("Punishment Date", this.punishmentDate)
|
|
.addField("Ban Reason", this.banReason)
|
|
.addField("Appeal Reason", this.appealReason)
|
|
.addField("Additional Info", this.additionalInfo || "*None provided*");
|
|
} else {
|
|
embed.addField("Full Message", `[Link](${this.link()})`);
|
|
}
|
|
|
|
if (["accepted", "rejected"].includes(this.status)) {
|
|
embed.fields[0] = {
|
|
name: "Decision By",
|
|
value: `<@${this.decisionBy}>`,
|
|
inline: false,
|
|
};
|
|
}
|
|
|
|
if (this.decisionReason) {
|
|
let decision = this.decisionReason.substr(0, full ? 1000 : 400);
|
|
if (decision !== this.decisionReason) decision += "\n\n*Message trimmed*";
|
|
embed.addField("Decision Note", decision);
|
|
}
|
|
|
|
let desc = `User: ${this.userTag}
|
|
User ID: ${this.userID}
|
|
Status: ${this.status}\n`;
|
|
|
|
if (this.status === "rejected") {
|
|
desc += `User re-appeal date: ${
|
|
this.rejectionExpiry?.toLocaleString() || "Never"
|
|
}\n`;
|
|
}
|
|
|
|
embed.setDescription(`${"```fix\n"}${desc}${"```"}`);
|
|
return embed;
|
|
}
|
|
|
|
link() {
|
|
return `https://discord.com/channels/${this.guild.guild}/${this.guild.channel}/${this.message}`;
|
|
}
|
|
|
|
avatar() {
|
|
const discrim = this.userTag.split("#")[1];
|
|
if (!this.userAvatar) {
|
|
return `${CDN_URL}/embed/avatars/${Number(discrim) % 5}.png`;
|
|
}
|
|
return `${CDN_URL}/avatars/${this.userID}/${this.userAvatar}.png`;
|
|
}
|
|
|
|
/**
|
|
* activeAppeals queries the database for all appeals that are currently
|
|
* active for a specifc userID in a guild.
|
|
*
|
|
* @param {string} userID The userID to query for.
|
|
* @param {string} guildID The guildID to query for.
|
|
*/
|
|
static async activeAppeals(userID: string, guild: string) {
|
|
return await Appeal.createQueryBuilder("appeals")
|
|
// where limits the appeals guild and user
|
|
.where("appeals.userID = :userID", { userID })
|
|
.andWhere("appeals.guildId = :guildID", { guildID: guild })
|
|
.andWhere(
|
|
new Brackets((qb) =>
|
|
qb
|
|
// if it's accepted or rejected, it's not active anymore
|
|
.where("appeals.status NOT IN (:...statuses)", {
|
|
statuses: ["accepted", "rejected"],
|
|
})
|
|
.orWhere(
|
|
new Brackets((qb) =>
|
|
qb
|
|
// however, if the appeal is rejected but the rejection hasn't expired, we still consider the appeal to be active
|
|
.where(`appeals.status = "rejected"`)
|
|
.andWhere(
|
|
"appeals.rejectionExpiry IS NULL OR appeals.rejectionExpiry > datetime(:now)",
|
|
{ now: new Date().toISOString() }
|
|
)
|
|
)
|
|
)
|
|
)
|
|
)
|
|
.orderBy("appeals.created")
|
|
.getMany();
|
|
}
|
|
|
|
/**
|
|
* allAppeals gets all appeals for a specific userID in a guild.
|
|
*
|
|
* @param {string} userID The userID to query for.
|
|
* @param {string} guildID The guildID to query for. If not provided, will query for all appeals.
|
|
*/
|
|
static async allAppeals(userID: string, guild?: string) {
|
|
const query = Appeal.createQueryBuilder("appeals")
|
|
.where("appeals.userID = :userID", { userID })
|
|
.leftJoinAndSelect("appeals.guild", "guilds")
|
|
.orderBy("appeals.created", "DESC");
|
|
|
|
// Conditionally add guild filter
|
|
if (guild) query.andWhere("appeals.guildId = :guild", { guild });
|
|
|
|
return await query.getMany();
|
|
}
|
|
|
|
/**
|
|
* appeal gets a specific appeal, and will return a rejected promise if the appeal is not found.
|
|
* While the appeal ID is not a unique identifier, also specifying the
|
|
* guildID is done so staff can't accidentally query appeals from other
|
|
* guilds.
|
|
*/
|
|
static async appeal(id: string, guild?: string) {
|
|
const query = Appeal.createQueryBuilder("appeals")
|
|
.where("appeals.id = :id", { id })
|
|
.leftJoinAndSelect("appeals.guild", "guilds");
|
|
|
|
if (guild) query.andWhere("guilds.guild = :guild", { guild });
|
|
|
|
return await query.getOne();
|
|
}
|
|
}
|