Skip to content
Draft
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
9 changes: 4 additions & 5 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@
- [x] Chat (`/`) commands
- [x] User commands
- [x] Message commands
- [ ] Autocomplete
- [ ] Error Handling
- [ ] Permissions
- [ ] Owner
- [ ] Admin
- [x] Autocomplete
- [ ] Components
- [ ] Modals
- [x] Error Handling
- [ ] Documentation
4 changes: 2 additions & 2 deletions dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}
WORKDIR /usr/src/app
COPY package*.json .
RUN npm ci --only=production
RUN npm ci --omit=dev
COPY --from=build /usr/src/app/dist ./dist
CMD ["node", "dist/index.js"]
CMD ["node", "dist/index.js"]
317 changes: 194 additions & 123 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"typescript": "^4.8.2"
},
"dependencies": {
"discord.js": "^14.3.0"
"discord.js": "^14.7.1",
"fuse.js": "^6.6.2"
}
}
24 changes: 10 additions & 14 deletions src/classes/Client.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { Client } from "discord.js";
import { Client, GatewayIntentBits } from "discord.js";
import config from "../config";
import * as modules from "../modules";
import { modules } from "../modules";
import logger from "../modules/core/logger";

export default class ExtendedClient extends Client {
constructor() {
super({
intents: [
"Guilds",
"GuildBans",
"GuildMembers"
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildModeration,
GatewayIntentBits.GuildMembers,
]
});
}
Expand All @@ -20,17 +21,12 @@ export default class ExtendedClient extends Client {
}

loadModules() {
// console.log("[🧩] Loading modules...");
let loaded = 0;
Object.values(modules).forEach((v) => {
modules.forEach((v) => {
try {
new v.default(this);
loaded++;
// console.log(`[🧩] Loaded module ${v.default.id}`);
new v().load(this);
} catch (err) {
console.log(`[⚠️] Failed to load module ${v.default.id}: ${err}`);
logger.warn(`Failed to load ${v.name}: ${err}`);
}
});
// console.log(`[🧩] Loaded ${loaded} modules`);
}
}
}
27 changes: 10 additions & 17 deletions src/classes/Command.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
import { ApplicationCommandDataResolvable, ChatInputApplicationCommandData, ChatInputCommandInteraction, Interaction, MessageApplicationCommandData, MessageContextMenuCommandInteraction, UserApplicationCommandData, UserContextMenuCommandInteraction } from "discord.js";
import { ApplicationCommandData, AutocompleteInteraction, BaseInteraction } from "discord.js";
import ExtendedClient from "./Client";

export interface Command {
data: ApplicationCommandDataResolvable;
run(interaction: Interaction, client?: ExtendedClient): void | Promise<void>;
}

export interface ChatCommand extends Command {
data: ChatInputApplicationCommandData;
run(interaction: ChatInputCommandInteraction, client?: ExtendedClient): void | Promise<void>;
export enum CommandType {
CHAT,
MESSAGE,
USER,
}

export interface UserCtxCommand extends Command {
data: UserApplicationCommandData;
run(interaction: UserContextMenuCommandInteraction, client?: ExtendedClient): void | Promise<void>;
export interface Command {
type: CommandType;
data: ApplicationCommandData;
run(interaction: BaseInteraction, client?: ExtendedClient): void | Promise<void>;
autocomplete?(interaction: AutocompleteInteraction): void | Promise<void>;
}

export interface MessageCtxCommand extends Command {
data: MessageApplicationCommandData;
run(interaction: MessageContextMenuCommandInteraction, client?: ExtendedClient): void | Promise<void>;
}
10 changes: 3 additions & 7 deletions src/classes/Module.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import ExtendedClient from "./Client";

export default class Module {
public static id = "core.default";

constructor(client: ExtendedClient) {
throw new Error("Module does not implement a constructor.");
}
}
export default interface Module {
load(client: ExtendedClient): void | Promise<void>;
}
43 changes: 43 additions & 0 deletions src/commands/chat/autocomplete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ApplicationCommandOptionType, AutocompleteInteraction, ChatInputApplicationCommandData, ChatInputCommandInteraction } from "discord.js";
import Client from "../../classes/Client";
import { Command, CommandType } from "../../classes/Command";
import Fuse from 'fuse.js';
import { stringToOption } from "../../utils/autocomplete";

const colors = ["Red", "Green", "Blue"];
const completions = new Fuse<string>(colors);

export default class AutocompleteCommand implements Command {
type = CommandType.CHAT;
data: ChatInputApplicationCommandData = {
name: "autocomplete",
description: "Test command for autocomplete feature.",
dmPermission: true,
options: [
{
type: ApplicationCommandOptionType.String,
name: "color",
nameLocalizations: {
"en-GB": "colour",
},
description: "The color to select.",
descriptionLocalizations: {
"en-GB": "The colour to select."
},
autocomplete: true,
required: true
}
]
}

async run(interaction: ChatInputCommandInteraction, _client: Client) {
await interaction.reply({ content: interaction.options.getString("color", true), ephemeral: true });
}

async autocomplete(interaction: AutocompleteInteraction) {
let text = interaction.options.getFocused();
if (text.length == 0) return await interaction.respond(colors.map(stringToOption));
let matches = completions.search(text);
await interaction.respond(matches.map(v => stringToOption(v.item)));
}
}
1 change: 0 additions & 1 deletion src/commands/chat/index.ts

This file was deleted.

19 changes: 0 additions & 19 deletions src/commands/chat/info/ping.ts

This file was deleted.

20 changes: 20 additions & 0 deletions src/commands/chat/ping.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ChatInputApplicationCommandData, ChatInputCommandInteraction, EmbedBuilder } from "discord.js";
import Client from "../../classes/Client";
import { Command, CommandType } from "../../classes/Command";

export default class PingCommand implements Command {
type = CommandType.CHAT;
data: ChatInputApplicationCommandData = {
name: "ping",
description: "Displays the bot's API Latency."
};

async run(interaction: ChatInputCommandInteraction, client: Client) {
let embed = new EmbedBuilder()
.setTitle("🏓 Ping")
.setDescription("```yaml\nAPI Latency: " + client.ws.ping + "ms\n```")
.setColor("Random")
.setTimestamp()
await interaction.reply({ embeds: [embed] })
}
}
12 changes: 12 additions & 0 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Command } from "../classes/Command";
type Commands = { new(): Command; }[];

