Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 17 additions & 11 deletions src/events/$ready.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { ActivityType } from "discord.js";
import type { YuukoEvent } from "#structures/index";
import { YuukoEvent } from "#structures/index";
import { registerCommands, registerComponents, updateBotStats } from "#utils/index";

export const run: YuukoEvent<"ready"> = async (client) => {
if (!client.user) return;
setInterval(async () => {
await updateBotStats(client);
client.user?.setPresence({ activities: [{ type: ActivityType.Watching, name: `${client.guilds.cache.size} servers` }], status: "online" });
}, 15000);
const ready = new YuukoEvent({
event: "ready",
isOnce: true,
run: async (client) => {
if (!client.user) return;
setInterval(async () => {
await updateBotStats(client);
client.user?.setPresence({ activities: [{ type: ActivityType.Watching, name: `${client.guilds.cache.size} servers` }], status: "online" });
}, 15000);

await registerCommands(client);
await registerComponents(client);
await registerCommands(client);
await registerComponents(client);

client.log(`${client.user.tag} is ready!`, "Info");
};
client.log(`${client.user.tag} is ready!`, "Info");
}
});

export default [ready];
18 changes: 18 additions & 0 deletions src/events/guild.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { YuukoEvent } from "#structures/index";
import { updateBotStats } from "#utils/index";

const guildCreate = new YuukoEvent({
event: "guildCreate",
run: async (client) => {
await updateBotStats(client);
}
});

const guildDelete = new YuukoEvent({
event: "guildDelete",
run: async (client) => {
await updateBotStats(client);
}
});

export default [guildCreate, guildDelete];
6 changes: 0 additions & 6 deletions src/events/guildCreate.ts

This file was deleted.

6 changes: 0 additions & 6 deletions src/events/guildDelete.ts

This file was deleted.

137 changes: 71 additions & 66 deletions src/events/interactionCreate.ts
Original file line number Diff line number Diff line change
@@ -1,85 +1,90 @@
import { type Interaction, Collection, MessageFlags, time } from "discord.js";

import { embedError, logging, YuukoError } from "#utils/index";
import type { Client, ClientCommand, UsableInteraction, YuukoEvent, Middleware } from "#structures/index";
import { type Client, type ClientCommand, type UsableInteraction, YuukoEvent, type Middleware } from "#structures/index";

/* discord doesn't have the commands property in their class for somea reason */
export const run: YuukoEvent<"interactionCreate"> = async (client, interaction) => {
try {
// If the interaction wasn't a chat command, we ignore it
const interactionCreate = new YuukoEvent({
event: "interactionCreate",
run: async (client, interaction) => {
try {
// If the interaction wasn't a chat command, we ignore it

if (interaction.isChatInputCommand()) {
// We run the command based on the interaction
const command = client.commands.find((cmd) => cmd.name == interaction.commandName);
if (!command) return;
logging(command, interaction);
client.log(`Interaction received in: ${Date.now() - interaction.createdTimestamp}ms`, "Debug");
checkCooldown(client, command, interaction);
const args = await runMiddlewares(command.middlewares, interaction, client);
client.log(`Ran middleware in: ${Date.now() - interaction.createdTimestamp}ms`, "Debug");
if (interaction.isChatInputCommand()) {
// We run the command based on the interaction
const command = client.commands.find((cmd) => cmd.name == interaction.commandName);
if (!command) return;
logging(command, interaction);
client.log(`Interaction received in: ${Date.now() - interaction.createdTimestamp}ms`, "Debug");
checkCooldown(client, command, interaction);
const args = await runMiddlewares(command.middlewares, interaction, client);
client.log(`Ran middleware in: ${Date.now() - interaction.createdTimestamp}ms`, "Debug");

if (args.isCommand() && args.isChatInputCommand()) {
await command.run({ interaction: args, client });
client.log(`Ran command in: ${Date.now() - interaction.createdTimestamp}ms`, "Debug");
}

// Check for autocomplete
} else if (interaction.isAutocomplete()) {
const command = client.commands.find((cmd) => cmd.name == interaction.commandName);
if (!command || !interaction) return;
if (!command.autocomplete) return;
if (args.isCommand() && args.isChatInputCommand()) {
await command.run({ interaction: args, client });
client.log(`Ran command in: ${Date.now() - interaction.createdTimestamp}ms`, "Debug");
}

command.autocomplete(await runMiddlewares(command.middlewares, interaction, client));

// Check for modal
} else if (interaction.isModalSubmit()) {
const component = client.components.find((comp) => comp.name == interaction.customId);
if (!component) return;
component.run(await runMiddlewares(component.middlewares, interaction, client), null, client);
}
// Check for autocomplete
} else if (interaction.isAutocomplete()) {
const command = client.commands.find((cmd) => cmd.name == interaction.commandName);
if (!command || !interaction) return;
if (!command.autocomplete) return;

} catch (e: any) {
console.error(e);
command.autocomplete(await runMiddlewares(command.middlewares, interaction, client));

if (e instanceof YuukoError) {
if (!interaction.isCommand()) return;
// Check for modal
} else if (interaction.isModalSubmit()) {
const component = client.components.find((comp) => comp.name == interaction.customId);
if (!component) return;
component.run(await runMiddlewares(component.middlewares, interaction, client), null, client);
}

if (interaction.deferred)
return void interaction.editReply({ embeds: [embedError(e)] });
else
return void interaction.reply({
embeds: [embedError(e)],
flags: e.ephemeral ? MessageFlags.Ephemeral : undefined,
});
}
};
} catch (e: any) {
console.error(e);

async function runMiddlewares(middlewares: Middleware[] | undefined, interaction: Interaction, client: Client): Promise<Interaction> {
if (!interaction.isChatInputCommand()) return interaction;
if (!middlewares) return interaction;
if (middlewares.some((mw) => mw.defer)) await interaction.deferReply();
await Promise.all(middlewares.map((mw) => mw.run(interaction, client))).catch((e: any) => {
throw e;
});
return interaction;
}
if (e instanceof YuukoError) {
if (!interaction.isCommand()) return;

if (interaction.deferred)
return void interaction.editReply({ embeds: [embedError(e)] });
else
return void interaction.reply({
embeds: [embedError(e)],
flags: e.ephemeral ? MessageFlags.Ephemeral : undefined,
});
}
};

function checkCooldown(client: Client, command: ClientCommand, interaction: UsableInteraction): UsableInteraction {
if (!interaction.isChatInputCommand()) return interaction;
const commandCooldown = client.cooldowns.get(command.name);
if (!commandCooldown) {
client.cooldowns.set(command.name, new Collection());
async function runMiddlewares(middlewares: Middleware[] | undefined, interaction: Interaction, client: Client): Promise<Interaction> {
if (!interaction.isChatInputCommand()) return interaction;
if (!middlewares) return interaction;
if (middlewares.some((mw) => mw.defer)) await interaction.deferReply();
await Promise.all(middlewares.map((mw) => mw.run(interaction, client))).catch((e: any) => {
throw e;
});
return interaction;
}
if (commandCooldown.has(interaction.user.id)) {
const cooldownExpires = commandCooldown.get(interaction.user.id);
console.log(cooldownExpires);
if (!cooldownExpires) return interaction;
if (cooldownExpires > Date.now()) {
throw new YuukoError(`User ${interaction.user.tag} is on cooldown for command ${command.name}`, null, false, `Cooldown expires on ${time(Math.ceil(cooldownExpires / 1000), "f")} (${time(Math.ceil(cooldownExpires / 1000), "R")})`);


function checkCooldown(client: Client, command: ClientCommand, interaction: UsableInteraction): UsableInteraction {
if (!interaction.isChatInputCommand()) return interaction;
const commandCooldown = client.cooldowns.get(command.name);
if (!commandCooldown) {
client.cooldowns.set(command.name, new Collection());
return interaction;
}
if (commandCooldown.has(interaction.user.id)) {
const cooldownExpires = commandCooldown.get(interaction.user.id);
console.log(cooldownExpires);
if (!cooldownExpires) return interaction;
if (cooldownExpires > Date.now()) {
throw new YuukoError(`User ${interaction.user.tag} is on cooldown for command ${command.name}`, null, false, `Cooldown expires on ${time(Math.ceil(cooldownExpires / 1000), "f")} (${time(Math.ceil(cooldownExpires / 1000), "R")})`);
}
}
return interaction;
}
return interaction;
}
}
});

export default [interactionCreate];
21 changes: 20 additions & 1 deletion src/structures/event.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,28 @@
import type { ClientEvents as DiscordClientEvents, Interaction } from 'discord.js'
import type { Client } from './client'
import type { MaybePromise } from './command'

export type UsableClientEvents = DiscordClientEvents & {
interactionCreate: [Interaction]
}
export type ClientEvent = keyof UsableClientEvents
export type YuukoEvent<Event extends ClientEvent> = (client: Client, ...args: UsableClientEvents[Event]) => void


type YuukoEventRun<Event extends ClientEvent> = (client: Client, ...args: UsableClientEvents[Event]) => MaybePromise<void>;
interface YuukoEventOptions<Event extends ClientEvent> {
event: Event
isOnce?: boolean,
run: YuukoEventRun<Event>;
}

export class YuukoEvent<Event extends ClientEvent> {
event: Event
isOnce: boolean
run: YuukoEventRun<Event>;

constructor(options: YuukoEventOptions<Event>) {
this.event = options.event;
this.isOnce = options.isOnce ?? false;
this.run = options.run;
}
}
37 changes: 22 additions & 15 deletions src/utils/registerEvents.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
import path from "path";
import fs from "fs";
import { removeExtension } from ".";
import type { Client } from "#structures/index";
import type { Client, YuukoEvent } from "#structures/index";

export async function registerEvents(client: Client) {
const eventsPath = path.join(import.meta.dir, "..", "events");

fs.readdirSync(eventsPath)
.filter((x) => x.endsWith(".ts"))
.forEach((fileName) => {
const event = require(`${eventsPath}/${fileName}`);
let eventName = removeExtension(fileName);
const events: YuukoEvent<any>[] = (
await Promise.all(
fs
.readdirSync(eventsPath)
.filter(file => file.endsWith('.ts'))
.map(async (file) => {
const event = await import(path.join(eventsPath, file))
return event.default
}),
)
).flat();

if (!event?.run) {
client.log(`Event ${eventName} (${eventsPath}/${fileName}) does not have a run function`, "EventRegister");
process.exit(0);
}
const isOnce = eventName.startsWith("$");
eventName = eventName.substring(isOnce ? 1 : 0);
client[isOnce ? "once" : "on"](eventName, (...args) => event.run(client, ...args));
client.log(`Registered ${isOnce ? "once" : "on"}.${eventName} (${eventsPath}/${fileName})`, "EventRegister");
});
for (const event of events) {

if (!event.run) {
client.log(`Event ${event.event} does not have a run function`, "EventRegister");
process.exit(0);
}

client[event.isOnce ? "once" : "on"](event.event, (...args) => event.run(client, ...args));
client.log(`Registered ${event.isOnce ? "once" : "on"}.${event.event}`, "EventRegister");
}
}