A powerful and extendable Discord bot, with it's own module system :3
thevoid.cafe/projects/voidy
1import {
2 ChannelType,
3 MessageFlags,
4 PermissionFlagsBits,
5 SlashCommandSubcommandBuilder,
6 type TextChannel,
7} from "discord.js";
8import { Command, requirePermission } from "@voidy/framework";
9import { ChatbotChannel } from "../schemas/ChatbotChannel";
10import { ChatbotRepository } from "../schemas/ChatbotRepository";
11
12export default new Command({
13 id: "chatbot.setup",
14 use: [requirePermission(PermissionFlagsBits.ManageGuild)],
15 data: new SlashCommandSubcommandBuilder()
16 .setName("setup")
17 .setDescription("Set up chatbot in a channel (creates webhook automatically).")
18 .addChannelOption((option) =>
19 option
20 .setName("channel")
21 .setDescription("Channel to enable chatbot in.")
22 .addChannelTypes(ChannelType.GuildText)
23 .setRequired(true),
24 )
25 .addIntegerOption((option) =>
26 option
27 .setName("interval")
28 .setDescription("How often to send a message.")
29 .setRequired(true)
30 .setMinValue(1),
31 )
32 .addStringOption((option) =>
33 option
34 .setName("unit")
35 .setDescription("Interval unit.")
36 .setRequired(true)
37 .addChoices(
38 { name: "Seconds", value: "seconds" },
39 { name: "Minutes", value: "minutes" },
40 ),
41 )
42 .addStringOption((option) =>
43 option
44 .setName("mode")
45 .setDescription("Content source mode.")
46 .addChoices(
47 { name: "Global", value: "global" },
48 { name: "Local", value: "local" },
49 ),
50 )
51 .addStringOption((option) =>
52 option.setName("repository").setDescription("Local repository name (required if mode is local)."),
53 ),
54
55 execute: async ({ interaction }) => {
56 const channel = interaction.options.getChannel("channel", true) as TextChannel;
57 const interval = interaction.options.getInteger("interval", true);
58 const unit = interaction.options.getString("unit", true);
59 const mode = (interaction.options.getString("mode") ?? "global") as "global" | "local";
60 const repoName = interaction.options.getString("repository");
61
62 const intervalSeconds = unit === "minutes" ? interval * 60 : interval;
63
64 // Check if channel already has chatbot
65 const existing = await ChatbotChannel.findOne({ channelId: channel.id });
66 if (existing) {
67 await interaction.reply({
68 content: `Chatbot is already set up in <#${channel.id}>.`,
69 flags: [MessageFlags.Ephemeral],
70 });
71 return;
72 }
73
74 // Resolve repository for local mode
75 let repositoryId;
76 if (mode === "local") {
77 if (!repoName) {
78 await interaction.reply({
79 content: "You must specify a repository name when using local mode.",
80 flags: [MessageFlags.Ephemeral],
81 });
82 return;
83 }
84
85 const repo = await ChatbotRepository.findOne({
86 scope: "local",
87 guildId: interaction.guildId,
88 name: repoName,
89 });
90
91 if (!repo) {
92 await interaction.reply({
93 content: `Local repository "${repoName}" not found. Create one first with \`/chatbot repository create\`.`,
94 flags: [MessageFlags.Ephemeral],
95 });
96 return;
97 }
98
99 repositoryId = repo._id;
100 }
101
102 await interaction.deferReply({ flags: [MessageFlags.Ephemeral] });
103
104 // Create webhook in channel
105 const webhook = await channel.createWebhook({ name: "Voidy Chatbot" });
106
107 await ChatbotChannel.create({
108 guildId: interaction.guildId,
109 channelId: channel.id,
110 webhookId: webhook.id,
111 webhookToken: webhook.token,
112 mode,
113 repositoryId,
114 enabled: true,
115 intervalSeconds,
116 });
117
118 const display = unit === "minutes" ? `${interval} minute(s)` : `${interval} second(s)`;
119
120 await interaction.editReply({
121 content: `Chatbot enabled in <#${channel.id}> using **${mode}** mode, posting every **${display}**.`,
122 });
123 },
124});