feat: v1.0.0 contrition
parent
f8375dba6e
commit
4c53043c4b
@ -0,0 +1,9 @@
|
||||
CORS_HOST=https://theprogrammershangout.com
|
||||
REDIRECT_URI=https://theprogrammershangout.com/appeals/authorize
|
||||
|
||||
API_PORT=80
|
||||
API_CLIENT_SECRET=<enter>
|
||||
API_CLIENT_ID=<enter>
|
||||
|
||||
BOT_TOKEN=<enter>
|
||||
BOT_PREFIX=!
|
@ -0,0 +1,10 @@
|
||||
.idea/
|
||||
.vscode/
|
||||
node_modules/
|
||||
build/
|
||||
dist/
|
||||
temp/
|
||||
|
||||
.env
|
||||
ormlogs.log
|
||||
*.sqlite
|
@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "sqlite",
|
||||
"database": "./database.sqlite",
|
||||
"synchronize": true,
|
||||
"logging": [
|
||||
"error",
|
||||
"warn",
|
||||
"info",
|
||||
"log"
|
||||
],
|
||||
"logger": "file",
|
||||
"entities": [
|
||||
"src/entity/**/*.ts"
|
||||
],
|
||||
"cli": {
|
||||
"entitiesDir": "src/entity"
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "contrition",
|
||||
"version": "0.0.1",
|
||||
"description": "A group of projects that will be used to manage appeals for punished users on [The Programmer's Hangout](https://discord.gg/programming).",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/index.ts",
|
||||
"build": "ts-node --transpile-only src/index.ts",
|
||||
"typeorm": "ts-node ./node_modules/typeorm/cli.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "ssh://git@dev.teamortix.com:222/hamza/contrition"
|
||||
},
|
||||
"author": "Hamza Ali",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@types/dotenv": "^8.2.0",
|
||||
"axios": "^0.21.1",
|
||||
"base64url": "^3.0.1",
|
||||
"cors": "^2.8.5",
|
||||
"discord-akairo": "^8.1.0",
|
||||
"discord.js": "^12.5.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"prettier": "^2.3.2",
|
||||
"reflect-metadata": "^0.1.10",
|
||||
"sqlite3": "^5.0.0",
|
||||
"typeorm": "0.2.29"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/express": "^4.17.9",
|
||||
"@types/luxon": "^1.25.0",
|
||||
"@types/node": "^8.10.66",
|
||||
"concurrently": "^5.3.0",
|
||||
"nodemon": "^2.0.6",
|
||||
"ts-node": "^9.1.1",
|
||||
"typescript": "^4.1.3"
|
||||
}
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
import {
|
||||
AkairoClient,
|
||||
CommandHandler,
|
||||
InhibitorHandler,
|
||||
ListenerHandler,
|
||||
} from "discord-akairo";
|
||||
import { EventEmitter } from "events";
|
||||
import { Config } from "../config/Config";
|
||||
import { parseDuration } from "../config/Constants";
|
||||
import { Appeal } from "../entity/Appeal";
|
||||
import { errorEmbed } from "./Util";
|
||||
|
||||
class Bot extends AkairoClient {
|
||||
static config: Config;
|
||||
|
||||
inhibitorHandler: InhibitorHandler;
|
||||
commandHandler: CommandHandler;
|
||||
listenerHandler: ListenerHandler;
|
||||
|
||||
constructor(config: Config, appealEmitter: EventEmitter) {
|
||||
super({}, { partials: ["REACTION", "MESSAGE"] });
|
||||
|
||||
Bot.config = config;
|
||||
super.login(config.bot.token);
|
||||
|
||||
this.inhibitorHandler = new InhibitorHandler(this, {
|
||||
directory: `${__dirname}/inhibitors`,
|
||||
});
|
||||
|
||||
this.commandHandler = new CommandHandler(this, {
|
||||
directory: `${__dirname}/commands`,
|
||||
prefix: config.bot.prefix,
|
||||
argumentDefaults: {
|
||||
prompt: {
|
||||
retries: 0,
|
||||
time: 15000,
|
||||
cancel: () => errorEmbed("Command Cancelled"),
|
||||
ended: () =>
|
||||
errorEmbed("Invalid input").setDescription("Command ended."),
|
||||
timeout: () =>
|
||||
errorEmbed("Timed out").setDescription(
|
||||
"No response received for 15 seconds."
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
this.listenerHandler = new ListenerHandler(this, {
|
||||
directory: `${__dirname}/listeners`,
|
||||
});
|
||||
|
||||
this.listenerHandler.setEmitters({
|
||||
commandHandler: this.commandHandler,
|
||||
inhibitorHandler: this.inhibitorHandler,
|
||||
listenerHandler: this.listenerHandler,
|
||||
appealHandler: appealEmitter,
|
||||
});
|
||||
|
||||
this.commandHandler.useInhibitorHandler(this.inhibitorHandler);
|
||||
|
||||
this.commandHandler.resolver.addType("timestring", (_, phrase) => {
|
||||
if (!phrase) return null;
|
||||
|
||||
const d = parseDuration(phrase);
|
||||
if (d === "") return null;
|
||||
return d;
|
||||
});
|
||||
|
||||
this.commandHandler.resolver.addType("appeal", async (message, phrase) => {
|
||||
if (!phrase) return null;
|
||||
if (!message.guild) return null;
|
||||
|
||||
const appeal = await Appeal.appeal(phrase, message.guild.id)
|
||||
.then((a) => a)
|
||||
.catch((_) => null);
|
||||
return appeal;
|
||||
});
|
||||
|
||||
this.commandHandler.resolver.addType("optionalDate", (_, phrase) => {
|
||||
if (!phrase) return null;
|
||||
if (phrase === "never") return "never";
|
||||
if (phrase === "now") return "now";
|
||||
const timestamp = Date.parse(phrase);
|
||||
if (isNaN(timestamp)) return null;
|
||||
return new Date(timestamp);
|
||||
});
|
||||
|
||||
this.inhibitorHandler.loadAll();
|
||||
this.commandHandler.loadAll();
|
||||
this.listenerHandler.loadAll();
|
||||
}
|
||||
}
|
||||
|
||||
export default Bot;
|
@ -0,0 +1,85 @@
|
||||
import { Message, MessageEmbed, User } from "discord.js";
|
||||
import Bot from "./Bot";
|
||||
import { Guild } from "../entity/Guild";
|
||||
|
||||
export const successEmbed = (title: string) =>
|
||||
new MessageEmbed().setTitle(title).setColor("#00AE86");
|
||||
|
||||
export const infoEmbed = (title: string) =>
|
||||
new MessageEmbed().setTitle(title).setColor("#3498DB");
|
||||
|
||||
export const errorEmbed = (title: string) =>
|
||||
new MessageEmbed().setTitle(title).setColor("#B93758");
|
||||
|
||||
export const decision = (
|
||||
message: Message,
|
||||
user: User,
|
||||
filter: (m: Message) => boolean,
|
||||
initial: MessageEmbed,
|
||||
accept: (m: Message) => void,
|
||||
cancel: () => void,
|
||||
time: number = 15000
|
||||
) => {
|
||||
const init = message.channel.send(initial);
|
||||
let done = false;
|
||||
const collector = message.channel.createMessageCollector(
|
||||
(m: Message) => m.author == user && !done,
|
||||
{ time }
|
||||
);
|
||||
|
||||
collector.on("collect", (m: Message) => {
|
||||
collector.stop();
|
||||
|
||||
done = true;
|
||||
m.delete().catch(() => {});
|
||||
if (!filter(m)) return cancel();
|
||||
accept(m);
|
||||
});
|
||||
collector.on("end", async () => {
|
||||
(await init).delete();
|
||||
!done && cancel();
|
||||
});
|
||||
};
|
||||
|
||||
export const statusEmbed = async (message: Message) => {
|
||||
const author = message.member;
|
||||
const client = message.client;
|
||||
|
||||
const embed = infoEmbed("Contrition")
|
||||
.setDescription("A bot designed to give you a second chance.")
|
||||
.setThumbnail(client.user.displayAvatarURL())
|
||||
.addField("Prefix", Bot.config.bot.prefix, true)
|
||||
.addField("Ping", `${Date.now() - message.createdTimestamp}ms`, true)
|
||||
.addField("Author", "`elkell#6226`", true)
|
||||
.addField("\u200B", "\u200B");
|
||||
|
||||
const guild = await Guild.findOne({ where: { guild: author.guild.id } });
|
||||
if (!guild) {
|
||||
embed.addField("Configuration", "This guild is not configured yet.");
|
||||
} else {
|
||||
const channel = author.guild.channels.cache.get(guild.channel);
|
||||
const staff = author.guild.roles.cache.get(guild.staffRole);
|
||||
const jailed = author.guild.roles.cache.get(guild.jailedRole);
|
||||
|
||||
if (
|
||||
author.hasPermission("ADMINISTRATOR") ||
|
||||
author.roles.cache.some((role) => role == staff)
|
||||
) {
|
||||
embed.addField(
|
||||
"Configuration",
|
||||
"```fix\n" +
|
||||
`Guild Name: ${guild.name}\n` +
|
||||
`Logging Channel: #${channel.name}\n` +
|
||||
`Staff Role: @${staff.name}\n` +
|
||||
`Restricted Role: @${jailed.name}\n` +
|
||||
`Default Rejection Expiry: ${guild.defaultExpiry}s\n` +
|
||||
"```"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const uptime = (new Date().getTime() - client.uptime) / 1e3;
|
||||
embed.addField("Uptime", `Since <t:${Math.trunc(uptime)}:R>`, true);
|
||||
embed.addField("Source", "[GitHub](https://github.com/hhhapz)", true);
|
||||
return embed;
|
||||
};
|
@ -0,0 +1,126 @@
|
||||
import { Command } from "discord-akairo";
|
||||
import { TextChannel } from "discord.js";
|
||||
import { Message } from "discord.js";
|
||||
import { cancelString } from "../../config/Constants";
|
||||
import { Appeal } from "../../entity/Appeal";
|
||||
import { errorEmbed, infoEmbed, successEmbed } from "../Util";
|
||||
|
||||
class AcceptCommand extends Command {
|
||||
constructor() {
|
||||
super("accept", {
|
||||
category: "Review",
|
||||
aliases: ["Accept"],
|
||||
description: "<appeal> [decision]",
|
||||
clientPermissions: ["CREATE_INSTANT_INVITE", "BAN_MEMBERS"],
|
||||
channel: "guild",
|
||||
args: [
|
||||
{
|
||||
id: "appeal",
|
||||
type: "appeal",
|
||||
prompt: {
|
||||
start: () => infoEmbed("The appeal ID:").setFooter(cancelString),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "reason",
|
||||
type: "string",
|
||||
prompt: {
|
||||
start: () =>
|
||||
infoEmbed("(Optional) Decision note for appeal:")
|
||||
.setDescription("Enter `none` to provide no reason")
|
||||
.setFooter(cancelString),
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async banned(
|
||||
message: Message,
|
||||
args: { appeal: Appeal; reason: string },
|
||||
origMsg: Message,
|
||||
): Promise<boolean> {
|
||||
let { appeal, reason } = args;
|
||||
if (reason === "none") reason = null;
|
||||
|
||||
const banned = await message.guild
|
||||
.fetchBan(appeal.userID)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
if (!banned) {
|
||||
message.channel.send(
|
||||
infoEmbed("The user is not banned. Appeal status updated.")
|
||||
.addField("Original Message", `[link](${appeal.link()}).`)
|
||||
.setFooter(appeal.id)
|
||||
);
|
||||
|
||||
appeal.accept(message.author.id, reason);
|
||||
const embed = appeal.embed(true).setColor("#00AE86");
|
||||
origMsg.edit(embed);
|
||||
}
|
||||
|
||||
return banned;
|
||||
}
|
||||
|
||||
async exec(message: Message, args: { appeal: Appeal; reason: string }) {
|
||||
let { appeal, reason } = args;
|
||||
if (reason === "none") reason = null;
|
||||
|
||||
const channel = await message.client.channels
|
||||
.fetch(appeal.guild.channel)
|
||||
.catch(() => null);
|
||||
if (!channel || !channel?.isText()) {
|
||||
return errorEmbed("Could not update. Guild configuration invalid.");
|
||||
}
|
||||
|
||||
const origMsg = await (channel as TextChannel).messages
|
||||
.fetch(appeal.message)
|
||||
.catch(() => null);
|
||||
if (!origMsg) {
|
||||
return errorEmbed("Could not update original message");
|
||||
}
|
||||
|
||||
if (["accepted", "rejected"].includes(appeal.status)) {
|
||||
return message.channel.send(
|
||||
errorEmbed("Could not accept")
|
||||
.addField("Error", "Cannot accept appeal with decision already made.")
|
||||
.addField("Status", appeal.status)
|
||||
.setFooter(appeal.id)
|
||||
);
|
||||
}
|
||||
|
||||
if (!(await this.banned(message, args, origMsg))) return;
|
||||
|
||||
let err: Error = await message.guild.members
|
||||
.unban(appeal.userID)
|
||||
.then(() => null)
|
||||
.catch((e) => e);
|
||||
|
||||
if (err) {
|
||||
message.channel.send(
|
||||
errorEmbed("An error occurred")
|
||||
.setDescription("Could not unban user.")
|
||||
.addField("Original Message", `[link](${appeal.link()}).`)
|
||||
.addField("Status", "The appeal NOT updated.")
|
||||
.addField("Error", err.message)
|
||||
.setFooter(appeal.id)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
appeal.accept(message.author.id, reason);
|
||||
origMsg.edit(appeal.embed(true).setColor("#00AE86"));
|
||||
|
||||
message.channel.send(
|
||||
successEmbed("The appeal has been accepted.")
|
||||
.setDescription(`${appeal.userTag} is unbanned`)
|
||||
.addField("Original Message", `[link](${appeal.link()})`)
|
||||
.setFooter(appeal.id)
|
||||
);
|
||||
|
||||
appeal.save();
|
||||
}
|
||||
}
|
||||
|
||||
export default AcceptCommand;
|
@ -0,0 +1,33 @@
|
||||
import { Command } from "discord-akairo";
|
||||
import { Message } from "discord.js";
|
||||
import { cancelString } from "../../config/Constants";
|
||||
import { Appeal } from "../../entity/Appeal";
|
||||
import { infoEmbed } from "../Util";
|
||||
|
||||
class AppealCommand extends Command {
|
||||
constructor() {
|
||||
super("appeal", {
|
||||
category: "Review",
|
||||
aliases: ["Appeal"],
|
||||
description: "<appeal>",
|
||||
clientPermissions: ["CREATE_INSTANT_INVITE", "BAN_MEMBERS"],
|
||||
channel: "guild",
|
||||
args: [
|
||||
{
|
||||
id: "appeal",
|
||||
type: "appeal",
|
||||
prompt: {
|
||||
start: () => infoEmbed("The appeal ID:").setFooter(cancelString),
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async exec(message: Message, args: { appeal: Appeal }) {
|
||||
const { appeal } = args;
|
||||
message.reply(appeal.embed(false));
|
||||
}
|
||||
}
|
||||
|
||||
export default AppealCommand;
|
@ -0,0 +1,30 @@
|
||||
import { Category, Command } from "discord-akairo";
|
||||
import { Message } from "discord.js";
|
||||
import { infoEmbed } from "../Util";
|
||||
|
||||
class Help extends Command {
|
||||
constructor() {
|
||||
super("help", {
|
||||
category: "Utility",
|
||||
aliases: ["Help"],
|
||||
});
|
||||
}
|
||||
|
||||
exec(message: Message) {
|
||||
const embed = infoEmbed("Help menu").setDescription(
|
||||
"Here is a list of available commands."
|
||||
);
|
||||
|
||||
this.handler.categories.forEach((c: Category<any, any>, k: string) => {
|
||||
const value = c
|
||||
.map((cmd: Command) => cmd.aliases[0] + " " + (cmd.description || ""))
|
||||
.join("\n");
|
||||
|
||||
embed.addField(k, "```fix\n" + value + "```");
|
||||
});
|
||||
|
||||
message.reply(embed);
|
||||
}
|
||||
}
|
||||
|
||||
export default Help;
|
@ -0,0 +1,98 @@
|
||||
import { Command } from "discord-akairo";
|
||||
import { Message } from "discord.js";
|
||||
import { paginate, reactions } from "../../config/Constants";
|
||||
import { Appeal } from "../../entity/Appeal";
|
||||
import { Guild } from "../../entity/Guild";
|
||||
import { infoEmbed } from "../Util";
|
||||
|
||||
class History extends Command {
|
||||
constructor() {
|
||||
super("history", {
|
||||
category: "Review",
|
||||
aliases: ["History"],
|
||||
description: "<user> [page]",
|
||||
args: [
|
||||
{
|
||||
id: "user",
|
||||
type: "string",
|
||||
prompt: {
|
||||
start: () =>
|
||||
infoEmbed("The ID for the user:").setFooter(
|
||||
'You can cancel this at any time by saying "cancel".'
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "pageNum",
|
||||
type: "number",
|
||||
default: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async exec(
|
||||
message: Message,
|
||||
{ user, pageNum }: { user: string; pageNum: number }
|
||||
) {
|
||||
const userRe = /<@!?(\d+)>/.exec(user);
|
||||
if (userRe) {
|
||||
user = userRe[1];
|
||||
}
|
||||
|
||||
const guild = await Guild.findOneOrFail({
|
||||
where: { guild: message.member.guild.id },
|
||||
});
|
||||
const appeals = await Appeal.allAppeals(user, guild.id);
|
||||
const page = paginate(appeals, 10, pageNum);
|
||||
|
||||
if (appeals.length === 0) {
|
||||
message.channel.send(infoEmbed(`The specified user has 0 appeals.`));
|
||||
return;
|
||||
}
|
||||
|
||||
const start = (pageNum - 1) * 10 + 1;
|
||||
const footer = `Showing appeals ${start} to ${
|
||||
start + page.length - 1
|
||||
}. Total ${appeals.length} appeal(s).`;
|
||||
|
||||
const embed = this.createEmbed(page[0], start, footer);
|
||||
const msg = await message.channel.send(embed);
|
||||
|
||||
if (appeals.length === 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
page.forEach((_, i) => msg.react(reactions[i]).catch(() => {}));
|
||||
|
||||
const collector = msg.createReactionCollector(
|
||||
(reaction, user) => {
|
||||
return (
|
||||
!user.bot &&
|
||||
reactions.slice(0, page.length).indexOf(reaction.emoji.name) != -1
|
||||
);
|
||||
},
|
||||
{ time: 60000 }
|
||||
);
|
||||
collector.on("collect", (reaction, user) => {
|
||||
const index = appeals.indexOf(
|
||||
page[reactions.indexOf(reaction.emoji.name)]
|
||||
);
|
||||
msg.edit(this.createEmbed(appeals[index], index + 1, footer));
|
||||
msg.reactions.resolve(reaction.emoji.name).users.remove(user);
|
||||
});
|
||||
collector.on("end", () => {
|
||||
msg.reactions.removeAll().catch(() => {});
|
||||
});
|
||||
}
|
||||
|
||||
createEmbed(appeal: Appeal, index: number, footer: string) {
|
||||
const embed = appeal.embed(false);
|
||||
embed
|
||||
.setTitle(`${embed.title} - (${index})`)
|
||||
.setFooter(`${embed.footer.text}\n${footer}`);
|
||||
return embed;
|
||||
}
|
||||
}
|
||||
|
||||
export default History;
|
@ -0,0 +1,19 @@
|
||||
import { Command } from "discord-akairo";
|
||||
import { Message } from "discord.js";
|
||||
import { statusEmbed } from "../Util";
|
||||
|
||||
class Info extends Command {
|
||||
constructor() {
|
||||
super("info", {
|
||||
category: "Utility",
|
||||
aliases: ["Info"],
|
||||
});
|
||||
}
|
||||
|
||||
async exec(message: Message) {
|
||||
const embed = await statusEmbed(message);
|
||||
message.channel.send(embed);
|
||||
}
|
||||
}
|
||||
|
||||
export default Info;
|
@ -0,0 +1,80 @@
|
||||
import { Command } from "discord-akairo";
|
||||
import { Message, TextChannel } from "discord.js";
|
||||
import { cancelString } from "../../config/Constants";
|
||||
import { Appeal } from "../../entity/Appeal";
|
||||
import { errorEmbed, infoEmbed, successEmbed } from "../Util";
|
||||
|
||||
class RejectCommand extends Command {
|
||||
constructor() {
|
||||
super("reject", {
|
||||
category: "Review",
|
||||
aliases: ["Reject"],
|
||||
description: "<appeal> [decision]",
|
||||
clientPermissions: ["CREATE_INSTANT_INVITE", "BAN_MEMBERS"],
|
||||
channel: "guild",
|
||||
args: [
|
||||
{
|
||||
id: "appeal",
|
||||
type: "appeal",
|
||||
prompt: {
|
||||
start: () => infoEmbed("The appeal ID:").setFooter(cancelString),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "reason",
|
||||
type: "string",
|
||||
prompt: {
|
||||
start: () =>
|
||||
infoEmbed("(Optional) Decision note for appeal:")
|
||||
.setDescription("Enter `none` to provide no reason")
|
||||
.setFooter(cancelString),
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async exec(message: Message, args: { appeal: Appeal; reason: string }) {
|
||||
let { appeal, reason } = args;
|
||||
if (reason === "none") reason = null;
|
||||
|
||||
if (["accepted", "rejected"].includes(appeal.status)) {
|
||||
return message.channel.send(
|
||||
errorEmbed("Could not accept")
|
||||
.addField("Error", "Cannot reject appeal with decision already made.")
|
||||
.addField("Status", appeal.status)
|
||||
.setFooter(appeal.id)
|
||||
);
|
||||
}
|
||||
|
||||
appeal.reject(message.author.id, reason);
|
||||
|
||||
const channel = await message.client.channels
|
||||
.fetch(appeal.guild.channel)
|
||||
.catch(() => null);
|
||||
|
||||
if (!channel || !channel?.isText()) {
|
||||
return errorEmbed("Could not update. Guild configuration invalid.");
|
||||
}
|
||||
|
||||
const origMsg = await (channel as TextChannel).messages
|
||||
.fetch(appeal.message)
|
||||
.catch(() => null);
|
||||
|
||||
if (!origMsg) {
|
||||
return errorEmbed("Could not update original message");
|
||||
}
|
||||
|
||||
message.channel.send(
|
||||
successEmbed("The appeal has been rejected.")
|
||||
.setDescription(`${appeal.userTag} will stay banned.`)
|
||||
.addField("Original Message", `[link](${appeal.link()})`)
|
||||
.setFooter(appeal.id)
|
||||
);
|
||||
origMsg.edit(appeal.embed(true).setColor("#B93758"));
|
||||
|
||||
appeal.save();
|
||||
}
|
||||
}
|
||||
|
||||
export default RejectCommand;
|
@ -0,0 +1,58 @@
|
||||
import { Command } from "discord-akairo";
|
||||
import { Channel } from "discord.js";
|
||||
import { Message } from "discord.js";
|
||||
import { cancelString } from "../../config/Constants";
|
||||
import { Guild } from "../../entity/Guild";
|
||||
|
||||
import { infoEmbed, successEmbed } from "../Util";
|
||||
|
||||
class SetChannel extends Command {
|
||||
constructor() {
|
||||
super("setChannel", {
|
||||
category: "Server Settings",
|
||||
aliases: ["SetChannel"],
|
||||
description: "<channel>",
|
||||
clientPermissions: ["CREATE_INSTANT_INVITE", "BAN_MEMBERS"],
|
||||
channel: "guild",
|
||||
args: [
|
||||
{
|
||||
id: "newChannel",
|
||||
type: "channel",
|
||||
prompt: {
|
||||
start: () =>
|
||||
infoEmbed("The new channel for appeal posts:").setFooter(
|
||||
cancelString
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async exec(message: Message, args: { newChannel: Channel }) {
|
||||
const { newChannel } = args;
|
||||
const author = message.member;
|
||||
if (author === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const guild = await Guild.findOneOrFail({
|
||||
where: { guild: author.guild.id },
|
||||
});
|
||||
guild.channel = newChannel.id;
|
||||
guild.save();
|
||||
|
||||
message.reply(
|
||||
successEmbed("Guild Settings Updated")
|
||||
.addField("Name", guild.name, true)
|
||||
.addField("Post Channel", `<#${guild.channel}>`, true)
|
||||
.addField("\u200b", "\u200b", true)
|
||||
.addField("Staff Role", `<@&${guild.staffRole}>`, true)
|
||||
.addField("Restricted Role", `<@&${guild.jailedRole}>`, true)
|
||||
.addField("\u200b", "\u200b", true)
|
||||
.addField("Minimum time before re-appeal", guild.defaultExpiry)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SetChannel;
|
@ -0,0 +1,64 @@
|
||||
import { Command } from "discord-akairo";
|
||||
import { Message } from "discord.js";
|
||||
import { cancelString } from "../../config/Constants";
|
||||
import { Guild } from "../../entity/Guild";
|
||||
import { infoEmbed, successEmbed } from "../Util";
|
||||
|
||||
class SetDefaultExpiry extends Command {
|
||||
constructor() {
|
||||
super("setDefaultExpiry", {
|
||||
category: "Server Settings",
|
||||
aliases: ["SetDefaultExpiry"],
|
||||
description: "<time>",
|
||||
clientPermissions: ["CREATE_INSTANT_INVITE", "BAN_MEMBERS"],
|
||||
channel: "guild",
|
||||
args: [
|
||||
{
|
||||
id: "newExpiry",
|
||||
type: "timestring",
|
||||
prompt: {
|
||||
start: () =>
|
||||
infoEmbed("The timestring:")
|
||||
.setDescription(
|
||||
"The minimum default time before users may appeal again after a rejection.\n" +
|
||||
"For no expiry, specify `never`"
|
||||
)
|
||||
.addField(
|
||||
"Valid Durations",
|
||||
"```1d - days\n1w - weeks\n1m - months\n1y - years```"
|
||||
)
|
||||
.addField(
|
||||
"Examples",
|
||||
"```\n1m - 1 month\n1d - 1 day\n1m 3d - 1 month 3 days\n1y1m - 1 year 1 month```"
|
||||
)
|
||||
.setFooter(cancelString),
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async exec(message: Message, args: { newExpiry: string }) {
|
||||
const { newExpiry } = args;
|
||||
const author = message.member;
|
||||
|
||||
const guild = await Guild.findOneOrFail({
|
||||
where: { guild: author.guild.id },
|
||||
});
|
||||
guild.defaultExpiry = newExpiry;
|
||||
guild.save();
|
||||
|
||||
message.reply(
|
||||
successEmbed("Guild Settings Updated")
|
||||
.addField("Name", guild.name, true)
|
||||
.addField("Post Channel", `<#${guild.channel}>`, true)
|
||||
.addField("\u200b", "\u200b", true)
|
||||
.addField("Staff Role", `<@&${guild.staffRole}>`, true)
|
||||
.addField("Restricted Role", `<@&${guild.jailedRole}>`, true)
|
||||
.addField("\u200b", "\u200b", true)
|
||||
.addField("Minimum time before re-appeal", guild.defaultExpiry)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SetDefaultExpiry;
|
@ -0,0 +1,91 @@
|
||||
import { Command } from "discord-akairo";
|
||||
import { TextChannel } from "discord.js";
|
||||
import { Message } from "discord.js";
|
||||
import { cancelString } from "../../config/Constants";
|
||||
import { Appeal } from "../../entity/Appeal";
|
||||
import { errorEmbed, infoEmbed, successEmbed } from "../Util";
|
||||
|
||||
class SetExpiry extends Command {
|
||||
constructor() {
|
||||
super("setExpiry", {
|
||||
category: "Review",
|
||||
aliases: ["SetExpiry"],
|
||||
description: "<appeal> <date>",
|
||||
clientPermissions: ["CREATE_INSTANT_INVITE", "BAN_MEMBERS"],
|
||||
channel: "guild",
|
||||
args: [
|
||||
{
|
||||
id: "appeal",
|
||||
type: "appeal",
|
||||
prompt: {
|
||||
start: () => infoEmbed("The appeal ID:").setFooter(cancelString),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "date",
|
||||
type: "optionalDate",
|
||||
prompt: {
|
||||
start: () =>
|
||||
infoEmbed("The date of expiry:")
|
||||
.setDescription(
|
||||
"Specify JS dateString,\nor specify `never`,\nor specify `now`.\n\nDate must be in the future."
|
||||
)
|
||||
.setFooter(cancelString),
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async exec(
|
||||
message: Message,
|
||||
args: { appeal: Appeal; date: Date | "never" | "now" }
|
||||
) {
|
||||
const { appeal, date } = args;
|
||||
if (appeal.status !== "rejected") {
|
||||
return message.reply(
|
||||
errorEmbed("Invalid Appeal Status")
|
||||
.setDescription("The specified appeal's status must be 'rejected'.")
|
||||
.addField("Current Appeal Status", appeal.status)
|
||||
);
|
||||
}
|
||||
|
||||
if (date === "never") appeal.rejectionExpiry = null;
|
||||
else if (date === "now") appeal.rejectionExpiry = new Date();
|
||||
else appeal.rejectionExpiry = date;
|
||||
|
||||
if (date < new Date()) {
|
||||
return message.reply(
|
||||
errorEmbed("Invalid Date provided").addField(
|
||||
"Parsed Date:",
|
||||
date.toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
appeal.save();
|
||||
message.reply(
|
||||
successEmbed("Appeal expiry updated").setDescription(
|
||||
`View original message: [link](${appeal.link()}).`
|
||||
)
|
||||
);
|
||||
|
||||
const channel = await message.client.channels
|
||||
.fetch(appeal.guild.channel)
|
||||
.catch(() => null);
|
||||
|
||||
if (!channel || !channel?.isText()) {
|
||||
return errorEmbed("Could not update. Guild configuration invalid.");
|
||||
}
|
||||
|
||||
const msg = await (channel as TextChannel).messages
|
||||
.fetch(appeal.message)
|
||||
.catch(() => null);
|
||||
if (!msg) {
|
||||
return errorEmbed("Could not update original message");
|
||||
}
|
||||
msg.edit(appeal.embed(true).setColor("#B93758"));
|
||||
}
|
||||
}
|
||||
|
||||
export default SetExpiry;
|
@ -0,0 +1,60 @@
|
||||
import { Command } from "discord-akairo";
|
||||
import { Message } from "discord.js";
|
||||
import { cancelString } from "../../config/Constants";
|
||||
import { Guild } from "../../entity/Guild";
|
||||
|
||||
import { errorEmbed, infoEmbed, successEmbed } from "../Util";
|
||||
|
||||
class SetName extends Command {
|
||||
constructor() {
|
||||
super("setName", {
|
||||
category: "Server Settings",
|
||||
aliases: ["SetName"],
|
||||
description: "<name>",
|
||||
clientPermissions: ["CREATE_INSTANT_INVITE", "BAN_MEMBERS"],
|
||||
channel: "guild",
|
||||
args: [
|
||||
{
|
||||
id: "newName",
|
||||
type: "string",
|
||||
prompt: {
|
||||
start: () =>
|
||||
infoEmbed("The new name for the guild:").setFooter(cancelString),
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async exec(message: Message, { newName }: { newName: string }) {
|
||||
const author = message.member;
|
||||
if (author === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (await Guild.count({ where: { name: newName } })) {
|
||||
return message.reply(
|
||||
errorEmbed("A guild has already registered with that name.")
|
||||
);
|
||||
}
|
||||
|
||||
const guild = await Guild.findOneOrFail({
|
||||
where: { guild: author.guild.id },
|
||||
});
|
||||
guild.name = newName;
|
||||
guild.save();
|
||||
|
||||
message.reply(
|
||||
successEmbed("Guild Settings Updated")
|
||||
.addField("Name", guild.name, true)
|
||||
.addField("Post Channel", `<#${guild.channel}>`, true)
|
||||
.addField("\u200b", "\u200b", true)
|
||||
.addField("Staff Role", `<@&${guild.staffRole}>`, true)
|
||||
.addField("Restricted Role", `<@&${guild.jailedRole}>`, true)
|
||||
.addField("\u200b", "\u200b", true)
|
||||
.addField("Minimum time before re-appeal", guild.defaultExpiry)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SetName;
|
@ -0,0 +1,58 @@
|
||||
import { Command } from "discord-akairo";
|
||||
import { Role } from "discord.js";
|
||||
import { Message } from "discord.js";
|
||||
import { cancelString } from "../../config/Constants";
|
||||
import { Guild } from "../../entity/Guild";
|
||||
|
||||
import { infoEmbed, successEmbed } from "../Util";
|
||||
|
||||
class SetRestrictedRole extends Command {
|
||||
constructor() {
|
||||
super("setRestrictedRole", {
|
||||
category: "Server Settings",
|
||||
aliases: ["SetRestrictedRole"],
|
||||
description: "<role>",
|
||||
clientPermissions: ["CREATE_INSTANT_INVITE", "BAN_MEMBERS"],
|
||||
channel: "guild",
|
||||
args: [
|
||||
{
|
||||
id: "newRole",
|
||||
type: "role",
|
||||
prompt: {
|
||||
start: () =>
|
||||
infoEmbed("The new role for restricted access:").setFooter(
|
||||
cancelString
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async exec(message: Message, args: { newRole: Role }) {
|
||||
const { newRole } = args;
|
||||
const author = message.member;
|
||||
if (author === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const guild = await Guild.findOneOrFail({
|
||||
where: { guild: author.guild.id },
|
||||
});
|
||||
guild.jailedRole = newRole.id;
|
||||
guild.save();
|
||||
|
||||
message.reply(
|
||||
successEmbed("Guild Settings Updated")
|
||||
.addField("Name", guild.name, true)
|
||||
.addField("Post Channel", `<#${guild.channel}>`, true)
|
||||
.addField("\u200b", "\u200b", true)
|
||||
.addField("Staff Role", `<@&${guild.staffRole}>`, true)
|
||||
.addField("Restricted Role", `<@&${guild.jailedRole}>`, true)
|
||||
.addField("\u200b", "\u200b", true)
|
||||
.addField("Minimum time before re-appeal", guild.defaultExpiry)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SetRestrictedRole;
|
@ -0,0 +1,58 @@
|
||||
import { Command } from "discord-akairo";
|
||||
import { Role } from "discord.js";
|
||||
import { Message } from "discord.js";
|
||||
import { Guild } from "../../entity/Guild";
|
||||
|
||||
import { infoEmbed, successEmbed } from "../Util";
|
||||
|
||||
const cancelString = 'You can cancel this at any time by saying "cancel".';
|
||||
class SetStaffRole extends Command {
|
||||
constructor() {
|
||||
super("SetStaffRole", {
|
||||
category: "Server Settings",
|
||||
aliases: ["SetStaffRole"],
|
||||
description: "<role>",
|
||||
clientPermissions: ["CREATE_INSTANT_INVITE", "BAN_MEMBERS"],
|
||||
channel: "guild",
|
||||
args: [
|
||||
{
|
||||
id: "newRole",
|
||||
type: "role",
|
||||
prompt: {
|
||||
start: () =>
|
||||
infoEmbed("The new role for staff users:").setFooter(
|
||||
cancelString
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async exec(message: Message, args: { newRole: Role }) {
|
||||
const { newRole } = args;
|
||||
const author = message.member;
|
||||
if (author === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const guild = await Guild.findOneOrFail({
|
||||
where: { guild: author.guild.id },
|
||||
});
|
||||
guild.jailedRole = newRole.id;
|
||||
guild.save();
|
||||
|
||||
message.reply(
|
||||
successEmbed("Guild Settings Updated")
|
||||
.addField("Name", guild.name, true)
|
||||
.addField("Post Channel", `<#${guild.channel}>`, true)
|
||||
.addField("\u200b", "\u200b", true)
|
||||
.addField("Staff Role", `<@&${guild.staffRole}>`, true)
|
||||
.addField("Restricted Role", `<@&${guild.jailedRole}>`, true)
|
||||
.addField("\u200b", "\u200b", true)
|
||||
.addField("Minimum time before re-appeal", guild.defaultExpiry)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SetStaffRole;
|
@ -0,0 +1,157 @@
|
||||
import { Command } from "discord-akairo";
|
||||
import { Channel } from "discord.js";
|
||||
import { Role } from "discord.js";
|
||||
import { Message } from "discord.js";
|
||||
import { cancelString } from "../../config/Constants";
|
||||
import { Guild } from "../../entity/Guild";
|
||||
|
||||
import { errorEmbed, infoEmbed, successEmbed } from "../Util";
|
||||
|
||||
class Setup extends Command {
|
||||
constructor() {
|
||||
super("setup", {
|
||||
category: "Utility",
|
||||
aliases: ["Setup"],
|
||||
clientPermissions: ["CREATE_INSTANT_INVITE", "BAN_MEMBERS"],
|
||||
userPermissions: ["ADMINISTRATOR"],
|
||||
channel: "guild",
|
||||
args: [
|
||||
{
|
||||
id: "name",
|
||||
type: "string",
|
||||
prompt: {
|
||||
start: () =>
|
||||
infoEmbed("Please provide the following information.")
|
||||
.setDescription("1. Supply a name for this guild.")
|
||||
.setFooter(cancelString),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "channel",
|
||||
type: "textChannel",
|
||||
prompt: {
|
||||
start: () =>
|
||||
successEmbed("Success")
|
||||
.setDescription("2. Supply a channel for posting appeals.")
|
||||
.setFooter(cancelString),
|
||||
retry: errorEmbed("Error")
|
||||
.setDescription(
|
||||
"Please try again. Mention a channel or provide its ID."
|
||||
)
|
||||
.setFooter(cancelString),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "staffRole",
|
||||
type: "role",
|
||||
prompt: {
|
||||
start: () =>
|
||||
successEmbed("Success")
|
||||
.setDescription(
|
||||
"3. Enter the staff role ID or mention the staff role to access and use this bot"
|
||||
)
|
||||
.setFooter(cancelString),
|
||||
retry: errorEmbed("Error")
|
||||
.setDescription(
|
||||
"Please try again. Mention a role or provide its ID."
|
||||
)
|
||||
.setFooter(cancelString),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "jailedRole",
|
||||
type: "role",
|
||||
prompt: {
|
||||
start: () =>
|
||||
successEmbed("Success")
|
||||
.setDescription(
|
||||
"3. Enter the role ID or mention the role for users who join for further questioning (i.e the muted role)"
|
||||
)
|
||||
.setFooter(cancelString),
|
||||
retry: errorEmbed("Error")
|
||||
.setDescription(
|
||||
"Please try again. Mention a role or provide its ID."
|
||||
)
|
||||
.setFooter(cancelString),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "defaultExpiry",
|
||||
type: "timestring",
|
||||
prompt: {
|
||||
start: () =>
|
||||
successEmbed("Success")
|
||||
.setDescription(
|
||||
"4. Enter a time string for the minimum time before users can appeal after a rejection."
|
||||
)
|
||||
.addField(
|
||||
"Valid Durations",
|
||||
"```1d - days\n1w - weeks\n1m - months\n1y - years```"
|
||||
)
|
||||
.addField(
|
||||
"Examples",
|
||||
"```\n1m - 1 month\n1d - 1 day\n1m 3d - 1 month 3 days\n1y1m - 1 year 1 month```"
|
||||
)
|
||||
.setFooter(cancelString),
|
||||
retry: errorEmbed("Error")
|
||||
.setDescription("Please try again with a valid time string.")
|
||||
.setFooter(cancelString),
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async exec(
|
||||
message: Message,
|
||||
args: {
|
||||
name: string;
|
||||
channel: Channel;
|
||||
staffRole: Role;
|
||||
jailedRole: Role;
|
||||
defaultExpiry: string;
|
||||
}
|
||||
) {
|
||||
const { name, channel, staffRole, jailedRole, defaultExpiry } = args;
|
||||
const author = message.member;
|
||||
if (author === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (await Guild.count({ where: { guild: author.guild.id } })) {
|
||||
return message.channel.send(errorEmbed("This guild is already setup."));
|
||||
}
|
||||
|
||||
if (await Guild.count({ where: { name: args.name } })) {
|
||||
return message.channel.send(
|
||||
errorEmbed("A guild has already registered with that name.")
|
||||
);
|
||||
}
|
||||
|
||||
const guild = new Guild(
|
||||
name,
|
||||
author.guild.id,
|
||||
channel.id,
|
||||
staffRole.id,
|
||||
jailedRole.id,
|
||||
defaultExpiry
|
||||
);
|
||||
guild.save();
|
||||
|
||||
message.channel.send(
|
||||
successEmbed("Guild Setup Complete")
|
||||
.setDescription(
|
||||
"The guild has successfully been setup, and will listen for appeal requests."
|
||||
)
|
||||
.addField("Name", name, true)
|
||||
.addField("Post Channel", `<#${guild.channel}>`, true)
|
||||
.addField("\u200b", "\u200b", true)
|
||||
.addField("Staff Role", `<@&${guild.staffRole}>`, true)
|
||||
.addField("Restricted Role", `<@&${guild.jailedRole}>`, true)
|
||||
.addField("\u200b", "\u200b", true)
|
||||
.addField("Minimum time before re-appeal", defaultExpiry || "Never")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Setup;
|
@ -0,0 +1,28 @@
|
||||
import { Command, Inhibitor } from "discord-akairo";
|
||||
import { Role } from "discord.js";
|
||||
import { Message } from "discord.js";
|
||||
import { Guild } from "../../entity/Guild";
|
||||
|
||||
class RegisteredGuild extends Inhibitor {
|
||||
constructor() {
|
||||
super("HasRole", {
|
||||
reason: "permissions",
|
||||
type: "post",
|
||||
priority: 0
|
||||
})
|
||||
}
|
||||
|
||||
async exec(message: Message, command: Command) {
|
||||
const author = message.member
|
||||
if (!author) return false
|
||||
|
||||
if (author.hasPermission("ADMINISTRATOR")) return false
|
||||
if (command.id === "setup") return true
|
||||
|
||||
const guild = await Guild.findOneOrFail({ where: { guild: author.guild.id } })
|
||||
|
||||
return !author.roles.cache.some((role: Role) => role.id === guild.staffRole)
|
||||
}
|
||||
}
|
||||
|
||||
export default RegisteredGuild;
|
@ -0,0 +1,21 @@
|
||||
import { Command, Inhibitor } from "discord-akairo";
|
||||
import { Message } from "discord.js";
|
||||
|
||||
class InGuild extends Inhibitor {
|
||||
constructor() {
|
||||
super("InGuild", {
|
||||
reason: "no guild",
|
||||
type: "post",
|
||||
priority: 2,
|
||||
});
|
||||
}
|
||||
|
||||
async exec(message: Message, command: Command) {
|
||||
// we handle no guild somewhere else
|
||||
if (message.guild) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default InGuild;
|
@ -0,0 +1,29 @@
|
||||
import { Command, Inhibitor } from "discord-akairo";
|
||||
import { Message } from "discord.js";
|
||||
import { Guild } from "../../entity/Guild";
|
||||
|
||||
class RegisteredGuild extends Inhibitor {
|
||||
constructor() {
|
||||
super("RegisteredGuild", {
|
||||
reason: "unregistered",
|
||||
type: "post",
|
||||
priority: 1,
|
||||
});
|
||||
}
|
||||
|
||||
async exec(message: Message, command: Command) {
|
||||
// we handle no guild somewhere else
|
||||
if (!message.guild) return false;
|
||||
|
||||
if (command.id === "setup") return false;
|
||||
if (command.id === "help") return false;
|
||||
|
||||
const author = message.member;
|
||||
if (!author) return true;
|
||||
|
||||
const guild = await Guild.count({ where: { guild: author.guild.id } });
|
||||
return guild == 0;
|
||||
}
|
||||
}
|
||||
|
||||
export default RegisteredGuild;
|
@ -0,0 +1,28 @@
|
||||
import { Command, Inhibitor } from "discord-akairo";
|
||||
import { Message } from "discord.js";
|
||||
import { Guild } from "../../entity/Guild";
|
||||
|
||||
class RegisteredGuild extends Inhibitor {
|
||||
constructor() {
|
||||
super("SetupWhileRegistered", {
|
||||
reason: "already setup",
|
||||
type: "post",
|
||||
priority: 1,
|
||||
});
|
||||
}
|
||||
|
||||
async exec(message: Message, command: Command) {
|
||||
// we handle no guild somewhere else
|
||||
if (!message.guild) return false;
|
||||
|
||||
if (command.id !== "setup") return false;
|
||||
|
||||
const author = message.member;
|
||||
if (!author) return true;
|
||||
|
||||
const guild = await Guild.count({ where: { guild: author.guild.id } });
|
||||
return guild !== 0;
|
||||
}
|
||||
}
|
||||
|
||||
export default RegisteredGuild;
|
@ -0,0 +1,40 @@
|
||||
import { Command, Listener } from "discord-akairo";
|
||||
import { Message } from "discord.js";
|
||||
import Bot from "../Bot";
|
||||
import { errorEmbed } from "../Util";
|
||||
|
||||
class CommandBlockedListener extends Listener {
|
||||
constructor() {
|
||||
super("commandBlocked", {
|
||||
emitter: "commandHandler",
|
||||
event: "commandBlocked",
|
||||
});
|
||||
}
|
||||
|
||||
exec(message: Message, command: Command, reason: string) {
|
||||
const prefix = Bot.config.bot.prefix;
|
||||
if (reason === "unregistered")
|
||||
return message.reply(
|
||||
errorEmbed("You must setup the guild first.").setDescription(
|
||||
`Try ${prefix}setup.`
|
||||
)
|
||||
);
|
||||
|
||||
if (reason === "permissions")
|
||||
return message.reply(
|
||||
errorEmbed("You do not have access to this command.")
|
||||
);
|
||||
|
||||
if (reason === "no guild")
|
||||
return message.reply(errorEmbed("You cannot run commands in DMs."));
|
||||
|
||||
if (reason === "already setup")
|
||||
return message.reply(
|
||||
errorEmbed("This guild is already setup").setDescription(
|
||||
"To update configuration, use other commands."
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CommandBlockedListener;
|
@ -0,0 +1,26 @@
|
||||
import {Listener} from "discord-akairo";
|
||||
import {Message} from "discord.js";
|
||||
import {errorEmbed, statusEmbed} from "../Util";
|
||||
|
||||
class MessageCreate extends Listener {
|
||||
constructor() {
|
||||
super("message", {
|
||||
emitter: "client",
|
||||
event: "message",
|
||||
});
|
||||
}
|
||||
|
||||
async exec(message: Message) {
|
||||
const author = message.member;
|
||||
if (!author) {
|
||||
return errorEmbed("This bot can only be used in guilds.");
|
||||
}
|
||||
|
||||
if (!message.content.match(`^<@!?${message.client.user.id}>$`)) return;
|
||||
|
||||
const embed = await statusEmbed(message);
|
||||
return message.channel.send(embed);
|
||||
}
|
||||
}
|
||||
|
||||
export default MessageCreate;
|
@ -0,0 +1,23 @@
|
||||
import { Command, Listener } from "discord-akairo"
|
||||
import { Message } from "discord.js"
|
||||
import { errorEmbed } from "../Util"
|
||||
|
||||
class MissingPermissions extends Listener {
|
||||
constructor() {
|
||||
super('missingPermissions', {
|
||||
emitter: 'commandHandler',
|
||||
event: 'missingPermissions'
|
||||
});
|
||||
}
|
||||
|
||||
exec(message: Message, command: Command, reason: string, missing: Array<string>) {
|
||||
if (reason === "user") return message.reply(errorEmbed("Missing permissions."))
|
||||
if (reason === "client") {
|
||||
const embed = errorEmbed("The bot is missing required permissions")
|
||||
.setDescription("Permisisons required: `" + missing.join(", ") + "`.")
|
||||
return message.reply(embed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default MissingPermissions
|
@ -0,0 +1,51 @@
|
||||
import { Listener } from "discord-akairo";
|
||||
import { TextChannel } from "discord.js";
|
||||
import { Appeal } from "../../entity/Appeal";
|
||||
import { Guild } from "../../entity/Guild";
|
||||
|
||||
class NewAppeal extends Listener {
|
||||
constructor() {
|
||||
super("newAppealHandler", {
|
||||
emitter: "appealHandler",
|
||||
event: "newAppeal",
|
||||
});
|
||||
}
|
||||
|
||||
async exec(
|
||||
guild: Guild,
|
||||
appeal: Appeal,
|
||||
success: () => {},
|
||||
fail: (msg: string) => {}
|
||||
) {
|
||||
const g = await this.client.guilds.fetch(guild.guild).catch(() => null);
|
||||
if (!g) {
|
||||
return fail("Could not submit appeal. Guild not found.");
|
||||
}
|
||||
|
||||
const ban = await g.fetchBan(appeal.userID).catch(() => null);
|
||||
if (!ban) {
|
||||
return fail("Could not submit appeal. No ban found for user.");
|
||||
}
|
||||
|
||||
const channel = await this.client.channels
|
||||
.fetch(guild.channel)
|
||||
.catch(() => null);
|
||||
if (!channel?.isText()) {
|
||||
return fail("Could not submit appeal. Guild configuration invalid.");
|
||||
}
|
||||
|
||||
success();
|
||||
|
||||
const embed = appeal.embed(true)
|
||||
const msg = await (channel as TextChannel).send(embed);
|
||||
appeal.message = msg.id;
|
||||
await appeal.save();
|
||||
|
||||
msg.edit(embed.setFooter("Appeal ID: " + appeal.id));
|
||||
msg.react("✅");
|
||||
msg.react("❔");
|
||||
msg.react("❌");
|
||||
}
|
||||
}
|
||||
|
||||
export default NewAppeal;
|
@ -0,0 +1,215 @@
|
||||
import { Listener } from "discord-akairo";
|
||||
import { Message } from "discord.js";
|
||||
import { MessageReaction } from "discord.js";
|
||||
import { User } from "discord.js";
|
||||
import { Appeal } from "../../entity/Appeal";
|
||||
import { decision, errorEmbed, infoEmbed, successEmbed } from "../Util";
|
||||
|
||||
class ReactionAdd extends Listener {
|
||||
constructor() {
|
||||
super("embedDecision", {
|
||||
emitter: "client",
|
||||
event: "messageReactionAdd",
|
||||
});
|
||||
}
|
||||
|
||||
async exec(reaction: MessageReaction, user: User) {
|
||||
if (reaction.message.partial) await reaction.message.fetch();
|
||||
|
||||
if (!reaction.message.guild) return;
|
||||
if (user === this.client.user) return;
|
||||
if (reaction.message.author !== this.client.user) return;
|
||||
if (!"✅❔❌".includes(reaction.emoji.name)) return;
|
||||
|
||||
const embed = reaction.message.embeds[0];
|
||||
if (!embed) return;
|
||||
|
||||
const footer = embed.footer.text;
|
||||
if (!footer) return;
|
||||
|
||||
const id = /Appeal ID: ([A-z\d-]*)$/i.exec(footer);
|
||||
if (!id) return;
|
||||
|
||||
const appeal = await Appeal.findOne(id[1]);
|
||||
if (!appeal) return;
|
||||
|
||||
reaction.message.reactions.resolve(reaction.emoji.name).users.remove(user);
|
||||
|
||||
switch (reaction.emoji.name) {
|
||||
case "✅":
|
||||
this.onAccept(reaction.message, appeal, user);
|
||||
break;
|
||||
case "❔":
|
||||
reaction.message.channel.send("Not implemented yet :(");
|
||||
break;
|
||||
case "❌":
|
||||
this.onReject(reaction.message, appeal, user);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onAccept(message: Message, appeal: Appeal, user: User) {
|
||||
decision(
|
||||
message,
|
||||
user,
|
||||
(m) => m.content === "Yes",
|
||||
infoEmbed("Are you sure you would like to accept this appeal?")
|
||||
.setDescription(
|
||||
'Respond with "Yes" to continue.\n' +
|
||||
"Sending any other message will cancel the process."
|
||||
)
|
||||
.setFooter(appeal.id),
|
||||
|
||||
() =>
|
||||
this.decisionReason(
|
||||
message,
|
||||
appeal,
|
||||
user,
|
||||
"Provide Reason",
|
||||
this.accept
|
||||
),
|
||||
|
||||
() => this.onCancel(message, appeal)
|
||||
);
|
||||
}
|
||||
|
||||
onReject(message: Message, appeal: Appeal, user: User) {
|
||||
decision(
|
||||
message,
|
||||
user,
|
||||
(m) => m.content === "Yes",
|
||||
infoEmbed("Are you sure you would like to reject this appeal?")
|
||||
.setDescription(
|
||||
'Respond with "Yes" to continue.\n' +
|
||||
"Sending any other message will cancel the process."
|
||||
)
|
||||
.setFooter(appeal.id),
|
||||
|
||||
() =>
|
||||
this.decisionReason(
|
||||
message,
|
||||
appeal,
|
||||
user,
|
||||
"Rejection Reason",
|
||||
this.reject
|
||||
),
|
||||
|
||||
() => this.onCancel(message, appeal)
|
||||
);
|
||||
}
|
||||
|
||||
decisionReason(
|
||||
message: Message,
|
||||
appeal: Appeal,
|
||||
user: User,
|
||||
title: string,
|
||||
final: (
|
||||
message: Message,
|
||||
appeal: Appeal,
|
||||
user: User,
|
||||
reason?: string
|
||||
) => void
|
||||
) {
|
||||
decision(
|
||||
message,
|
||||
user,
|
||||
(m) => m.content !== "cancel",
|
||||
infoEmbed(title)
|
||||
.setDescription(
|
||||
"Enter `none` to provide no reason.\n" +
|
||||
"Enter `cancel` to cancel.\n\n" +
|
||||
"*Timeout in 120 seconds*"
|
||||
)
|
||||
.setFooter(appeal.id),
|
||||
(m) => {
|
||||
let content = m.content;
|
||||
if (content === "none") content = null;
|
||||
final(message, appeal, user, content);
|
||||
},
|
||||
() => this.onCancel(message, appeal),
|
||||
120_000
|
||||
);
|
||||
}
|
||||
|
||||
async accept(message: Message, appeal: Appeal, user: User, reason?: string) {
|
||||
const banned = await message.guild
|
||||
.fetchBan(appeal.userID)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
if (!banned) {
|
||||
message.channel
|
||||
.send(infoEmbed("The user is not banned. Appeal status updated."))
|
||||
.then((m) => setTimeout(() => m.delete(), 5000));
|
||||
|
||||
appeal.accept(user.id, reason);
|
||||
message.reactions.removeAll();
|
||||
|
||||
const embed = appeal.embed(true).setColor("#00AE86");
|
||||
message.edit(embed);
|
||||
return;
|
||||
}
|
||||
|
||||
let err: Error = await message.guild.members
|
||||
.unban(appeal.userID)
|
||||
.then(() => null)
|
||||
.catch((e) => e);
|
||||
|
||||
if (err) {
|
||||
message.channel
|
||||
.send(
|
||||
errorEmbed("An error occurred")
|
||||
.setDescription(
|
||||
"Could not unban user.\nThe appeal status was NOT updated."
|
||||
)
|
||||
.addField("Error", err.message)
|
||||
.setFooter(appeal.id)
|
||||
)
|
||||
.then((m) => setTimeout(() => m.delete(), 5000));
|
||||
return;
|
||||
}
|
||||
|
||||
appeal.accept(user.id, reason);
|
||||
|
||||
message.channel
|
||||
.send(
|
||||
successEmbed("The appeal has been accepted.")
|
||||
.setDescription(`${user.username}#${user.discriminator} is unbanned`)
|
||||
.setThumbnail(user.displayAvatarURL())
|
||||
.setFooter(appeal.id)
|
||||
)
|
||||
.then((m) => setTimeout(() => m.delete(), 5000));
|
||||
|
||||
message.reactions.removeAll();
|
||||
const embed = appeal.embed(true).setColor("#00AE86");
|
||||
message.edit(embed);
|
||||
}
|
||||
|
||||
reject(message: Message, appeal: Appeal, user: User, reason?: string) {
|
||||
appeal.reject(user.id, reason);
|
||||
|
||||
message.channel
|
||||
.send(
|
||||
successEmbed("The appeal has been rejected.")
|
||||
.setDescription(`${appeal.userID} will stay banned.`)
|
||||
.setFooter(appeal.id)
|
||||
.addField(
|
||||
"Rejection expiry",
|
||||
appeal.rejectionExpiry?.toLocaleDateString() || "Never"
|
||||
)
|
||||
)
|
||||
.then((m) => setTimeout(() => m.delete(), 5000));
|
||||
|
||||
message.reactions.removeAll();
|
||||
const embed = appeal.embed(true).setColor("#B93758");
|
||||
message.edit(embed);
|
||||
}
|
||||
|
||||
onCancel(message: Message, appeal: Appeal) {
|
||||
message.channel
|
||||
.send(errorEmbed("Action cancelled.").setFooter(appeal.id))
|
||||
.then((m) => setTimeout(() => m.delete(), 5000));
|
||||
}
|
||||
}
|
||||
|
||||
export default ReactionAdd;
|
@ -0,0 +1,16 @@
|
||||
import { Listener } from "discord-akairo"
|
||||
|
||||
class Ready extends Listener {
|
||||
constructor() {
|
||||
super("ready", {
|
||||
emitter: "client",
|
||||
event: "ready",
|
||||
});
|
||||
}
|
||||
|
||||
public exec() {
|
||||
console.log("Connected to discord.")
|
||||
}
|
||||
}
|
||||
|
||||
export default Ready
|
@ -0,0 +1,36 @@
|
||||
export class Config {
|
||||
api: ApiConfig;
|
||||
bot: BotConfig;
|
||||
}
|
||||
|
||||
export class ApiConfig {
|
||||
cors_host: string[];
|
||||
redirect_uri: string;
|
||||
port: number;
|
||||
client_secret: string;
|
||||
client_id: string;
|
||||
|
||||
constructor(
|
||||
cors_host: string[],
|
||||
redirect_uri: string,
|
||||
port: number,
|
||||
client_secret: string,
|
||||
client_id: string
|
||||
) {
|
||||
this.cors_host = cors_host;
|
||||
this.redirect_uri = redirect_uri;
|
||||
this.port = port;
|
||||
this.client_secret = client_secret;
|
||||
this.client_id = client_id;
|
||||
}
|
||||
}
|
||||
|
||||
export class BotConfig {
|
||||
token: string;
|
||||
prefix: string;
|
||||
|
||||
constructor(token: string, prefix: string) {
|
||||
this.token = token;
|
||||
this.prefix = prefix;
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
import { Request, Response } from "express";
|
||||
|
||||
/**
|
||||
* List of API routes with examples
|
||||
* @route GET /api
|
||||
*/
|
||||
export const getApi = (_: Request, res: Response) => {
|
||||
res.json({
|
||||
"POST /oauth": {
|
||||
summary: "Create a request to generate an OAuth token URL from Discord.",
|
||||
responses: {
|
||||
200: {
|
||||
description: "The OAuth URL",
|
||||
format: {
|
||||
redirectURL: {
|
||||
type: "string",
|
||||
format: "url",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"GET /authorize": {
|
||||
summary: "Callback from discord with auth token",
|
||||
parameters: {
|
||||
query: {
|
||||
code: {
|
||||
type: "string",
|
||||
},
|
||||
state: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successfully complete OAuth exchange",
|
||||
},
|
||||
400: {
|
||||
description: "An invalid code or state parameter was provided",
|
||||
format: {
|
||||
reason: {
|
||||
type: "string",
|
||||
format: "error",
|
||||
},
|
||||
},
|
||||
},
|
||||
500: {
|
||||
description: "Something went wrong on the server",
|
||||
format: {
|
||||
reason: {
|
||||
type: "string",
|
||||
format: "error",
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"POST /submit": {
|
||||
summary: "Post an unban request appeal",
|
||||
},
|
||||
});
|
||||
};
|
@ -0,0 +1,29 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Config } from "../config/Config";
|
||||
import { DiscordToken } from "../entity/DiscordToken";
|
||||
import { StateToken } from "../entity/StateToken";
|
||||
import { fail, respond } from "./Util";
|
||||
|
||||
export const authorize = async (req: Request, res: Response) => {
|
||||
const config: Config = req.app.get("config");
|
||||
const { code, state } = req.body;
|
||||
|
||||
// enforce types
|
||||
if (!(typeof code === "string" && typeof state === "string")) {
|
||||
return fail(res, 500, "Invalid request body");
|
||||
}
|
||||
|
||||
const stateToken = await StateToken.findOne({ where: { token: state } });
|
||||
if (!stateToken) {
|
||||
return fail(res, 400, "Invalid state provided");
|
||||
}
|
||||
|
||||
let dToken: DiscordToken;
|
||||
try {
|
||||
dToken = await stateToken.verify(config, code.toString());
|
||||
} catch ({ reason, status }) {
|
||||
fail(res, status, reason);
|
||||
}
|
||||
|
||||
respond(res, { token: dToken.id });
|
||||
};
|
@ -0,0 +1,46 @@
|
||||
import { Request, Response } from "express";
|
||||
import { DiscordCache } from "../entity/DiscordCache";
|
||||
import { DiscordToken } from "../entity/DiscordToken";
|
||||
import { Guild } from "../entity/Guild";
|
||||
import { fail, respond } from "./Util";
|
||||
|
||||
export const info = async (req: Request, res: Response) => {
|
||||
const { token } = req.body;
|
||||
|
||||
// enforce types
|
||||
if (!(typeof token === "string")) {
|
||||
return fail(res, 500, "Invalid request body");
|
||||
}
|
||||
|
||||
const discordToken = await DiscordToken.findOne({ where: { id: token } });
|
||||
if (!discordToken) {
|
||||
return fail(res, 400, "Invalid token provided");
|
||||
}
|
||||
|
||||
let cache: DiscordCache;
|
||||
try {
|
||||
cache = await discordToken.queryData();
|
||||
} catch ({ status, reason }) {
|
||||
fail(res, status, reason);
|
||||
return;
|
||||
}
|
||||
|
||||
let platforms: Guild[];
|
||||
try {
|
||||
platforms = await Guild.all();
|
||||
} catch (err) {
|
||||
console.error("Could not fetch guilds", err);
|
||||
return fail(res, 500, "An internal error occurred.");
|
||||
}
|
||||
|
||||
respond(res, {
|
||||
id: cache.userID,
|
||||
username: cache.username,
|
||||
discriminator: cache.discriminator,
|
||||
avatar: cache.avatar,
|
||||
platforms: platforms.map((g) => ({
|
||||
name: g.name,
|
||||
id: g.id,
|
||||
})),
|
||||
});
|
||||
};
|
@ -0,0 +1,28 @@
|
||||
import base64url from "base64url";
|
||||
import * as crypto from "crypto";
|
||||
import { Request, Response } from "express";
|
||||
import { OAUTH_SCOPE } from "../config/Constants";
|
||||
import { StateToken } from "../entity/StateToken";
|
||||
import { respond } from "./Util";
|
||||
|
||||
export const genOAuth = async (req: Request, res: Response) => {
|
||||
const { api } = req.app.get("config");
|
||||
|
||||
let token: string;
|
||||
do {
|
||||
token = base64url(crypto.randomBytes(18));
|
||||
} while ((await StateToken.count({ where: { token } })) !== 0);
|
||||
|
||||
new StateToken(token).save();
|
||||
|
||||
let url =
|
||||
"https://discord.com/api/oauth2/authorize" +
|
||||
`?response_type=code` +
|
||||
`&prompt=consent` +
|
||||
`&client_id=${api.client_id}` +
|
||||
`&scope=${encodeURIComponent(OAUTH_SCOPE)}` +
|
||||
`&state=${token}` +
|
||||
`&redirect_uri=${encodeURIComponent(api.redirect_uri)}`;
|
||||
|
||||
respond(res, { redirectURL: url });
|
||||
};
|
@ -0,0 +1,51 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Appeal } from "../entity/Appeal";
|
||||
import { DiscordCache } from "../entity/DiscordCache";
|
||||
import { DiscordToken } from "../entity/DiscordToken";
|
||||
import { fail, respond } from "./Util";
|
||||
|
||||
export const status = async (req: Request, res: Response) => {
|
||||
const { token } = req.body;
|
||||
|
||||
// enforce types
|
||||
if (!(typeof token === "string")) {
|
||||
return fail(res, 500, "Invalid request body");
|
||||
}
|
||||
|
||||
const discordToken = await DiscordToken.findOne({ where: { id: token } });
|
||||
if (!discordToken) {
|
||||
res.status(400);
|
||||
res.json({ reason: "Invalid token provided", code: 400 });
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: check with bot if the user is banned.
|
||||
let cache: DiscordCache;
|
||||
try {
|
||||
cache = await discordToken.queryData();
|
||||
} catch ({ status, reason }) {
|
||||
return fail(res, status, reason);
|
||||
}
|
||||
|
||||
let appeals: Appeal[];
|
||||
try {
|
||||
appeals = await Appeal.allAppeals(cache.userID);
|
||||
} catch (err) {
|
||||
console.error("Could not fetch appeals", err);
|
||||
return fail(res, 500, "An internal error occurred.");
|
||||
}
|
||||
|
||||
respond(
|
||||
res,
|
||||
appeals.map((appeal) => {
|
||||
return {
|
||||
...appeal,
|
||||
message: undefined,
|
||||
decisionBy: undefined,
|
||||
userTag: undefined,
|
||||
userAvatar: undefined,
|
||||
guild: appeal.guild.name,
|
||||
};
|
||||
})
|
||||
);
|
||||
};
|
@ -0,0 +1,82 @@
|
||||
import { Request, Response } from "express";
|
||||
import { EventEmitter } from "typeorm/platform/PlatformTools";
|
||||
import { Appeal } from "../entity/Appeal";
|
||||
import { DiscordCache } from "../entity/DiscordCache";
|
||||
import { DiscordToken } from "../entity/DiscordToken";
|
||||
import { Guild } from "../entity/Guild";
|
||||
import { fail, respond } from "./Util";
|
||||
|
||||
export const submit = async (req: Request, res: Response) => {
|
||||
const {
|
||||
token,
|
||||
platform,
|
||||
punishment_date,
|
||||
ban_reason,
|
||||
appeal_reason,
|
||||
additional_info,
|
||||
} = req.body;
|
||||
|
||||
// enforce types
|
||||
if (
|
||||
!(
|
||||
typeof token === "string" &&
|
||||
typeof platform === "string" &&
|
||||
typeof punishment_date === "string" &&
|
||||
typeof ban_reason === "string" &&
|
||||
typeof appeal_reason === "string" &&
|
||||
typeof additional_info === "string"
|
||||
)
|
||||
) {
|
||||
return fail(res, 500, "Invalid request body")
|
||||
}
|
||||
|
||||
const discordToken = await DiscordToken.findOne({ where: { id: token } });
|
||||
if (!discordToken) {
|
||||
return fail(res, 400, "Invalid token provided");
|
||||
}
|
||||
|
||||
let cache: DiscordCache;
|
||||
try {
|
||||
cache = await discordToken.queryData();
|
||||
} catch ({ status, reason }) {
|
||||
return fail(res, status, reason);
|
||||
}
|
||||
|
||||
const guild = await Guild.findOne({ where: { id: platform } });
|
||||
if (!guild) {
|
||||
return fail(res, 400, "Guild not found");
|
||||
}
|
||||
|
||||
const appeals = await Appeal.activeAppeals(cache.userID, guild.id);
|
||||
if (appeals.length != 0) {
|
||||
return fail(
|
||||
res,
|
||||
400,
|
||||
`You already have an active appeal for ${guild.name}`
|
||||
);
|
||||
}
|
||||
|
||||
const appeal = new Appeal(
|
||||
cache.userID,
|
||||
`${cache.username}#${cache.discriminator}`,
|
||||
cache.avatar,
|
||||
guild,
|
||||
punishment_date,
|
||||
ban_reason,
|
||||
appeal_reason,
|
||||
additional_info
|
||||
);
|
||||
|
||||
const msg = appeal.validationMessage();
|
||||
if (msg !== null) {
|
||||
return fail(res, 422, msg);
|
||||
}
|
||||
|
||||
const emitter: EventEmitter = req.app.get("emitter");
|
||||
|
||||
await new Promise((res, rej) => {
|
||||
emitter.emit("newAppeal", guild, appeal, res, rej);
|
||||
})
|
||||
.then(() => respond(res, {}))
|
||||
.catch((msg) => fail(res, 400, msg));
|
||||
};
|
@ -0,0 +1,17 @@
|
||||
import {Response} from "express";
|
||||
|
||||
export const fail = (res: Response, status: number, msg: string) => {
|
||||
res.status(status);
|
||||
res.json({
|
||||
status,
|
||||
reason: msg,
|
||||
});
|
||||
};
|
||||
|
||||
export const respond = (res: Response, data: any) => {
|
||||
res.status(200);
|
||||
res.json({
|
||||
status: 200,
|
||||
data,
|
||||
});
|
||||
};
|
@ -0,0 +1,245 @@
|
||||
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`;
|
||||
}
|
||||
|
||||
static async activeAppeals(userID: string, guild: string) {
|
||||
return await Appeal.createQueryBuilder("appeals")
|
||||
.where("appeals.userID = :userID", { userID })
|
||||
.andWhere("appeals.guildId = :guildID", { guildID: guild })
|
||||
.andWhere(
|
||||
new Brackets((qb) =>
|
||||
qb
|
||||
.where("appeals.status NOT IN (:...statuses)", {
|
||||
statuses: ["accepted", "rejected"],
|
||||
})
|
||||
.orWhere(
|
||||
new Brackets((qb) =>
|
||||
qb
|
||||
.where(`appeals.status = "rejected"`)
|
||||
.andWhere(
|
||||
"appeals.rejectionExpiry IS NULL OR appeals.rejectionExpiry > datetime(:now)",
|
||||
{ now: new Date().toISOString() }
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
.orderBy("appeals.created")
|
||||
.getMany();
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
if (guild) query.andWhere("appeals.guildId = :guild", { guild });
|
||||
|
||||
return await query.getMany();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
import { getManager } from "typeorm";
|
||||
import { DiscordCache } from "./DiscordCache";
|
||||
import { DiscordToken } from "./DiscordToken";
|
||||
import { StateToken } from "./StateToken";
|
||||
|
||||
export const pruneDB = (interval: number) => {
|
||||
const manager = getManager();
|
||||
|
||||
setInterval(() => {
|
||||
const cleaning = [
|
||||
{ type: StateToken, name: "state_tokens", col: "expiring" },
|
||||
{ type: DiscordToken, name: "discord_tokens", col: "expiring" },
|
||||
{ type: DiscordCache, name: "discord_cache", col: "expiring" }
|
||||
]
|
||||
|
||||
cleaning.forEach(({ type, name, col }) => {
|
||||
manager
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from(type, name)
|
||||
.where(`${name}.${col} < datetime(:now)`, { now: new Date().toISOString() })
|
||||
.execute()
|
||||
.catch(err => {
|
||||
console.error("Error while pruning db: " + err)
|
||||
})
|
||||
})
|
||||
}, interval);
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
import {BaseEntity, Column, Entity, PrimaryColumn} from "typeorm";
|
||||
import {DiscordToken} from "./DiscordToken";
|
||||
|
||||
@Entity({ name: "discord_cache" })
|
||||
export class DiscordCache extends BaseEntity {
|
||||
@PrimaryColumn({ unique: true })
|
||||
authToken: string;
|
||||
|
||||
@Column()
|
||||
userID: string;
|
||||
|
||||
@Column()
|
||||
username: string;
|
||||
|
||||
@Column()
|
||||
discriminator: string;
|
||||
|
||||
@Column({nullable: true })
|
||||
avatar?: string;
|
||||
|
||||
@Column()
|
||||
expiring: Date;
|
||||
|
||||
constructor(
|
||||
token: string,
|
||||
userID: string,
|
||||
name: string,
|
||||
discr: string,
|
||||
avatar?: string
|
||||
) {
|
||||
super();
|
||||
this.authToken = token;
|
||||
this.userID = userID;
|
||||
this.username = name;
|
||||
this.discriminator = discr;
|
||||
this.avatar = avatar;
|
||||
this.expiring = new Date();
|
||||
this.expiring.setHours(this.expiring.getHours() + 1)
|
||||
}
|
||||
|
||||
json() {
|
||||
return {
|
||||
id: this.userID,
|
||||
username: this.username,
|
||||
discriminator: this.discriminator,
|
||||
avatar: this.avatar,
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
import axios, { AxiosResponse } from "axios";
|
||||
import { BaseEntity, Column, Entity, PrimaryColumn } from "typeorm";
|
||||
import { API_URL } from "../config/Constants";
|
||||
import { DiscordCache } from "./DiscordCache";
|
||||
import { StateToken } from "./StateToken";
|
||||
|
||||
@Entity({ name: "discord_tokens" })
|
||||
export class DiscordToken extends BaseEntity {
|
||||
@PrimaryColumn("uuid")
|
||||
id: string;
|
||||
|
||||
@Column({ unique: true })
|
||||
authToken: string;
|
||||
|
||||
@Column()
|
||||
expiring: Date;
|
||||
|
||||
constructor(id: string, token: string) {
|
||||
super();
|
||||
this.id = id;
|
||||
this.authToken = token;
|
||||
this.expiring = new Date();
|
||||
this.expiring.setDate(this.expiring.getDate() + 7);
|
||||
}
|
||||
|
||||
async queryData(): Promise<DiscordCache> {
|
||||
const cache = await DiscordCache.findOne({
|
||||
where: { authToken: this.authToken },
|
||||
});
|
||||
if (cache) {
|
||||
return cache;
|
||||
}
|
||||
|
||||
let res: AxiosResponse<any>;
|
||||
try {
|
||||
res = await axios.get(`${API_URL}/users/@me`, {
|
||||
headers: { Authorization: `Bearer ${this.authToken}` },
|
||||
validateStatus: (status) => status < 500,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Unexpected discord response", this, err.response);
|
||||
return Promise.reject({
|
||||
reason: "An internal error has occurred.",
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
|
||||
if (res.status != 200) {
|
||||
console.error("Unexpected result from discord:", this.authToken, res.data);
|
||||
return Promise.reject({
|
||||
reason: "Invalid authorization code. Please logout and try again.",
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
let discordCache = new DiscordCache(
|
||||
this.id,
|
||||
res.data.id,
|
||||
res.data.username,
|
||||
res.data.discriminator,
|
||||
res.data.avatar
|
||||
);
|
||||
discordCache.save();
|
||||
return discordCache;
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm"
|
||||
|
||||
|
||||
@Entity({ name: 'guilds' })
|
||||
export class Guild extends BaseEntity {
|
||||
|
||||
@PrimaryGeneratedColumn("uuid")
|
||||
id: string
|
||||
|
||||
@Column()
|
||||
name: string
|
||||
|
||||
@Column()
|
||||
guild: string
|
||||
|
||||
@Column()
|
||||
channel: string
|
||||
|
||||
@Column()
|
||||
staffRole: string
|
||||
|
||||
@Column()
|
||||
jailedRole: string
|
||||
|
||||
@Column()
|
||||
defaultExpiry?: string
|
||||
|
||||
constructor(name: string, guild: string, channel: string, staffRole: string, jailedRole: string, defaultExpiry: string) {
|
||||
super()
|
||||
this.name = name
|
||||
this.guild = guild
|
||||
this.channel = channel
|
||||
this.staffRole = staffRole
|
||||
this.jailedRole = jailedRole
|
||||
this.defaultExpiry = defaultExpiry
|
||||
}
|
||||
|
||||
static async all(): Promise<Array<Guild>> {
|
||||
return await this.find()
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
import axios, {AxiosResponse} from "axios";
|
||||
import {
|
||||
BaseEntity,
|
||||
|
||||
Column,
|
||||
Entity,
|
||||
PrimaryGeneratedColumn
|
||||
} from "typeorm";
|
||||
import {URLSearchParams} from "url";
|
||||
import {Config} from "../config/Config";
|
||||
import {API_URL, OAUTH_SCOPE} from "../config/Constants";
|
||||
import {DiscordToken} from "./DiscordToken";
|
||||
|
||||
@Entity({ name: "state_tokens" })
|
||||
export class StateToken extends BaseEntity {
|
||||
@PrimaryGeneratedColumn("uuid")
|
||||
id: string;
|
||||
|
||||
@Column({ unique: true })
|
||||
token: string;
|
||||
|
||||
@Column()
|
||||
expiring: Date;
|
||||
|
||||
constructor(token: string) {
|
||||
super();
|
||||
|
||||
if (token === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.token = token;
|
||||
this.expiring = new Date();
|
||||
this.expiring.setMinutes(this.expiring.getMinutes() + 30);
|
||||
}
|
||||
|
||||
async verify({ api }: Config, code: string): Promise<DiscordToken> {
|
||||
const params = new URLSearchParams();
|
||||
params.append("client_id", api.client_id);
|
||||
params.append("client_secret", api.client_secret);
|
||||
params.append("grant_type", "authorization_code");
|
||||
params.append("code", `${code}`);
|
||||
params.append("redirect_uri", `${api.redirect_uri}`);
|
||||
params.append("scope", OAUTH_SCOPE);
|
||||
|
||||
let r: AxiosResponse;
|
||||
try {
|
||||
r = await axios.post(`${API_URL}/oauth2/token`, params.toString(), {
|
||||
validateStatus: (status) => status < 500,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Could not decode request data: ", this, err);
|
||||
Promise.reject({
|
||||
reason: "An internal error has occurred.",
|
||||
status: 500,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (r.status != 200) {
|
||||
console.error("Bad request from discord", r);
|
||||
Promise.reject({ reason: r.data["error_description"], status: 400 });
|
||||
return;
|
||||
}
|
||||
|
||||
const authToken = r.data["access_token"];
|
||||
|
||||
let dToken = await DiscordToken.findOne({ where: { authToken } });
|
||||
if (!dToken) {
|
||||
dToken = new DiscordToken(this.token, authToken);
|
||||
dToken.save();
|
||||
}
|
||||
|
||||
this.remove();
|
||||
return dToken;
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
import * as cors from "cors";
|
||||
import * as dotenv from "dotenv";
|
||||
import * as express from "express";
|
||||
import { createConnection } from "typeorm";
|
||||
import { EventEmitter } from "typeorm/platform/PlatformTools";
|
||||
import Bot from "./bot/Bot";
|
||||
import { ApiConfig, BotConfig, Config } from "./config/Config";
|
||||
import { PORT } from "./config/Constants";
|
||||
import { getApi } from "./controllers/Api";
|
||||
import { authorize } from "./controllers/Authorize";
|
||||
import { info } from "./controllers/Info";
|
||||
import { genOAuth } from "./controllers/OAuth";
|
||||
import { status } from "./controllers/Status";
|
||||
import { submit } from "./controllers/Submit";
|
||||
import { fail } from "./controllers/Util";
|
||||
import { pruneDB } from "./entity/Cleaner";
|
||||
|
||||
dotenv.config();
|
||||
const config = new Config();
|
||||
config.api = new ApiConfig(
|
||||
process.env.CORS_HOST.split(","),
|
||||
process.env.REDIRECT_URI,
|
||||
Number(process.env.API_PORT),
|
||||
process.env.API_CLIENT_SECRET,
|
||||
process.env.API_CLIENT_ID
|
||||
);
|
||||
config.bot = new BotConfig(
|
||||
process.env.BOT_TOKEN,
|
||||
process.env.BOT_PREFIX || "con!"
|
||||
);
|
||||
|
||||
const appealEmitter = new EventEmitter();
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use(cors({ origin: config.api.cors_host }));
|
||||
app.use(
|
||||
(err: Error, _req: express.Request, res: express.Response, _next: any) => {
|
||||
if (err instanceof SyntaxError) {
|
||||
fail(res, 400, "Invalid JSON: " + err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(err.message, err instanceof SyntaxError);
|
||||
fail(res, 500, "Something went wrong!");
|
||||
}
|
||||
);
|
||||
|
||||
app.set("config", config);
|
||||
app.set("emitter", appealEmitter);
|
||||
|
||||
app.get("/api", getApi);
|
||||
app.get("/oauth", genOAuth);
|
||||
app.post("/authorize", authorize);
|
||||
app.post("/info", info);
|
||||
app.post("/status", status);
|
||||
app.post("/submit", submit);
|
||||
|
||||
createConnection()
|
||||
.then(async () => {
|
||||
app.listen(PORT, () => console.log("Server is listening on port " + PORT));
|
||||
new Bot(config, appealEmitter);
|
||||
pruneDB(60 * 1000);
|
||||
})
|
||||
.catch((err) => console.error(err));
|
@ -0,0 +1,175 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Contrition
|
||||
version: 0.0.1
|
||||
contact:
|
||||
name: Project Maintainer
|
||||
email: 32350@jisedu.or.id
|
||||
paths:
|
||||
/oauth:
|
||||
post:
|
||||
tags:
|
||||
- OAuth Endpoints
|
||||
summary: Create a request to generate an OAuth Token
|
||||
responses:
|
||||
200:
|
||||
description: Successfully retrieve OAuth URL
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/OAuth'
|
||||
/verify:
|
||||
post:
|
||||
tags:
|
||||
- OAuth Endpoints
|
||||
summary: Callback with discord auth request
|
||||
responses:
|
||||
200:
|
||||
description: Successfully complete OAuth exchange
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/OAuth'
|
||||
400:
|
||||
$ref: '#/components/responses/MalformedBody'
|
||||
500:
|
||||
$ref: '#/components/responses/ServerError'
|
||||
|
||||
/submit:
|
||||
post:
|
||||
tags:
|
||||
- Form Endpoints
|
||||
summary: Post an unban request appeal
|
||||
requestBody:
|
||||
description: Form body
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
token:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
platform:
|
||||
type: string
|
||||
punishment_date:
|
||||
type: string
|
||||
ban_reason:
|
||||
type: string
|
||||
appeal_reason:
|
||||
type: string
|
||||
additional_info:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: Successfully posted appeal request
|
||||
400:
|
||||
$ref: '#/components/responses/MalformedBody'
|
||||
401:
|
||||
$ref: '#/components/responses/InvalidToken'
|
||||
422:
|
||||
$ref: '#/components/responses/Unprocessable'
|
||||
500:
|
||||
$ref: '#/components/responses/ServerError'
|
||||
|
||||
components:
|
||||
requestBodies:
|
||||
AuthCredentials:
|
||||
description: User credentials.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
required:
|
||||
- username
|
||||
- password
|
||||
type: object
|
||||
properties:
|
||||
username:
|
||||
description: The username of the user.
|
||||
type: string
|
||||
password:
|
||||
description: The password of the user.
|
||||
type: string
|
||||
responses:
|
||||
MalformedBody:
|
||||
description: The information in your body is incomplete, or your JSON format is invalid.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
example:
|
||||
reason: Invalid request body provided.
|
||||
code: 400
|
||||
InvalidToken:
|
||||
description: The access token you provided is invalid or missing.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
example:
|
||||
reason: The token provided is invalid.
|
||||
code: 401
|
||||
NotFound:
|
||||
description: The requested resource was not found.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
example:
|
||||
reason: The specified resource does not exist.
|
||||
code: 404
|
||||
Unprocessable:
|
||||
description: The request was valid, however some of the parameters have an invalid value.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
example:
|
||||
reason: Varies
|
||||
code: 422
|
||||
ServerError:
|
||||
description: The server could not complete a request due to an unknown error. More information may be available in the 'reason' parameter.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
example:
|
||||
reason: The internal server reached an unknown state.
|
||||
code: 500
|
||||
schemas:
|
||||
OAuth:
|
||||
type: object
|
||||
properties:
|
||||
redirectUrl:
|
||||
type: string
|
||||
format: url
|
||||
Verify:
|
||||
type: object
|
||||
properties:
|
||||
token:
|
||||
type: string
|
||||
user:
|
||||
type: object
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
discriminator:
|
||||
type: string
|
||||
banned:
|
||||
type: boolean
|
||||
Error:
|
||||
required:
|
||||
- reason
|
||||
- code
|
||||
type: object
|
||||
properties:
|
||||
reason:
|
||||
description: The cause of the error message.
|
||||
type: string
|
||||
code:
|
||||
description: The HTTP error code value accompanying the error.
|
||||
type: integer
|
||||
|
@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"es5",
|
||||
"es6"
|
||||
],
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "./dist",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"sourceMap": true
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue