Initial commit

This commit is contained in:
2025-05-03 12:03:31 +02:00
commit b55ff5829c
19 changed files with 983 additions and 0 deletions

View File

@ -0,0 +1,22 @@
{
"name": "Bun Development Container",
"image": "ghcr.io/nhaef/devcontainer-bun:latest",
"features": {
"ghcr.io/devcontainers/features/common-utils:2": {
"installZsh": true,
"installOhMyZsh": true,
"configureZshAsDefaultShell": true
}
},
"customizations": {
"vscode": {
"extensions": [
"ms-vscode.cpptools",
"ms-python.python",
"esbenp.prettier-vscode"
]
}
},
"remoteUser": "vscode",
"postCreateCommand": "sudo chown -R vscode:vscode /workspaces/DiscordBots && sudo chown -R vscode:vscode /workspaces/DiscordBots/node_modules || true"
}

42
.gitignore vendored Normal file
View File

@ -0,0 +1,42 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel
**/*.trace
**/*.zip
**/*.tar.gz
**/*.tgz
**/*.log
package-lock.json
**/*.bun

32
Dockerfile Normal file
View File

@ -0,0 +1,32 @@
FROM oven/bun AS build
WORKDIR /app
COPY package.json package.json
COPY bun.lock bun.lock
RUN bun install
COPY ./src ./src
ENV NODE_ENV=production
RUN bun build \
--compile \
--minify-whitespace \
--minify-syntax \
--target bun \
--outfile server \
./src/index.ts
FROM gcr.io/distroless/base
WORKDIR /app
COPY --from=build /app/server server
ENV NODE_ENV=production
CMD ["./server"]
EXPOSE 3000

22
Makefile Normal file
View File

@ -0,0 +1,22 @@
# Makefile
BW_PROJECT_NAME=BoredGods.dev
BW_PROJECT_ID=9d72d613-8d1f-4455-8676-b2c10167aa94
BW_DOMAIN=https://vault.bitwarden.eu
run:
@if ! command -v bws >/dev/null 2>&1; then \
echo "Error: bws is not installed."; \
exit 1; \
fi
@if [ -z "$$BWS_ACCESS_TOKEN" ]; then \
echo "Error: BWS_ACCESS_TOKEN environment variable is not set."; \
exit 1; \
fi
@bws config server-base $(BW_DOMAIN)
@echo "Set EU connection $(BW_DOMAIN)"
@echo "Starting app with project ${BW_PROJECT_NAME}"
@bws run --project-id $(BW_PROJECT_ID) -- bun src/index.ts

0
a Normal file
View File

16
package.json Normal file
View File

@ -0,0 +1,16 @@
{
"name": "app",
"version": "1.0.50",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "bun run --watch src/index.ts"
},
"dependencies": {
"discord.js": "^14.18.0",
"elysia": "^1.2.25"
},
"devDependencies": {
"bun-types": "^1.2.9"
},
"module": "src/index.js"
}

264
pnpm-lock.yaml generated Normal file
View File

