develop #2
11
.dockerignore
Normal file
11
.dockerignore
Normal 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
8
.env.example
Normal 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=
|
||||||
72
.gitea/workflows/build-and-publish.yml
Normal file
72
.gitea/workflows/build-and-publish.yml
Normal 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
6
.gitignore
vendored
@@ -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
38
Dockerfile
Normal 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"]
|
||||||
70
README.md
70
README.md
@@ -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
5
docker-compose.yml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
services:
|
||||||
|
butlerbot:
|
||||||
|
image: git.3t.network/3t.network/butlerbot:latest
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
@@ -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
2198
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
26
package.json
26
package.json
@@ -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
76
scripts/build.sh
Executable 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."
|
||||||
3
scripts/register-slash-commands.sh
Executable file
3
scripts/register-slash-commands.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
docker compose run --rm butlerbot register-slash-commands
|
||||||
3
scripts/start.sh
Executable file
3
scripts/start.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
docker compose up
|
||||||
@@ -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
24
src/commands/corrupt.ts
Normal 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
31
src/commands/countdown.ts
Normal 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
62
src/commands/eyecandy.ts
Normal 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
224
src/commands/game.ts
Normal 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
113
src/commands/image.ts
Normal 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
107
src/commands/imdb.ts
Normal 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
23
src/commands/kanye.ts
Normal 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);
|
||||||
|
}
|
||||||
@@ -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 };
|
|
||||||
@@ -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 };
|
|
||||||
@@ -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 };
|
|
||||||
31
src/commands/magic8Ball.ts
Normal file
31
src/commands/magic8Ball.ts
Normal 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);
|
||||||
|
}
|
||||||
@@ -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 };
|
|
||||||
@@ -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 };
|
|
||||||
@@ -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 };
|
|
||||||
@@ -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 };
|
|
||||||
@@ -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 };
|
|
||||||
@@ -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
132
src/commands/music.js
Normal 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 };
|
||||||
@@ -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
122
src/commands/plant.ts
Normal 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
22
src/commands/reminder.ts
Normal 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);
|
||||||
|
}
|
||||||
22
src/commands/servertime.ts
Normal file
22
src/commands/servertime.ts
Normal 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
23
src/commands/taylor.ts
Normal 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);
|
||||||
|
}
|
||||||
22
src/commands/twentyTwenty.ts
Normal file
22
src/commands/twentyTwenty.ts
Normal 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);
|
||||||
|
}
|
||||||
@@ -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 };
|
|
||||||
@@ -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 };
|
|
||||||
124
src/commands/utils/constants/kanyeQuotes.json
Normal file
124
src/commands/utils/constants/kanyeQuotes.json
Normal 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."
|
||||||
|
]
|
||||||
22
src/commands/utils/constants/magicEightBallResponses.json
Normal file
22
src/commands/utils/constants/magicEightBallResponses.json
Normal 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."
|
||||||
|
]
|
||||||
@@ -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 };
|
|
||||||
91
src/commands/utils/db/game.ts
Normal file
91
src/commands/utils/db/game.ts
Normal 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]: '' } });
|
||||||
|
}
|
||||||
81
src/commands/utils/music/trackProvider.ts
Normal file
81
src/commands/utils/music/trackProvider.ts
Normal 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
36
src/config.ts
Normal 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;
|
||||||
45
src/index.js
45
src/index.js
@@ -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
60
src/index.ts
Normal 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);
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
12
src/registerSlashCommands.ts
Normal file
12
src/registerSlashCommands.ts
Normal 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);
|
||||||
|
});
|
||||||
@@ -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
58
src/utils/commands.ts
Normal 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
18
src/utils/types.ts
Normal 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
15
tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user