import c_ping from "./chat/ping";
import c_autocomplete from "./chat/autocomplete";
export const chat: Commands = [c_ping, c_autocomplete];

import m_info from "./message/info";
export const message: Commands = [m_info];

import u_info from "./user/info";
export const user: Commands = [u_info];
1 change: 0 additions & 1 deletion src/commands/message/index.ts

This file was deleted.

25 changes: 25 additions & 0 deletions src/commands/message/info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ApplicationCommandType, EmbedBuilder, MessageApplicationCommandData, MessageContextMenuCommandInteraction } from "discord.js";
import { Command, CommandType } from "../../classes/Command";

export default class UserInfoCtxCommand implements Command {
type = CommandType.MESSAGE;
data: MessageApplicationCommandData = {
type: ApplicationCommandType.Message,
name: "Message Info",
dmPermission: false
};

run(interaction: MessageContextMenuCommandInteraction) {
let embed = new EmbedBuilder()
.setAuthor({ name: interaction.user.tag })
.setDescription(interaction.targetMessage.cleanContent)
.setThumbnail(interaction.targetMessage.member?.displayAvatarURL() ?? interaction.targetMessage.author.displayAvatarURL())
.addFields({ name: "Information", value: "```yaml\n" + `ID: ${interaction.targetId}\nSent: ${interaction.targetMessage.createdAt.toUTCString()}\nEdited: ${interaction.targetMessage.editedAt?.toUTCString() ?? "(never)"}` + "\n```" })
.setColor("Random")
.setTimestamp();
interaction.reply({
embeds: [embed],
ephemeral: true
});
}
}
24 changes: 0 additions & 24 deletions src/commands/message/info/info.ts

This file was deleted.

1 change: 0 additions & 1 deletion src/commands/user/index.ts

This file was deleted.