@ -0,0 +1,264 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
discord.js:
specifier: ^14.18.0
version: 14.18.0
elysia:
specifier: ^1.2.25
version: 1.2.25(@sinclair/typebox@0.34.33)
devDependencies:
bun-types:
specifier: ^1.2.9
version: 1.2.9
packages:
'@discordjs/builders@1.10.1':
resolution: {integrity: sha512-OWo1fY4ztL1/M/DUyRPShB4d/EzVfuUvPTRRHRIt/YxBrUYSz0a+JicD5F5zHFoNs2oTuWavxCOVFV1UljHTng==}
engines: {node: '>=16.11.0'}
'@discordjs/collection@1.5.3':
resolution: {integrity: sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==}
engines: {node: '>=16.11.0'}
'@discordjs/collection@2.1.1':
resolution: {integrity: sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==}
engines: {node: '>=18'}
'@discordjs/formatters@0.6.0':
resolution: {integrity: sha512-YIruKw4UILt/ivO4uISmrGq2GdMY6EkoTtD0oS0GvkJFRZbTSdPhzYiUILbJ/QslsvC9H9nTgGgnarnIl4jMfw==}
engines: {node: '>=16.11.0'}
'@discordjs/rest@2.4.3':
resolution: {integrity: sha512-+SO4RKvWsM+y8uFHgYQrcTl/3+cY02uQOH7/7bKbVZsTfrfpoE62o5p+mmV+s7FVhTX82/kQUGGbu4YlV60RtA==}
engines: {node: '>=18'}
'@discordjs/util@1.1.1':
resolution: {integrity: sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==}
engines: {node: '>=18'}
'@discordjs/ws@1.2.1':
resolution: {integrity: sha512-PBvenhZG56a6tMWF/f4P6f4GxZKJTBG95n7aiGSPTnodmz4N5g60t79rSIAq7ywMbv8A4jFtexMruH+oe51aQQ==}
engines: {node: '>=16.11.0'}
'@sapphire/async-queue@1.5.5':
resolution: {integrity: sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==}
engines: {node: '>=v14.0.0', npm: '>=7.0.0'}
'@sapphire/shapeshift@4.0.0':
resolution: {integrity: sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==}
engines: {node: '>=v16'}
'@sapphire/snowflake@3.5.3':
resolution: {integrity: sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==}
engines: {node: '>=v14.0.0', npm: '>=7.0.0'}
'@sinclair/typebox@0.34.33':
resolution: {integrity: sha512-5HAV9exOMcXRUxo+9iYB5n09XxzCXnfy4VTNW4xnDv+FgjzAGY989C28BIdljKqmF+ZltUwujE3aossvcVtq6g==}
'@types/node@22.14.1':
resolution: {integrity: sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==}
'@types/ws@8.18.1':
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
'@vladfrangu/async_event_emitter@2.4.6':
resolution: {integrity: sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==}
engines: {node: '>=v14.0.0', npm: '>=7.0.0'}
bun-types@1.2.9:
resolution: {integrity: sha512-dk/kOEfQbajENN/D6FyiSgOKEuUi9PWfqKQJEgwKrCMWbjS/S6tEXp178mWvWAcUSYm9ArDlWHZKO3T/4cLXiw==}
cookie@1.0.2:
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
engines: {node: '>=18'}
discord-api-types@0.37.120:
resolution: {integrity: sha512-7xpNK0EiWjjDFp2nAhHXezE4OUWm7s1zhc/UXXN6hnFFU8dfoPHgV0Hx0RPiCa3ILRpdeh152icc68DGCyXYIw==}
discord.js@14.18.0:
resolution: {integrity: sha512-SvU5kVUvwunQhN2/+0t55QW/1EHfB1lp0TtLZUSXVHDmyHTrdOj5LRKdR0zLcybaA15F+NtdWuWmGOX9lE+CAw==}
engines: {node: '>=18'}
elysia@1.2.25:
resolution: {integrity: sha512-WsdQpORJvb4uszzeqYT0lg97knw1iBW1NTzJ1Jm57tiHg+DfAotlWXYbjmvQ039ssV0fYELDHinLLoUazZkEHg==}
peerDependencies:
'@sinclair/typebox': '>= 0.34.0'
openapi-types: '>= 12.0.0'
typescript: '>= 5.0.0'
peerDependenciesMeta:
openapi-types:
optional: true
typescript:
optional: true
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
lodash.snakecase@4.1.1:
resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==}
lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
magic-bytes.js@1.10.0:
resolution: {integrity: sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ==}
memoirist@0.3.0:
resolution: {integrity: sha512-wR+4chMgVPq+T6OOsk40u9Wlpw1Pjx66NMNiYxCQQ4EUJ7jDs3D9kTCeKdBOkvAiqXlHLVJlvYL01PvIJ1MPNg==}
ts-mixer@6.0.4:
resolution: {integrity: sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
undici@6.21.1:
resolution: {integrity: sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==}
engines: {node: '>=18.17'}
ws@8.18.1:
resolution: {integrity: sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
snapshots:
'@discordjs/builders@1.10.1':
dependencies:
'@discordjs/formatters': 0.6.0
'@discordjs/util': 1.1.1
'@sapphire/shapeshift': 4.0.0
discord-api-types: 0.37.120
fast-deep-equal: 3.1.3
ts-mixer: 6.0.4
tslib: 2.8.1
'@discordjs/collection@1.5.3': {}
'@discordjs/collection@2.1.1': {}
'@discordjs/formatters@0.6.0':
dependencies:
discord-api-types: 0.37.120
'@discordjs/rest@2.4.3':
dependencies:
'@discordjs/collection': 2.1.1
'@discordjs/util': 1.1.1
'@sapphire/async-queue': 1.5.5
'@sapphire/snowflake': 3.5.3
'@vladfrangu/async_event_emitter': 2.4.6
discord-api-types: 0.37.120
magic-bytes.js: 1.10.0
tslib: 2.8.1
undici: 6.21.1
'@discordjs/util@1.1.1': {}
'@discordjs/ws@1.2.1':
dependencies:
'@discordjs/collection': 2.1.1
'@discordjs/rest': 2.4.3
'@discordjs/util': 1.1.1
'@sapphire/async-queue': 1.5.5
'@types/ws': 8.18.1
'@vladfrangu/async_event_emitter': 2.4.6
discord-api-types: 0.37.120
tslib: 2.8.1
ws: 8.18.1
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@sapphire/async-queue@1.5.5': {}
'@sapphire/shapeshift@4.0.0':
dependencies:
fast-deep-equal: 3.1.3
lodash: 4.17.21
'@sapphire/snowflake@3.5.3': {}
'@sinclair/typebox@0.34.33': {}
'@types/node@22.14.1':
dependencies:
undici-types: 6.21.0
'@types/ws@8.18.1':
dependencies:
'@types/node': 22.14.1
'@vladfrangu/async_event_emitter@2.4.6': {}
bun-types@1.2.9:
dependencies:
'@types/node': 22.14.1
'@types/ws': 8.18.1
cookie@1.0.2: {}
discord-api-types@0.37.120: {}
discord.js@14.18.0:
dependencies:
'@discordjs/builders': 1.10.1
'@discordjs/collection': 1.5.3
'@discordjs/formatters': 0.6.0
'@discordjs/rest': 2.4.3
'@discordjs/util': 1.1.1
'@discordjs/ws': 1.2.1
'@sapphire/snowflake': 3.5.3
discord-api-types: 0.37.120
fast-deep-equal: 3.1.3
lodash.snakecase: 4.1.1
tslib: 2.8.1
undici: 6.21.1
transitivePeerDependencies:
- bufferutil
- utf-8-validate
elysia@1.2.25(@sinclair/typebox@0.34.33):
dependencies:
'@sinclair/typebox': 0.34.33
cookie: 1.0.2
memoirist: 0.3.0
fast-deep-equal@3.1.3: {}
lodash.snakecase@4.1.1: {}
lodash@4.17.21: {}
magic-bytes.js@1.10.0: {}
memoirist@0.3.0: {}
ts-mixer@6.0.4: {}
tslib@2.8.1: {}
undici-types@6.21.0: {}
undici@6.21.1: {}
ws@8.18.1: {}

17
src/boredBot/commands.ts Normal file
View File

@ -0,0 +1,17 @@
import { SlashCommandBuilder } from "discord.js";
import { Command } from "../discordBot/types";
export const boredBotCommands = [
new SlashCommandBuilder()
.setName(Command.Roll)
.setDescription("Roll dice (e.g. 'd20', '6d12-4', '2d8 + 1d6+4')")
.addStringOption((option) =>
option
.setName("input")
.setDescription("The dice you want to roll")
.setRequired(true),
),
new SlashCommandBuilder()
.setName(Command.Help)
.setDescription("Displays a list of available commands."),
];

123
src/boredBot/index.ts Normal file
View File

@ -0,0 +1,123 @@
import type { SlashCommandOptionsOnlyBuilder } from "discord.js";
import { DiceRoller } from "../diceRoller";
import type { DiceParseResult } from "../diceRoller/types";
import { parseDiceUserInput } from "../diceRoller/utils/parseDiceUserInput";
import { DiscordBot } from "../discordBot";
import { Command, MessageContent } from "../discordBot/types";
export class BoredBot extends DiscordBot {
private static instance: BoredBot;
private constructor(
token: string,
applicationId: string,
commands: SlashCommandOptionsOnlyBuilder[],
) {
super(token, applicationId, commands);
this.useCommand();
}
public static async getInstance(
token: string,
applicationId: string,
commands: SlashCommandOptionsOnlyBuilder[],
): Promise<BoredBot> {
if (!BoredBot.instance) {
BoredBot.instance = new BoredBot(token, applicationId, commands);
await BoredBot.instance.initialize("Bored Bot");
}
return BoredBot.instance;
}
private async useCommand() {
this.client.on("interactionCreate", async (interaction) => {
if (!interaction.isChatInputCommand()) return;
const command = interaction.commandName as Command;
const userName = interaction.user.displayName;
const userInput = interaction.options.getString(MessageContent.Input);
if (!userInput) throw new Error("Expected input to be defined");
console.log("got command");
switch (command) {
case Command.Roll: {
await interaction.reply(
await this.handleRollCommand(userInput, userName),
);
}
}
});
}
private async handleRollCommand(
inputStringFromUser: string,
username: string,
): Promise<string> {
let parsedInputResult: DiceParseResult = { dices: [], mod: 0 };
try {
parsedInputResult = parseDiceUserInput(inputStringFromUser);
} catch (error) {
if (error instanceof Error) return error.message;
}
const roller = new DiceRoller(username);
const resultsMessages: string[] = [];
if (parsedInputResult.dices.length === 1) {
const singleDie = parsedInputResult.dices[0];
const { rollResult, message } = await roller.roll(singleDie);
const mod = parsedInputResult.mod;
const finalResult = rollResult + mod;
let singleLine = `(${singleDie.toString()}) => ${rollResult}`;
if (mod !== 0) {
const sign = mod > 0 ? "+" : "-";
singleLine += ` ${sign}${Math.abs(mod)} = ${finalResult}`;
}
if (message) {
singleLine += `\n${message}`;
}
resultsMessages.push(singleLine);
} else {
resultsMessages.push(
`You rolled ${parsedInputResult.dices.length} dice!`,
);
let sum = 0;
let rollCount = 1;
for (const dieInput of parsedInputResult.dices) {
const { rollResult, message } = await roller.roll(dieInput);
sum += rollResult;
const prefix = `Roll #${rollCount}: `;
const suffix = message ? ` **${message}**` : "";
resultsMessages.push(
`${prefix}(d${dieInput.toString()}) => ${rollResult}${suffix}`,
);
rollCount++;
}
const mod = parsedInputResult.mod;
const finalResult = sum + mod;
if (mod !== 0) {
const sign = mod > 0 ? "+" : "-";
resultsMessages.push(
`Result: ${sum} ${sign} ${Math.abs(mod)} = ${finalResult}`,
);
} else {
resultsMessages.push(`**Final result: ${sum}**`);
}
}
return resultsMessages.join("\n");
}
}

53
src/diceRoller/index.ts Normal file
View File

@ -0,0 +1,53 @@
import { Critical, Dice } from "./types";
export class DiceRoller {
private username: string;
constructor(user: string) {
this.username = user;
}
public async roll(
dice: (typeof Dice)[keyof typeof Dice],
): Promise<{ rollResult: number; message?: string }> {
if (!Object.values(Dice).includes(dice))
throw new Error(`Invalid dice type: ${dice}`);
const { outcome: result, crit } = this.getSingleDiceRollOutcome(dice);
if (crit.failure || crit.success) {
return {
rollResult: result,
message: crit.success
? await this.handleCritical(Critical.Success)
: await this.handleCritical(Critical.Fail),
};
}
return { rollResult: result };
}
private getSingleDiceRollOutcome(dice: (typeof Dice)[keyof typeof Dice]): {
outcome: number;
crit: { failure: boolean; success: boolean };
} {
const roll = Math.floor(Math.random() * dice) + 1;
switch (dice) {
case Dice.Twenty:
return {
outcome: roll,
crit: { failure: roll === 1, success: roll === 20 },
};
default:
return { outcome: roll, crit: { failure: false, success: false } };
}
}
private async handleCritical(critical: Critical): Promise<string> {
switch (critical) {
case Critical.Fail:
return "Critical FAIL!";
case Critical.Success:
return "Critical SUCCESS!";
}
}
}

18
src/diceRoller/types.ts Normal file
View File

@ -0,0 +1,18 @@
export const Dice = {
Four: 4,
Six: 6,
Ten: 10,
Twelve: 12,
Twenty: 20,
Hundrer: 100,
};
export enum Critical {
Fail = "fail",
Success = "success",
}
export type DiceParseResult = {
dices: number[];
mod: number;
};

View File

@ -0,0 +1,71 @@
import { DiceRoller } from "../discord/dice";
import { parseDiceUserInput } from "./parseDiceUserInput";
import type { DiceParseResult } from "../types";
export async function handleRollCommand(
inputStringFromUser: string,
username: string,
): Promise<string> {
let parsedInputResult: DiceParseResult = { dices: [], mod: 0 };
try {
parsedInputResult = parseDiceUserInput(inputStringFromUser);
} catch (error) {
if (error instanceof Error) return error.message;
}
const roller = new DiceRoller(username);
const resultsMessages: string[] = [];
if (parsedInputResult.dices.length === 1) {
const singleDie = parsedInputResult.dices[0];
const { rollResult, message } = await roller.roll(singleDie);
const mod = parsedInputResult.mod;
const finalResult = rollResult + mod;
let singleLine = `(${singleDie.toString()}) => ${rollResult}`;
if (mod !== 0) {
const sign = mod > 0 ? "+" : "-";
singleLine += ` ${sign}${Math.abs(mod)} = ${finalResult}`;
}
if (message) {
singleLine += `\n${message}`;
}
resultsMessages.push(singleLine);
} else {
resultsMessages.push(`You rolled ${parsedInputResult.dices.length} dice!`);
let sum = 0;
let rollCount = 1;
for (const dieInput of parsedInputResult.dices) {
const { rollResult, message } = await roller.roll(dieInput);
sum += rollResult;
const prefix = `Roll #${rollCount}: `;
const suffix = message ? ` **${message}**` : "";
resultsMessages.push(
`${prefix}(d${dieInput.toString()}) => ${rollResult}${suffix}`,
);
rollCount++;
}
const mod = parsedInputResult.mod;
const finalResult = sum + mod;
if (mod !== 0) {
const sign = mod > 0 ? "+" : "-";
resultsMessages.push(
`Result: ${sum} ${sign} ${Math.abs(mod)} = ${finalResult}`,
);
} else {
resultsMessages.push(`**Final result: ${sum}**`);
}
}
return resultsMessages.join("\n");
}

View File

@ -0,0 +1,69 @@
import { Dice } from "../types";
const ALLOWED_DICE_SIDES = new Set(Object.values(Dice));
export function parseDiceUserInput(input: string): DiceParseResult {
if (typeof input !== "string" || !input.trim()) {
throw new Error("Input must be a non-empty string.");
}
let clean = input.toLowerCase().trim();
clean = clean.replace(/\b(roll|dice|die)\b/g, "");
clean = clean.replace(/\s+/g, "");
if (/^\d+$/.test(clean)) {
const sides = Number.parseInt(clean, 10);
if (!ALLOWED_DICE_SIDES.has(sides)) {
throw new Error(
`Allowed dice sides are ${[...ALLOWED_DICE_SIDES].join(", ")}. Received: ${sides}`,
);
}
return { dices: [sides], mod: 0 };
}
const tokenRegex = /(\d*d\d+|[+\-]\d+)/g;
const tokens = clean.match(tokenRegex);
if (!tokens) {
throw new Error(
"Unable to parse any dice or modifiers. Examples: '3d6+5', 'd20-2', '2d8+1d6+4'.",
);
}
const dices: number[] = [];
let mod = 0;
for (const token of tokens) {
if (token.includes("d")) {
const [countPart, sidesPart] = token.split("d");
let diceCount = countPart ? Number.parseInt(countPart, 10) : 1;
if (Number.isNaN(diceCount) || diceCount < 1) {
diceCount = 1;
}
const sides = Number.parseInt(sidesPart, 10);
if (Number.isNaN(sides) || sides <= 0) {
throw new Error(`Invalid number of sides: "${sidesPart}"`);
}
if (!ALLOWED_DICE_SIDES.has(sides)) {
throw new Error(
`Allowed dice sides are ${[...ALLOWED_DICE_SIDES].join(", ")}. Received: ${sides}`,
);
}
for (let i = 0; i < diceCount; i++) {
dices.push(sides);
}
} else {
const parsedMod = Number.parseInt(token, 10);
if (Number.isNaN(parsedMod)) {
throw new Error(`Invalid modifier: "${token}"`);
}
mod += parsedMod;
}
}
return { dices, mod };
}

65
src/discordBot/index.ts Normal file
View File

@ -0,0 +1,65 @@
import {
Client,
GatewayIntentBits,
REST,
Routes,
type SlashCommandOptionsOnlyBuilder,
} from "discord.js";
export abstract class DiscordBot {
private token: string;
private applicationId: string;
protected client: Client;
private commands: SlashCommandOptionsOnlyBuilder[];
protected constructor(
token: string,
applicationId: string,
commands: SlashCommandOptionsOnlyBuilder[],
) {
this.client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
],
});
this.token = token;
this.applicationId = applicationId;
this.commands = commands;
}
private async login(): Promise<void> {
if (!this.token) {
throw new Error("No token provided when attempting to log in bot");
}
try {
await this.client.login(this.token);
} catch (error) {
throw new Error(`Bot failed to log in: ${error}`);
}
}
private async registerCommands(): Promise<void> {
try {
await new REST({ version: "10" })
.setToken(this.token)
.put(Routes.applicationCommands(this.applicationId), {
body: this.commands,
});
} catch (error) {
throw new Error(`Failed to register slash commands: ${error}`);
}
}
protected async initialize(botName: string): Promise<void> {
console.log(`Initializing bot '${botName}'...`);
try {
await this.login();
await this.registerCommands();
console.log(`${botName} is ready!`);
} catch (error) {
console.log("Initialization error:", error);
}
}
}

