Add features
This commit is contained in:
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
|
||||
6
.env.example
Normal file
6
.env.example
Normal file
@@ -0,0 +1,6 @@
|
||||
DISCORD_APPLICATION_ID=
|
||||
DISCORD_API_KEY=
|
||||
GIPHY_API_KEY=
|
||||
PLANTNET_API_KEY=
|
||||
OMDB_API_KEY=
|
||||
REPLICATE_API_KEY=
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,3 +1,6 @@
|
||||
# ---> Build specific exclusions
|
||||
docker-compose.override.yml
|
||||
|
||||
# ---> Windows
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
@@ -48,6 +51,7 @@ $RECYCLE.BIN/
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
|
||||
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM node:lts-alpine
|
||||
|
||||
# Create app directory and copy the app
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
|
||||
# Run as non-root user
|
||||
RUN addgroup -g 1001 -S nodejs
|
||||
RUN adduser -S nodejs -u 1001
|
||||
RUN chown -R nodejs:nodejs /app
|
||||
USER nodejs
|
||||
|
||||
# Install
|
||||
RUN npm install
|
||||
|
||||
# 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
|
||||
$ npm install
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
1. Create `.env` (see `example.env`) and populate with all necessary details.
|
||||
2. Register slash commands.
|
||||
### Pre-built Docker Image
|
||||
|
||||
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
|
||||
$ 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
|
||||
$ 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
|
||||
|
||||
All commands are dynamically loaded at runtime, and are stored in `src/commands/[CATEGORY]/[NAME].js`.
|
||||
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 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 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...
|
||||
329
package-lock.json
generated
329
package-lock.json
generated
@@ -12,8 +12,11 @@
|
||||
"axios": "^1.7.7",
|
||||
"date-fns": "^3.6.0",
|
||||
"discord.js": "^14.16.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"glob": "^11.0.0",
|
||||
"prettier": "^3.3.3"
|
||||
"mongodb": "^6.8.1",
|
||||
"prettier": "^3.3.3",
|
||||
"replicate": "^0.32.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^9.9.1"
|
||||
@@ -322,6 +325,15 @@
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@mongodb-js/saslprep": {
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz",
|
||||
"integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sparse-bitfield": "^3.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -412,6 +424,21 @@
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/webidl-conversions": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
|
||||
"integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/whatwg-url": {
|
||||
"version": "11.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz",
|
||||
"integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/webidl-conversions": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.5.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz",
|
||||
@@ -431,6 +458,19 @@
|
||||
"npm": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/abort-controller": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"event-target-shim": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.5"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.12.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
||||
@@ -525,6 +565,27 @@
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
@@ -536,6 +597,40 @@
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/bson": {
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/bson/-/bson-6.8.0.tgz",
|
||||
"integrity": "sha512-iOJg8pr7wq2tg/zSlCCHMi3hMm5JTOxLTagf3zxhcenHsFp+c6uOs6K7W5UE7A4QIJGtqh/ZovFNMP4mOPJynQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=16.20.1"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
@@ -690,6 +785,18 @@
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.4.5",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
|
||||
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
@@ -869,6 +976,26 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/event-target-shim": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
@@ -1083,6 +1210,27 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
@@ -1294,6 +1442,12 @@
|
||||
"integrity": "sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/memory-pager": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
|
||||
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
@@ -1337,6 +1491,62 @@
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb": {
|
||||
"version": "6.8.1",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.8.1.tgz",
|
||||
"integrity": "sha512-qsS+gl5EJb+VzJqUjXSZ5Y5rbuM/GZlZUEJ2OIVYP10L9rO9DQ0DGp+ceTzsmoADh6QYMWd9MSdG9IxRyYUkEA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@mongodb-js/saslprep": "^1.1.5",
|
||||
"bson": "^6.7.0",
|
||||
"mongodb-connection-string-url": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.20.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@aws-sdk/credential-providers": "^3.188.0",
|
||||
"@mongodb-js/zstd": "^1.1.0",
|
||||
"gcp-metadata": "^5.2.0",
|
||||
"kerberos": "^2.0.1",
|
||||
"mongodb-client-encryption": ">=6.0.0 <7",
|
||||
"snappy": "^7.2.2",
|
||||
"socks": "^2.7.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@aws-sdk/credential-providers": {
|
||||
"optional": true
|
||||
},
|
||||
"@mongodb-js/zstd": {
|
||||
"optional": true
|
||||
},
|
||||
"gcp-metadata": {
|
||||
"optional": true
|
||||
},
|
||||
"kerberos": {
|
||||
"optional": true
|
||||
},
|
||||
"mongodb-client-encryption": {
|
||||
"optional": true
|
||||
},
|
||||
"snappy": {
|
||||
"optional": true
|
||||
},
|
||||
"socks": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz",
|
||||
"integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/whatwg-url": "^11.0.2",
|
||||
"whatwg-url": "^13.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
@@ -1480,6 +1690,16 @@
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
@@ -1490,7 +1710,6 @@
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -1517,6 +1736,38 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "4.5.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz",
|
||||
"integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"abort-controller": "^3.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"events": "^3.3.0",
|
||||
"process": "^0.11.10",
|
||||
"string_decoder": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/replicate": {
|
||||
"version": "0.32.1",
|
||||
"resolved": "https://registry.npmjs.org/replicate/-/replicate-0.32.1.tgz",
|
||||
"integrity": "sha512-OmzyDVx3P9FXVXcFrzWft8XW8LjyGo65qRCwUPJwbjBZS4Z89pQFX+ZUfYlHwcfIHGtP4/Pf0uis+XJiznKacQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"git": ">=2.11.0",
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=7.19.0",
|
||||
"yarn": ">=1.7.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"readable-stream": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-from": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
@@ -1562,6 +1813,27 @@
|
||||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
@@ -1595,6 +1867,25 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/sparse-bitfield": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
|
||||
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"memory-pager": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
@@ -1718,6 +2009,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz",
|
||||
"integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"punycode": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-mixer": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz",
|
||||
@@ -1768,6 +2071,28 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz",
|
||||
"integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "^4.1.1",
|
||||
"webidl-conversions": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
"main": "src/index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node --env-file .env src/index.js",
|
||||
"register-slash-commands": "node --env-file .env src/register-slash-commands.js",
|
||||
"start": "node src/index.js",
|
||||
"register-slash-commands": "node src/register-slash-commands.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
@@ -19,8 +19,11 @@
|
||||
"axios": "^1.7.7",
|
||||
"date-fns": "^3.6.0",
|
||||
"discord.js": "^14.16.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"glob": "^11.0.0",
|
||||
"prettier": "^3.3.3"
|
||||
"mongodb": "^6.8.1",
|
||||
"prettier": "^3.3.3",
|
||||
"replicate": "^0.32.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^9.9.1"
|
||||
|
||||
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
|
||||
99
src/commands/ai/image.js
Normal file
99
src/commands/ai/image.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import { SlashCommandBuilder, AttachmentBuilder } from 'discord.js';
|
||||
import axios from 'axios';
|
||||
import Replicate from 'replicate';
|
||||
|
||||
if (!process.env.REPLICATE_API_KEY) {
|
||||
throw new Error('REPLICATE_API_KEY is not set in the environment variables.');
|
||||
}
|
||||
|
||||
const replicate = new Replicate({
|
||||
auth: process.env.REPLICATE_API_KEY,
|
||||
});
|
||||
|
||||
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)
|
||||
);
|
||||
|
||||
// Helper function to poll the prediction status
|
||||
async function pollPredictionStatus(
|
||||
predictionId,
|
||||
maxAttempts = 5,
|
||||
interval = 2000
|
||||
) {
|
||||
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.');
|
||||
}
|
||||
|
||||
// Helper function to download the image from a URL
|
||||
async function downloadImage(url) {
|
||||
try {
|
||||
const response = await axios.get(url, { responseType: 'arraybuffer' });
|
||||
return Buffer.from(response.data);
|
||||
} catch (error) {
|
||||
throw new Error('Failed to download the image.');
|
||||
}
|
||||
}
|
||||
|
||||
async function execute(interaction) {
|
||||
await interaction.deferReply();
|
||||
|
||||
if (!process.env.REPLICATE_API_KEY) {
|
||||
await interaction.reply('The bot is missing the Replicate API key.');
|
||||
return;
|
||||
}
|
||||
|
||||
const prompt = interaction.options.getString('prompt');
|
||||
|
||||
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) {
|
||||
// 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}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default { data, execute };
|
||||
207
src/commands/db/game.js
Normal file
207
src/commands/db/game.js
Normal file
@@ -0,0 +1,207 @@
|
||||
import { SlashCommandBuilder, EmbedBuilder } from 'discord.js';
|
||||
import {
|
||||
listGameNames,
|
||||
getGame,
|
||||
setGame,
|
||||
deleteGame,
|
||||
deleteField,
|
||||
} from '../../utils/db/game.js';
|
||||
|
||||
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 an 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)
|
||||
)
|
||||
);
|
||||
|
||||
function createEmbedFromGame(game) {
|
||||
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;
|
||||
}
|
||||
|
||||
async function execute(interaction) {
|
||||
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');
|
||||
|
||||
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');
|
||||
const key = interaction.options.getString('key');
|
||||
const value = interaction.options.getString('value');
|
||||
|
||||
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');
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
export default { data, execute };
|
||||
@@ -1,6 +1,10 @@
|
||||
import { EmbedBuilder, SlashCommandBuilder } from 'discord.js';
|
||||
import axios from 'axios';
|
||||
|
||||
if (!process.env.OMDB_API_KEY) {
|
||||
throw new Error('OMDB_API_KEY is not set in the environment variables.');
|
||||
}
|
||||
|
||||
const OMDB_API_URL = 'http://www.omdbapi.com/?apikey={apiKey}';
|
||||
const FIELDS = [
|
||||
'Title',
|
||||
@@ -32,40 +36,65 @@ const data = new SlashCommandBuilder()
|
||||
.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())}`;
|
||||
|
||||
// Helper function to construct OMDB query URL
|
||||
function buildOmdbUrl(filmName, filmYear) {
|
||||
let url = OMDB_API_URL.replace('{apiKey}', process.env.OMDB_API_KEY);
|
||||
url += `&t=${encodeURIComponent(filmName.toLowerCase())}`;
|
||||
if (filmYear) {
|
||||
omdbQueryUrl += `&y=${filmYear}`;
|
||||
url += `&y=${filmYear}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get(omdbQueryUrl, { timeout: 5000 });
|
||||
const result = response.data;
|
||||
async function execute(interaction) {
|
||||
await interaction.deferReply();
|
||||
|
||||
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.');
|
||||
if (!process.env.OMDB_API_KEY) {
|
||||
await interaction.editReply('The bot is missing the OMDB API key.');
|
||||
return;
|
||||
}
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
const filmName = interaction.options.getString('film_name');
|
||||
const filmYear = interaction.options.getInteger('film_year');
|
||||
|
||||
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) {
|
||||
console.error(`Error looking up film: ${filmName}`, error);
|
||||
await interaction.editReply(
|
||||
'An error occurred when querying the OMDB API.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default { data, execute };
|
||||
|
||||
@@ -5,6 +5,10 @@ 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';
|
||||
|
||||
if (!process.env.PLANTNET_API_KEY) {
|
||||
throw new Error('PLANTNET_API_KEY is not set in the environment variables.');
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName('plant')
|
||||
.setDescription('Identify a plant by uploading an image.')
|
||||
@@ -15,31 +19,8 @@ const data = new SlashCommandBuilder()
|
||||
.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);
|
||||
|
||||
// Helper function to generate plant details
|
||||
function generatePlantDetail(commonName, scientificName, confidence) {
|
||||
let detail = '';
|
||||
|
||||
if (0.0 <= confidence && confidence < 0.25) {
|
||||
@@ -52,24 +33,78 @@ async function execute(interaction) {
|
||||
detail = `That is a really cool **${commonName}**!`;
|
||||
}
|
||||
|
||||
const imageUrl = `https://www.google.com/search?q=${encodeURIComponent(commonName || scientificName)}&tbm=isch`;
|
||||
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 detail;
|
||||
}
|
||||
|
||||
async function execute(interaction) {
|
||||
await interaction.deferReply();
|
||||
|
||||
if (!process.env.PLANTNET_API_KEY) {
|
||||
await interaction.editReply('The bot is missing the PlantNet API key.');
|
||||
return;
|
||||
}
|
||||
|
||||
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),
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
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) {
|
||||
console.error(error); // Log the error for debugging
|
||||
await interaction.editReply(
|
||||
'An error occurred when querying the PlantNet API.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default { data, execute };
|
||||
|
||||
@@ -7,33 +7,42 @@ 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;
|
||||
}
|
||||
|
||||
async function fetchGif() {
|
||||
const randomOffset = Math.floor(Math.random() * 100);
|
||||
const giphyQueryUrl = `${GIPHY_API_URL}?api_key=${giphyApiKey}&q=gerard+butler&limit=1&offset=${randomOffset}`;
|
||||
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) {
|
||||
await interaction.reply('No gifs found.');
|
||||
throw new Error('No gifs found for the given query.');
|
||||
}
|
||||
|
||||
return result.data[0].images.original.url;
|
||||
} catch (error) {
|
||||
console.error('Error fetching GIF:', error.message);
|
||||
throw new Error('Failed to retrieve a GIF from Giphy.');
|
||||
}
|
||||
}
|
||||
|
||||
async function execute(interaction) {
|
||||
await interaction.deferReply();
|
||||
|
||||
if (!process.env.GIPHY_API_KEY) {
|
||||
await interaction.reply('The bot is missing the GIPHY API key.');
|
||||
return;
|
||||
}
|
||||
|
||||
const imageUrl = result.data[0].images.original.url;
|
||||
await interaction.reply(imageUrl);
|
||||
try {
|
||||
// Fetch a random gif of Gerard Butler.
|
||||
const gifUrl = await fetchGif();
|
||||
|
||||
// Reply with the gif.
|
||||
await interaction.editReply(gifUrl);
|
||||
} catch (error) {
|
||||
console.error('Error querying the Giphy API:', error);
|
||||
await interaction.reply('An error occurred when querying the Giphy API.');
|
||||
console.error('Error executing the command:', error.message);
|
||||
await interaction.editReply(`An error occurred: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,19 +5,15 @@ const data = new SlashCommandBuilder()
|
||||
.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! 🎉' });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dotenv/config';
|
||||
import { Client, Collection, Events, GatewayIntentBits } from 'discord.js';
|
||||
import { loadCommandModules } from './utilities/commandModules.js';
|
||||
|
||||
@@ -42,4 +43,4 @@ client.once(Events.ClientReady, (readyClient) => {
|
||||
console.log(`Ready! Logged in as ${readyClient.user.tag}`);
|
||||
});
|
||||
|
||||
client.login(process.env.DISCORD_TOKEN);
|
||||
client.login(process.env.DISCORD_API_KEY);
|
||||
|
||||
132
src/music/music.js
Normal file
132
src/music/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,3 +1,4 @@
|
||||
import 'dotenv/config';
|
||||
import { REST, Routes } from 'discord.js';
|
||||
import { loadCommandModules } from './utilities/commandModules.js';
|
||||
|
||||
@@ -7,7 +8,7 @@ const commands = commandModules.map((commandModule) =>
|
||||
commandModule.data.toJSON()
|
||||
);
|
||||
|
||||
const rest = new REST().setToken(process.env.DISCORD_TOKEN);
|
||||
const rest = new REST().setToken(process.env.DISCORD_API_KEY);
|
||||
|
||||
try {
|
||||
console.log('Started refreshing application (/) commands.');
|
||||
@@ -19,6 +20,8 @@ try {
|
||||
);
|
||||
|
||||
console.log(`Successfully reloaded ${data.length} application (/) commands.`);
|
||||
// Exit the process.
|
||||
process.exit();
|
||||
} catch (error) {
|
||||
// And of course, make sure you catch and log any errors!
|
||||
console.error(error);
|
||||
|
||||
37
src/utils/db/game.js
Normal file
37
src/utils/db/game.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { MongoClient } from 'mongodb';
|
||||
|
||||
if (!process.env.MONGODB_URI) {
|
||||
throw new Error('MONGODB_URI is not set in the environment variables.');
|
||||
}
|
||||
|
||||
const client = new MongoClient(process.env.MONGODB_URI);
|
||||
await client.connect();
|
||||
|
||||
const database = client.db('butler_db');
|
||||
const collection = database.collection('GameFeatures');
|
||||
|
||||
export async function listGameNames() {
|
||||
return collection.find({}, { projection: { game: 1 } }).toArray();
|
||||
}
|
||||
|
||||
export async function getGame(game) {
|
||||
return collection.findOne({ game });
|
||||
}
|
||||
|
||||
export async function setGame(game, key, value) {
|
||||
// If game is not found, create a new document with game field set to game, and key field set to value.
|
||||
// Overwrite the value of the key field if it already exists.
|
||||
return collection.updateOne(
|
||||
{ game },
|
||||
{ $set: { game, [key]: value } },
|
||||
{ upsert: true }
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteGame(game) {
|
||||
return collection.deleteOne({ game });
|
||||
}
|
||||
|
||||
export async function deleteField(game, key) {
|
||||
return collection.updateOne({ game }, { $unset: { [key]: '' } });
|
||||
}
|
||||
Reference in New Issue
Block a user