Initial version of bot

This commit is contained in:
2025-04-24 02:02:39 -04:00
parent 5cec4c8122
commit dc3a6e5793
11 changed files with 2461 additions and 18 deletions

1
app.ts
View File

@ -1 +0,0 @@
console.log('hello world');

2245
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -15,11 +15,18 @@
"type": "module", "type": "module",
"main": "app.ts", "main": "app.ts",
"scripts": { "scripts": {
"build": "npx tsc app.ts --outDir dist", "build": "tsup src/app.ts --minify --format esm",
"dev": "tsx watch src/app.ts",
"start": "npm run build && node dist/app.js" "start": "npm run build && node dist/app.js"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.14.1", "@types/node": "^22.14.1",
"tsup": "^8.4.0",
"tsx": "^4.19.3",
"typescript": "^5.8.3" "typescript": "^5.8.3"
},
"dependencies": {
"discord.js": "^14.18.0",
"dotenv": "^16.5.0"
} }
} }

48
src/app.ts Normal file
View File

@ -0,0 +1,48 @@
import { Client } from "discord.js";
import { deployCommands } from "./deploy-commands";
import { commands } from "./cmds";
import { config } from "./config";
import { generateDiscordMessage, safeParseInput } from "./lib/dice";
const client = new Client({
intents: ["Guilds", "GuildMessages", "DirectMessages", "MessageContent"],
});
client.once('ready', () => {
console.log('Successfully logged in as ' + client.user?.tag);
});
client.on('guildCreate', async (guild) => {
await deployCommands({ guildId: guild.id });
});
client.on('messageCreate', async (message) => {
if (message.author.bot) return;
const result = safeParseInput(message.content);
if (result !== null) {
const response = generateDiscordMessage(result);
if (response.length > 2000) {
await message.reply("stop that.\n-# (The result is too long to display in a single message. Please try a smaller input.)");
} else {
await message.reply(response);
}
}
});
client.on('interactionCreate', async (interaction) => {
if (!interaction.isCommand()) return;
const { commandName } = interaction;
if (commands[commandName as keyof typeof commands]) {
commands[commandName as keyof typeof commands].execute(interaction);
}
});
client.login(config.DISCORD_TOKEN).then(async () => {
console.log('Logged in successfully!');
});
client.on('error', (error) => {
console.error('Error occurred:', error);
});

7
src/cmds/index.ts Normal file
View File

@ -0,0 +1,7 @@
import * as ping from './ping.js';
import * as roll from './roll.js';
export const commands = {
ping,
roll,
}

9
src/cmds/ping.ts Normal file
View File

@ -0,0 +1,9 @@
import { CommandInteraction, SlashCommandBuilder } from "discord.js";
export const data = new SlashCommandBuilder()
.setName("ping")
.setDescription("Replies with a keep alive check!");
export async function execute(interaction: CommandInteraction) {
await interaction.reply("I am still alive!");
}

36
src/cmds/roll.ts Normal file
View File

@ -0,0 +1,36 @@
import { CommandInteraction, SlashCommandBuilder } from "discord.js";
import { generateDiscordMessage, safeParseInput } from "../lib/dice";
export const data = new SlashCommandBuilder()
.setName("roll")
.addStringOption(option =>
option.setName("input")
.setDescription("The expression to roll")
.setRequired(true))
.setDescription("Rolls dice using hallowed arc syntax");
export async function execute(interaction: CommandInteraction) {
if (!interaction.isCommand()) return;
const { options } = interaction;
const input = options.get('input', true)?.value as string;
let result = safeParseInput(input);
if (result === null) {
await interaction.reply("Invalid input. Please provide a valid expression.");
return;
}
if (!Array.isArray(result)) {
result = [result];
}
const response = generateDiscordMessage(result);
if (response.length > 2000) {
await interaction.reply("stop that.\n-# (The result is too long to display in a single message. Please try a smaller input.)");
return;
}
await interaction.reply(response);
}

14
src/config.ts Normal file
View File

@ -0,0 +1,14 @@
import dotenv from 'dotenv';
dotenv.config();
const { DISCORD_TOKEN, DISCORD_CLIENT_ID } = process.env;
if (!DISCORD_TOKEN || !DISCORD_CLIENT_ID) {
throw new Error('Missing required environment variables: DISCORD_TOKEN or DISCORD_CLIENT_ID');
}
export const config = {
DISCORD_TOKEN,
DISCORD_CLIENT_ID,
};

28
src/deploy-commands.ts Normal file
View File

@ -0,0 +1,28 @@
import { REST, Routes } from 'discord.js';
import { config } from './config';
import { commands } from './cmds/index.js';
const commandsData = Object.values(commands).map((command) => command.data);
const rest = new REST({ version: '10' }).setToken(config.DISCORD_TOKEN);
type DeployCommandsProps = {
guildId: string;
}
export async function deployCommands({ guildId }: DeployCommandsProps) {
try {
console.log("Started refreshing application (/) commands.");
await rest.put(
Routes.applicationGuildCommands(config.DISCORD_CLIENT_ID, guildId),
{
body: commandsData,
}
);
console.log("Successfully reloaded application (/) commands.");
} catch (error) {
console.error(error);
}
}

View File

