A powerful and extendable Discord bot, with it's own module system :3 thevoid.cafe/projects/voidy

✨ Add economy system base, client logger and more!

Changed files
+292 -11
packages
bot
src
modules
core
economy
commands
currency
events
schemas
toys
user
framework
+3 -1
packages/bot/src/index.ts
··· 4 4 5 5 // Client initialization with intents and stuff... 6 6 const client = new VoidyClient({ 7 - intents: [GatewayIntentBits.Guilds], 7 + intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages], 8 + developers: ["423520077246103563"], 9 + logChannelId: "1451025628206731459" 8 10 }); 9 11 10 12 // Database URI validation and connection check
+1 -1
packages/bot/src/modules/core/module.ts
··· 4 4 id: "core", 5 5 name: "Core", 6 6 description: "Initializes the bot and registers all commands.", 7 - author: "jokiller230", 7 + author: "thevoid.cafe", 8 8 9 9 exports: [ 10 10 {
+86
packages/bot/src/modules/economy/commands/currency/set.ts
··· 1 + import { MessageFlags, SlashCommandSubcommandBuilder } from "discord.js"; 2 + import { UserCurrencyType, UserCurrency } from "../../schemas/UserCurrency"; 3 + import type { Command } from "@voidy/framework"; 4 + 5 + export default { 6 + id: "currency.set", 7 + devOnly: true, 8 + data: new SlashCommandSubcommandBuilder() 9 + .setName("set") 10 + .setDescription("Override a users current balance.") 11 + .addUserOption((option) => 12 + option.setName("user").setDescription("The user whose balance to override.").setRequired(true) 13 + ) 14 + .addStringOption((option) => 15 + option 16 + .setName("currency") 17 + .setDescription("The currency to override.") 18 + .setRequired(true) 19 + .setChoices( 20 + { name: "BiTS", value: UserCurrencyType.BITS }, 21 + { name: "Gems", value: UserCurrencyType.GEMS } 22 + ) 23 + ) 24 + .addIntegerOption((option) => 25 + option 26 + .setName("new_balance") 27 + .setDescription("The new balance to set.") 28 + .setRequired(true) 29 + ), 30 + 31 + execute: async (interaction, _client) => { 32 + const { options } = interaction; 33 + 34 + // Retrieve the requested user from our interaction options 35 + const user = options.getUser("user"); 36 + if (!user) { 37 + await interaction.reply({ 38 + content: "Please provide a valid user.", 39 + flags: [MessageFlags.Ephemeral], 40 + }); 41 + 42 + return; 43 + }; 44 + 45 + // Retrieve the selected currency from our interaction options 46 + const selectedCurrency = options.getString("currency"); 47 + if (!selectedCurrency) { 48 + await interaction.reply({ 49 + content: "Please provide a valid currency.", 50 + flags: [MessageFlags.Ephemeral], 51 + }); 52 + 53 + return; 54 + } 55 + 56 + // Retrieve the new balance from our interaction options 57 + const newBalance = options.getInteger("new_balance"); 58 + if (!newBalance) { 59 + await interaction.reply({ 60 + content: "Please provide a valid new balance.", 61 + flags: [MessageFlags.Ephemeral], 62 + }); 63 + 64 + return; 65 + } 66 + 67 + // Retrieve the requested user's balance from our database 68 + let userBalance = await UserCurrency.findOne({ userId: user.id, type: selectedCurrency }); 69 + if (!userBalance) { 70 + userBalance = await UserCurrency.create({ 71 + userId: user.id, 72 + type: selectedCurrency, 73 + amount: newBalance, 74 + }); 75 + } else { 76 + userBalance.amount = newBalance; 77 + await userBalance.save(); 78 + } 79 + 80 + // Reply with the requested user's current balance 81 + await interaction.reply({ 82 + content: `Balance of user \`${user.username}\` has been set to \`${userBalance.amount} ${selectedCurrency.toUpperCase()}\`.`, 83 + flags: [MessageFlags.Ephemeral], 84 + }); 85 + }, 86 + } as Command;
+66
packages/bot/src/modules/economy/commands/currency/view.ts
··· 1 + import { MessageFlags, SlashCommandSubcommandBuilder } from "discord.js"; 2 + import { UserCurrencyType, UserCurrency } from "../../schemas/UserCurrency"; 3 + import type { Command } from "@voidy/framework"; 4 + 5 + export default { 6 + id: "currency.view", 7 + data: new SlashCommandSubcommandBuilder() 8 + .setName("view") 9 + .setDescription("Retrieve a users current balance.") 10 + .addUserOption((option) => 11 + option.setName("user").setDescription("The user whose balance to view.").setRequired(true) 12 + ) 13 + .addStringOption((option) => 14 + option 15 + .setName("currency") 16 + .setDescription("The currency to view.") 17 + .setRequired(true) 18 + .setChoices( 19 + { name: "BiTS", value: UserCurrencyType.BITS }, 20 + { name: "Gems", value: UserCurrencyType.GEMS } 21 + ) 22 + ), 23 + 24 + execute: async (interaction, _client) => { 25 + const { options } = interaction; 26 + 27 + // Retrieve the requested user from our interaction options 28 + const user = options.getUser("user"); 29 + if (!user) { 30 + await interaction.reply({ 31 + content: "Please provide a valid user.", 32 + flags: [MessageFlags.Ephemeral], 33 + }); 34 + 35 + return; 36 + }; 37 + 38 + // Retrieve the selected currency from our interaction options 39 + const selectedCurrency = options.getString("currency"); 40 + if (!selectedCurrency) { 41 + await interaction.reply({ 42 + content: "Please provide a valid currency.", 43 + flags: [MessageFlags.Ephemeral], 44 + }); 45 + 46 + return; 47 + } 48 + 49 + // Retrieve the requested user's balance from our database 50 + const userBalance = await UserCurrency.findOne({ userId: user.id, type: selectedCurrency }); 51 + if (!userBalance || UserCurrencyType === undefined || userBalance.amount === 0) { 52 + await interaction.reply({ 53 + content: `The requested user hasn't earned any \`${selectedCurrency.toUpperCase()}\` yet.`, 54 + flags: [MessageFlags.Ephemeral], 55 + }); 56 + 57 + return; 58 + } 59 + 60 + // Reply with the requested user's current balance 61 + await interaction.reply({ 62 + content: `User \`${user.username}\` has \`${userBalance.amount} ${selectedCurrency.toUpperCase()}\`.`, 63 + flags: [MessageFlags.Ephemeral], 64 + }); 65 + }, 66 + } as Command;
+31
packages/bot/src/modules/economy/events/messageCreate.ts
··· 1 + import type { Event, VoidyClient } from "@voidy/framework"; 2 + import { UserCurrency, UserCurrencyType } from "../schemas/UserCurrency"; 3 + import { Events, Message } from "discord.js"; 4 + 5 + export default { 6 + id: "messageCreate", 7 + name: Events.MessageCreate, 8 + execute: async (client: VoidyClient, message: Message) => { 9 + // Don't collect currencies for bots 10 + if (message.author.bot) return; 11 + 12 + // Retrieve user balance of BITS 13 + let userCurrency = await UserCurrency.findOne({ userId: message.author.id, type: UserCurrencyType.BITS }); 14 + if (!userCurrency) { 15 + userCurrency = await UserCurrency.create({ userId: message.author.id, type: UserCurrencyType.BITS }); 16 + } 17 + 18 + // Increase user balance of BITS at random 19 + if (Math.random() < 0.5) { // 50% chance 20 + // Calculate amount of BITS to give, between 1 and 10 21 + const amount = Math.floor(Math.random() * 10) + 1; 22 + 23 + // Update user balance of BITS and commit changes 24 + userCurrency.amount += amount; 25 + await userCurrency.save(); 26 + 27 + // Log to logging channel 28 + await client.logger.send(`User \`${message.author.tag}\` received \`${amount} BITS\``); 29 + } 30 + } 31 + } as Event;
+20
packages/bot/src/modules/economy/module.ts
··· 1 + import { CommandLoader, EventLoader } from "@voidy/framework"; 2 + 3 + export default { 4 + id: "economy", 5 + name: "Economy Management Services", 6 + description: 7 + "Provides various resources for working with users economies and currencies, including storage, transactions, and economy-related commands.", 8 + author: "thevoid.cafe", 9 + 10 + exports: [ 11 + { 12 + source: `${import.meta.dir}/commands`, 13 + loader: CommandLoader, 14 + }, 15 + { 16 + source: `${import.meta.dir}/events`, 17 + loader: EventLoader, 18 + }, 19 + ], 20 + };
+17
packages/bot/src/modules/economy/schemas/UserCurrency.ts
··· 1 + import { Schema, model } from "mongoose"; 2 + 3 + // Define valid currency types, 4 + // this should be the only source of truth for currency types. 5 + export enum UserCurrencyType { 6 + BITS = "bits", 7 + GEMS = "gems" 8 + } 9 + 10 + export const UserCurrency = model( 11 + "UserCurrency", 12 + new Schema({ 13 + userId: { type: String, required: true }, 14 + type: { type: String, enum: UserCurrencyType, required: true }, 15 + amount: { type: Number, required: true, default: 0 }, 16 + }), 17 + );
+3 -1
packages/bot/src/modules/toys/module.ts
··· 4 4 id: "toys", 5 5 name: "Toys", 6 6 description: "Provides various fun commands, e.g. coinflip, dice or simple API interactions", 7 + author: "thevoid.cafe", 8 + 7 9 exports: [ 8 10 { 9 - src: `${import.meta.dir}/commands`, 11 + source: `${import.meta.dir}/commands`, 10 12 loader: CommandLoader, 11 13 }, 12 14 ],
+3 -1
packages/bot/src/modules/user/module.ts
··· 5 5 name: "User Management Services", 6 6 description: 7 7 "Provides various resources for working with users, like global preferences and consent statements.", 8 + author: "thevoid.cafe", 9 + 8 10 exports: [ 9 11 { 10 - src: `${import.meta.dir}/commands`, 12 + source: `${import.meta.dir}/commands`, 11 13 loader: CommandLoader, 12 14 }, 13 15 ],
+1 -1
packages/bot/src/modules/user/schemas/UserConfig.ts
··· 3 3 export const UserConfig = model( 4 4 "UserConfig", 5 5 new Schema({ 6 - id: { type: String, required: true, primary: true }, 6 + userId: { type: String, required: true }, 7 7 consent: { 8 8 type: Object, 9 9 required: true,
+24
packages/framework/src/core/Logger.ts
··· 1 + import type { VoidyClient } from "./VoidyClient"; 2 + 3 + export class Logger { 4 + private logChannelId?: string; 5 + 6 + constructor(private client: VoidyClient) {} 7 + 8 + public async send(message: string) { 9 + if (!this.logChannelId) return; 10 + 11 + try { 12 + const loggingChannel = this.client.channels.cache.get(this.logChannelId); 13 + if (loggingChannel && loggingChannel.isSendable()) { 14 + await loggingChannel.send(message); 15 + } 16 + } catch {} 17 + 18 + console.log(message); 19 + } 20 + 21 + public setChannelId(id: string) { 22 + this.logChannelId = id; 23 + } 24 + }
+22 -1
packages/framework/src/core/VoidyClient.ts
··· 10 10 Events, 11 11 } from "discord.js"; 12 12 import { ModuleManager, type CacheMap } from "./ModuleManager"; 13 + import { Logger } from "./Logger"; 13 14 import type { Command } from "./types/Command"; 14 15 import type { Button } from "./types/Button"; 15 16 import type { Event } from "./types/Event"; 16 17 17 18 //=============================================== 19 + // ClientOptions Override 20 + //=============================================== 21 + export interface VoidyClientOptions extends ClientOptions { 22 + developers?: string[]; // List of developer user ids 23 + logChannelId?: string; // ID of the channel to log events to 24 + } 25 + 26 + //=============================================== 18 27 // VoidyClient Implementation 19 28 //=============================================== 20 29 export class VoidyClient extends Client { 21 30 public moduleManager = new ModuleManager(); 31 + public developers: string[] = []; 32 + public logger: Logger = new Logger(this); 22 33 23 - public constructor(options: ClientOptions) { 34 + public constructor(options: VoidyClientOptions) { 24 35 super(options); 36 + 37 + // Set developers, if provided. 38 + if (options.developers) { 39 + this.developers = options.developers; 40 + } 41 + 42 + // Inject channel ID into logger, if provided. 43 + if (options.logChannelId) { 44 + this.logger.setChannelId(options.logChannelId); 45 + } 25 46 } 26 47 27 48 /**
+1
packages/framework/src/core/types/Command.ts
··· 15 15 //=============================================== 16 16 export interface Command extends Resource { 17 17 data: SlashCommandBuilder | SlashCommandSubcommandBuilder | SlashCommandSubcommandGroupBuilder; 18 + devOnly: boolean | null; 18 19 execute: (interaction: ChatInputCommandInteraction, client: VoidyClient) => Promise<void>; 19 20 }
+1 -1
packages/framework/src/handlers/ButtonHandler.ts
··· 1 1 import type { ButtonInteraction } from "discord.js"; 2 - import type { Button } from "../loaders/ButtonLoader"; 2 + import type { Button } from "../core/types/Button"; 3 3 import type { VoidyClient } from "../core/VoidyClient"; 4 4 5 5 export class ButtonHandler {
+13 -4
packages/framework/src/handlers/CommandHandler.ts
··· 1 - import type { ChatInputCommandInteraction } from "discord.js"; 2 - import type { Command } from "../loaders/CommandLoader"; 1 + import { MessageFlags, type ChatInputCommandInteraction } from "discord.js"; 2 + import type { Command } from "../core/types/Command"; 3 3 import type { VoidyClient } from "../core/VoidyClient"; 4 4 5 5 export class ChatInputCommandHandler { 6 - public static invoke( 6 + public static async invoke( 7 7 interaction: ChatInputCommandInteraction, 8 8 payload: Command, 9 9 client: VoidyClient, 10 - ): void { 10 + ): Promise<void> { 11 + if (!client.developers.includes(interaction.user.id)) { 12 + await interaction.reply({ 13 + content: "You are not authorized to use this command.", 14 + flags: [MessageFlags.Ephemeral] 15 + }); 16 + 17 + return; 18 + } 19 + 11 20 payload.execute(interaction, client); 12 21 } 13 22 }