Skip to content

Commit

Permalink
refactor and improve bot core a lot
Browse files Browse the repository at this point in the history
using proper typing and generics to get fully implicitly inferred command and
component types
  • Loading branch information
3vorp committed Jan 31, 2024
1 parent 64ac6d0 commit 5a45a97
Show file tree
Hide file tree
Showing 70 changed files with 302 additions and 324 deletions.
22 changes: 10 additions & 12 deletions .vscode/templates.code-snippets
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
"description": "Client-emitted event template.",
"body": [
"import { Event } from \"@interfaces/events\";",
"import { Client } from \"@client\"",
"\nexport default {",
"\tname: \"${1:Name}\",",
"\tasync execute(client: Client, ${2:/* args */}) {",
"\tasync execute(client, ${2:/* args */}) {",
"\t\t${3://todo: implement event",
"\t}},\n} as Event;"
]
Expand All @@ -17,19 +16,19 @@
"prefix": "nact",
"description": "For non-static slash command data.",
"body": [
"import { SlashCommand, SyncSlashCommandBuilder } from \"@interfaces/commands\";",
"import { Client, ChatInputCommandInteraction, EmbedBuilder } from \"@client\";",
"import { SlashCommand, SyncSlashCommandBuilder } from \"@interfaces/interactions\";",
"import { EmbedBuilder } from \"@client\";",
"import { SlashCommandBuilder } from \"discord.js\";",
"\nexport const command: SlashCommand = {",
"\tasync data(client: Client): Promise<SyncSlashCommandBuilder> {",
"\tasync data(client): Promise<SyncSlashCommandBuilder> {",
"\t\t/**",
"\t\t * todo: implement fetched stuff here",
"\t\t */",
"\t\treturn new SlashCommandBuilder()",
"\t\t\t.setName(\"${1:Name}\")",
"\t\t\t.setDescription(\"${2:Description}\");",
"\t},",
"\tasync execute(interaction: ChatInputCommandInteraction) {",
"\tasync execute(interaction) {",
"\t\t${3://todo: implement command}",
"\t},\n};"
]
Expand All @@ -39,14 +38,14 @@
"prefix": "nct",
"description": "Regular slash command template.",
"body": [
"import { SlashCommand } from \"@interfaces/commands\";",
"import { Client, ChatInputCommandInteraction, EmbedBuilder } from \"@client\";",
"import { SlashCommand } from \"@interfaces/interactions\";",
"import { Client, EmbedBuilder } from \"@client\";",
"import { SlashCommandBuilder } from \"discord.js\";",
"\nexport const command: SlashCommand = {",
"\tdata: new SlashCommandBuilder()",
"\t\t.setName(\"${1:Name}\")",
"\t\t.setDescription(`${2:Description}`),",
"\tasync execute(interaction: ChatInputCommandInteraction) {",
"\tasync execute(interaction) {",
"\t\t${3://todo: implement command}",
"\t},\n};"
]
Expand All @@ -57,13 +56,12 @@
"description": "Generic component template for buttons, menus, and modals."
"body": [
"import { Component } from \"@interfaces\";",
"import { Client } from \"@client\";",
"\nexport default {",
"\tid: \"${1:Name}\"",
"\tasync execute(client: Client, interaction: \"${2:Interaction}\") {"
"\tasync execute(client, interaction) {"
"\t\t${3://todo: implement component}",
"\t}"
"} as Component;"
"} as Component<${2:Interaction}>;"
]
},

