develop #2

Merged
jack merged 10 commits from develop into main 2024-09-16 14:50:32 +00:00
54 changed files with 4112 additions and 788 deletions

11
.dockerignore Normal file
View File

@@ -0,0 +1,11 @@
node_modules/
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
Dockerfile
docker-compose.yml
docker-compose.override.yml

8
.env.example Normal file
View File

@@ -0,0 +1,8 @@
DISCORD_APPLICATION_ID=
DISCORD_API_KEY=
GIPHY_API_KEY=
PLANTNET_API_KEY=
OMDB_API_KEY=
REPLICATE_API_KEY=
MONGODB_URI=
REGISTER_SLASH_COMMANDS=

View File

@@ -0,0 +1,72 @@
name: Build and Publish Docker Image
on:
push:
branches:
- main
- develop
- feature/devops
jobs:
build_and_push:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 'lts/Iron'
- name: Log in to Docker Registry
uses: docker/login-action@v3
with:
username: ${{ secrets.USERNAME }}
password: ${{ secrets.TOKEN }}
registry: git.3t.network
- name: Set Docker Image Tags
id: set_tags
run: |
GIT_HASH=${{env.GITHUB_SHA}}
IMAGE_NAME=git.3t.network/3t.network/butlerbot:${GIT_HASH:0:7}
# Initialize the TAGS variable with the image name and hash
TAGS=$IMAGE_NAME
# Calculate additional tags based on branch
if [ "${{ gitea.ref_name }}" = 'main' ]; then
VERSION=$(node -p -e "require('./package.json').version")
TAGS+=",git.3t.network/3t.network/butlerbot:latest"
TAGS+=",git.3t.network/3t.network/butlerbot:$VERSION"
elif [ "${{ gitea.ref_name }}" = 'develop' ]; then
VERSION=$(node -p -e "require('./package.json').version")-dev
TAGS+=",git.3t.network/3t.network/butlerbot:dev"
TAGS+=",git.3t.network/3t.network/butlerbot:$VERSION"
fi
# Write the tags to the GITHUB_OUTPUT file to set the output
echo "image_tags=$TAGS" >> $GITHUB_OUTPUT
- name: Build and Push Docker Image
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ steps.set_tags.outputs.image_tags }}
# - name: Set Portainer Webhook URL
# run: |
# if [[ "${{ gitea.ref_name }}" == "main" ]]; then
# echo "PORTAINER_WEBHOOK_URL=${{ secrets.PORTAINER_WEBHOOK_URL_MAIN }}" >> $GITHUB_ENV
# elif [[ "${{ gitea.ref_name }}" == "develop" ]]; then
# echo "PORTAINER_WEBHOOK_URL=${{ secrets.PORTAINER_WEBHOOK_URL_DEV }}" >> $GITHUB_ENV
# fi
# - name: Trigger Portainer Webhook
# run: |
# curl -X POST --silent --fail "$PORTAINER_WEBHOOK_URL"
# env:
# PORTAINER_WEBHOOK_URL: ${{ env.PORTAINER_WEBHOOK_URL }}

6
.gitignore vendored
View File

@@ -1,3 +1,6 @@
# ---> Build specific exclusions
docker-compose.override.yml
# ---> Windows # ---> Windows
# Windows thumbnail cache files # Windows thumbnail cache files
Thumbs.db Thumbs.db
@@ -46,7 +49,8 @@ $RECYCLE.BIN/
.LSOverride .LSOverride
# Icon must end with two \r # Icon must end with two \r
Icon Icon
# Thumbnails # Thumbnails
._* ._*

38
Dockerfile Normal file
View File

@@ -0,0 +1,38 @@
################################################
FROM node:lts-alpine AS base
# Create user and group
RUN mkdir /app && \
addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001 && \
chown -R nodejs:nodejs /app
WORKDIR /app
################################################
FROM base AS build
# Create app directory and copy the app
COPY package*.json tsconfig.json ./
# Install
RUN npm install
# Copy the app
COPY src ./src
# Build
RUN npm run build
################################################
FROM node:lts-alpine
COPY --from=build /app/package*.json ./
COPY --from=build /app/dist ./dist
# Install dependencies but skip dev dependencies
RUN npm install --only=production
# Start the app
ENTRYPOINT ["npm", "run"]
CMD ["start"]

View File

@@ -1,28 +1,76 @@
# ButlerBotNG # ButlerBot
## Installation ## Requirements
ButlerBot is deployed using Docker. To run ButlerBot, you will need to have Docker installed on your machine. You can download Docker [here](https://www.docker.com/products/docker-desktop), or by using the following convenience script:
```bash ```bash
$ npm install curl -fsSL https://get.docker.com | sh
``` ```
## Usage ## Usage
1. Create `.env` (see `example.env`) and populate with all necessary details. ### Pre-built Docker Image
2. Register slash commands.
The easiest way to install ButlerBot is to use the provided Docker image with the supplied `docker-compose.yml` file, found in the project directory.
Edit the values in the `.env` file to match your environment. An example `.env` file is provided in the project directory as `.env.example`.
Before ButlerBot can be used, you will need to register any new commands with the bot. To do this, run the following command:
```bash ```bash
$ npm run register-slash-commands ./scripts/register-slash-commands.sh
``` ```
3. Run ButlerBot. To run ButlerBot using the latest release version, use the following command:
```bash ```bash
$ npm run start ./scripts/start.sh
``` ```
To run ButlerBot using another version, first update the `docker-compose.yml` file to use the appropriate image. Release and development versions of ButlerBot are available on 3t.network's package repository.
| Version | Image |
| ----------- | -------------------------------------------- |
| Latest | `git.3t.network/3t.network/butlerbot:latest` |
| Development | `git.3t.network/3t.network/butlerbot:dev` |
### Building the Docker Image
Alternatively, you can build the Docker image from source. To install ButlerBot, clone the repository and navigate to the project directory. Then, build the Docker image:
```bash
./scripts/build.sh
```
This will create a Docker image called `butlerbot`, tagged with the version number found in package.json. A `docker-compose.override.yml` file is created, which automatically overrides the project's `docker-compose.yml` file to use the newly built image.
## Development ## Development
All commands are dynamically loaded at runtime, and are stored in `src/commands/[CATEGORY]/[NAME].js`. To run ButlerBot in development mode, you will need to have Node.js installed on your machine. You can download Node.js [here](https://nodejs.org/en/). ButlerBot is built using the latest Node.js LTS version.
To add a new command, just create a new .js file. It must export a `data` object and `execute` promise. See existing commands for an example.
Remember to re-register slash commands using `npm run register-slash-commands` if adding a new command, or modifying an existing command's parameters. To install the project dependencies, run:
```bash
npm install
```
To update the bot's commands, run:
```bash
npm run register-slash-commands
```
To start ButlerBot, run:
```bash
npm run start
```
N.B. ButlerBot requires several environment variables to run. These are stored in a `.env` file in the project directory. An example `.env` file is provided in the project directory as `.env.example`.
## DevOps
ButlerBot is deployed using Docker. The project includes a `Dockerfile` and `docker-compose.yml` file for building and running the bot. The `docker-compose.yml` file includes a `butlerbot` service, which runs the bot using the latest release version.
When a new release is ready, the version number in the `package.json` file should be updated using `npm version patch`, and merged into the `main` branch. This will trigger a new release on 3t.network's package repository, which can be used to update the bot's Docker image via Watchtower.

5
docker-compose.yml Normal file
View File

@@ -0,0 +1,5 @@
services:
butlerbot:
image: git.3t.network/3t.network/butlerbot:latest
env_file:
- .env

View File

@@ -1,5 +0,0 @@
DISCORD_TOKEN=MTI4MDQ0NDY1MjUyODQ2Nz...
DISCORD_APPLICATION_ID=12804446...
GIPHY_API_KEY=mg7MHuxn42R4TE33...
PLANTNET_API_KEY=2b10p4Zb7K...
OMDB_API_KEY=96e...

2198
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,17 @@
{ {
"name": "butlerbotng", "name": "butlerbot",
"version": "1.0.0", "version": "1.0.0",
"main": "src/index.js", "main": "dist/index.js",
"type": "module",
"scripts": { "scripts": {
"start": "node --env-file .env src/index.js", "start": "node dist/index.js",
"register-slash-commands": "node --env-file .env src/register-slash-commands.js", "build": "tsup src/index.ts --minify",
"register-slash-commands": "tsx src/registerSlashCommands.ts",
"dev": "tsx watch src/index.ts",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.3t.network/terriblecodeclub/butlerbotng.git" "url": "https://git.3t.network/terriblecodeclub/butlerbot.git"
}, },
"author": "Butlersaurus", "author": "Butlersaurus",
"license": "ISC", "license": "ISC",
@@ -19,10 +20,19 @@
"axios": "^1.7.7", "axios": "^1.7.7",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"discord.js": "^14.16.1", "discord.js": "^14.16.1",
"dotenv": "^16.4.5",
"glob": "^11.0.0", "glob": "^11.0.0",
"prettier": "^3.3.3" "mongodb": "^6.8.1",
"replicate": "^0.32.1"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^9.9.1" "@types/glob": "^8.1.0",
"@types/node": "^20.4.0",
"eslint": "^9.9.1",
"prettier": "^3.3.3",
"ts-node": "^10.9.1",
"tsup": "^8.2.4",
"tsx": "^4.19.1",
"typescript": "^5.6.2"
} }
} }

76
scripts/build.sh Executable file
View File

@@ -0,0 +1,76 @@
#!/bin/bash
# Get version from package.json.
VERSION=$(node -p -e "require('./package.json').version")
# Set alias to latest if branch is main, dev if develop, or commit hash if others.
if [ "$(git branch --show-current)" == "main" ]; then
# Check if the working directory is clean.
if [ -n "$(git status --porcelain)" ]; then
echo "Working directory is not clean. Commit changes before releasing."
exit 1
fi
# Check if the local main branch is up-to-date with the remote main branch.
git fetch
if [ "$(git rev-parse HEAD)" != "$(git rev-parse origin/main)" ]; then
echo "Local main branch is not up-to-date with the remote main branch. Push changes before releasing."
exit 1
fi
elif [ "$(git branch --show-current)" == "develop" ]; then
# Check if the working directory is clean.
if [ -n "$(git status --porcelain)" ]; then
echo "Working directory is not clean. Commit changes before releasing."
exit 1
fi
# Check if the local main branch is up-to-date with the remote main branch.
git fetch
if [ "$(git rev-parse HEAD)" != "$(git rev-parse origin/develop)" ]; then
echo "Local main branch is not up-to-date with the remote main branch. Push changes before releasing."
exit 1
fi
else
VERSION=$(git rev-parse --short HEAD)
fi
# Set tags.
TAGS=(
"3t.network/butlerbot:$VERSION"
"git.3t.network/3t.network/butlerbot:$VERSION"
)
# Append to tags if the branch is main or develop.
if [ "$ALIAS" == "latest" ]; then
TAGS+=("3t.network/butlerbot:latest")
TAGS+=("git.3t.network/3t.network/butlerbot:latest")
elif [ "$ALIAS" == "dev" ]; then
TAGS+=("3t.network/butlerbot:dev")
TAGS+=("git.3t.network/3t.network/butlerbot:dev")
fi
# cd to the project root directory.
cd "$(dirname "$0")/.."
docker build -t butlerbot:$VERSION .
# Tag the Docker image.
for TAG in "${TAGS[@]}"; do
docker tag butlerbot:$VERSION "$TAG"
done
# Create docker-compose.override.yml with the version.
cat > docker-compose.override.yml <<EOF
services:
butlerbot:
image: git.3t.network/3t.network/butlerbot:$VERSION
EOF
echo "docker-compose.override.yml created."
# Print build information.
echo "butlerbot:$VERSION built successfully."
echo "Tags:"
for TAG in "${TAGS[@]}"; do
echo " $TAG"
done
echo "Run 'docker compose up' to start the bot."

View File

@@ -0,0 +1,3 @@
#!/bin/bash
docker compose run --rm butlerbot register-slash-commands

3
scripts/start.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
docker compose up

View File