@ -1,6 +1,6 @@
type DiceType = 'f' | 'r' | 't'; export type DiceType = 'f' | 'r' | 't';
interface RollResult { export interface RollResult {
input: string; input: string;
value: number; value: number;
rolls: number[]; // Kept dice rolls: number[]; // Kept dice
@ -9,7 +9,7 @@ interface RollResult {
steps?: string[]; // Arithmetic operations performed steps?: string[]; // Arithmetic operations performed
} }
function rollDie(diceType: DiceType): number { export function rollDie(diceType: DiceType): number {
const roll = { const roll = {
f: Math.floor(Math.random() * 12) + 1 - 6, f: Math.floor(Math.random() * 12) + 1 - 6,
r: Math.floor(Math.random() * 18) + 1 - 9, r: Math.floor(Math.random() * 18) + 1 - 9,
@ -18,11 +18,11 @@ function rollDie(diceType: DiceType): number {
return roll[diceType]; return roll[diceType];
} }
function rollDice(count: number, diceType: DiceType): number[] { export function rollDice(count: number, diceType: DiceType): number[] {
return Array.from({ length: count }, () => rollDie(diceType)); return Array.from({ length: count }, () => rollDie(diceType));
} }
function getDieRange(diceType: DiceType): { min: number; max: number } { export function getDieRange(diceType: DiceType): { min: number; max: number } {
const ranges = { const ranges = {
f: { min: -5, max: 6 }, f: { min: -5, max: 6 },
r: { min: -8, max: 9 }, r: { min: -8, max: 9 },
@ -31,7 +31,7 @@ function getDieRange(diceType: DiceType): { min: number; max: number } {
return ranges[diceType]; return ranges[diceType];
} }
function formatSummary(rolls: number[], discarded: number[], diceType: DiceType): string { export function formatSummary(rolls: number[], discarded: number[], diceType: DiceType): string {
const { min, max } = getDieRange(diceType); const { min, max } = getDieRange(diceType);
const all = rolls.map(r => ({ value: r, kept: true })) const all = rolls.map(r => ({ value: r, kept: true }))
.concat(discarded.map(r => ({ value: r, kept: false }))); .concat(discarded.map(r => ({ value: r, kept: false })));
@ -42,13 +42,13 @@ function formatSummary(rolls: number[], discarded: number[], diceType: DiceType)
const isMinOrMax = value === min || value === max; const isMinOrMax = value === min || value === max;
let formatted = `${value}`; let formatted = `${value}`;
if (isMinOrMax) formatted = `**${formatted}**`; if (isMinOrMax) formatted = `**${formatted}**`;
if (!kept) formatted = `~${formatted}~`; if (!kept) formatted = `~~${formatted}~~`;
return formatted; return formatted;
}) })
.join(' '); .join(' ');
} }
function parseExpression(input: string): RollResult | RollResult[] { export function parseExpression(input: string): RollResult | RollResult[] {
const batchMatch = input.match(/^(\d+)#(.+)$/); const batchMatch = input.match(/^(\d+)#(.+)$/);
if (batchMatch) { if (batchMatch) {
const [, batchCountStr, innerExpr] = batchMatch; const [, batchCountStr, innerExpr] = batchMatch;
@ -59,7 +59,7 @@ function parseExpression(input: string): RollResult | RollResult[] {
} }
} }
function parseSingleExpression(expr: string): RollResult { export function parseSingleExpression(expr: string): RollResult {
const baseMatch = expr.match(/^(\d+)?([frt])(?:(kh|kl)(\d+))?/); const baseMatch = expr.match(/^(\d+)?([frt])(?:(kh|kl)(\d+))?/);
if (!baseMatch) throw new Error(`Invalid expression: ${expr}`); if (!baseMatch) throw new Error(`Invalid expression: ${expr}`);
@ -101,7 +101,7 @@ function parseSingleExpression(expr: string): RollResult {
} }
return { return {
input: expr, input: expr.replaceAll('*', '\\*'),
value: result, value: result,
rolls, rolls,
discarded, discarded,
@ -110,7 +110,7 @@ function parseSingleExpression(expr: string): RollResult {
}; };
} }
function safeParseInput(message: string): RollResult | RollResult[] | null { export function safeParseInput(message: string): RollResult | RollResult[] | null {
const trimmed = message.trim(); const trimmed = message.trim();
const isSingleWord = !trimmed.includes(' '); const isSingleWord = !trimmed.includes(' ');
@ -158,6 +158,18 @@ function safeParseInput(message: string): RollResult | RollResult[] | null {
return results.length > 0 ? results : null; return results.length > 0 ? results : null;
} }
console.log(safeParseInput('Hello I roll a 5rkh3+1')); export function generateDiscordMessage(result: RollResult | RollResult[]): string {
console.log(safeParseInput('I ROLL asdf[2#3fkl1*2]asdf')); if (!Array.isArray(result)) {
console.log(safeParseInput('[2#3fkl1*2] [2#3fkl1*2]')); result = [result];
}
let response = `\` `;
for (const resultset of result) {
response += resultset.value + ' ` ⟵ ';
response += `[${resultset.summary}] ${resultset.input}\n\` `;
}
response = response.slice(0, response.length - 3);
return response;
}

42
tscofnig.json Normal file
View File

@ -0,0 +1,42 @@
{
"compilerOptions": {
"allowJs": true,
"alwaysStrict": true,
"baseUrl": "./",
"checkJs": false,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"lib": [
"ESNext"
],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"paths": {
"@/*": [
"./src/*", "./dist/*"
]
},
"rootDir": "src",
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"target": "ESNext",
"types": [
"node"
]
},
"include": [
"src/**/*.ts",
"src/**/*.js",
"@types"
],
"exclude": [
"node_modules",
"<node_internals>/**"
],
"ts-node": {
"esm": true,
"swc": true
}
}