Initial version of bot
This commit is contained in:
2245
package-lock.json
generated
2245
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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
48
src/app.ts
Normal 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
7
src/cmds/index.ts
Normal 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
9
src/cmds/ping.ts
Normal 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
36
src/cmds/roll.ts
Normal 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
14
src/config.ts
Normal 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
28
src/deploy-commands.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
42
tscofnig.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user