8
src/discordBot/types.ts Normal file
View File

@ -0,0 +1,8 @@
export enum Command {
Roll = "roll",
Help = "help",
}
export enum MessageContent {
Input = "input",
}

10
src/index.ts Normal file
View File

@ -0,0 +1,10 @@
// import { Elysia } from "elysia";
import { BoredBot } from "./boredBot";
import { boredBotCommands } from "./boredBot/commands";
// new Elysia().get("/", () => "Hello Elysia").listen(3000);
if (!process.env.DISCORD_BOT_TOKEN || !process.env.APPLICATION_ID) {
throw new Error("DISCORD_BOT_TOKEN and APPLICATION_ID must be defined in the environment variables.");
}
await BoredBot.getInstance(process.env.DISCORD_BOT_TOKEN, process.env.APPLICATION_ID, boredBotCommands);

103
tsconfig.json Normal file
View File

@ -0,0 +1,103 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "ES2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "ES2022", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
"types": ["bun-types"], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}

43
wps/compose.yml Normal file
View File

@ -0,0 +1,43 @@
services:
bookstack:
image: lscr.io/linuxserver/bookstack:latest
container_name: bookstack
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Oslo
- APP_URL=$APP_URL
- APP_KEY=$APP_KEY
- DB_HOST=mariadb
- DB_PORT=3306
- DB_DATABASE=bookstack
- DB_USERNAME=bookstack
- DB_PASSWORD=$DB_PASSWORD
volumes:
- ~/bookstack/bookstack_app_data:/config
ports:
- 6875:80
restart: always
networks:
- bookstack_network
mariadb:
image: lscr.io/linuxserver/mariadb:latest
container_name: mariadb
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Oslo
- MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASSWORD
- MYSQL_DATABASE=bookstack
- MYSQL_USER=bookstack
- MYSQL_PASSWORD=$DB_PASSWORD
volumes:
- ~/bookstack/bookstack_db_data:/config
restart: always
networks:
- bookstack_network
networks:
bookstack_network:
driver: bridge

5
wps/todo.md Normal file
View File

@ -0,0 +1,5 @@
- import stack from prod
- reverse proxy
- point domain provider to hostmanet
- ???
- Profit