11 changes: 6 additions & 5 deletions src/commands/user/info/info.ts → src/commands/user/info.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { ApplicationCommandType, EmbedBuilder, GuildMember, UserApplicationCommandData, UserContextMenuCommandInteraction } from "discord.js";
import { UserCtxCommand } from "../../../classes/Command";
import { Command, CommandType } from "../../classes/Command";

export default class UserInfoCtxCommand implements UserCtxCommand {
export default class UserInfoCtxCommand implements Command {
type = CommandType.USER;
data: UserApplicationCommandData = {
type: ApplicationCommandType.User,
name: "User Info",
Expand All @@ -11,14 +12,14 @@ export default class UserInfoCtxCommand implements UserCtxCommand {
run(interaction: UserContextMenuCommandInteraction) {
let member = interaction.targetMember as GuildMember;
let embed = new EmbedBuilder()
.setAuthor({name: member.displayName})
.setAuthor({ name: member.displayName })
.setThumbnail(member.displayAvatarURL())
.setDescription("```yaml\n"+`ID: ${member.id}\nUsername: ${member.user.tag}`+"\n```")
.setDescription("```yaml\n" + `ID: ${member.id}\nUsername: ${member.user.tag}` + "\n```")
.setColor("Random")
.setTimestamp();
interaction.reply({
embeds: [embed],
ephemeral: true
});
}
}
}
2 changes: 1 addition & 1 deletion src/config.example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ export default {

type Config = {
TOKEN: string;
}
}
21 changes: 10 additions & 11 deletions src/modules/core/logger.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
import ExtendedClient from "../../classes/Client";
import { Events } from "discord.js";
import Client from "../../classes/Client";
import Module from "../../classes/Module";

export default class LoggerModule implements Module {
public static id = "core.logger";

constructor(client: ExtendedClient) {
client.on("ready", () => this.onReady(client));
client.on("error", LoggerModule.error);
client.on("warn", LoggerModule.warn);
// client.on("debug", LoggerModule.debug);
load(client: Client) {
client.on(Events.ClientReady, () => this.onReady(client));
client.on(Events.Error, LoggerModule.error);
client.on(Events.Warn, LoggerModule.warn);
// client.on(Events.Debug, LoggerModule.debug);
}

private onReady(client: ExtendedClient) {
private onReady(client: Client) {
console.log(`[📝] Logged in as ${client.user?.tag ?? "(unknown)"}`);
}

public static error(err: Error) {
console.log(`[🛑] ${err.name}: ${err.message}`+err.stack?`\n\t${err.stack?.replaceAll("\n","\n\t")}`:"");
console.log(`[🛑] ${err.name}: ${err.message}` + err.stack ? `\n\t${err.stack?.replaceAll("\n", "\n\t")}` : "");
}

public static warn(msg: string) {
Expand All @@ -26,4 +25,4 @@ export default class LoggerModule implements Module {
public static debug(msg: string) {
console.log(`[🐛] ${msg}`);
}
}
}
24 changes: 11 additions & 13 deletions src/modules/core/status.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
import { ActivityOptions, ActivityType } from "discord.js";
import ExtendedClient from "../../classes/Client";
import { ActivityOptions, ActivityType, Events } from "discord.js";
import Client from "../../classes/Client";
import Module from "../../classes/Module";

const activities: ActivityOptions[] = [
{type: ActivityType.Watching, name: "over the chats."},
{type: ActivityType.Listening, name: "various beeps and boops."}
{ type: ActivityType.Watching, name: "over the chats." },
{ type: ActivityType.Listening, name: "various beeps and boops." }
]

export default class LoggerModule implements Module {
public static id = "core.status";

constructor(client: ExtendedClient) {
client.on("ready", () => this.onReady(client));
export default class StatusModule implements Module {
load(client: Client): void {
client.on(Events.ClientReady, () => this.onReady(client));
}

onReady(client: ExtendedClient) {
client.user?.setActivity({type: ActivityType.Watching, name: "myself start up..."});
private onReady(client: Client) {
client.user?.setActivity({ type: ActivityType.Watching, name: "myself start up..." });
let act = 0;
setInterval(() => {
client.user?.setActivity(activities[act]);
if(++act>=activities.length) act = 0;
if (++act >= activities.length) act = 0;
}, 30_000);
}
}
}
Loading