@@ -1,4 +1,4 @@
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { import {
format, format,
differenceInYears, differenceInYears,
@@ -10,11 +10,21 @@ import {
const BIRTHDAY_TIMESTAMP = 1582576229; const BIRTHDAY_TIMESTAMP = 1582576229;
const data = new SlashCommandBuilder() // Initialise the command data.
export const data = new SlashCommandBuilder()
.setName('birthday') .setName('birthday')
.setDescription("Returns ButlerBot's Birthday information."); .setDescription("Returns ButlerBot's Birthday information.");
async function execute(interaction) { console.log(`Loaded ${data.name} command.`);
/**
* Responds with ButlerBot's age and the time remaining until the next birthday.
* @param interaction The interaction that triggered the command.
* @returns A promise that resolves when the command is finished executing.
*/
export async function execute(
interaction: ChatInputCommandInteraction
): Promise<void> {
const today = new Date(); const today = new Date();
const birthday = new Date(BIRTHDAY_TIMESTAMP * 1000); const birthday = new Date(BIRTHDAY_TIMESTAMP * 1000);
@@ -56,5 +66,3 @@ async function execute(interaction) {
const fullMessage = `${ageMessage} ${birthdayMessage}`; const fullMessage = `${ageMessage} ${birthdayMessage}`;
await interaction.reply(fullMessage); await interaction.reply(fullMessage);
} }
export default { data, execute };

24
src/commands/corrupt.ts Normal file
View File

@@ -0,0 +1,24 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
const CORRUPT_IMAGE =
'https://media.discordapp.net/attachments/506852356898422797/717395817626861638/isntthatcorrupt2.PNG';
// Initialise the command data.
export const data = new SlashCommandBuilder()
.setName('corrupt')
.setDescription(
"Returns a powerful quote from Neil Breen's infamous indie film, Pass Thru (2016)."
);
console.log(`Loaded ${data.name} command.`);
/**
* Replies with an image from Neil Breen's Pass Thru (2016).
* @param interaction The interaction that triggered the command.
* @returns A promise that resolves when the command is finished executing.
*/
export async function execute(
interaction: ChatInputCommandInteraction
): Promise<void> {
await interaction.reply(CORRUPT_IMAGE);
}

31
src/commands/countdown.ts Normal file
View File

@@ -0,0 +1,31 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
// Initialise the command data.
export const data = new SlashCommandBuilder()
.setName('countdown')
.setDescription('Start a five second countdown.');
console.log(`Loaded ${data.name} command.`);
/**
* Start a five second countdown and send updates to the user.
* @param interaction The interaction that triggered the command.
* @returns A promise that resolves when the countdown is finished.
*/
export async function execute(
interaction: ChatInputCommandInteraction
): Promise<void> {
await interaction.reply({ content: 'Starting countdown...' });
// 2-second delay before countdown starts
await new Promise((resolve) => setTimeout(resolve, 2000));
// Countdown from 5 to 1
for (let i = 5; i > 0; i--) {
await interaction.editReply({ content: String(i) });
await new Promise((resolve) => setTimeout(resolve, 1000)); // 1-second delay between numbers
}
// Notify the user that the countdown is over
await interaction.editReply({ content: '🎉 GO! 🎉' });
}

62
src/commands/eyecandy.ts Normal file
View File

@@ -0,0 +1,62 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import axios from 'axios';
const GIPHY_API_URL = 'http://api.giphy.com/v1/gifs/search';
// Initialise the command data.
export const data = new SlashCommandBuilder()
.setName('eyecandy')
.setDescription('Returns a random gif of Gerard Butler.');
console.log(`Loaded ${data.name} command.`);
/**
* Execute the command, fetch a random Gerard Butler gif, and send it to the user.
* @param interaction The interaction that triggered the command.
* @returns A promise that resolves when the command is finished executing.
*/
export async function execute(
interaction: ChatInputCommandInteraction
): Promise<void> {
await interaction.deferReply();
if (!process.env.GIPHY_API_KEY) {
await interaction.reply('The bot is missing the GIPHY API key.');
return;
}
try {
// Fetch a random gif of Gerard Butler.
const gifUrl = await fetchGif();
// Reply with the gif.
await interaction.editReply(gifUrl);
} catch (error: any) {
console.error('Error executing the command:', error.message);
await interaction.editReply(`An error occurred: ${error.message}`);
}
}
/**
* Fetch a random gif of Gerard Butler from Giphy.
* @returns A promise that resolves to the URL of the gif.
* @throws An error if no gif is found or if the request fails.
*/
async function fetchGif(): Promise<string> {
const randomOffset = Math.floor(Math.random() * 100);
const giphyQueryUrl = `${GIPHY_API_URL}?api_key=${process.env.GIPHY_API_KEY}&q=gerard+butler&limit=1&offset=${randomOffset}`;
try {
const response = await axios.get(giphyQueryUrl, { timeout: 5000 });
const result = response.data;
if (result.data.length === 0) {
throw new Error('No gifs found for the given query.');
}
return result.data[0].images.original.url;
} catch (error: any) {
console.error('Error fetching GIF:', error.message);
throw new Error('Failed to retrieve a GIF from Giphy.');
}
}

224
src/commands/game.ts Normal file
View File

@@ -0,0 +1,224 @@
import {
SlashCommandBuilder,
EmbedBuilder,
ChatInputCommandInteraction,
} from 'discord.js';
import {
listGameNames,
getGame,
setGame,
deleteGame,
deleteField,
} from './utils/db/game';
// Initialise the command data.
export const data = new SlashCommandBuilder()
.setName('game')
.setDescription('Perform ButlerBot game database operations.')
.addSubcommand((subcommand) =>
subcommand.setName('list').setDescription('List all games in the database.')
)
.addSubcommand((subcommand) =>
subcommand
.setName('get')
.setDescription('Get a game from the database.')
.addStringOption((option) =>
option
.setName('game')
.setDescription('The name of the game to get.')
.setRequired(true)
)
)
.addSubcommand((subcommand) =>
subcommand
.setName('set')
.setDescription('Set a key value pair on a game in the database.')
.addStringOption((option) =>
option
.setName('game')
.setDescription('The name of the game to set the field in.')
.setRequired(true)
)
.addStringOption((option) =>
option
.setName('key')
.setDescription('The key or label to set.')
.setRequired(true)
)
.addStringOption((option) =>
option
.setName('value')
.setDescription('The value of the field to set.')
.setRequired(true)
.setMinLength(1)
.setMaxLength(1024)
)
)
.addSubcommand((subcommand) =>
subcommand
.setName('delete')
.setDescription('Delete a game, or a field from a game.')
.addStringOption((option) =>
option
.setName('game')
.setDescription(
'The name of the game to remove, or remove a field from.'
)
.setRequired(true)
)
.addStringOption((option) =>
option
.setName('key')
.setDescription('The key or label of the field to remove.')
.setRequired(false)
)
);
console.log(`Loaded ${data.name} command.`);
/**
* Handle the interaction for the game command.
* @param interaction The interaction that triggered the command.
* @returns A promise that resolves when the command is finished executing.
*/
export async function execute(
interaction: ChatInputCommandInteraction
): Promise<void> {
await interaction.deferReply();
const subcommand = interaction.options.getSubcommand();
switch (subcommand) {
case 'list': {
const gameNames = await listGameNames();
if (gameNames.length === 0) {
await interaction.editReply('No games found in the database.');
return;
}
const embed = new EmbedBuilder()
.setTitle('Game Database')
.setColor(0xff0000)
.setDescription('List of games in the database.');
embed.addFields({
name: 'Games',
value: gameNames.map((game) => game.game).join('\n'),
});
await interaction.editReply({ embeds: [embed] });
break;
}
case 'get': {
const gameName = interaction.options.getString('game', true);
const game = await getGame(gameName);
if (!game) {
await interaction.editReply(
`Game ${gameName} not found in the database.`
);
return;
}
try {
const embed = createEmbedFromGame(game);
await interaction.editReply({ embeds: [embed] });
} catch (error) {
await interaction.editReply(
'Embed too large to send. Game has not been retrieved.'
);
}
break;
}
case 'set': {
const gameName = interaction.options.getString('game', true);
const key = interaction.options.getString('key', true);
const value = interaction.options.getString('value', true);
const oldGame = await getGame(gameName);
await setGame(gameName, key, value);
const updatedGame = await getGame(gameName);
try {
const embed = createEmbedFromGame(updatedGame);
await interaction.editReply({ embeds: [embed] });
} catch (error) {
// Revert game back to original state if the embed is too large.
if (oldGame) {
// If key already existed, revert to old value, else delete the key.
if (oldGame[key]) {
await setGame(gameName, key, oldGame[key]);
} else {
await deleteField(gameName, key);
}
}
await interaction.editReply(
'Embed too large to send. Game has not been updated.'
);
}
break;
}
case 'delete': {
const gameName = interaction.options.getString('game', true);
const key = interaction.options.getString('key');
if (key) {
await deleteField(gameName, key);
const updatedGame = await getGame(gameName);
if (!updatedGame) {
await interaction.editReply(
`Game ${gameName} deleted from the database.`
);
return;
}
try {
const embed = createEmbedFromGame(updatedGame);
await interaction.editReply({ embeds: [embed] });
} catch (error) {
await interaction.editReply(
'Embed too large to send. Field deleted from game but game has not been retrieved.'
);
}
} else {
await deleteGame(gameName);
await interaction.editReply(
`Game ${gameName} deleted from the database.`
);
}
break;
}
default:
await interaction.editReply('Unknown subcommand.');
break;
}
}
/**
* Create an embed from a game object.
* @param game The game object containing key-value pairs.
* @returns The embed object to be sent in the interaction.
*/
function createEmbedFromGame(game: any): EmbedBuilder {
const embed = new EmbedBuilder().setTitle(game.game).setColor(0xff0000);
// Add fields for each key value pair - skip name, _id and guild.
embed.addFields(
Object.entries(game)
.filter(([key]) => !['_id', 'game', 'guild'].includes(key))
.map(([key, value]) => ({ name: key, value: String(value) }))
);
if (embed.length > 6000) {
throw new Error('Embed size exceeds maximum.');
}
return embed;
}

113
src/commands/image.ts Normal file
View File

@@ -0,0 +1,113 @@
import {
SlashCommandBuilder,
AttachmentBuilder,
ChatInputCommandInteraction,
} from 'discord.js';
import axios from 'axios';
import Replicate, { Prediction } from 'replicate';
import config from '../config';
const replicate = new Replicate({
auth: config.replicateApiKey,
});
// Initialise the command data.
export const data = new SlashCommandBuilder()
.setName('image')
.setDescription('Generate an image based on a prompt.')
.addStringOption((option) =>
option
.setName('prompt')
.setDescription('The prompt to generate an image from')
.setRequired(true)
);
console.log(`Loaded ${data.name} command.`);
/**
* Generate an image based on a prompt and send it back to the user.
* @param interaction The interaction that triggered the command.
* @returns A promise that resolves when the command is finished executing.
*/
export async function execute(
interaction: ChatInputCommandInteraction
): Promise<void> {
await interaction.deferReply();
const prompt = interaction.options.get('prompt')?.value as string;
try {
// Create image generation prediction
const prediction = await replicate.predictions.create({
model: 'black-forest-labs/flux-schnell',
input: { prompt },
});
// Poll until the image generation is complete
const completedPrediction = await pollPredictionStatus(prediction.id);
if (!completedPrediction || !completedPrediction.output) {
throw new Error('Failed to generate the image.');
}
const imageUrl = completedPrediction.output[0];
// Download the generated image
const imageBuffer = await downloadImage(imageUrl);
// Create an attachment to send the image back to the user
const attachment = new AttachmentBuilder(imageBuffer, {
name: 'image.png',
});
// Edit the deferred reply to include the generated image
await interaction.editReply({ files: [attachment] });
} catch (error: any) {
// Provide a more informative error message to the user
console.error(error); // Log the error for debugging purposes
await interaction.editReply(`An error occurred: ${error.message}`);
}
}
/**
* Poll the status of a prediction until it is no longer in progress.
* @param predictionId The ID of the prediction to poll.
* @param maxAttempts The maximum number of attempts to poll the prediction.
* @param interval The interval between each polling attempt in milliseconds.
* @returns The final status of the prediction.
*/
async function pollPredictionStatus(
predictionId: string,
maxAttempts = 5,
interval = 2000
): Promise<Prediction> {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const latestPrediction = await replicate.predictions.get(predictionId);
if (
latestPrediction.status !== 'starting' &&
latestPrediction.status !== 'processing'
) {
return latestPrediction;
}
// Wait before checking again
await new Promise((resolve) => setTimeout(resolve, interval));
}
throw new Error('Prediction timed out.');
}
/**
* Download an image from a URL and return it as a Buffer.
* @param url The URL of the image to download.
* @returns A Buffer containing the downloaded image.
*/
async function downloadImage(url: string): Promise<Buffer> {
try {
const response = await axios.get(url, { responseType: 'arraybuffer' });
return Buffer.from(response.data);
} catch (error) {
throw new Error('Failed to download the image.');
}
}

107
src/commands/imdb.ts Normal file
View File

@@ -0,0 +1,107 @@
import {
EmbedBuilder,
SlashCommandBuilder,
ChatInputCommandInteraction,
} from 'discord.js';
import axios from 'axios';
import config from '../config';
const OMDB_API_URL = `http://www.omdbapi.com/?apikey=${config.omdbApiKey}`;
const FIELDS = [
'Title',
'Year',
'Rated',
'Released',
'Genre',
'Director',
'Actors',
'Plot',
'imdbRating',
'BoxOffice',
];
export const data = new SlashCommandBuilder()
.setName('imdb')
.setDescription('Return IMDB listing for the specified film.')
.addStringOption((option) =>
option
.setName('film_name')
.setDescription('The name of the film')
.setRequired(true)
)
.addIntegerOption((option) =>
option
.setName('film_year')
.setDescription('The year the specified film was released')
.setMinValue(1800)
.setMaxValue(2100)
);
console.log(`Loaded ${data.name} command.`);
/**
* Executes the 'imdb' command, retrieving film data from the OMDB API and sending it to the user.
* @param interaction The interaction that triggered the command.
* @returns A promise that resolves when the command is finished executing.
*/
export async function execute(
interaction: ChatInputCommandInteraction
): Promise<void> {
await interaction.deferReply();
const filmName = interaction.options.getString('film_name') as string;
const filmYear = interaction.options.getInteger('film_year') ?? undefined;
try {
const omdbQueryUrl = buildOmdbUrl(filmName, filmYear);
const response = await axios.get(omdbQueryUrl, { timeout: 5000 });
const result = response.data;
// Check if the movie was found
if (result.Response === 'False') {
await interaction.editReply(
`No results found for the film: ${filmName}.`
);
return;
}
// Build the embed with film details
const embed = new EmbedBuilder()
.setTitle(result.Title || 'IMDB Film Info')
.setColor(0xf5de50);
// Add the fields to the embed
FIELDS.forEach((field) => {
if (result[field]) {
embed.addFields({ name: field, value: result[field], inline: true });
}
});
// Add poster image if available
if (result.Poster && result.Poster !== 'N/A') {
embed.setImage(result.Poster);
}
// Send the result to the user
await interaction.editReply({ embeds: [embed] });
} catch (error: any) {
console.error(`Error looking up film: ${filmName}`, error);
await interaction.editReply(
'An error occurred when querying the OMDB API.'
);
}
}
/**
* Helper function to construct OMDB query URL.
* @param filmName The name of the film.
* @param filmYear The year the film was released.
* @returns The OMDB API URL with the specified query parameters.
*/
function buildOmdbUrl(filmName: string, filmYear?: number): string {
let url = OMDB_API_URL + `&t=${encodeURIComponent(filmName.toLowerCase())}`;
if (filmYear) {
url += `&y=${filmYear}`;
}
return url;
}

23
src/commands/kanye.ts Normal file
View File

@@ -0,0 +1,23 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import kanyeQuotes from './utils/constants/kanyeQuotes.json';
// Initialise the command data.
export const data = new SlashCommandBuilder()
.setName('kanye')
.setDescription('Returns a Kanye West quote.');
console.log(`Loaded ${data.name} command.`);
/**
* Returns a random Kanye West quote.
* @param interaction The interaction that triggered the command.
* @returns A promise that resolves when the command is finished executing.
*/
export async function execute(
interaction: ChatInputCommandInteraction
): Promise<void> {
const randomIndex = Math.floor(Math.random() * kanyeQuotes.length);
const response = kanyeQuotes[randomIndex];
await interaction.reply(response);
}

View File

@@ -1,56 +0,0 @@
import { EmbedBuilder, SlashCommandBuilder } from 'discord.js';
import axios from 'axios';
const GODADDY_API_URL = 'https://api.godaddy.com/v1/domains/available';
const data = new SlashCommandBuilder()
.setName('domain')
.setDescription('Check the availability and price of a domain name.')
.addStringOption((option) =>
option
.setName('domain')
.setDescription('The domain to check')
.setRequired(true)
);
async function execute(interaction) {
const domain = interaction.options.getString('domain');
let embed = new EmbedBuilder().setTitle('Domain checker');
try {
const response = await axios.get(GODADDY_API_URL, {
params: { domain: domain },
headers: { Authorization: `sso-key ${process.env.GODADDY_API_KEY}` },
timeout: 5000,
});
console.log(`Got response for domain: ${domain}.`, response.data);
const result = response.data;
let isDomainAvailable = 'No.';
let color = 0xff0000;
let price = null;
if (response.status === 200 && result.available) {
isDomainAvailable = 'Yes!';
color = 0x00ff00;
price = (result.price / 1000000).toFixed(2);
}
embed.setColor(color).addFields({
name: 'Is the domain available?',
value: isDomainAvailable,
});
if (price) {
embed.addFields({ name: 'Price', value: `$${price}` });
}
} catch (error) {
console.error(`Error looking up domain: ${domain}.`, error);
await interaction.reply('An error occurred when querying the GoDaddy API.');
return;
}
await interaction.reply({ embeds: [embed] });
}
export default { data, execute };

View File

@@ -1,71 +0,0 @@
import { EmbedBuilder, SlashCommandBuilder } from 'discord.js';
import axios from 'axios';
const OMDB_API_URL = 'http://www.omdbapi.com/?apikey={apiKey}';
const FIELDS = [
'Title',
'Year',
'Rated',
'Released',
'Genre',
'Director',
'Actors',
'Plot',
'imdbRating',
'BoxOffice',
];
const data = new SlashCommandBuilder()
.setName('imdb')
.setDescription('Return IMDB listing for the specified film.')
.addStringOption((option) =>
option
.setName('film_name')
.setDescription('The name of the film')
.setRequired(true)
)
.addIntegerOption((option) =>
option
.setName('film_year')
.setDescription('The year the specified film was released')
.setMinValue(1800)
.setMaxValue(2100)
);
async function execute(interaction) {
const filmName = interaction.options.getString('film_name');
const filmYear = interaction.options.getInteger('film_year');
let embed = new EmbedBuilder().setTitle('IMDB').setColor(0xf5de50);
let omdbQueryUrl = OMDB_API_URL.replace('{apiKey}', process.env.OMDB_API_KEY);
omdbQueryUrl += `&t=${encodeURIComponent(filmName.toLowerCase())}`;
if (filmYear) {
omdbQueryUrl += `&y=${filmYear}`;
}
try {
const response = await axios.get(omdbQueryUrl, { timeout: 5000 });
const result = response.data;
FIELDS.forEach((field) => {
const fieldValue = result[field];
if (fieldValue) {
embed.addFields({ name: field, value: fieldValue });
}
});
const poster = result.Poster;
if (poster) {
embed.setImage(poster);
}
} catch (error) {
console.error(`Error looking up film: ${filmName}.`, error);
await interaction.reply('An error occurred when querying the OMDB API.');
return;
}
await interaction.reply({ embeds: [embed] });
}
export default { data, execute };

View File

@@ -1,75 +0,0 @@
import { EmbedBuilder, SlashCommandBuilder } from 'discord.js';
import axios from 'axios';
const API_URL = 'https://my-api.plantnet.org/v2/identify/all?api-key={apiKey}';
const LEAF_THUMBNAIL =
'https://cdn.discordapp.com/attachments/870024275556446328/1006249009201033287/monstera.png';
const data = new SlashCommandBuilder()
.setName('plant')
.setDescription('Identify a plant by uploading an image.')
.addAttachmentOption((option) =>
option
.setName('image')
.setDescription('The image of the plant to identify')
.setRequired(true)
);
async function execute(interaction) {
const image = interaction.options.getAttachment('image');
let embed = new EmbedBuilder()
.setTitle('Plant Detector™')
.setThumbnail(LEAF_THUMBNAIL)
.setColor(0xff0000);
try {
const response = await axios.get(
API_URL.replace('{apiKey}', process.env.PLANTNET_API_KEY),
{
params: {
images: image.url,
organs: 'leaf',
},
timeout: 5000,
}
);
const bestMatch = response.data.results[0];
const commonName =
bestMatch.species.commonNames[0] || 'No common name found';
const scientificName = bestMatch.species.scientificNameWithoutAuthor;
const confidence = parseFloat(bestMatch.score);
let detail = '';
if (0.0 <= confidence && confidence < 0.25) {
detail = `I'm really not sure, but I think that looks like a **${commonName}**?`;
} else if (0.25 <= confidence && confidence < 0.5) {
detail = `I think that looks like a **${commonName}**!`;
} else if (0.5 <= confidence && confidence < 0.75) {
detail = `That looks like a **${commonName}**!`;
} else if (confidence >= 0.75) {
detail = `That is a really cool **${commonName}**!`;
}
const imageUrl = `https://www.google.com/search?q=${encodeURIComponent(commonName || scientificName)}&tbm=isch`;
detail += `\n\n**Scientific Name**: ${scientificName}`;
detail += `\n\n**Images**: [More images](${imageUrl})`;
detail += `\n\n**Confidence**: ${(confidence * 100).toFixed(3)}%`;
embed
.setColor(0x00ff00)
.addFields({ name: 'Plant Details', value: detail });
} catch (error) {
console.error(`Error looking up plant: ${image.url}`, error);
await interaction.reply(
'An error occurred when querying the PlantNet API.'
);
return;
}
await interaction.reply({ embeds: [embed] });
}
export default { data, execute };

View File

@@ -0,0 +1,31 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import magicEightBallResponses from './utils/constants/magicEightBallResponses.json';
// Initialise the command data.
export const data = new SlashCommandBuilder()
.setName('magic8ball')
.setDescription('Returns a Magic 8 ball response.')
.addStringOption((option) =>
option
.setName('question')
.setDescription('The question to ask the Magic 8 ball')
.setRequired(true)
);
console.log(`Loaded ${data.name} command.`);
/**
* Returns a random response from the Magic 8 ball.
* @param interaction The interaction that triggered the command.
* @returns A promise that resolves when the command is finished executing.
*/
export async function execute(
interaction: ChatInputCommandInteraction
): Promise<void> {
const randomIndex = Math.floor(
Math.random() * magicEightBallResponses.length
);
const response = magicEightBallResponses[randomIndex];
await interaction.reply(response);
}

View File

@@ -1,14 +0,0 @@
import { SlashCommandBuilder } from 'discord.js';
const TWENTY_TWENTY_IMAGE =
'https://cdn.discordapp.com/attachments/506852356898422797/765256712063025172/unknown.png';
const data = new SlashCommandBuilder()
.setName('2020')
.setDescription('Returns a tweet from Boris, posted on 2 January 2020.');
async function execute(interaction) {
await interaction.reply(TWENTY_TWENTY_IMAGE);
}
export default { data, execute };

View File

@@ -1,45 +0,0 @@
import { SlashCommandBuilder } from 'discord.js';
const data = new SlashCommandBuilder()
.setName('8ball')
.setDescription('Returns a Magic 8 ball response.')
.addStringOption((option) =>
option
.setName('question')
.setDescription('The question to ask the Magic 8 ball')
.setRequired(true)
);
const MAGIC_EIGHT_BALL_RESPONSES = [
'As I see it, yes.',
'Ask again later.',
'Better not tell you now.',
'Cannot predict now.',
'Concentrate and ask again.',
"Don't count on it.",
'It is certain.',
'It is decidedly so.',
'Most likely.',
'My reply is no.',
'My sources say no.',
'Outlook not so good.',
'Outlook good.',
'Reply hazy, try again.',
'Signs point to yes.',
'Very doubtful.',
'Without a doubt.',
'Yes.',
'Yes - definitely.',
'You may rely on it.',
];
async function execute(interaction) {
const randomIndex = Math.floor(
Math.random() * MAGIC_EIGHT_BALL_RESPONSES.length
);
const response = MAGIC_EIGHT_BALL_RESPONSES[randomIndex];
await interaction.reply(response);
}
export default { data, execute };

View File

@@ -1,16 +0,0 @@
import { SlashCommandBuilder } from 'discord.js';
const CORRUPT_IMAGE =
'https://media.discordapp.net/attachments/506852356898422797/717395817626861638/isntthatcorrupt2.PNG';
const data = new SlashCommandBuilder()
.setName('corrupt')
.setDescription(
"Returns a powerful quote from Neil Breen's infamous indie film, Pass Thru (2016)."
);
async function execute(interaction) {
await interaction.reply(CORRUPT_IMAGE);
}
export default { data, execute };

View File

@@ -1,40 +0,0 @@
import { SlashCommandBuilder } from 'discord.js';
import axios from 'axios';
const GIPHY_API_URL = 'http://api.giphy.com/v1/gifs/search';
const data = new SlashCommandBuilder()
.setName('eyecandy')
.setDescription('Returns a random gif of Gerard Butler.');
async function execute(interaction) {
const giphyApiKey = process.env.GIPHY_API_KEY;
if (!giphyApiKey) {
await interaction.reply(
'The bot has not been configured with a Giphy API key.'
);
return;
}
const randomOffset = Math.floor(Math.random() * 100);
const giphyQueryUrl = `${GIPHY_API_URL}?api_key=${giphyApiKey}&q=gerard+butler&limit=1&offset=${randomOffset}`;
try {
const response = await axios.get(giphyQueryUrl, { timeout: 5000 });
const result = response.data;
if (result.data.length === 0) {
await interaction.reply('No gifs found.');
return;
}
const imageUrl = result.data[0].images.original.url;
await interaction.reply(imageUrl);
} catch (error) {
console.error('Error querying the Giphy API:', error);
await interaction.reply('An error occurred when querying the Giphy API.');
}
}
export default { data, execute };

View File

@@ -1,139 +0,0 @@
import { SlashCommandBuilder } from 'discord.js';
const KANYE_QUOTES = [
'2024.',
'All you have to be is yourself.',
'Believe in your flyness... conquer your shyness.',
'Burn that Excel spreadsheet.',
'Decentralize.',
'Distraction is the enemy of vision.',
'Everything you do in life stems from either fear or love.',
'For me giving up is way harder than trying.',
'For me, money is not my definition of success. Inspiring people is a definition of success.',
'Fur pillows are hard to actually sleep on.',
"George Bush doesn't care about black people.",
'Have you ever thought you were in love with someone but then realized you were just staring in a mirror for 20 minutes?',
'I care. I care about everything. Sometimes not giving a f#%k is caring the most.',
'I feel calm but energized.',
"I feel like I'm too busy writing history to read it.",
'I feel like me and Taylor might still have sex.',
'I give up drinking every week.',
'I leave my emojis Bart Simpson color.',
"I love sleep; it's my favorite.",
'I make awesome decisions in bike stores!!!',
"I really love my Tesla. I'm in the future. Thank you Elon.",
'I still think I am the greatest.',
"I think I do myself a disservice by comparing myself to Steve Jobs and Walt Disney and human beings that we've seen before. It should be more like Willy Wonka...and welcome to my chocolate factory.",
'I want the world to be better! All I want is positive! All I want is dopeness!',
'I wish I had a friend like me.',
"I'd like to meet with Tim Cook. I got some ideas.",
"I'll say things that are serious and put them in a joke form so people can enjoy them. We laugh to keep from crying.",
"I'm a creative genius.",
"I'm nice at ping pong.",
"I'm the best.",
"If I don't scream, if I don't say something then no one's going to say anything.",
'If I got any cooler I would freeze to death.',
"If you have the opportunity to play this game of life you need to appreciate every moment. a lot of people don't appreciate the moment until it's passed.",
'Just stop lying about shit. Just stop lying.',
'Keep squares out yo circle.',
'Keep your nose out the sky, keep your heart to god, and keep your face to the rising sun.',
"Let's be like water.",
'Man... whatever happened to my antique fish tank?',
'My dad got me a drone for Christmas.',
"My greatest award is what I'm about to do.",
'My greatest pain in life is that I will never be able to see myself perform live.',
"One day I'm gon' marry a porn star.",
"One of my favorite of many things about what the Trump hat represents to me is that people can't tell me what to do because I'm black.",
'Only free thinkers.',
"People always say that you can't please everybody. I think that's a cop-out. Why not attempt it? Cause think of all the people that you will please if you try.",
"People always tell you 'Be humble. Be humble.' When was the last time someone told you to be amazing? Be great! Be awesome! Be awesome!",
'People only get jealous when they care.',
'Perhaps I should have been more like water today.',
'Pulling up in the may bike.',
'Shut the fuck up I will fucking laser you with alien fucking eyes and explode your fucking head.',
'Sometimes I push the door close button on people running towards the elevator. I just need my own elevator sometimes. My sanctuary.',
'Sometimes you have to get rid of everything.',
'Style is genderless.',
'The thought police want to suppress freedom of thought.',
'The world is our family.',
'The world is our office.',
"Today is the best day ever and tomorrow's going to be even better.",
"Truth is my goal. Controversy is my gym. I'll do a hundred reps of controversy for a 6 pack of truth.",
'Tweeting is legal and also therapeutic.',
"We all self-conscious. I'm just the first to admit it.",
"We came into a broken world. And we're the cleanup crew.",
"You can't look at a glass half full or empty if it's overflowing.",
"I hate when I'm on a flight and I wake up with a water bottle next to me like oh great now I gotta be responsible for this water bottle.",
'All the musicians will be free.',
'Artists are founders.',
'Buy property.',
'Culture is the most powerful force in humanity under God.',
'Empathy is the glue.',
'I am one of the most famous people on the planet.',
'I am running for President of the United States.',
'I am the head of Adidas. I will bring Adidas and Puma back together and bring me and jay back together.',
'I am Warhol. I am the No. 1 most impactful artist of our generation. I am Shakespeare in the flesh.',
"I channel Will Ferrell when I'm at the daddy daughter dances.",
"I don't wanna see no woke tweets or hear no woke raps ... it's show time ... it's a whole different energy right now.",
'I hear people say this person is cool and this person is not cool. People are cool. Man has never invented anything as awesome as a an actual person but sometimes we value the objects we create over life itself.',
'I honestly need all my Royeres to be museum quality... if I see a fake Royere Ima have to Rick James your couch.',
'I love UZI. I be saying the same thing about Steve Jobs. I be feeling just like UZI.',
'I need an army of angels to cover me while I pull this sword out of the stone.',
'I spoke to Dave Chapelle for two hours this morning. He is our modern day Socrates.',
'I was just speaking with someone that told me their life story and they used to be homeless.',
'I watch Bladerunner on repeat.',
"I'm giving all Good music artists back the 50% share I have of their masters.",
"I'm going to personally see to it that Taylor Swift gets her masters back. Scooter is a close family friend.",
"I'm the new Moses.",
'Life is the ultimate gift.',
'Ma$e is one of my favorite rappers and I based a lot of my flows off of him.',
'Manga all day.',
"My first pillar when I'm on the board of adidas will be an adidas Nike collaboration to support community growth.",
"My mama was a' English teacher. I know how to use correct English but sometimes I just don't feel like it aaaand I ain't got to.",
'My memories are from the future.',
'My mother in law Kris Jenner ... makes the best music playlist.',
"People say it's enough and I got my point across ... the point isn't across until we cross the point.",
'People tried to talk me out of running for President. Never let weak controlling people kill your spirit.',
'So many of us need so much less than we have especially when so many of us are in need.',
"Speak God's truth to power.",
'The media tries to kill our heroes one at a time.',
'The world needs more Joy... this idea is super fresh.',
'There are 5 main pillars in a professional musicians business - Recording, Publishing, Touring, Merchandise & Name and likeness.',
'There are people sleeping in parking lots.',
"There's a crying need for civility across the board. We need to and will come together in the name of Jesus.",
"There's so many lonely emojis man.",
"Trust me ... I won't stop.",
'Two years ago we had 50 million people subscribed to music streaming services around the world. Today we have 400 million.',
'We are here to complete the revolution. We are building the future.',
'We as a people will heal. We will insure the well being of each other.',
'We have to evolve.',
'We must and will cure homelessness and hunger. We have the capability as a species.',
'We must form a union. We must unify.',
'We used to diss Michael Jackson the media made us call him crazy ... then they killed him.',
'We will be recognized.',
'We will change the paradigm.',
'We will cure hunger.',
'We will heal. We will cure.',
"We're going to move the entire music industry into the 21st Century.",
"We've gotten comfortable with not having what we deserve.",
'Who made up the term major label in the first place???',
'Winning is the only option.',
"For me to say I wasn't a genius I'd just be lying to you and to myself.",
"I've known my mom since I was zero years old. She is quite dope.",
"I don't expect to be understood at all.",
"I'm on the pursuit of awesomeness, excellence is the bare minimum.",
'You basically can say anything to someone on an email or text as long as you put LOL at the end.',
];
const data = new SlashCommandBuilder()
.setName('kanye')
.setDescription('Returns a random Kanye West quote.');
async function execute(interaction) {
const randomIndex = Math.floor(Math.random() * KANYE_QUOTES.length);
const quote = KANYE_QUOTES[randomIndex];
await interaction.reply(quote);
}
export default { data, execute };

View File

@@ -1,14 +0,0 @@
import { SlashCommandBuilder } from 'discord.js';
const REMINDER_IMAGE =
'https://media.discordapp.net/attachments/506852356898422797/715690132883111996/Capture.PNG';
const data = new SlashCommandBuilder()
.setName('reminder')
.setDescription('Returns an image which must always be remembered.');
async function execute(interaction) {
await interaction.reply(REMINDER_IMAGE);
}
export default { data, execute };

132
src/commands/music.js Normal file
View File

@@ -0,0 +1,132 @@
import { SlashCommandBuilder } from 'discord.js';
import { Interaction } from 'discord.js';
// Maps to track queues and voice connections per guild
const queueMap = new Map(); // { guildId: [track1, track2, ...] }
const connectionMap = new Map(); // { guildId: VoiceConnection }
const data = new SlashCommandBuilder()
.setName('music')
.setDescription('Perform ButlerBot music operations.')
.addSubcommand((subcommand) =>
subcommand
.setName('play')
.setDescription('Play a track.')
.addStringOption((option) =>
option
.setName('track')
.setDescription('The track to play.')
.setRequired(true)
)
.addStringOption((option) =>
option
.setName('provider')
.setDescription('The provider of the track.')
.setRequired(false)
.addChoices(
{ name: 'YouTube', value: 'youtube' },
{ name: 'Spotify', value: 'spotify' },
{ name: 'Local', value: 'local' }
)
)
)
.addSubcommand((subcommand) =>
subcommand.setName('pause').setDescription('Pause the current track.')
)
.addSubcommand((subcommand) =>
subcommand.setName('resume').setDescription('Resume the current track.')
)
.addSubcommand((subcommand) =>
subcommand.setName('skip').setDescription('Skip the current track.')
)
.addSubcommand((subcommand) =>
subcommand.setName('stop').setDescription('Stop the current track.')
)
.addSubcommand((subcommand) =>
subcommand.setName('queue').setDescription('Show the current track queue.')
)
.addSubcommand((subcommand) =>
subcommand.setName('clear').setDescription('Clear the current track queue.')
)
.addSubcommand((subcommand) =>
subcommand.setName('leave').setDescription('Leave the voice channel.')
)
.addSubcommand((subcommand) =>
subcommand.setName('join').setDescription('Join the voice channel.')
);
/**
* Handles the music command.
* @param {Interaction} interaction The interaction object.
*/
async function execute(interaction) {
await interaction.deferReply();
const subcommand = interaction.options.getSubcommand();
const guildId = interaction.guild.id;
switch (subcommand) {
case 'play':
await handlePlay(interaction, guildId);
break;
case 'pause':
await handlePause(interaction, guildId);
break;
case 'resume':
await handleResume(interaction, guildId);
break;
case 'skip':
await handleSkip(interaction, guildId);
break;
case 'stop':
await handleStop(interaction, guildId);
break;
case 'queue':
await handleQueue(interaction, guildId);
break;
case 'clear':
await handleClear(interaction, guildId);
break;
case 'leave':
await handleLeave(interaction, guildId);
break;
case 'join':
await handleJoin(interaction, guildId);
break;
default:
await interaction.editReply('Invalid music subcommand.');
}
}
/**
* Handles the play subcommand.
* @param {Interaction} interaction The interaction object.
* @param {string} guildId The guild ID.
* @returns {Promise<void>}
*/
async function handlePlay(interaction, guildId) {
const track = interaction.options.getString('track');
const provider = interaction.options.getString('provider') || 'youtube';
// Check if the user is in a voice channel
const voiceChannel = interaction.member.voice.channel;
if (!voiceChannel) {
await interaction.editReply(
'You need to be in a voice channel to play music.'
);
return;
}
// Get the queue for the guild
const queue = queueMap.get(guildId) || [];
queue.push({ track, provider });
queueMap.set(guildId, queue);
// Join the voice channel
await handleJoin(interaction, guildId);
// Play the track
await interaction.editReply(`Playing track: ${track}`);
}
export default { data, execute };

View File

@@ -1,6 +1,9 @@
import { EmbedBuilder, SlashCommandBuilder } from 'discord.js';
import { import {
format, EmbedBuilder,
SlashCommandBuilder,
ChatInputCommandInteraction,
} from 'discord.js';
import {
addDays, addDays,
subDays, subDays,
getDay, getDay,
@@ -9,51 +12,27 @@ import {
set, set,
} from 'date-fns'; } from 'date-fns';
const ARM_EMPLOYEES = [141339536453140480]; const ARM_EMPLOYEES = ['141339536453140480'];
const ARM_THUMBNAIL = const ARM_THUMBNAIL =
'https://cdn.discordapp.com/attachments/724000975626698894/928379448301088868/unknown.png'; 'https://cdn.discordapp.com/attachments/724000975626698894/928379448301088868/unknown.png';
const BAE_THUMBNAIL = const BAE_THUMBNAIL =
'https://cdn.discordapp.com/attachments/724000975626698894/928380073965408306/Untitled-1.png'; 'https://cdn.discordapp.com/attachments/724000975626698894/928380073965408306/Untitled-1.png';
// Adjust payday for weekends // Initialise the command data.
function adjustPayday(payday) { export const data = new SlashCommandBuilder()
const dayOfWeek = getDay(payday);
if (dayOfWeek === 6) {
// Saturday
return subDays(payday, 1);
} else if (dayOfWeek === 0) {
// Sunday
return subDays(payday, 2);
}
return payday;
}
// Calculate next payday based on the current date
function calculateNextPayday(
payday = 22,
currentDate = new Date(),
adjust = 0
) {
let theoreticalPayday = set(currentDate, { date: payday });
let adjustedPayday = adjustPayday(theoreticalPayday);
if (adjustedPayday < currentDate) {
theoreticalPayday = set(addDays(currentDate, adjust * 30), {
date: payday,
});
adjustedPayday = adjustPayday(theoreticalPayday);
}
return adjustedPayday;
}
const data = new SlashCommandBuilder()
.setName('payday') .setName('payday')
.setDescription('Calculate the number of days until the next payday.'); .setDescription('Calculate the number of days until the next payday.');
async function execute(interaction) { console.log(`Loaded ${data.name} command.`);
/**
* Handle the payday command and reply with the number of days until the next payday.
* @param interaction The interaction that triggered the command.
* @returns A promise that resolves when the command is finished executing.
*/
export async function execute(
interaction: ChatInputCommandInteraction
): Promise<void> {
const authorId = interaction.user.id; const authorId = interaction.user.id;
const embed = new EmbedBuilder().setTitle('Payday'); const embed = new EmbedBuilder().setTitle('Payday');
@@ -108,4 +87,46 @@ async function execute(interaction) {
await interaction.reply({ embeds: [embed] }); await interaction.reply({ embeds: [embed] });
} }
export default { data, execute }; /**
* Adjust payday if it falls on a weekend.
* @param payday The date of the payday.
* @returns The adjusted payday if necessary.
*/
function adjustPayday(payday: Date): Date {
const dayOfWeek = getDay(payday);
if (dayOfWeek === 6) {
// Saturday
return subDays(payday, 1);
} else if (dayOfWeek === 0) {
// Sunday
return subDays(payday, 2);
}
return payday;
}
/**
* Calculate the next payday based on the current date.
* @param payday The day of the month for payday.
* @param currentDate The current date to compare with.
* @param adjust Number of months to adjust the payday.
* @returns The next payday date.
*/
function calculateNextPayday(
payday = 22,
currentDate = new Date(),
adjust = 0
): Date {
let theoreticalPayday = set(currentDate, { date: payday });
let adjustedPayday = adjustPayday(theoreticalPayday);
if (adjustedPayday < currentDate) {
theoreticalPayday = set(addDays(currentDate, adjust * 30), {
date: payday,
});
adjustedPayday = adjustPayday(theoreticalPayday);
}
return adjustedPayday;
}

122
src/commands/plant.ts Normal file
View File

@@ -0,0 +1,122 @@
import {
EmbedBuilder,
SlashCommandBuilder,
ChatInputCommandInteraction,
} from 'discord.js';
import axios from 'axios';
import config from '../config';
const API_URL = `https://my-api.plantnet.org/v2/identify/all?api-key=${config.plantnetApiKey}`;
const LEAF_THUMBNAIL =
'https://cdn.discordapp.com/attachments/870024275556446328/1006249009201033287/monstera.png';
// Initialise the command data.
export const data = new SlashCommandBuilder()
.setName('plant')
.setDescription('Identify a plant by uploading an image.')
.addAttachmentOption((option) =>
option
.setName('image')
.setDescription('The image of the plant to identify')
.setRequired(true)
);
/**
* Execute the plant identification command.
* @param interaction The interaction that triggered the command.
* @returns A promise that resolves when the command is finished executing.
*/
export async function execute(
interaction: ChatInputCommandInteraction
): Promise<void> {
await interaction.deferReply();
const image = interaction.options.getAttachment('image');
try {
const embed = new EmbedBuilder()
.setTitle('Plant Detector™')
.setThumbnail(LEAF_THUMBNAIL)
.setColor(0xff0000);
// API call to identify the plant
const response = await axios.get(
API_URL.replace('{apiKey}', process.env.PLANTNET_API_KEY as string),
{
params: {
images: image?.url,
organs: 'leaf',
},
timeout: 5000,
}
);
const bestMatch = response.data.results[0];
// Check if there is a valid result
if (!bestMatch) {
await interaction.editReply(
'No plant could be identified from the image.'
);
return;
}
const commonName =
bestMatch.species.commonNames[0] || 'No common name found';
const scientificName = bestMatch.species.scientificNameWithoutAuthor;
const confidence = parseFloat(bestMatch.score);
const plantDetails = generatePlantDetail(
commonName,
scientificName,
confidence
);
embed
.setColor(0x00ff00)
.addFields({ name: 'Plant Details', value: plantDetails });
// Send the result to the user
await interaction.editReply({ embeds: [embed] });
} catch (error: any) {
console.error(error); // Log the error for debugging
await interaction.editReply(
'An error occurred when querying the PlantNet API.'
);
}
}
/**
* Generate plant details based on common name, scientific name, and confidence level.
* @param commonName The common name of the plant.
* @param scientificName The scientific name of the plant.
* @param confidence The confidence score of the identification.
* @returns A formatted string with the plant details.
*/
function generatePlantDetail(
commonName: string,
scientificName: string,
confidence: number
): string {
let detail = '';
if (0.0 <= confidence && confidence < 0.25) {
detail = `I'm really not sure, but I think that looks like a **${commonName}**?`;
} else if (0.25 <= confidence && confidence < 0.5) {
detail = `I think that looks like a **${commonName}**!`;
} else if (0.5 <= confidence && confidence < 0.75) {
detail = `That looks like a **${commonName}**!`;
} else if (confidence >= 0.75) {
detail = `That is a really cool **${commonName}**!`;
}
const imageUrl = `https://www.google.com/search?q=${encodeURIComponent(
commonName || scientificName
)}&tbm=isch`;
detail += `\n\n**Scientific Name**: ${scientificName}`;
detail += `\n\n**Images**: [More images](${imageUrl})`;
detail += `\n\n**Confidence**: ${(confidence * 100).toFixed(3)}%`;
return detail;
}

22
src/commands/reminder.ts Normal file
View File

@@ -0,0 +1,22 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
const REMINDER_IMAGE =
'https://media.discordapp.net/attachments/506852356898422797/715690132883111996/Capture.PNG';
// Initialise the command data.
export const data = new SlashCommandBuilder()
.setName('reminder')
.setDescription('Returns an image which must always be remembered.');
console.log(`Loaded ${data.name} command.`);
/**
* Replies with an image that must always be remembered.
* @param interaction The interaction that triggered the command.
* @returns A promise that resolves when the command is finished executing.
*/
export async function execute(
interaction: ChatInputCommandInteraction
): Promise<void> {
await interaction.reply(REMINDER_IMAGE);
}

View File

@@ -0,0 +1,22 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { format } from 'date-fns';
// Initialise the command data.
export const data = new SlashCommandBuilder()
.setName('servertime')
.setDescription("Returns the server's current time.");
console.log(`Loaded ${data.name} command.`);
/**
* Replies with the server's current time.
* @param interaction The interaction that triggered the command.
* @returns A promise that resolves when the command is finished executing.
*/
export async function execute(
interaction: ChatInputCommandInteraction
): Promise<void> {
const now = new Date();
const formattedTime = format(now, 'dd/MM/yyyy HH:mm:ss');
await interaction.reply(`The server's current time is: ${formattedTime}`);
}

23
src/commands/taylor.ts Normal file
View File

@@ -0,0 +1,23 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import taylorQuotes from './utils/constants/taylorQuotes.json';
// Initialise the command data.
export const data = new SlashCommandBuilder()
.setName('taylor')
.setDescription('Returns a Taylor Swift quote.');
console.log(`Loaded ${data.name} command.`);
/**
* Returns a random Taylor Swift quote.
* @param interaction The interaction that triggered the command.
* @returns A promise that resolves when the command is finished executing.
*/
export async function execute(
interaction: ChatInputCommandInteraction
): Promise<void> {
const randomIndex = Math.floor(Math.random() * taylorQuotes.length);
const response = taylorQuotes[randomIndex];
await interaction.reply(response);
}

View File

@@ -0,0 +1,22 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
const TWENTY_TWENTY_IMAGE =
'https://cdn.discordapp.com/attachments/506852356898422797/765256712063025172/unknown.png';
// Initialize the command data
export const data = new SlashCommandBuilder()
.setName('twentytwenty')
.setDescription('Returns a tweet from Boris, posted on 2 January 2020.');
console.log(`Loaded ${data.name} command.`);
/**
* Sends an image of a tweet from Boris, posted on 2 January 2020.
* @param interaction The interaction that triggered the command.
* @returns A promise that resolves when the command is finished executing.
*/
export async function execute(
interaction: ChatInputCommandInteraction
): Promise<void> {
await interaction.reply(TWENTY_TWENTY_IMAGE);
}

View File

@@ -1,24 +0,0 @@
import { SlashCommandBuilder } from 'discord.js';
const data = new SlashCommandBuilder()
.setName('countdown')
.setDescription('Start a five second countdown.');
async function execute(interaction) {
// Initial response to acknowledge the command
await interaction.reply({ content: 'Starting countdown...' });
// Initial delay before starting the countdown
await new Promise((resolve) => setTimeout(resolve, 2000)); // 2-second delay
// Edit the response with the countdown
for (let i = 5; i > 0; i--) {
await interaction.editReply({ content: String(i) });
await new Promise((resolve) => setTimeout(resolve, 1000)); // 1-second delay between numbers
}
// Final message
await interaction.editReply({ content: '🎉 GO! 🎉' });
}
export default { data, execute };

View File

@@ -1,14 +0,0 @@
import { SlashCommandBuilder } from 'discord.js';
import { format } from 'date-fns';
const data = new SlashCommandBuilder()
.setName('servertime')
.setDescription("Returns the server's current time.");
async function execute(interaction) {
const now = new Date();
const formattedTime = format(now, 'dd/MM/yyyy HH:mm:ss');
await interaction.reply(`The server's current time is: ${formattedTime}`);
}
export default { data, execute };

View File

@@ -0,0 +1,124 @@
[
"2024.",
"All you have to be is yourself.",
"Believe in your flyness... conquer your shyness.",
"Burn that Excel spreadsheet.",
"Decentralize.",
"Distraction is the enemy of vision.",
"Everything you do in life stems from either fear or love.",
"For me giving up is way harder than trying.",
"For me, money is not my definition of success. Inspiring people is a definition of success.",
"Fur pillows are hard to actually sleep on.",
"George Bush doesn't care about black people.",
"Have you ever thought you were in love with someone but then realized you were just staring in a mirror for 20 minutes?",
"I care. I care about everything. Sometimes not giving a f#%k is caring the most.",
"I feel calm but energized.",
"I feel like I'm too busy writing history to read it.",
"I feel like me and Taylor might still have sex.",
"I give up drinking every week.",
"I leave my emojis Bart Simpson color.",
"I love sleep; it's my favorite.",
"I make awesome decisions in bike stores!!!",
"I really love my Tesla. I'm in the future. Thank you Elon.",
"I still think I am the greatest.",
"I think I do myself a disservice by comparing myself to Steve Jobs and Walt Disney and human beings that we've seen before. It should be more like Willy Wonka...and welcome to my chocolate factory.",
"I want the world to be better! All I want is positive! All I want is dopeness!",
"I wish I had a friend like me.",
"I'd like to meet with Tim Cook. I got some ideas.",
"I'll say things that are serious and put them in a joke form so people can enjoy them. We laugh to keep from crying.",
"I'm a creative genius.",
"I'm nice at ping pong.",
"I'm the best.",
"If I don't scream, if I don't say something then no one's going to say anything.",
"If I got any cooler I would freeze to death.",
"If you have the opportunity to play this game of life you need to appreciate every moment. a lot of people don't appreciate the moment until it's passed.",
"Just stop lying about shit. Just stop lying.",
"Keep squares out yo circle.",
"Keep your nose out the sky, keep your heart to god, and keep your face to the rising sun.",
"Let's be like water.",
"Man... whatever happened to my antique fish tank?",
"My dad got me a drone for Christmas.",
"My greatest award is what I'm about to do.",
"My greatest pain in life is that I will never be able to see myself perform live.",
"One day I'm gon' marry a porn star.",
"One of my favorite of many things about what the Trump hat represents to me is that people can't tell me what to do because I'm black.",
"Only free thinkers.",
"People always say that you can't please everybody. I think that's a cop-out. Why not attempt it? Cause think of all the people that you will please if you try.",
"People always tell you 'Be humble. Be humble.' When was the last time someone told you to be amazing? Be great! Be awesome! Be awesome!",
"People only get jealous when they care.",
"Perhaps I should have been more like water today.",
"Pulling up in the may bike.",
"Shut the fuck up I will fucking laser you with alien fucking eyes and explode your fucking head.",
"Sometimes I push the door close button on people running towards the elevator. I just need my own elevator sometimes. My sanctuary.",
"Sometimes you have to get rid of everything.",
"Style is genderless.",
"The thought police want to suppress freedom of thought.",
"The world is our family.",
"The world is our office.",
"Today is the best day ever and tomorrow's going to be even better.",
"Truth is my goal. Controversy is my gym. I'll do a hundred reps of controversy for a 6 pack of truth.",
"Tweeting is legal and also therapeutic.",
"We all self-conscious. I'm just the first to admit it.",
"We came into a broken world. And we're the cleanup crew.",
"You can't look at a glass half full or empty if it's overflowing.",
"I hate when I'm on a flight and I wake up with a water bottle next to me like oh great now I gotta be responsible for this water bottle.",
"All the musicians will be free.",
"Artists are founders.",
"Buy property.",
"Culture is the most powerful force in humanity under God.",
"Empathy is the glue.",
"I am one of the most famous people on the planet.",
"I am running for President of the United States.",
"I am the head of Adidas. I will bring Adidas and Puma back together and bring me and jay back together.",
"I am Warhol. I am the No. 1 most impactful artist of our generation. I am Shakespeare in the flesh.",
"I channel Will Ferrell when I'm at the daddy daughter dances.",
"I don't wanna see no woke tweets or hear no woke raps ... it's show time ... it's a whole different energy right now.",
"I hear people say this person is cool and this person is not cool. People are cool. Man has never invented anything as awesome as a an actual person but sometimes we value the objects we create over life itself.",
"I honestly need all my Royeres to be museum quality... if I see a fake Royere Ima have to Rick James your couch.",
"I love UZI. I be saying the same thing about Steve Jobs. I be feeling just like UZI.",
"I need an army of angels to cover me while I pull this sword out of the stone.",
"I spoke to Dave Chapelle for two hours this morning. He is our modern day Socrates.",
"I was just speaking with someone that told me their life story and they used to be homeless.",
"I watch Bladerunner on repeat.",
"I'm giving all Good music artists back the 50% share I have of their masters.",
"I'm going to personally see to it that Taylor Swift gets her masters back. Scooter is a close family friend.",
"I'm the new Moses.",
"Life is the ultimate gift.",
"Ma$e is one of my favorite rappers and I based a lot of my flows off of him.",
"Manga all day.",
"My first pillar when I'm on the board of adidas will be an adidas Nike collaboration to support community growth.",
"My mama was a' English teacher. I know how to use correct English but sometimes I just don't feel like it aaaand I ain't got to.",
"My memories are from the future.",
"My mother in law Kris Jenner ... makes the best music playlist.",
"People say it's enough and I got my point across ... the point isn't across until we cross the point.",
"People tried to talk me out of running for President. Never let weak controlling people kill your spirit.",
"So many of us need so much less than we have especially when so many of us are in need.",
"Speak God's truth to power.",
"The media tries to kill our heroes one at a time.",
"The world needs more Joy... this idea is super fresh.",
"There are 5 main pillars in a professional musicians business - Recording, Publishing, Touring, Merchandise & Name and likeness.",
"There are people sleeping in parking lots.",
"There's a crying need for civility across the board. We need to and will come together in the name of Jesus.",
"There's so many lonely emojis man.",
"Trust me ... I won't stop.",
"Two years ago we had 50 million people subscribed to music streaming services around the world. Today we have 400 million.",
"We are here to complete the revolution. We are building the future.",
"We as a people will heal. We will insure the well being of each other.",
"We have to evolve.",
"We must and will cure homelessness and hunger. We have the capability as a species.",
"We must form a union. We must unify.",
"We used to diss Michael Jackson the media made us call him crazy ... then they killed him.",
"We will be recognized.",
"We will change the paradigm.",
"We will cure hunger.",
"We will heal. We will cure.",
"We're going to move the entire music industry into the 21st Century.",
"We've gotten comfortable with not having what we deserve.",
"Who made up the term major label in the first place???",
"Winning is the only option.",
"For me to say I wasn't a genius I'd just be lying to you and to myself.",
"I've known my mom since I was zero years old. She is quite dope.",
"I don't expect to be understood at all.",
"I'm on the pursuit of awesomeness, excellence is the bare minimum.",
"You basically can say anything to someone on an email or text as long as you put LOL at the end."
]

View File

@@ -0,0 +1,22 @@
[
"As I see it, yes.",
"Ask again later.",
"Better not tell you now.",
"Cannot predict now.",
"Concentrate and ask again.",
"Don't count on it.",
"It is certain.",
"It is decidedly so.",
"Most likely.",
"My reply is no.",
"My sources say no.",
"Outlook not so good.",
"Outlook good.",
"Reply hazy, try again.",
"Signs point to yes.",
"Very doubtful.",
"Without a doubt.",
"Yes.",
"Yes - definitely.",
"You may rely on it."
]

View File

@@ -1,142 +1,140 @@
import { SlashCommandBuilder } from 'discord.js'; [
const TAYLOR_QUOTES = [
"I don't know what I want, so don't ask me, 'cause I'm still trying to figure it out.", "I don't know what I want, so don't ask me, 'cause I'm still trying to figure it out.",
'When you think Tim McGraw, I hope you think of me.', "When you think Tim McGraw, I hope you think of me.",
'So watch me strike a match on all my wasted time.', "So watch me strike a match on all my wasted time.",
"I'll be strong, I'll be wrong, oh but life goes on…", "I'll be strong, I'll be wrong, oh but life goes on…",
'And when you take, you take the very best of me.', "And when you take, you take the very best of me.",
"But no one notices until it's too late to do anything.", "But no one notices until it's too late to do anything.",
"Our song is the slamming screen door, sneakin' out late, tapping on your window.", "Our song is the slamming screen door, sneakin' out late, tapping on your window.",
"And I don't know why, but with you I'd dance in a storm in my best dress, fearless.", "And I don't know why, but with you I'd dance in a storm in my best dress, fearless.",
"But in your life, you'll do things greater than dating the boy on the football team…But I didn't know it at fifteen.", "But in your life, you'll do things greater than dating the boy on the football team…But I didn't know it at fifteen.",
"I've found time can heal most anything and you just might find who you're supposed to be.", "I've found time can heal most anything and you just might find who you're supposed to be.",
"Romeo, save me. They're trying to tell me how to feel. This love is difficult but it's real.", "Romeo, save me. They're trying to tell me how to feel. This love is difficult but it's real.",
'Why are people always leaving? I think you and I should stay the same.', "Why are people always leaving? I think you and I should stay the same.",
"My mistake, I didn't know to be in love you had to fight to have the upper hand.", "My mistake, I didn't know to be in love you had to fight to have the upper hand.",
'This is a big world, that was a small town there in my rear view mirror disappearing now.', "This is a big world, that was a small town there in my rear view mirror disappearing now.",
"You've got a smile that could light up this whole town.", "You've got a smile that could light up this whole town.",
"And we know it's never simple, never easy. Never a clean break, no one here to save me.", "And we know it's never simple, never easy. Never a clean break, no one here to save me.",
'You took a swing, I took it hard. And down here from the ground I see who you are.', "You took a swing, I took it hard. And down here from the ground I see who you are.",
"All this time I was wasting, hoping you would come around… I've been giving out chances every time and all you do is let me down.", "All this time I was wasting, hoping you would come around… I've been giving out chances every time and all you do is let me down.",
"And then you feel so low you can't feel nothing at all.", "And then you feel so low you can't feel nothing at all.",
"It rains when you're here and it rains when you're gone.", "It rains when you're here and it rains when you're gone.",
'These walls that they put up to hold us back will fall down', "These walls that they put up to hold us back will fall down…",
"My mind forgets to remind me you're a bad idea.", "My mind forgets to remind me you're a bad idea.",
"It turns out freedom ain't nothing but missing you.", "It turns out freedom ain't nothing but missing you.",
"She floats down the aisle like a pageant queen, but I know you wish it was me… don't you?", "She floats down the aisle like a pageant queen, but I know you wish it was me… don't you?",
'I lived in your chess game, but you changed the rules every day.', "I lived in your chess game, but you changed the rules every day.",
"I'm shining like fireworks over your sad, empty town.", "I'm shining like fireworks over your sad, empty town.",
"Someday I'll be living in a big, old city and all you're ever gonna be is mean.", "Someday I'll be living in a big, old city and all you're ever gonna be is mean.",
"I'd tell you I miss you, but I don't know how, I've never heard silence quite this loud.", "I'd tell you I miss you, but I don't know how, I've never heard silence quite this loud.",
'This is looking like a contest of who can act like they care less. But I liked it better when you were on my side.', "This is looking like a contest of who can act like they care less. But I liked it better when you were on my side.",
"And don't lose the way that you dance around in your pj's getting ready for school.", "And don't lose the way that you dance around in your pj's getting ready for school.",
"This night is sparkling, don't you let it go. I'm wonderstruck, blushing all the way home.", "This night is sparkling, don't you let it go. I'm wonderstruck, blushing all the way home.",
'2AM, who do you love?', "2AM, who do you love?",
"Your string of lights is still bright to me… Who you are is not where you've been.", "Your string of lights is still bright to me… Who you are is not where you've been.",
'Today is never too late to be brand new.', "Today is never too late to be brand new.",
"You and I walk a fragile line; I have known it all this time. But I never thought I'd live to see it break.", "You and I walk a fragile line; I have known it all this time. But I never thought I'd live to see it break.",
"I don't know how to be something you miss.", "I don't know how to be something you miss.",
'Bring on all the pretenders. One day, we will be remembered.', "Bring on all the pretenders. One day, we will be remembered.",
"So don't you worry your pretty, little mind, people throw rocks at things that shine.", "So don't you worry your pretty, little mind, people throw rocks at things that shine.",
"We are alone with our changing minds. We fall in love 'til it hurts or bleeds or fades in time.", "We are alone with our changing minds. We fall in love 'til it hurts or bleeds or fades in time.",
'Love is a ruthless game unless you play it good and right.', "Love is a ruthless game unless you play it good and right.",
"He's long gone when he's next to me and I realize the blame is on me.", "He's long gone when he's next to me and I realize the blame is on me.",
"No apologies. He'll never see you cry. Pretends he doesn't know that he's the reason why.", "No apologies. He'll never see you cry. Pretends he doesn't know that he's the reason why.",
'The saddest fear comes creeping in - that you never loved me or her, or anyone, or anything...', "The saddest fear comes creeping in - that you never loved me or her, or anyone, or anything...",
"…That magic's not here no more. And I might be OK, but I'm not fine at all.", "…That magic's not here no more. And I might be OK, but I'm not fine at all.",
"And your mother's telling stories about you on a tee ball team. You taught me 'bout your past, thinking your future was me.", "And your mother's telling stories about you on a tee ball team. You taught me 'bout your past, thinking your future was me.",
'I forget about you long enough to forget why I needed to...', "I forget about you long enough to forget why I needed to...",
"Maybe we got lost in translation, maybe I asked for too much. But maybe this thing was a masterpiece 'til you tore it all up. Running scared, I was there, I remember it all too well.", "Maybe we got lost in translation, maybe I asked for too much. But maybe this thing was a masterpiece 'til you tore it all up. Running scared, I was there, I remember it all too well.",
'You call me up again just to break me like a promise, so casually cruel in the name of being honest.', "You call me up again just to break me like a promise, so casually cruel in the name of being honest.",
"Time won't fly, it's like I'm paralyzed by it. I'd like to be my old self again, but I'm still trying to find it.", "Time won't fly, it's like I'm paralyzed by it. I'd like to be my old self again, but I'm still trying to find it.",
"Cause there we are again, when I loved you so, back before you lost the one, real thing you've ever known.", "Cause there we are again, when I loved you so, back before you lost the one, real thing you've ever known.",
"Now you mail back my things and I walk home alone, but you keep my old scarf from that very first week, 'cause it reminds you of innocence and it smells like me.", "Now you mail back my things and I walk home alone, but you keep my old scarf from that very first week, 'cause it reminds you of innocence and it smells like me.",
"Stay, and I'll be loving you for quite some time. No one else is gonna love me when I get mad.", "Stay, and I'll be loving you for quite some time. No one else is gonna love me when I get mad.",
"We're happy, free, confused, and lonely at the same time. It's miserable and magical.", "We're happy, free, confused, and lonely at the same time. It's miserable and magical.",
"I wish I could run to you. And I hope you know that every time I don't I almost do.", "I wish I could run to you. And I hope you know that every time I don't I almost do.",
'You wear your best apology, but I was there to watch you leave.', "You wear your best apology, but I was there to watch you leave.",
'But sometimes I wonder how you think about it now.', "But sometimes I wonder how you think about it now.",
"But I don't wanna dance if I'm not dancing with you.", "But I don't wanna dance if I'm not dancing with you.",
"Words, how little they mean when you're a little too late.", "Words, how little they mean when you're a little too late.",
"And they tell you that you're lucky, but you're so confused, 'cause you don't feel pretty, you just feel used.", "And they tell you that you're lucky, but you're so confused, 'cause you don't feel pretty, you just feel used.",
"I've been spending the last eight months thinking all love ever does is break and burn and end...", "I've been spending the last eight months thinking all love ever does is break and burn and end...",
"And what do you do when the one who means the most to you is the one who didn't show?", "And what do you do when the one who means the most to you is the one who didn't show?",
"Your close friends always seem to know when there's something really wrong", "Your close friends always seem to know when there's something really wrong",
'You called me later and said, "I\'m sorry I didn\'t make it". And I said, "I\'m sorry, too".', "You called me later and said, \"I'm sorry I didn't make it\". And I said, \"I'm sorry, too\".",
'Loving him is like driving a new Maserati down a dead-end street - faster than the wind, passionate as sin, ending so suddenly.', "Loving him is like driving a new Maserati down a dead-end street - faster than the wind, passionate as sin, ending so suddenly.",
'The lights are so bright, but they never blind me.', "The lights are so bright, but they never blind me.",
"Love's a game, wanna play?", "Love's a game, wanna play?",
"So it's gonna be forever or it's gonna go down in flames.", "So it's gonna be forever or it's gonna go down in flames.",
"But you'll come back each time you leave 'cause darling, I'm a nightmare dressed like a daydream.", "But you'll come back each time you leave 'cause darling, I'm a nightmare dressed like a daydream.",
"When we go crashing down, we come back every time 'cause we never go out of style.", "When we go crashing down, we come back every time 'cause we never go out of style.",
'I got that red lip, classic thing that you like.', "I got that red lip, classic thing that you like.",
'The rest of the world was black and white, but we were in screaming color.', "The rest of the world was black and white, but we were in screaming color.",
'The more I think about it now, the less I know, all I know is that you drove us off the road.', "The more I think about it now, the less I know, all I know is that you drove us off the road.",
'People like you always want back the love they pushed aside, but people like me are gone forever when you say goodbye.', "People like you always want back the love they pushed aside, but people like me are gone forever when you say goodbye.",
"Why'd you have to go and lock me out when I let you in?", "Why'd you have to go and lock me out when I let you in?",
"While you've been getting down and out about the liars and the dirty, dirty cheats of the world… You could've been getting down to this sick beat.", "While you've been getting down and out about the liars and the dirty, dirty cheats of the world… You could've been getting down to this sick beat.",
"We're a crooked love in a straight line down.", "We're a crooked love in a straight line down.",
'And I wish you knew that I miss you too much to be mad anymore.', "And I wish you knew that I miss you too much to be mad anymore.",
'You give me everything and nothing.', "You give me everything and nothing.",
'Makes you wanna run and hide, but it made us turn right back around.', "Makes you wanna run and hide, but it made us turn right back around.",
"Band-aids don't fix bullet holes. You say sorry just for show.", "Band-aids don't fix bullet holes. You say sorry just for show.",
'Someday when you leave me, I bet these memories follow you around.', "Someday when you leave me, I bet these memories follow you around.",
"When you're young, you just run, but you come back to what you need.", "When you're young, you just run, but you come back to what you need.",
'This love left a permanent mark.', "This love left a permanent mark.",
'Your kiss, my cheek; I watch you leave. Your smile, my ghost; I fall to my knees.', "Your kiss, my cheek; I watch you leave. Your smile, my ghost; I fall to my knees.",
"It was months and months of back and forth, you're still all over me like a wine-stained dress I can't wear anymore.", "It was months and months of back and forth, you're still all over me like a wine-stained dress I can't wear anymore.",
"When I was drowning that's when I could finally breathe.", "When I was drowning that's when I could finally breathe.",
"Just because you're clean, don't mean you don't miss it.", "Just because you're clean, don't mean you don't miss it.",
"Didn't it all seem new and exciting? …It's all fun and games 'til somebody loses their mind.", "Didn't it all seem new and exciting? …It's all fun and games 'til somebody loses their mind.",
'You search the world for something else to make you feel like what we had. And in the end in wonderland, we both went mad.', "You search the world for something else to make you feel like what we had. And in the end in wonderland, we both went mad.",
'Heartbreak is the national anthem, we sing it proudly.', "Heartbreak is the national anthem, we sing it proudly.",
"They'll take their shots, but we are bulletproof.", "They'll take their shots, but we are bulletproof.",
'So I punched a hole in the roof; let the flood carry away all my pictures of you.', "So I punched a hole in the roof; let the flood carry away all my pictures of you.",
'When all you wanted was to be wanted; wish you could go back and tell yourself what you know now.', "When all you wanted was to be wanted; wish you could go back and tell yourself what you know now.",
"32 and still growing up now. Who you are is not what you did. You're still an innocent.", "32 and still growing up now. Who you are is not what you did. You're still an innocent.",
"We play dumb but we know exactly what we're doing.", "We play dumb but we know exactly what we're doing.",
"Please don't ever become a stranger whose laugh I could recognize anywhere.", "Please don't ever become a stranger whose laugh I could recognize anywhere.",
'Can we always be this close forever and ever?', "Can we always be this close forever and ever?",
"I'm only seventeen. I don't know anything but I know I miss you.", "I'm only seventeen. I don't know anything but I know I miss you.",
'I was walking home on broken cobblestones just thinking of you, when she pulled up like a figment of my worst intentions.', "I was walking home on broken cobblestones just thinking of you, when she pulled up like a figment of my worst intentions.",
'You play stupid games, you win stupid prizes.', "You play stupid games, you win stupid prizes.",
'I had a marvelous time ruining everything.', "I had a marvelous time ruining everything.",
"Untouchable, burning brighter than the sun, and when you're close I feel like coming undone.", "Untouchable, burning brighter than the sun, and when you're close I feel like coming undone.",
'I could build a castle out of all the bricks they threw at me.', "I could build a castle out of all the bricks they threw at me.",
'Cold was the steel of my axe to grind for the boys who broke my heart. Now I send their babies presents.', "Cold was the steel of my axe to grind for the boys who broke my heart. Now I send their babies presents.",
'Back when you fit in my poems like a perfect rhyme.', "Back when you fit in my poems like a perfect rhyme.",
"I once believed love would be burning red... but it's golden.", "I once believed love would be burning red... but it's golden.",
'The monsters turned out to be just trees, when the sun came up you were looking at me.', "The monsters turned out to be just trees, when the sun came up you were looking at me.",
"I can't decide if it's a choice: getting swept away?", "I can't decide if it's a choice: getting swept away?",
'They told me all of my cages were mental, so I got wasted like all my potential.', "They told me all of my cages were mental, so I got wasted like all my potential.",
"I don't like that falling feels like flying till the bone crush.", "I don't like that falling feels like flying till the bone crush.",
'I persist and resist the temptation to ask you: "If one thing had been different, would everything be different today?"', "I persist and resist the temptation to ask you: \"If one thing had been different, would everything be different today?\"",
'When you are young they assume you know nothing.', "When you are young they assume you know nothing.",
"You drew stars around my scars, but now I'm bleeding.", "You drew stars around my scars, but now I'm bleeding.",
'You wear the same jewels that I gave you, as you bury me.', "You wear the same jewels that I gave you, as you bury me.",
"We gather stones, never knowing what they'll mean - some to throw, some to make a diamond ring.", "We gather stones, never knowing what they'll mean - some to throw, some to make a diamond ring.",
'Bold was the waitress on our three-year trip... Getting lunch down by the Lakes, she said I looked like an American singer.', "Bold was the waitress on our three-year trip... Getting lunch down by the Lakes, she said I looked like an American singer.",
'One single thread of gold tied me to you.', "One single thread of gold tied me to you.",
"I swear I don't love the drama, it loves me.", "I swear I don't love the drama, it loves me.",
"I bury hatchets, but I keep maps of where I put 'em.", "I bury hatchets, but I keep maps of where I put 'em.",
"Love made me crazy, if it doesn't, you ain't doin' it right.", "Love made me crazy, if it doesn't, you ain't doin' it right.",
"My name is whatever you decide, and I'm just gonna call you mine.", "My name is whatever you decide, and I'm just gonna call you mine.",
"Handsome, you're a mansion with a view.", "Handsome, you're a mansion with a view.",
'Sometimes I wonder when you sleep, are you ever dreaming of me?', "Sometimes I wonder when you sleep, are you ever dreaming of me?",
'The world goes on another day, another drama. But not for me, all I think about is karma.', "The world goes on another day, another drama. But not for me, all I think about is karma.",
'You asked me for a place to sleep, locked me out and threw a feast.', "You asked me for a place to sleep, locked me out and threw a feast.",
"You know I'm not a bad girl, but I do bad things with you.", "You know I'm not a bad girl, but I do bad things with you.",
"You did a number on me but, honestly, baby, who's counting?", "You did a number on me but, honestly, baby, who's counting?",
'Ocean blue eyes looking in mine. I feel like I might sink and drown and die.', "Ocean blue eyes looking in mine. I feel like I might sink and drown and die.",
"Your love is a secret I'm hoping, dreaming, dying to keep.", "Your love is a secret I'm hoping, dreaming, dying to keep.",
'Is this the end of all the endings? My broken bones are mending.', "Is this the end of all the endings? My broken bones are mending.",
'I loved you in spite of deep fears that the world would divide us.', "I loved you in spite of deep fears that the world would divide us.",
"Say that we got it! I'm a mess, but I'm the mess that you wanted!", "Say that we got it! I'm a mess, but I'm the mess that you wanted!",
'I brought a knife to a gunfight.', "I brought a knife to a gunfight.",
'He built a fire just to keep me warm.', "He built a fire just to keep me warm.",
'I want to wear his initial on a chain round my neck, not because he owns me, but cause he really knows me, which is more than they can say.', "I want to wear his initial on a chain round my neck, not because he owns me, but cause he really knows me, which is more than they can say.",
'Holding my breath, slowly, I said "You don\'t need to save me, but would you run away with me?"', "Holding my breath, slowly, I said \"You don't need to save me, but would you run away with me?\"",
"Would've been right there, front row even if nobody came to your show.", "Would've been right there, front row even if nobody came to your show.",
"I'm always waiting for you to be waiting below.", "I'm always waiting for you to be waiting below.",
"I don't wanna keep secrets just to keep you.", "I don't wanna keep secrets just to keep you.",
@@ -147,38 +145,25 @@ const TAYLOR_QUOTES = [
"I'll never let you go 'cause I know this is a fight that someday we're gonna win.", "I'll never let you go 'cause I know this is a fight that someday we're gonna win.",
"I'm with you even if it makes me blue.", "I'm with you even if it makes me blue.",
"Without all the exes, fights, and flaws, we wouldn't be standing here so tall.", "Without all the exes, fights, and flaws, we wouldn't be standing here so tall.",
'We were a fresh page on the desk, filling in the blanks as we go.', "We were a fresh page on the desk, filling in the blanks as we go.",
'We were in the backseat drunk on something stronger than the drinks in the bar.', "We were in the backseat drunk on something stronger than the drinks in the bar.",
'If the story is over, why am I still writing pages?', "If the story is over, why am I still writing pages?",
"I ask the traffic lights if it will be alright, they say I don't know.", "I ask the traffic lights if it will be alright, they say I don't know.",
"They say home is where the heart is, but that's not where mine lives.", "They say home is where the heart is, but that's not where mine lives.",
'I pinned your hands behind your back. Thought I had reason to attack, but no.', "I pinned your hands behind your back. Thought I had reason to attack, but no.",
"Fighting with a true love is boxing with no gloves... Chemistry 'til it blows up, 'til there's no us.", "Fighting with a true love is boxing with no gloves... Chemistry 'til it blows up, 'til there's no us.",
"And I can't talk to you when you're like this, staring out the window like I'm not your favorite town.", "And I can't talk to you when you're like this, staring out the window like I'm not your favorite town.",
'They say the road gets hard and you get lost.', "They say the road gets hard and you get lost.",
"Remember how I said I'd die for you?", "Remember how I said I'd die for you?",
"I come back stronger than a '90s trend.", "I come back stronger than a '90s trend.",
'"This dorm was once a madhouse." I made a joke: "Well, it\'s made for me."', "\"This dorm was once a madhouse.\" I made a joke: \"Well, it's made for me.\"",
"Sometimes you just don't know the answer 'til someone's on their knees and asks you.", "Sometimes you just don't know the answer 'til someone's on their knees and asks you.",
"I can't dare to dream about you anymore.", "I can't dare to dream about you anymore.",
'I parked my car right between the Methodist and the school that used to be ours.', "I parked my car right between the Methodist and the school that used to be ours.",
"I'll go back to L.A. and the so-called friends who'll write books about me if I ever make it and wonder about the only soul who can tell which smiles I'm fakin'.", "I'll go back to L.A. and the so-called friends who'll write books about me if I ever make it and wonder about the only soul who can tell which smiles I'm fakin'.",
'What would you do if I break free and leave us in ruins, took this dagger in me and removed it, gain the weight of you then loose it?', "What would you do if I break free and leave us in ruins, took this dagger in me and removed it, gain the weight of you then loose it?",
'Your nemesis will defeat themselves before you get the chance to swing.', "Your nemesis will defeat themselves before you get the chance to swing.",
'My waves meet your shore ever and evermore.', "My waves meet your shore ever and evermore.",
'I replay my footsteps on each stepping stone, trying to find the one where I went wrong.', "I replay my footsteps on each stepping stone, trying to find the one where I went wrong.",
"He can't see the smile I'm faking and my heart's not breaking 'cause I'm not feeling anything at all.", "He can't see the smile I'm faking and my heart's not breaking 'cause I'm not feeling anything at all."
]; ]
const data = new SlashCommandBuilder()
.setName('taylor')
.setDescription('Returns a random Taylor Swift quote.');
async function execute(interaction) {
const randomIndex = Math.floor(Math.random() * TAYLOR_QUOTES.length);
const quote = TAYLOR_QUOTES[randomIndex];
await interaction.reply(quote);
}
export default { data, execute };

View File

@@ -0,0 +1,91 @@
import {
MongoClient,
Db,
Collection,
Document,
UpdateResult,
DeleteResult,
} from 'mongodb';
import config from '../../../config';
let client: MongoClient;
let database: Db;
let collection: Collection<Document>;
async function initializeDbConnection() {
if (!client) {
client = new MongoClient(config.mongodbUri);
await client.connect();
database = client.db('butler_db');
collection = database.collection('GameFeatures');
}
}
/**
* Retrieves a list of game names from the database.
*
* @returns An array of game objects containing game names.
*/
export async function listGameNames(): Promise<any[]> {
await initializeDbConnection();
return collection.find({}, { projection: { game: 1 } }).toArray();
}
/**
* Retrieves a specific game by its name from the database.
*
* @param game - The name of the game to retrieve.
* @returns The game object if found, or null if not found.
*/
export async function getGame(game: string): Promise<any | null> {
await initializeDbConnection();
return collection.findOne({ game });
}
/**
* Sets a key-value pair for a specific game in the database.
* If the game does not exist, it will be created.
*
* @param game - The name of the game to update or create.
* @param key - The key to set or update in the game object.
* @param value - The value to set for the specified key.
* @returns The result of the update operation.
*/
export async function setGame(
game: string,
key: string,
value: any
): Promise<UpdateResult> {
await initializeDbConnection();
return collection.updateOne(
{ game },
{ $set: { game, [key]: value } },
{ upsert: true }
);
}
/**
* Deletes a game from the database by its name.
*
* @param game - The name of the game to delete.
* @returns The result of the delete operation.
*/
export async function deleteGame(game: string): Promise<DeleteResult> {
await initializeDbConnection();
return collection.deleteOne({ game });
}
/**
* Deletes a specific field from a game object in the database.
*
* @param game - The name of the game to update.
* @param key - The field key to remove from the game object.
* @returns The result of the update operation.
*/
export async function deleteField(
game: string,
key: string
): Promise<UpdateResult> {
await initializeDbConnection();
return collection.updateOne({ game }, { $unset: { [key]: '' } });
}

View File

@@ -0,0 +1,81 @@
import axios from 'axios';
import { Readable } from 'stream';
/**
* Enum representing the supported music providers.
*/
export enum MusicProvider {
YouTube = 'youtube',
Spotify = 'spotify',
Local = 'local',
}
/**
* Interface representing a track's metadata.
*/
export interface Track {
provider: MusicProvider;
id: string;
title: string;
artist: string;
imageUrl: string;
}
/**
* Fetches track information based on the provider and query.
*
* @param provider - The music provider (YouTube, Spotify, Local).
* @param query - The search query for the track.
* @returns A promise that resolves to a Track object.
*/
export async function fetchTrack(
provider: MusicProvider,
query: string
): Promise<Track | null> {
try {
const response = await axios.get(
`https://music.3t.network/${provider}/${provider === MusicProvider.Local ? 'search' : 'track'}`,
{
params: { query },
}
);
const data = response.data;
if (data && data.id) {
return {
provider,
id: data.id,
title: data.title,
artist: data.artist || data.channel,
imageUrl: data.imageUrl,
};
}
return null;
} catch (error) {
console.error(`Error fetching track from ${provider}:`, error);
return null;
}
}
/**
* Streams the audio file from the download endpoint based on the provider and track ID.
*
* @param track - The track to stream.
* @returns A Readable stream of the audio.
*/
export function streamAudio(track: Track): Readable {
const downloadUrl =
track.provider === MusicProvider.YouTube
? `https://music.3t.network/youtube/video/${track.id}/download`
: track.provider === MusicProvider.Spotify
? `https://music.3t.network/spotify/track/${track.id}/download`
: `https://music.3t.network/local/${track.id}/download`;
return axios({
method: 'get',
url: downloadUrl,
responseType: 'stream',
}).then((response) => response.data as Readable);
}

36
src/config.ts Normal file
View File

@@ -0,0 +1,36 @@
import dotenv from 'dotenv';
dotenv.config(); // Load variables from .env file
interface Config {
discordApplicationId: string;
discordApiKey: string;
giphyApiKey: string;
plantnetApiKey: string;
omdbApiKey: string;
replicateApiKey: string;
mongodbUri: string;
registerSlashCommands: boolean;
}
const getEnv = (key: string, required = true): string | boolean | undefined => {
const value = process.env[key];
if (!value && required) {
throw new Error(`${key} is not set in the environment variables.`);
}
return value;
};
const config: Config = {
discordApplicationId: getEnv('DISCORD_APPLICATION_ID') as string,
discordApiKey: getEnv('DISCORD_API_KEY') as string,
giphyApiKey: getEnv('GIPHY_API_KEY') as string,
plantnetApiKey: getEnv('PLANTNET_API_KEY') as string,
omdbApiKey: getEnv('OMDB_API_KEY') as string,
replicateApiKey: getEnv('REPLICATE_API_KEY') as string,
mongodbUri: getEnv('MONGODB_URI') as string,
registerSlashCommands:
(getEnv('REGISTER_SLASH_COMMANDS', false) as boolean) || false, // Default to false
};
export default config;

View File

@@ -1,45 +0,0 @@
import { Client, Collection, Events, GatewayIntentBits } from 'discord.js';
import { loadCommandModules } from './utilities/commandModules.js';
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
// Load all command modules and set them in the client.
client.commands = new Collection();
const commandModules = await loadCommandModules();
commandModules.forEach((module) => {
client.commands.set(module.data.name, module);
});
// Register the event listener for command interactions.
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
const command = client.commands.get(interaction.commandName);
if (!command) {
console.warn(`Command ${interaction.commandName} not found.`);
return;
}
try {
await command.execute(interaction);
} catch (error) {
console.error(error);
if (interaction.replied || interaction.deferred) {
await interaction.followUp({
content: 'There was an error while executing this command!',
ephemeral: true,
});
} else {
await interaction.reply({
content: 'There was an error while executing this command!',
ephemeral: true,
});
}
}
});
client.once(Events.ClientReady, (readyClient) => {
console.log(`Ready! Logged in as ${readyClient.user.tag}`);
});
client.login(process.env.DISCORD_TOKEN);

60
src/index.ts Normal file
View File

@@ -0,0 +1,60 @@
import 'dotenv/config';
import {
Client,
Collection,
Events,
GatewayIntentBits,
Interaction,
} from 'discord.js';
import { getCommands, registerSlashCommands } from './utils/commands';
import { Command } from './utils/types';
import config from './config';
// Define an extended version of the Client interface to include commands
interface ExtendedClient extends Client {
commands: Collection<string, Command>;
}
const client: ExtendedClient = new Client({
intents: [GatewayIntentBits.Guilds],
}) as ExtendedClient;
// Add the commands to the client
client.commands = getCommands();
// If REGISTER_SLASH_COMMANDS is set to true, register the commands.
if (config.registerSlashCommands) {
registerSlashCommands(client.commands)
.then(() => {
console.log('Successfully registered slash commands');
})
.catch((error) => {
console.error('Failed to register slash commands:', error);
process.exit(1);
});
}
// Register the event listener for command interactions.
client.on(Events.InteractionCreate, async (interaction: Interaction) => {
if (!interaction.isChatInputCommand()) return;
const command = client.commands.get(interaction.commandName);
if (command) {
try {
await command.execute(interaction);
} catch (error) {
console.error(`Error executing ${interaction.commandName}:`, error);
await interaction.reply({
content: 'There was an error executing that command!',
ephemeral: true,
});
}
}
});
client.once(Events.ClientReady, (readyClient) => {
console.log(`Ready! Logged in as ${readyClient.user.tag}`);
});
client.login(process.env.DISCORD_API_KEY);

View File

@@ -1,25 +0,0 @@
import { REST, Routes } from 'discord.js';
import { loadCommandModules } from './utilities/commandModules.js';
// Register all slash commands, globally across all Guilds.
const commandModules = await loadCommandModules();
const commands = commandModules.map((commandModule) =>
commandModule.data.toJSON()
);
const rest = new REST().setToken(process.env.DISCORD_TOKEN);
try {
console.log('Started refreshing application (/) commands.');
// Refresh all slash commands globally.
const data = await rest.put(
Routes.applicationCommands(process.env.DISCORD_APPLICATION_ID),
{ body: commands }
);
console.log(`Successfully reloaded ${data.length} application (/) commands.`);
} catch (error) {
// And of course, make sure you catch and log any errors!
console.error(error);
}

View File

@@ -0,0 +1,12 @@
import { getCommands, registerSlashCommands } from './utils/commands';
const commands = getCommands();
registerSlashCommands(commands)
.then(() => {
console.log('Successfully registered slash commands');
})
.catch((error) => {
console.error('Failed to register slash commands:', error);
process.exit(1);
});

View File

@@ -1,46 +0,0 @@
import path from 'path';
import { glob } from 'glob';
import { ApplicationCommand } from 'discord.js';
/**
* Get all command modules from the commands directory.
* @returns {Promise<string[]>} The file paths of all command modules.
*/
async function getCommandModulePaths() {
return await glob(path.join(process.cwd(), 'src/commands/**/*.js'));
}
/**
* Load a single command module.
* @param {string} modulePath The path to the command module.
* @returns {Promise<ApplicationCommand>} The loaded command module.
*/
async function loadCommandModule(modulePath) {
try {
const module = await import(path.resolve(modulePath));
const commandModule = module.default;
if (!commandModule.data || !commandModule.execute) {
console.warn(`Invalid command module at ${modulePath}`);
return;
}
console.info(`Loaded command module: ${commandModule.data.name}`);
return commandModule;
} catch (error) {
console.error(`Error loading module at ${modulePath}.`, error);
}
}
/**
* Load all command modules.
* @returns {Promise<ApplicationCommand[]>} The loaded command modules.
*/
export async function loadCommandModules() {
const commandModulePaths = await getCommandModulePaths();
const commandModules = await Promise.all(
commandModulePaths.map(loadCommandModule)
);
return commandModules.filter((module) => module);
}

58
src/utils/commands.ts Normal file
View File

@@ -0,0 +1,58 @@
import { Collection, REST, Routes } from 'discord.js';
import * as birthday from '../commands/birthday';
import * as corrupt from '../commands/corrupt';
import * as countdown from '../commands/countdown';
import * as eyecandy from '../commands/eyecandy';
import * as game from '../commands/game';
import * as image from '../commands/image';
import * as imdb from '../commands/imdb';
import * as kanye from '../commands/kanye';
import * as magicEightBall from '../commands/magic8Ball';
import * as payday from '../commands/payday';
import * as plant from '../commands/plant';
import * as reminder from '../commands/reminder';
import * as servertime from '../commands/servertime';
import * as taylor from '../commands/taylor';
import * as twentyTwenty from '../commands/twentyTwenty';
import config from '../config';
import { Command } from './types';
/**
* Get all commands as a collection.
* @returns A collection of commands.
*/
export function getCommands(): Collection<string, Command> {
const commands = new Collection<string, Command>();
commands.set(birthday.data.name, birthday);
commands.set(corrupt.data.name, corrupt);
commands.set(countdown.data.name, countdown);
commands.set(eyecandy.data.name, eyecandy);
commands.set(game.data.name, game);
commands.set(image.data.name, image);
commands.set(imdb.data.name, imdb);
commands.set(kanye.data.name, kanye);
commands.set(magicEightBall.data.name, magicEightBall);
commands.set(payday.data.name, payday);
commands.set(plant.data.name, plant);
commands.set(reminder.data.name, reminder);
commands.set(servertime.data.name, servertime);
commands.set(taylor.data.name, taylor);
commands.set(twentyTwenty.data.name, twentyTwenty);
return commands;
}
/**
* Register all slash commands globally across all Guilds.
* @param commands A collection of commands to register.
*/
export async function registerSlashCommands(
commands: Collection<string, Command>
) {
const commandData = commands.map((command) => command.data.toJSON());
const rest = new REST({ version: '10' }).setToken(config.discordApiKey);
await rest.put(Routes.applicationCommands(config.discordApplicationId), {
body: commandData,
});
}

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

@@ -0,0 +1,18 @@
import {
SlashCommandBuilder,
SlashCommandSubcommandsOnlyBuilder,
SlashCommandOptionsOnlyBuilder,
ChatInputCommandInteraction,
} from 'discord.js';
// Create a generic type to cover all relevant SlashCommandBuilder types
export type CommandBuilder =
| SlashCommandBuilder
| SlashCommandSubcommandsOnlyBuilder
| SlashCommandOptionsOnlyBuilder;
// Define the Command interface
export interface Command {
data: CommandBuilder; // Use the generic CommandBuilder type
execute: (interaction: ChatInputCommandInteraction) => Promise<void>;
}

15
tsconfig.json Normal file
View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "CommonJS",
"moduleResolution": "node",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}