Expand Down
105 changes: 57 additions & 48 deletions src/client/client.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import {
ActivityType,
ButtonInteraction,
Client,
ClientOptions,
Collection,
ChatInputCommandInteraction,
Guild,
StringSelectMenuInteraction,
ModalSubmitInteraction,
RESTPostAPIApplicationCommandsJSONBody,
Routes,
REST,
} from "discord.js";
import { Message, Automation } from "@client";
import {
Message,
Automation,
StringSelectMenuInteraction,
ButtonInteraction,
ModalSubmitInteraction,
ChatInputCommandInteraction,
} from "@client";
import { Tokens } from "@interfaces/tokens";
import { Component } from "@interfaces/components";
import { SlashCommand } from "@interfaces/commands";
import { AnyInteraction, SlashCommand } from "@interfaces/interactions";
import { Event } from "@interfaces/events";
import { EmittingCollection } from "@helpers/emittingCollection";
import { setData, getData } from "@utility/handleJSON";
Expand Down Expand Up @@ -59,13 +62,7 @@ export type LogAction =
| "guildMemberUpdate"
| "guildJoined";

export type LogData =
| Message
| Guild
| ButtonInteraction
| StringSelectMenuInteraction
| ChatInputCommandInteraction
| ModalSubmitInteraction;
export type LogData = Message | Guild | AnyInteraction;

export type Log = {
type: LogAction;
Expand All @@ -87,11 +84,10 @@ export class ExtendedClient<Ready extends boolean = boolean> extends Client<Read
private maxLogs = 50;
private lastLogIndex = 0;

// we reuse the same component type so one function can load all collections
public menus = new Collection<string, Component>();
public buttons = new Collection<string, Component>();
public modals = new Collection<string, Component>();
public slashCommands = new Collection<string, SlashCommand>();
public readonly menus = new Collection<string, Component<StringSelectMenuInteraction>>();
public readonly buttons = new Collection<string, Component<ButtonInteraction>>();
public readonly modals = new Collection<string, Component<ModalSubmitInteraction>>();
public readonly slashCommands = new Collection<string, SlashCommand>();

public polls = new EmittingCollection<string, Poll>();
public commandsProcessed = new EmittingCollection<string, number>();
Expand All @@ -103,7 +99,7 @@ export class ExtendedClient<Ready extends boolean = boolean> extends Client<Read
this.firstStart = firstStart;
}

