architecture besides db setup, say+roll

This commit is contained in:
stjet
2024-07-22 09:19:12 +00:00
commit f8780249ce
12 changed files with 783 additions and 0 deletions

2
commands/error.ts Normal file
View File

@@ -0,0 +1,2 @@
export class BotError extends Error {};

78
commands/index.ts Normal file
View File

@@ -0,0 +1,78 @@
import { EmbedBuilder } from "discord.js";
import type { ChatInputCommandInteraction } from "discord.js";
import { BotError } from "./error";
import say from "./say";
import roll from "./roll";
export interface CommandData {
name: string;
description: string;
ephemeral: boolean;
admin_only: boolean;
run: (interaction: ChatInputCommandInteraction) => Promise<any>;
//
};
const commands: CommandData[] = [say, roll];
//todo: look from config. also, have a config
function is_admin(interaction: ChatInputCommandInteraction): boolean {
//
//placeholder
return true;
}
export default async function run(interaction: ChatInputCommandInteraction) {
const name = interaction.commandName;
//help command is "auto-generated"
if (name === "help") {
//max of 25 fields per embed, so if too many commands, this section needs a rewrite
let embeds = [];
let help_embed = new EmbedBuilder();
help_embed.setTitle("Help");
for (const c of commands.filter((c) => !c.admin_only)) {
help_embed.addFields([{
name: `/${c.name}`,
value: c.description,
}]);
}
embeds.push(help_embed);
if (is_admin(interaction)) {
let admin_help_embed = new EmbedBuilder();
admin_help_embed.setTitle("Help (admin only)");
for (const c of commands.filter((c) => c.admin_only)) {
admin_help_embed.addFields([{
name: `/${c.name}`,
value: c.description,
}]);
}
embeds.push(admin_help_embed);
}
return await interaction.reply({ embeds, ephemeral: true });
}
const found = commands.find((c) => c.name === name);
try {
//admin stuff should be ideally handled by register.ts, but this is a fallback
if (found.admin_only && !is_admin(interaction)) throw new BotError("Admin permission needed to run that command");
await found.run(interaction);
} catch (e) {
if (e instanceof BotError) {
//send error message to that channel
if (interaction.deferred) {
return await interaction.editReply(String(e));
} else {
return await interaction.reply({ content: String(e), ephemeral: found.ephemeral });
}
} else {
//an actual error
//console.log(e);
throw e; //crash it
}
}
}

39
commands/roll.ts Normal file
View File

@@ -0,0 +1,39 @@
import type { ChatInputCommandInteraction } from "discord.js";
import { randomInt } from "crypto";
import type { CommandData } from "./index";
import { BotError } from "./error";
const MAX_DICE: number = 100;
const MAX_FACES: number = 9999;
async function run(interaction: ChatInputCommandInteraction) {
await interaction.deferReply(); //do these options.get things need to be await?? kinda stupid
const options = interaction.options;
const dice_num: number = (await options.get("dice_num")).value as number;
const dice_faces: number = (await options.get("dice_faces")).value as number;
const show_calc: boolean = ((await options.get("show_calc"))?.value ?? true) as boolean;
const include_zero: boolean = ((await options.get("include_zero"))?.value ?? false) as boolean;
if (dice_num < 1) throw new BotError("Must roll at least 1 dice, obviously");
//semi-arbitrary limits, discord messages can be max 2000 chars long
if (dice_num > MAX_DICE) throw new BotError(`Max of ${MAX_DICE} dice`);
if (dice_faces > 9999) throw new BotError(`Max of ${MAX_FACES} faces`);
let rolls: number[] = [];
for (let i = 0; i < dice_num; i++) {
rolls.push(randomInt(include_zero ? 0 : 1, dice_faces + 1));
}
const result: number = rolls.reduce((accum, roll) => accum + roll, 0);
return await interaction.editReply(`Result: **${result}** ${ show_calc ? `(${ rolls.join(" + ")} = ${result})` : "" }`);
}
const data: CommandData = {
name: "roll",
description: "Roll dice",
ephemeral: false,
admin_only: false,
run,
//
};
export default data;

35
commands/say.ts Normal file
View File

@@ -0,0 +1,35 @@
import type { ChatInputCommandInteraction } from "discord.js";
import type { CommandData } from "./index";
import { BotError } from "./error";
import { is_text_channel } from "../guards";
async function run(interaction: ChatInputCommandInteraction) {
const options = interaction.options;
const text: string = (await options.get("text")).value as string; //100% this is a string
const channel = (await options.get("channel")).channel;
//screw threads, news, announcements and shit, at least for now
if (is_text_channel(channel)) {
try {
await channel.send(text);
} catch (e) {
console.log(e);
throw new BotError("Couldn't send message");
}
return await interaction.reply({ content: "Sent", ephemeral: true });
} else {
throw new BotError("Must be guild text channel"); //I don't think DM channels are valid to pass in here so no worries there, probably
}
}
const data: CommandData = {
name: "say",
description: "Have the bot say something in a channel",
ephemeral: true,
admin_only: true,
run,
//
};
export default data;