+3
-1
packages/bot/src/index.ts
+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
+1
-1
packages/bot/src/modules/core/module.ts
+86
packages/bot/src/modules/economy/commands/currency/set.ts
+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
+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
+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
+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
+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
+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
+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
+1
-1
packages/bot/src/modules/user/schemas/UserConfig.ts
+24
packages/framework/src/core/Logger.ts
+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
+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
+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
packages/framework/src/handlers/ButtonHandler.ts
+13
-4
packages/framework/src/handlers/CommandHandler.ts
+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
}