contrition/src/entity/Appeal.ts

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();
}
}