public init(interaction?: ChatInputCommandInteraction) {
public init(interaction?: AnyInteraction) {
// pretty stuff so it doesnt print the logo upon restart
if (!this.firstStart) {
console.log(`${success}Restarted`);
Expand Down Expand Up @@ -140,7 +136,7 @@ export class ExtendedClient<Ready extends boolean = boolean> extends Client<Read
return this;
}

public async restart(interaction?: ChatInputCommandInteraction) {
public async restart(interaction?: AnyInteraction) {
console.log(`${info}Restarting bot...`);
this.destroy();
startClient(false, interaction);
Expand Down Expand Up @@ -172,62 +168,69 @@ export class ExtendedClient<Ready extends boolean = boolean> extends Client<Read
}

/**
* Read & Load data from json file into emitting collection & setup events handler
* @param collection
* Read and load data from a JSON file into emitting collection with events
* @author Nick, Juknum
* @param collection collection to load into
* @param filename
* @param relative_path
* @param relativePath
*/
private loadCollection(
collection: EmittingCollection<any, any>,
private loadCollection<V>(
collection: EmittingCollection<string, V>,
filename: string,
relative_path: string,
relativePath: string,
) {
const obj = getData({ filename, relative_path });
Object.keys(obj).forEach((key: string) => collection.set(key, obj[key]));
const obj: Record<string, V> = getData({ filename, relativePath });
Object.entries(obj).forEach(([k, v]) => collection.set(k, v));

collection.events.on("dataSet", (key: string, value: any) =>
this.saveEmittingCollection(collection, filename, relative_path),
collection.events.on("dataSet", () =>
this.saveEmittingCollection(collection, filename, relativePath),
);
collection.events.on("dataDeleted", (key: string) =>
this.saveEmittingCollection(collection, filename, relative_path),

collection.events.on("dataDeleted", () =>
this.saveEmittingCollection(collection, filename, relativePath),
);
}

/**
* Save an emitting collection into a JSON file
* @author Nick, Juknum
* @param collection
* @param filename
* @param relative_path
* @param relativePath
*/
private saveEmittingCollection(
collection: EmittingCollection<any, any>,
private saveEmittingCollection<V>(
collection: EmittingCollection<string, V>,
filename: string,
relative_path: string,
relativePath: string,
) {
let data = {};
[...collection.keys()].forEach((k: string) => {
[...collection.keys()].forEach((k) => {
data[k] = collection.get(k);
});
setData({ filename, relative_path, data: JSON.parse(JSON.stringify(data)) });

setData({ filename, relativePath, data: JSON.parse(JSON.stringify(data)) });
}

/**
* SLASH COMMANDS DELETION
* Remove slash commands from the menu
* @author Nick
*/
public deleteGlobalSlashCommands() {
public async deleteGlobalSlashCommands() {
console.log(`${success}deleting slash commands`);

const rest = new REST({ version: "10" }).setToken(this.tokens.token);
rest.get(Routes.applicationCommands(this.user.id)).then((data: any) => {
const promises = [];
for (const command of data)
promises.push(rest.delete(`${Routes.applicationCommands(this.user.id)}/${command.id}`));
return Promise.all(promises).then(() => console.log(`${success}Delete complete`));
});
const commands = (await rest.get(Routes.applicationCommands(this.user.id))) as any[];
await Promise.all(
commands.map((command) =>
rest.delete(`${Routes.applicationCommands(this.user.id)}/${command.id}`),
),
);
console.log(`${success}Delete complete`);
}

/**
* SLASH COMMANDS HANDLER
* Load slash commands
* @author Nick, Juknum
*/
public async loadSlashCommands() {
const commandsArr: {
Expand Down Expand Up @@ -297,7 +300,8 @@ export class ExtendedClient<Ready extends boolean = boolean> extends Client<Read
}

/**
* Read "Events" directory and add them as events
* Load client events
* @author Nick
*/
private loadEvents() {
if (this.tokens.maintenance)
Expand All @@ -319,6 +323,7 @@ export class ExtendedClient<Ready extends boolean = boolean> extends Client<Read

/**
* Convenience method to load all components at once
* @author Evorp
*/
private loadComponents() {
for (const [key, path] of Object.entries(paths.components)) this.loadComponent(this[key], path);
Expand All @@ -327,6 +332,9 @@ export class ExtendedClient<Ready extends boolean = boolean> extends Client<Read

/**
* Read given directory and add them to needed collection
* @author Evorp
* @param collection collection to load into
* @param path filepath to read and load component data from
*/
private loadComponent(collection: Collection<string, Component>, path: string) {
const components = walkSync(path).filter((file) => file.endsWith(".ts"));
Expand All @@ -339,6 +347,7 @@ export class ExtendedClient<Ready extends boolean = boolean> extends Client<Read

/**
* Store any kind of action the bot does
* @author Juknum
* @param type
* @param data
*/
Expand Down
2 changes: 1 addition & 1 deletion src/client/interaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ async function complete() {
.catch(() => {});
}

// add methods to just the base so it applies to all interaction types
// no idea how adding methods here adds them everywhere but I'm not complaining
BaseInteraction.prototype.hasPermission = hasPermission;
BaseInteraction.prototype.strings = strings;
BaseInteraction.prototype.ephemeralReply = ephemeralReply;
Expand Down
9 changes: 4 additions & 5 deletions src/commands/bot/botban.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SlashCommand, SlashCommandI } from "@interfaces/commands";
import { EmbedBuilder, ChatInputCommandInteraction } from "@client";
import { SlashCommand, SlashCommandI } from "@interfaces/interactions";
import { EmbedBuilder } from "@client";
import {
Collection,
AttachmentBuilder,
Expand Down Expand Up @@ -52,13 +52,12 @@ export const command: SlashCommand = {
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages)
.setDMPermission(false),
execute: new Collection<string, SlashCommandI>()
.set("edit", async (interaction: ChatInputCommandInteraction) => {
.set("edit", async (interaction) => {
const isAdding = interaction.options.getString("action", true) == "add";

await interaction.deferReply({ ephemeral: true });
const banlist = require("@json/botbans.json");
const victim = interaction.options.getUser("subject");
const member = await interaction.guild.members.fetch(victim.id);

if (
interaction.client.tokens.developers.includes(victim.id) ||
Expand Down Expand Up @@ -91,7 +90,7 @@ export const command: SlashCommand = {
],
});
})
.set("view", async (interaction: ChatInputCommandInteraction) => {
.set("view", async (interaction) => {
if (!interaction.hasPermission("dev")) return;

await interaction.deferReply({ ephemeral: true });
Expand Down
6 changes: 3 additions & 3 deletions src/commands/bot/embed.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SlashCommand } from "@interfaces/commands";
import { ChatInputCommandInteraction, EmbedBuilder, Message } from "@client";
import { SlashCommand } from "@interfaces/interactions";
import { EmbedBuilder, Message } from "@client";
import { ColorResolvable, PermissionFlagsBits, SlashCommandBuilder } from "discord.js";
import { colors } from "@utility/colors";

Expand Down Expand Up @@ -38,7 +38,7 @@ export const command: SlashCommand = {
)
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
.setDMPermission(false),
async execute(interaction: ChatInputCommandInteraction) {
async execute(interaction) {
if (!interaction.options.getString("title") && !interaction.options.getString("description")) {
return interaction.reply({
embeds: [
Expand Down
5 changes: 2 additions & 3 deletions src/commands/bot/feedback.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { SlashCommand } from "@interfaces/commands";
import { SlashCommand } from "@interfaces/interactions";
import {
ActionRowBuilder,
SlashCommandBuilder,
TextInputBuilder,
TextInputStyle,
ModalBuilder,
} from "discord.js";
import { ChatInputCommandInteraction } from "@client";

// I'm sorry...
export const feedbackFormat = {
Expand Down Expand Up @@ -68,7 +67,7 @@ export const command: SlashCommand = {
)
.setRequired(true),
),
async execute(interaction: ChatInputCommandInteraction) {
async execute(interaction) {
const type = interaction.options.getString("type");
const modal = new ModalBuilder().setCustomId(`${type}Ticket`).setTitle(`New ${type} issue`);

Expand Down
6 changes: 3 additions & 3 deletions src/commands/bot/info.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { SlashCommand } from "@interfaces/commands";
import { SlashCommand } from "@interfaces/interactions";
import { SlashCommandBuilder } from "discord.js";
import { ChatInputCommandInteraction, EmbedBuilder } from "@client";
import { EmbedBuilder } from "@client";
import axios from "axios";

export const command: SlashCommand = {
data: new SlashCommandBuilder().setName("info").setDescription("General info about CompliBot."),
async execute(interaction: ChatInputCommandInteraction) {
async execute(interaction) {
const image: string = (
await axios.get(`${interaction.client.tokens.apiUrl}settings/images.bot`)
).data;
Expand Down
5 changes: 2 additions & 3 deletions src/commands/bot/logs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { SlashCommand } from "@interfaces/commands";
import { SlashCommand } from "@interfaces/interactions";
import { SlashCommandBuilder, PermissionFlagsBits } from "discord.js";
import { ChatInputCommandInteraction } from "@client";
import { constructLogFile } from "@functions/errorHandler";

export const command: SlashCommand = {
Expand All @@ -9,7 +8,7 @@ export const command: SlashCommand = {
.setDescription("Get logs of the bot.")
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
.setDMPermission(false),
async execute(interaction: ChatInputCommandInteraction) {
async execute(interaction) {
if (!interaction.hasPermission("dev")) return;

await interaction.deferReply();
Expand Down
Loading

0 comments on commit 5a45a97

Please sign in to comment.