Compare commits

..

116 Commits

Author SHA1 Message Date
f9bb94a4a1 Maybe working 2024-09-22 18:13:15 -04:00
1575d0047c ? 2024-09-22 18:00:52 -04:00
d173338d09 Fix runs on 2024-09-22 17:57:58 -04:00
aedbbec6a2 Updating CI/CD 2024-09-22 17:56:32 -04:00
fa41a5d61e Add badges 2024-09-22 15:54:14 -04:00
ac6eca4827 Merge pull request 'v1.2.9-dev' (#8) from v1.2.9-dev into main
Reviewed-on: #8
2024-04-06 13:16:15 +00:00
88cc4eda3c Remove reminder embed, only send text message
Some checks failed
Silvanus Production Dockerization / build (pull_request) Has been cancelled
2024-04-06 09:01:57 -04:00
7291216dcb Versioning 2024-04-06 09:01:43 -04:00
36f44f4b41 Merge pull request 'v1.2.8 Heartbeat Monitoring' (#6) from v1.2.8 into main
Reviewed-on: #6
2023-09-02 08:37:11 -07:00
825ec725d1 Timer planning
Some checks reported warnings
Silvanus Production Dockerization / build (pull_request) Has been cancelled
2023-09-02 09:42:10 -04:00
fb43d09d13 Fix debug mode detection
Some checks reported warnings
Silvanus Production Dockerization / build (pull_request) Has been cancelled
2023-08-27 20:27:11 -04:00
0463695323 Improved logging
All checks were successful
Silvanus Production Dockerization / build (pull_request) Successful in 12s
2023-08-27 19:34:15 -04:00
314b793042 Versioning
All checks were successful
Silvanus Production Dockerization / build (pull_request) Successful in 53s
2023-08-10 22:18:28 -04:00
eca3207d4e Remove excess console.log 2023-08-10 22:16:23 -04:00
e2658c41e5 Add heartbeat url for uptime monitoring 2023-08-10 22:13:08 -04:00
dd313faa60 Merge pull request 'Change dispatch to pull req' (#3) from testing into main
Reviewed-on: #3
2023-08-04 09:52:53 -07:00
ba81f01fa0 Even more dubious
All checks were successful
Silvanus Production Dockerization / build (pull_request) Successful in 27s
2023-07-31 16:43:54 -04:00
e303651f53 Dubious
Some checks failed
Silvanus Production Dockerization / build (pull_request) Failing after 2s
2023-07-31 16:43:24 -04:00
40fc6838a0 Debug
Some checks failed
Silvanus Production Dockerization / build (pull_request) Failing after 2s
2023-07-31 16:41:52 -04:00
67483697f0 Attempt #243095
Some checks failed
Silvanus Production Dockerization / build (pull_request) Failing after 2s
2023-07-31 16:40:48 -04:00
1e1d0cddaa Moar fixxed?!
Some checks failed
Silvanus Production Dockerization / build (pull_request) Failing after 2s
2023-07-31 16:35:33 -04:00
45418e2bd6 Fixed I hope
Some checks failed
Silvanus Production Dockerization / build (pull_request) Failing after 2s
2023-07-31 16:23:03 -04:00
22005ac68f asd;flkasdj;
Some checks reported warnings
Silvanus Production Dockerization / build (pull_request) Has been cancelled
2023-07-31 16:21:21 -04:00
805c20953c Fixes
Some checks failed
Silvanus Production Dockerization / build (pull_request) Failing after 7s
2023-07-31 16:12:58 -04:00
ba0e4625c0 Remove host
Some checks failed
Silvanus Production Dockerization / build (pull_request) Failing after 6s
2023-07-31 16:07:43 -04:00
11465287dd Update uses declaration
Some checks failed
Silvanus Production Dockerization / build (pull_request) Failing after 12s
2023-07-31 16:05:40 -04:00
36c59f83f4 Change dispatch to pull req
Some checks failed
Silvanus Production Dockerization / build (pull_request) Failing after 46s
2023-07-31 16:03:25 -04:00
4dda58f48e Merge pull request 'Slight verbiage change' (#2) from testing into main
Reviewed-on: #2
2023-07-31 12:58:33 -07:00
e85340245c Slight verbiage change 2023-07-31 15:57:04 -04:00
252593a327 Merge pull request 'Versioning' (#1) from v1.2.7-dev into main
Reviewed-on: #1
2023-07-31 12:55:29 -07:00
7999350f1e Versioning 2023-07-31 15:45:25 -04:00
87a468b620 Update workflow trigger for Gitea Actions 2023-07-31 15:44:17 -04:00
ae602d08de Fixed bug when updating relay settings 2023-07-31 15:12:23 -04:00
Skylar Grant
94c3244016
Merge pull request #17 from eilandert/main
remove if empty database statement
2023-07-31 10:02:01 -04:00
Skylar Grant
2f9126aa6e tweak errors 2023-07-10 19:52:18 -04:00
Skylar Grant
486a59715d vastly improved error handling 2023-07-10 19:46:30 -04:00
Skylar Grant
25859d364d Improved error handling? 2023-07-06 19:04:13 -04:00
Skylar Grant
c0cba3e14e Modified log output for better organization 2023-07-06 18:28:03 -04:00
Thijs Eilander
e3fb4da9f6 typo, it's late 2023-06-19 03:58:24 +02:00
Thijs Eilander
f2560a081e added my own id into the dev team 2023-06-19 03:52:33 +02:00
Thijs Eilander
e8f16d8fea change description /notifications update to /relay update 2023-06-19 00:02:47 +02:00
Thijs Eilander
7c56381141 due to this test, when starting with a new database the bot exits upon start, so there is no possibility to add a new entry in the database at all. 2023-06-18 22:51:35 +02:00
Skylar Grant
a1386dd688 Add owner IDs to db, clean up some logging 2023-06-16 20:01:29 -04:00
Skylar Grant
9e5a89eabb Add privacy policy 2023-06-13 21:10:21 -04:00
Skylar Grant
d6bab1c2da Include email in help 2023-06-13 20:54:35 -04:00
Skylar Grant
d117a69893 Add status updates and removal notifs 2023-06-10 15:09:45 -04:00
Skylar Grant
87c7a8e9ec Improve the About embed 2023-06-10 15:03:00 -04:00
Skylar Grant
a27dc1923e Fix message 2023-06-10 14:28:19 -04:00
Skylar Grant
c3967e0643 Add top.gg link to about command 2023-06-10 14:22:40 -04:00
Skylar Grant
4144dde4d6 Add guild add notif 2023-06-08 17:51:41 -04:00
Skylar Grant
e05bd20f1f Add about command 2023-06-07 20:10:49 -04:00
Skylar Grant
7003e12fa1 Add direct link to setup guide 2023-06-07 20:07:51 -04:00
Skylar Grant
246848e94a Add wiki link to readme 2023-06-07 19:56:18 -04:00
Skylar Grant
a13888c6c1 New help message 2023-06-07 19:54:33 -04:00
Skylar Grant
49747d4b54 Fix invite link 2023-06-06 19:39:07 -04:00
Skylar Grant
e94b8ce433 Fix permissions dot command 2023-06-06 19:37:36 -04:00
Skylar Grant
9a5a457123 Remove out of date information 2023-06-06 19:37:36 -04:00
Skylar Grant
c83e94f01d Implement permission troubleshooting 2023-06-06 19:37:36 -04:00
Skylar Grant
08f76fb996 Add notification relay status to setupinfo 2023-06-06 19:37:36 -04:00
Skylar Grant
d1ac674c6b Allow everyone ping dot command 2023-06-06 19:37:36 -04:00
Skylar Grant
e29a54b0d9 Add leave command, update util to show guildid 2023-06-06 19:37:36 -04:00
Skylar Grant
5b7c0c4225 Fix message command 2023-06-06 19:37:36 -04:00
Skylar Grant
4f0fa1e51f Update to allow enable/disable via relay update 2023-06-06 19:37:36 -04:00
Skylar Grant
946f05d57b Update to ignore all .env 2023-06-06 19:37:36 -04:00
Skylar Grant
92c06529ff Fix for broken relay 2023-06-06 19:37:36 -04:00
Skylar Grant
c4fc1a4ba6 Disable relay on guilds I'm no longer in 2023-06-06 19:37:36 -04:00
Skylar Grant
b0d6bdcf8a Minor changes 2023-06-06 19:37:36 -04:00
Skylar Grant
2704cac98f Fix for embeds w/o descriptions crashing the bot 2023-06-06 19:37:36 -04:00
Skylar Grant
a28957b956 Enable dev team dot commands 2023-06-06 19:37:36 -04:00
Skylar Grant
105fb2827f Versioning 2023-06-06 19:37:36 -04:00
Skylar Grant
9aa9511fd7 Misc updates 2023-06-03 15:02:31 -04:00
Skylar Grant
79821d7abc Rename file to match command name 2023-06-03 15:02:31 -04:00
Skylar Grant
e075a3a5d6 Implement error handling for rolemenu 2023-06-03 15:02:31 -04:00
Skylar Grant
3c01d37bd5 Bug fix so message updates will create guild entry 2023-06-03 15:02:31 -04:00
Skylar Grant
f7230ec0fb Trying error handling DO NOT PUSH 2 PROD 2023-06-03 15:02:31 -04:00
Skylar Grant
1927d3029a Begin implementing ability to message server owner 2023-06-03 15:02:31 -04:00
Skylar Grant
41b59f8b92 Add basic message command 2023-06-03 15:02:31 -04:00
Skylar Grant
f3573eeaed Stop sending name and owner IDs 2023-06-03 15:02:31 -04:00
Skylar Grant
44a4d572bb Fix file reference 2023-06-03 15:02:31 -04:00
Skylar Grant
de6b9ec4e4 Adding dot commands from nodbot 2023-06-03 15:02:31 -04:00
Skylar Grant
56d9cacfbe Change squash type to prevent further execution 2023-06-03 15:02:31 -04:00
Skylar Grant
0c28ec437c err -> error 2023-06-03 15:02:31 -04:00
Skylar Grant
8d64640193 Squash missing guild error 2023-06-03 15:02:31 -04:00
Skylar Grant
1cb5e94145 Better logging output 2023-06-03 15:02:31 -04:00
Skylar Grant
428cbc1597 Removed error dumping for missing messages 2023-06-03 15:02:31 -04:00
Skylar Grant
1c57cb70ae Better role error handling 2023-06-03 15:02:31 -04:00
Skylar Grant
6f3710bc07 Updating dependencies 2023-06-03 15:02:31 -04:00
Skylar Grant
0bdca58d03 Update stderr and stdout to files with timestamps 2023-06-03 15:02:31 -04:00
Skylar Grant
7121ab5390 Versioning 2023-06-03 15:02:31 -04:00
Skylar Grant
2353486150 Fix for hypens in tree name 2023-06-03 15:02:31 -04:00
Skylar Grant
26b79327be Update Discord.js / Breaking Discord API changes 2023-04-01 20:44:50 -04:00
Skylar Grant
9580a500fd extra tools to use with the bot 2023-04-01 20:44:50 -04:00
Skylar Grant
10f8ed9438 Remove setup compare 2023-04-01 20:44:50 -04:00
Skylar Grant
024defbbfc Automatically updating compare messages! 2023-04-01 20:44:50 -04:00
Skylar Grant
27169afcf0 Testing new tree update detection 2023-04-01 20:44:50 -04:00
Skylar Grant
5f37a3e5a3 Improvements to timetoheights calculations display 2023-04-01 20:44:50 -04:00
Skylar Grant
a44a83beeb Versioning 2023-04-01 20:44:50 -04:00
Skylar Grant
d7bd6b5d41 Stricter check 2023-04-01 20:44:50 -04:00
Skylar Grant
0f8e9a3e10 Add privacy switch 2023-04-01 20:44:50 -04:00
Skylar Grant
9c6204b19b Some more logging 2023-04-01 20:44:50 -04:00
Skylar Grant
02201b2bfa Add percentage modifiers to timetoheight 2023-04-01 20:44:50 -04:00
Skylar Grant
1f115252ca Rate limits 2023-04-01 20:44:50 -04:00
Skylar Grant
e632dce612 Stop checking ratelimits 2023-04-01 20:44:50 -04:00
Skylar Grant
cdb61cf602 Documentation update 2023-04-01 20:44:50 -04:00
Skylar Grant
b2e0b2b17d Stop deleting commands 2023-04-01 20:44:50 -04:00
Skylar Grant
e7a76eee65 Fix /setup compare 2023-04-01 20:44:50 -04:00
Skylar Grant
a70b27b112 Documentation update 2023-04-01 20:44:50 -04:00
Skylar Grant
1e358e582e CI Update 2023-04-01 20:44:50 -04:00
Skylar Grant
d5254dc58a Removed update event 2023-04-01 20:44:50 -04:00
Skylar Grant
8bc80dfaa8 Self-destructing water relay 2023-04-01 20:44:50 -04:00
Skylar Grant
663ef2297b Error update 2023-04-01 20:44:50 -04:00
Skylar Grant
56be24b5cd Bugfixes in notifications and update to relay 2023-04-01 20:44:50 -04:00
Skylar Grant
ebc31b5a0c Versioning 2023-04-01 20:44:50 -04:00
Skylar Grant
1367a90904 Start collector when the command is run 2023-04-01 20:44:50 -04:00
Skylar Grant
26b5728636 Disappearing fruit pings 2023-04-01 20:44:50 -04:00
Skylar Grant
b724229f06
V1.2.1 dev (#11)
* make the beginning height optional, defaulting to the trees current heigt

* improved workflow

* fix

* new fix

* Install modules

* remove unnecessary call to build the tables

* testing

* f

* f

* oops

* undev

* fix const

* new ci

* versioning

* Classes, Collections, and new Notification Relay

* Fix CI for new env vars

* Update to read contents of embeds

* Update filter to check for embeds

* Update to add includes check

* Begin implementing role menu

* Role menu ready for testing

* Add missing emojis

* Fixes, ready for testing

* Tentative deploy

* Documentation update

* I think this fixes unchanging notifications

* Update README.md

* Make reminders visually distinct

* Set Permissions

* Added import for PermissionFlagsBits

* Add ability to manually send ping from watch chan

* CI update

* Confusion

* Make dev dockerization manual only

* Dev Logging

* Update reset command

* Implement updating of notifications piecemeal

* Unified error handling

* Change to refresh commands not blindly update

* Change to manual run only

* Restructured file to allow async use

* Move role menu setup and allow everyone to send it

* Consolidate setupinfo into setup

* Improved error handling

* Update opt out method

* Cleaning up junk

* Added seconds parser and optout detection

* Add opt out method

* Consolidated to setup command

* Add privacy setting and use new parser
2023-02-19 20:42:14 -05:00
48 changed files with 1888 additions and 690 deletions

BIN
.DS_Store vendored Normal file → Executable file

Binary file not shown.

0
.dockerignore Normal file → Executable file
View File

0
.eslintrc.json Normal file → Executable file
View File

View File

@ -0,0 +1,23 @@
name: Silvanus-Dev Dockerization
on:
workflow_dispatch:
env:
DHUB_UNAME: ${{ secrets.DHUB_UNAME }}
DHUB_PWORD: ${{ secrets.DHUB_PWORD }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build the Docker image v0idf1sh/silvanus-dev
run: docker build . --file Dockerfile --tag v0idf1sh/silvanus-dev
- name: Log into Docker Hub
run: docker login -u $DHUB_UNAME -p $DHUB_PWORD
- name: Push image to Docker Hub v0idf1sh/silvanus-dev
run: docker push v0idf1sh/silvanus-dev

View File

@ -0,0 +1,47 @@
name: Silvanus Production Dockerization
on:
pull_request:
branches:
- main
env:
DHUB_UNAME: ${{ secrets.DHUB_UNAME }}
DHUB_PWORD: ${{ secrets.DHUB_PWORD }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Pull latest from Git
run: |
pwd
whoami
mkdir -p /var/lib/act_runner/
cd /var/lib/act_runner/
if [ ! -d "silvanus" ]; then
git clone https://git.vfsh.dev/voidf1sh/silvanus
cd silvanus
else
cd silvanus
git pull
fi
git checkout ${{ gitea.ref}}
- name: Build the Docker image
run: |
cd /var/lib/act_runner/silvanus
docker build . --file Dockerfile --tag v0idf1sh/silvanus
- name: Log into Docker Hub
run: docker login -u $DHUB_UNAME -p $DHUB_PWORD
- name: Push image to Docker Hub
run: |
cd /var/lib/act_runner/silvanus
docker push v0idf1sh/silvanus
- name: Restart the container
run: |
cd /srv/docker/silvanus
docker-compose down
docker-compose up -d

View File

@ -1,30 +0,0 @@
name: Silvanus-Dev Dockerization
on:
push:
branches: [ "*-dev" ]
env:
DHUB_UNAME: ${{ secrets.DHUB_UNAME }}
DHUB_PWORD: ${{ secrets.DHUB_PWORD }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build the Docker image
run: docker build . --file Dockerfile --tag v0idf1sh/silvanus-dev
- name: Log into Docker Hub
run: docker login -u $DHUB_UNAME -p $DHUB_PWORD
- name: Push image to Docker Hub
run: docker push v0idf1sh/silvanus-dev
- name: Set up a skeleton .env file
run: echo "TOKEN=${{secrets.DEVTOKEN}}" > .env && echo "BOTID=${{ secrets.CLIENTID }}" >> .env
- name: Install modules
run: npm i
- name: Refresh commands with Discord
run: node modules/_deploy-global.js

View File

@ -1,30 +0,0 @@
name: Silvanus-Dev Dockerization
on:
pull_request:
branches: [ "main" ]
env:
DHUB_UNAME: ${{ secrets.DHUB_UNAME }}
DHUB_PWORD: ${{ secrets.DHUB_PWORD }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build the Docker image
run: docker build . --file Dockerfile --tag v0idf1sh/silvanus
- name: Log into Docker Hub
run: docker login -u $DHUB_UNAME -p $DHUB_PWORD
- name: Push image to Docker Hub
run: docker push v0idf1sh/silvanus
- name: Set up a skeleton .env file
run: echo "TOKEN=${{secrets.PRODTOKEN}}" > .env && echo "clientId=${{ secrets.PRODCLIENTID }}" >> .env
- name: Install modules
run: npm i
- name: Refresh commands with Discord
run: node modules/_deploy-global.js

4
.gitignore vendored Normal file → Executable file
View File

@ -2,11 +2,11 @@
.vscode .vscode
package-lock.json package-lock.json
.VSCodeCounter/ .VSCodeCounter/
env.dev .env*
env.prod
.DS_Store .DS_Store
data/guildInfo.json data/guildInfo.json
data/rawstring.txt data/rawstring.txt
modules/input.txt
# Custom folders # Custom folders
# gifs/* # gifs/*

2
Dockerfile Normal file → Executable file
View File

@ -5,4 +5,4 @@ WORKDIR /usr/src/app
COPY package.json ./ COPY package.json ./
RUN npm install RUN npm install
COPY . . COPY . .
CMD [ "node", "main.js" ] CMD ["/bin/sh", "-c", "node main.js 2>&1 > /logs/$(date +%Y-%m-%d_%H-%M-%S).txt"]

35
README.md Normal file → Executable file
View File

@ -1,37 +1,18 @@
![Current Uptime Status](https://status.vfsh.dev/api/badge/1/status)
![Uptime Percentage](https://status.vfsh.dev/api/badge/1/uptime)
# Silvanus # Silvanus
Silvanus is the ultimate Grow A Tree companion bot! Quickly compare your server's tree to others on the leaderboard with automatic calculation of tree height differences, active growth detection, watering time calculations, and more! Silvanus is the ultimate Grow A Tree companion bot! Quickly compare your server's tree to others on the leaderboard with automatic calculation of tree height differences, active growth detection, watering time calculations, and more!
Important Note: Silvanus is only as up-to-date as your server's newest Tree and Tallest Trees messages. Make sure to refresh them before refreshing Silvanus' Compare message.
Silvanus is not affiliated with Grow A Tree or Limbo Labs. Silvanus is not affiliated with Grow A Tree or Limbo Labs.
## Add Silvanus to your server ## Add Silvanus to your server
[Invite Silvanus to your Discord Server](https://discord.com/api/oauth2/authorize?client_id=521624335119810561&permissions=274877908992&scope=bot%20applications.commands) [Invite Silvanus to your Discord Server](https://discord.com/api/oauth2/authorize?client_id=521624335119810561&permissions=275146475520&scope=applications.commands%20bot)
## voidf1sh Development Support Server ## Silvanus Support Server
[Join Discord Server](https://discord.gg/g5JRGn7PxU) [Join Discord Server](https://discord.gg/g5JRGn7PxU)
## Setup ## Silvanus Support Wiki
Find the most up-to-date guides and information about Silvanus at the [Silvanus Support Wiki](https://silvanus.vfsh.dev/).
[Silvanus Setup Guide](https://silvanus.vfsh.dev/en/setup)
If your `/tree` and `/top trees` messages are in the same channel, simple run `/compare` in that channel and you're good to go! Please find the most up-to-date guides and information at the Silvanus Support Discord Server!
Otherwise, run `/setup` to set the proper channels for the bot to look in for the `/tree` and `/top trees` messages.
Use `/commands` to view a description of all my commands.
## Permissions
Silvanus requires permissions to `Send Messages` and `Send Messages in Threads` if applicable.
## Commands
* `/setup` - You only need to run this command if your server has its `/tree` and `/top trees` messages in separate channels.
* `/setupinfo` - Displays links to the current Tree and Tallest Trees messages configured in your server.
* `/compare` - Sends a refreshable embed that calculcates the height difference between your tree and the trees currently displayed on your Tallest Trees message. There is also an Active Growth Indicator (`[💧]`).
* `/setping` - Guild members with the `Manage Roles` permission can run this command to set up automatic reminders when your tree is ready to be watered.
* Type a reminder message (including any `@pings` desired) and select a channel to send the reminders in.
* Once this command has been run a new `Reset Ping` button will appear next time you refresh the `/compare` message.
* Click the `Reset Ping` button to be sent a reminder the next time the tree is ready to be watered.
* Use `/optout` to disable reminder messages for your server.
* `/watertime` - Calculates the wait time between waters for a tree of a given height.
* `/timetoheight` - Calculates how long it would take to go from `beginheight` to `endheight`.
* `/reset` - Removes your server's configuration from the database.
* `/help` - Displays the bot's help page and links to each command.

24
TODO.md Normal file → Executable file
View File

@ -1,10 +1,3 @@
## In Progress
☑ Switch `/setup` to ask for the tree and leaderboard channels
* Switch `/compare` to check for newer trees and leaderboards when run **and** on every refresh
## Future Ideas
* Go through and comment the code
## Variable Structures ## Variable Structures
guildInfo = { guildInfo = {
@ -21,7 +14,18 @@ guildInfo = {
reminderOptIn: 0, reminderOptIn: 0,
} }
## Expected Behaviors ## New Table Planning
Table: `silvanus`.timers
* Run `/compare` before `/setup`: `/compare` will search the current channel for tree and leaderboard messages, then create a comparison embed. If it can't find `/tree` or `/top trees` messages, it'll return an error saying as much. id | INT | NOT NULL | AUTO INCREMENT | PRIMARY KEY
* Run `/compare` after `/setup`: ``/compare` will search the current channel for tree and leaderboard messages, then create a comparison embed. If it can't find `/tree` or `/top trees` messages, it'll just use old data silently (odds are `/compare` is being run from another channel, that's fine) status | VARCHAR(10) | NOT NULL | DEFAULT "WAITING"
dc_timecode | INT | NOT NULL | Discord timecode
guild_id | INT UNSIGNED | NOT NULL | Discord guild ID for referencing `silvanus`.guildInfo
Table: `silvanus`.auto_role_status
id | INT | NOT NULL | AUTO INCREMENT | PRIMARY KEY
user_id | INT UNSIGNED | NOT NULL
guild_id | INT UNSIGNED | NOT NULL
status | VARCHAR(10) | NOT NULL | DEFAULT "REMOVED" | OPTION: {"REMOVED", "ADDED"}

8
data/config.json Normal file → Executable file
View File

@ -5,5 +5,11 @@
"treeHeight": 0, "treeHeight": 0,
"validCommands": [], "validCommands": [],
"rankMessageId": "", "rankMessageId": "",
"rankings": [] "rankings": [],
"devTeamIds": [
"481933290912350209",
"269249959973355520",
"448606738669633536"
],
"ownerId": "481933290912350209"
} }

39
data/strings.json Normal file → Executable file
View File

@ -4,10 +4,13 @@
}, },
"help": { "help": {
"title": "Silvanus Help", "title": "Silvanus Help",
"info": "Silvanus is the ultimate Grow A Tree companion bot! Quickly compare your server's tree to others on the leaderboard with automatic calculation of tree height differences, active growth detection, watering time calculations, and more!\n\nImportant Note: Silvanus is only as up-to-date as your server's newest Tree and Tallest Trees messages. Make sure to refresh them before refreshing Silvanus' Compare message.", "aboutTitle": "About Silvanus",
"setup": "If your ``/tree`` and ``/top trees`` messages are in the same channel, simple run </compare:1065346941166297128> in that channel and you're good to go!\n\nOtherwise, run </setup:1065407649363005561> to set the proper channels for the bot to look in for the ``/tree`` and ``/top trees`` messages.\n\nUse </commands:1069501270454456331> to view a description of all my commands.", "info": "Silvanus is the ultimate companion bot designed for Grow A Tree, the popular Discord clicker game. With Silvanus by your side, you can take your tree-growing adventure to new heights! Enjoy customizable notifications, effortless tree height comparisons, and handy math features to optimize your gameplay.\n\n[Privacy Policy](https://assets.vfsh.dev/privacy.txt)",
"permissions": "At a minimum, Silvanus requires permissions to `Send Messages` and `Send Messages in Threads` if applicable. If Analyzer is given permission to `Manage Messages`, the bot will delete the `.settree` and `.setranks` messages to reduce spam.", "topggTitle": "Want to support Silvanus?",
"allCommands": "</setup:1065407649363005561> - You only need to run this command if your server has its ``/tree`` and ``/top trees`` messages in separate channels.\n</setupinfo:1065413032374706196> - Displays links to the current Tree and Tallest Trees messages configured in your server.\n</compare:1065346941166297128> - Sends a refreshable embed that calculcates the height difference between your tree and the trees currently displayed on your Tallest Trees message. There is also an Active Growth Indicator (``[💧]``).\n</setping:1068373237995683902> - Guild members with the ``Manage Roles`` permission can run this command to set up automatic reminders when your tree is ready to be watered.\n Type a reminder message (including any ``@pings`` desired) and select a channel to send the reminders in.\n Once this command has been run a new ``Reset Ping`` button will appear next time you refresh the </compare:1065346941166297128> message.\n Click the ``Reset Ping`` button to be sent a reminder the next time the tree is ready to be watered.\n Use </optout:1068753032801693758> to disable reminder messages for your server.\n</watertime:1066970330029113444> - Calculates the wait time between waters for a tree of a given height.\n</timetoheight:1067727254634889227> - Calculates how long it would take to go from ``beginheight`` to ``endheight``.\n</reset:1065412317052944476> - Removes your server's configuration from the database.\n</help:1065346941166297129> - Displays the bot's help page and links to each command." "topggBody": "Vote and leave a review for Silvanus [on Top.gg](https://top.gg/bot/521624335119810561)",
"setup": "For the most up to date guides and information, check out the Silvanus Support Wiki - https://silvanus.vfsh.dev/\n[Silvanus Setup Guide](https://silvanus.vfsh.dev/en/setup)\n\nNeed help and can't reach me on Discord? Send an email to SilvanusDev@gmail.com and I'll get back to you ASAP.",
"longDescription": "Silvanus, the ultimate companion bot for Grow A Tree, takes your tree-growing journey to the next level. Are you tired of cluttered channels filled with notifications? Silvanus has you covered! With its unique notification relay system, Silvanus listens for Grow A Tree's notifications in a hidden channel and sends customized, auto-deleting notifications to the channels of your choice. Customize your notifications and keep your server clean and organized.\n\nSilvanus simplifies leaderboard tree height comparisons with a simple command. No more manual calculations or guesswork. Silvanus shows you exactly how far you are from the next tree on the leaderboard, allowing you to gauge your progress effortlessly.",
"allCommands": "</compare:1077058896469966889> - Compare your tree to others on the leaderboard\n</relay set:1077322799032578152> - Setup a Notification Relay for the first time\n</relay update:1077322799032578152> - Update an already configured Notification Relay\n</relay disable:1077322799032578152> - Disable the Notification Relay\n</rolemenu:1077058896469966892> - Send a self-assignable role menu for relay pings\n</watertime:1077058896469966895> - Calculates the time between waters for a tree of a given height\n</timetoheight:1077058896469966894> - Calculates how long it would take a tree to grow to a given height\n</setup compare:1077058896469966893> - Set the channels to use with </compare:1077058896469966889>\n</setup view:1077058896469966893> - View your server's configuration\n</setup reset:1077058896469966893> - Delete your server's configuration\n</help:1077058896469966890> - Displays the bot's help page"
}, },
"commands": { "commands": {
"compare": "</compare:1065346941166297128>", "compare": "</compare:1065346941166297128>",
@ -19,7 +22,13 @@
}, },
"embeds": { "embeds": {
"footer": "Silvanus is not affiliated with Grow A Tree or Limbo Labs", "footer": "Silvanus is not affiliated with Grow A Tree or Limbo Labs",
"color": "0x55FF55", "color": 5635925,
"errorTitle": "Oops!",
"errorPrefix": "There seems to have been a problem.",
"waterColor": 5592575,
"fruitColor": 13391189,
"waterTitle": "Water Notification",
"fruitTitle": "Fruit Notification",
"roleMenuTitle": "Role Menu", "roleMenuTitle": "Role Menu",
"treeRoleMenu": [ "treeRoleMenu": [
"Use the buttons below to give yourself roles.\n\n", "Use the buttons below to give yourself roles.\n\n",
@ -44,7 +53,16 @@
"supportServer": "https://discord.gg/g5JRGn7PxU" "supportServer": "https://discord.gg/g5JRGn7PxU"
}, },
"error": { "error": {
"noGuild": "Setup has not been completed yet. Try running </setup:1065407649363005561> or </help setup:1065346941166297129>" "noGuild": "I was unable to find an entry for your server in the database. Try using the `/setup rolemenu` or `/relay` commands to generate a new entry. If that fails, try `/setup reset`, then kick me and re-add me. If you continue receiving this error, join the Support Server or DM @vfsh",
"invalidSubcommand": "Invalid subcommand detected.",
"noTreeMessage": "</tree:0> - Make sure you've sent or refreshed a Tree recently.",
"noLeaderboardMessage": "</top trees:0> - Make sure you've sent or refreshed the Tallest Trees leaderboard recently.",
"noCompareMessage": "</compare:0> - This is awkward, I've lost my own comparison message!",
"noFetchRole": "I was unable to find that role, please make sure it exists and I have access to it.",
"noGiveRole": "I was unable to give that role to you, please make sure I have permission to `Manage Roles` and that the role is below my role in the server settings role list.",
"noTakeRole": "I was unable to remove that role from you, please make sure I have permission to `Manage Roles` and that the role is below my role in the server settings role list.",
"yesGiveRole": "Successfully added the role to your profile!",
"yesTakeRole": "Successfully removed the role from your profile!"
}, },
"status": { "status": {
"treeAndLeaderboard": "Tree and leaderboard messages were both found, setup is complete. Run </setupinfo:1065413032374706196> to verify. Run </compare:1065346941166297128> to get started!", "treeAndLeaderboard": "Tree and leaderboard messages were both found, setup is complete. Run </setupinfo:1065413032374706196> to verify. Run </compare:1065346941166297128> to get started!",
@ -53,11 +71,18 @@
"missingLeaderboardMessage": "There was a problem finding the Tallest Trees message. Please make sure the ``/tree`` and ``/top trees`` messages are in this channel, or run </setup:1065407649363005561> to set the ``/tree`` and ``/top trees`` channels.", "missingLeaderboardMessage": "There was a problem finding the Tallest Trees message. Please make sure the ``/tree`` and ``/top trees`` messages are in this channel, or run </setup:1065407649363005561> to set the ``/tree`` and ``/top trees`` channels.",
"missingLeaderboardChannel": "There was a problem finding the Tallest Trees channel, was it deleted? Please make sure the ``/tree`` and ``/top trees`` messages are in this channel, or run </setup:1065407649363005561> to set the ``/tree`` and ``/top trees`` channels.", "missingLeaderboardChannel": "There was a problem finding the Tallest Trees channel, was it deleted? Please make sure the ``/tree`` and ``/top trees`` messages are in this channel, or run </setup:1065407649363005561> to set the ``/tree`` and ``/top trees`` channels.",
"missingTreeMessage": "There was a problem finding the Tree message. Please make sure the ``/tree`` and ``/top trees`` messages are this channel, or run </setup:1065407649363005561> to set the ``/tree`` and ``/top trees`` channels.", "missingTreeMessage": "There was a problem finding the Tree message. Please make sure the ``/tree`` and ``/top trees`` messages are this channel, or run </setup:1065407649363005561> to set the ``/tree`` and ``/top trees`` channels.",
"missingTreeChannel": "There was a problem finding the Tree channel, was it deleted? Please make sure the ``/tree`` and ``/top trees`` messages are in this channel, or run </setup:1065407649363005561> to set the ``/tree`` and ``/top trees`` channels." "missingTreeChannel": "There was a problem finding the Tree channel, was it deleted? Please make sure the ``/tree`` and ``/top trees`` messages are in this channel, or run </setup:1065407649363005561> to set the ``/tree`` and ``/top trees`` channels.",
"reset": "All guild configuration information has been removed from the database.",
"resetError": "There was a problem deleting your guild information, contact @voidf1sh#0420 for help.",
"noRoleMenu": "A role menu has not been created for this guild yet. Run </setup rolemenu:0> to create a role menu.",
"optout": "Notification relay has been disabled, to re-enable the relay use </relay update:0> with no options."
}, },
"notifications": { "notifications": {
"water": "is ready to be watered again!", "water": "is ready to be watered again!",
"fruit": "Fruit is appearing!" "fruit": "Fruit is appearing!"
}, },
"ids": {
"growATree": "972637072991068220"
},
"temp": {} "temp": {}
} }

18
dot-commands/_template Normal file
View File

@ -0,0 +1,18 @@
const fn = require('../modules/functions.js');
module.exports = {
name: "",
description: "",
usage: "",
permission: "devTeam", // "devTeam" or "owner"
async execute(message, commandData) {
if (fn.dotCommands.checkPermissions(this.permission, message.author.id)) {
try {
// Code Here
} catch (err) {
console.error(err);
await message.reply(fn.builders.errorEmbed("There was an error running the command."));
}
}
}
}

27
dot-commands/leave.js Normal file
View File

@ -0,0 +1,27 @@
const fn = require('../modules/functions.js');
module.exports = {
name: "leave",
description: "Leave a server",
usage: "<serverID> [<serverID>]".leave,
permission: "owner",
async execute(message, commandData) {
if (fn.dotCommands.checkPermissions(this.permission, message.author.id)) {
try {
// Code Here
const serverIds = commandData.args.split(" ");
for (let i = 0; i < serverIds.length; i++) {
const id = serverIds[i];
const guild = await message.client.guilds.fetch(id).catch(e => {
if (!(e.status === 404)) throw e;
});
await guild.leave();
await message.channel.send("Left Guild: " + id);
}
} catch (err) {
console.error(err);
await message.reply(fn.builders.errorEmbed("There was an error running the command: " + err));
}
}
}
}

23
dot-commands/message.js Normal file
View File

@ -0,0 +1,23 @@
const fn = require('../modules/functions.js');
module.exports = {
name: "message",
description: "Send a message to a server owner or server",
usage: "<serverID> <content>".message,
permission: "owner",
async execute(message, commandData) {
if (fn.dotCommands.checkPermissions(this.permission, message.author.id)) {
try {
// Code Here
args = commandData.args.split(" ");
const guildOwnerId = args.shift();
const content = args.join(" ");
const dmChannel = await message.client.users.createDM(guildOwnerId);
await dmChannel.send(content);
} catch (err) {
console.error(err);
await message.reply(fn.builders.errorEmbed("There was an error running the command: " + err));
}
}
}
}

View File

@ -0,0 +1,36 @@
const fn = require('../modules/functions.js');
const { PermissionsBitField } = require('discord.js');
module.exports = {
name: "permissions",
description: "",
usage: ".permissions",
permission: "devTeam", // "devTeam" or "owner"
async execute(message, commandData) {
if (fn.dotCommands.checkPermissions(this.permission, message.author.id)) {
try {
const me = message.guild.members.me;
const guildPerms = me.permissions;
const manageRoles = guildPerms.has(PermissionsBitField.Flags.ManageRoles);
const mentionEveryone = guildPerms.has(PermissionsBitField.Flags.MentionEveryone);
const channelPerms = me.permissionsIn(message.channel);
const viewChannel = channelPerms.has(PermissionsBitField.Flags.ViewChannel);
const sendMessages = channelPerms.has(PermissionsBitField.Flags.SendMessages);
const responseParts = [
`This is the status of my permissions in this server and this channel (<#${message.channel.id}>)`,
`**Guild Permissions**`,
`Manage Roles: ${manageRoles}`,
`Mention All Roles: ${mentionEveryone}`,
`**Channel Permissions**`,
`View Channel: ${viewChannel}`,
`Send Messages: ${sendMessages}`
];
const replyEmbed = fn.builders.embed(responseParts.join("\n"));
await message.reply(replyEmbed);
} catch (err) {
console.error(err);
await message.reply(fn.builders.errorEmbed("There was an error running the command."));
}
}
}
}

18
dot-commands/ping.js Normal file
View File

@ -0,0 +1,18 @@
const fn = require('../modules/functions.js');
module.exports = {
name: "ping",
description: "pong",
usage: "ping pong",
permission: "everyone",
async execute(message, commandData) {
if (fn.dotCommands.checkPermissions(this.permission, message.author.id)) {
try {
await message.reply("Pong!");
} catch (err) {
console.error(err);
await message.reply(fn.builders.errorEmbed("There was an error running the command."));
}
}
}
}

22
dot-commands/servers.js Normal file
View File

@ -0,0 +1,22 @@
const fn = require('../modules/functions.js');
module.exports = {
name: "servers",
description: "Get a list of servers the bot is in",
usage: ".servers",
permission: "owner",
async execute(message, commandData) {
if (fn.dotCommands.checkPermissions(this.permission, message.author.id)) {
try {
let servers = [];
const count = JSON.stringify(message.client.guilds.cache.size);
servers.push("I'm currently in " + count + " servers.");
const guilds = await message.client.guilds.cache;
await message.reply(servers.join("\n"));
} catch (err) {
console.error(err);
await message.reply(fn.builders.errorEmbed("There was an error running the command."));
}
}
}
}

24
dot-commands/setupview.js Normal file
View File

@ -0,0 +1,24 @@
const fn = require('../modules/functions.js');
module.exports = {
name: "setupview",
description: "",
usage: "",
permission: "devTeam",
async execute(message, commandData) {
// Code here...
if (fn.dotCommands.checkPermissions(this.permission, message.author.id)) {
try {
if (message.client.guildInfos.has(message.guildId)) {
let guildInfo = message.client.guildInfos.get(message.guildId);
await message.reply(fn.builders.embed(guildInfo.generateSetupInfo()));
} else {
await message.reply(fn.builders.errorEmbed("Guild doesn't exist in database!"));
}
} catch (err) {
console.error(err);
await message.reply(fn.builders.errorEmbed("There was an error running the command."));
}
}
}
}

139
main.js Normal file → Executable file
View File

@ -4,7 +4,9 @@
const dotenv = require('dotenv'); const dotenv = require('dotenv');
dotenv.config(); dotenv.config();
const token = process.env.TOKEN; const token = process.env.TOKEN;
const statusChannelId = process.env.statusChannelId; const statusChannelId = process.env.STATUSCHANNELID;
const heartbeatUrl = process.env.HEARTBEAT_URL;
const sendHeartbeat = typeof heartbeatUrl === 'string';
// Discord.JS // Discord.JS
const { Client, GatewayIntentBits, Partials, ActivityType } = require('discord.js'); const { Client, GatewayIntentBits, Partials, ActivityType } = require('discord.js');
@ -25,27 +27,43 @@ const client = new Client({
const fn = require('./modules/functions.js'); const fn = require('./modules/functions.js');
const strings = require('./data/strings.json'); const strings = require('./data/strings.json');
const dbfn = require('./modules/dbfn.js'); const dbfn = require('./modules/dbfn.js');
const isDev = process.env.isDev; const { GuildInfo } = require('./modules/CustomClasses.js');
const isDev = process.env.DEBUG === "true";
let statusChannel;
client.once('ready', async () => { client.once('ready', async () => {
await fn.collectionBuilders.slashCommands(client); fn.collectionBuilders.slashCommands(client);
fn.collectionBuilders.dotCommands(client);
fn.collectionBuilders.setvalidCommands(client);
await fn.collectionBuilders.guildInfos(client); await fn.collectionBuilders.guildInfos(client);
await fn.setupCollectors(client); await fn.collectionBuilders.messageCollectors(client);
const serverCount = client.guilds.cache.size;
// checkRateLimits();
console.log('Ready!'); console.log('Ready!');
client.user.setActivity({ name: strings.activity.name, type: ActivityType.Watching }); client.user.setActivity({ name: `${serverCount} trees grow.`, type: ActivityType.Watching });
statusChannel = await client.channels.fetch(statusChannelId);
if (isDev == 'false') { if (isDev == 'false') {
client.channels.fetch(statusChannelId).then(channel => { statusChannel.send(`${new Date().toISOString()} -- \nStartup Sequence Complete <@481933290912350209>`);
channel.send(`${new Date().toISOString()} -- \nStartup Sequence Complete <@481933290912350209>`); }
});
// Heartbeat Timer
if (sendHeartbeat) {
setInterval(() => {
fn.sendHeartbeat(heartbeatUrl);
}, 30000);
if (isDev) console.log("Heartbeat interval set.");
} else {
if (isDev) console.log("No heartbeat URL set, will not send heartbeats for uptime monitoring.");
} }
}); });
// slash-commands // slash-commands
client.on('interactionCreate', async interaction => { client.on('interactionCreate', async interaction => {
try {
if (interaction.isCommand()) { if (interaction.isCommand()) {
// if (isDev) { if (isDev) {
// console.log(interaction); // console.log(interaction);
// } }
const { commandName } = interaction; const { commandName } = interaction;
if (client.slashCommands.has(commandName)) { if (client.slashCommands.has(commandName)) {
@ -61,32 +79,123 @@ client.on('interactionCreate', async interaction => {
case 'refresh': case 'refresh':
// console.log(JSON.stringify(interaction)); // console.log(JSON.stringify(interaction));
await fn.refresh(interaction).catch(err => { await fn.refresh(interaction).catch(err => {
interaction.channel.send(fn.builders.errorEmbed(err)); interaction.channel.send(fn.builders.errorEmbed("Oops! Something went wrong!"));
}); });
break; break;
case 'deleteping': case 'deleteping':
if (interaction.message.deletable) { if (interaction.message.deletable) {
await interaction.message.delete().catch(err => { await interaction.message.delete().catch(err => {
console.error(err); // console.error(err);
}); });
} }
break; break;
case 'waterpingrole': case 'waterpingrole':
const waterPingStatus = await fn.buttonHandlers.waterPing(interaction); const waterPingStatus = await fn.buttonHandlers.waterPing(interaction);
await interaction.reply(waterPingStatus).catch(err => console.error(err)); await interaction.reply(waterPingStatus).catch(e => console.error(e));
break; break;
case 'fruitpingrole': case 'fruitpingrole':
const fruitPingStatus = await fn.buttonHandlers.fruitPing(interaction); const fruitPingStatus = await fn.buttonHandlers.fruitPing(interaction);
await interaction.reply(fruitPingStatus).catch(err => console.error(err)); await interaction.reply(fruitPingStatus).catch(e => console.error(e));
break; break;
default: default:
break; break;
} }
} }
} catch(err) {
if (err === "Guild doesn't exist in database!") {
interaction.channel.send(fn.builders.errorEmbed(strings.error.noGuild));
console.error(err);
} else {
interaction.channel.send("Oops! An error occurred... Sorry about that, please contact my owner @vfsh if this keeps happening.");
console.error(err);
}
}
}); });
client.on('messageUpdate', async (oldMessage, message) => {
await fn.messages.updateHandler(message).catch(async e => {
switch (e) {
case strings.error.noCompareMessage:
await message.channel.send(strings.error.noCompareMessage);
break;
default:
break;
}
});
});
client.on('messageCreate', async message => {
await fn.messages.updateHandler(message).catch(e => console.error(e));
// Dot Command Handling
// Some basic checking to prevent running unnecessary code
if (message.author.bot) return;
// Break the message down into its components and analyze it
const commandData = fn.dotCommands.getCommandData(message);
// if (isDev) console.log(commandData);
if (commandData.isValid && commandData.isCommand) {
try {
client.dotCommands.get(commandData.command).execute(message, commandData);
}
catch (error) {
console.error(error);
message.reply('There was an error trying to execute that command.');
}
}
return;
});
client.on('guildCreate', async guild => {
const serverCount = client.guilds.cache.size;
client.user.setActivity({ name: `${serverCount} trees grow.`, type: ActivityType.Watching });
await statusChannel.send(`I've been added to a new guild: ${guild.name} (${guild.id})`);
const guildInfo = new GuildInfo()
.setIds(guild.id, guild.ownerId);
const setBasicQuery = guildInfo.queryBuilder("setBasic");
await dbfn.setGuildInfo(setBasicQuery).catch(e => console.error(e));
});
client.on('guildDelete', async guild => {
const serverCount = client.guilds.cache.size;
client.user.setActivity({ name: `${serverCount} trees grow.`, type: ActivityType.Watching });
await statusChannel.send(`I've been removed from a guild: ${guild.name} (${guild.id})`);
if (client.guildInfos.has(guild.id)) {
let guildInfo = client.guildInfos.get(guild.id);
guildInfo.setReminders(undefined, undefined, undefined, undefined, false);
const setRemindersQuery = guildInfo.queryBuilder("setReminders");
await dbfn.setGuildInfo(setRemindersQuery);
}
});
async function checkRateLimits(hi) {
const axios = require('axios');
// Make a GET request to the Discord API
await axios.get('https://discord.com/api/v10/users/@me', {
headers: {
'Authorization': `Bot ${token}`
}
}).then(response => {
// Get the rate limit headers
const remaining = response.headers['x-ratelimit-remaining'];
const reset = response.headers['x-ratelimit-reset'];
// Log the rate limit headers
console.log(`Remaining requests: ${remaining}`);
console.log(`Reset time (Unix epoch seconds): ${reset}`);
}).catch(error => {
console.error(error);
});
await fn.sleep(500).then(async () =>{
await checkRateLimits();
})
}
process.on('unhandledRejection', error => { process.on('unhandledRejection', error => {
console.error('Unhandled promise rejection:', error); console.error('Unhandled promise rejection (pls dont break up with me):', error);
}); });
client.login(token); client.login(token);

148
modules/CustomClasses.js Normal file → Executable file
View File

@ -11,6 +11,7 @@ module.exports = {
GuildInfo: class { GuildInfo: class {
constructor() { constructor() {
this.guildId = ""; this.guildId = "";
this.ownerId = ""; // TODO Is ownerId fully implemented?
this.treeName = ""; this.treeName = "";
this.treeHeight = 0; this.treeHeight = 0;
this.treeMessageId = ""; this.treeMessageId = "";
@ -23,10 +24,14 @@ module.exports = {
this.fruitRoleId = ""; this.fruitRoleId = "";
this.reminderChannelId = ""; this.reminderChannelId = "";
this.watchChannelId = ""; this.watchChannelId = "";
this.notificationsEnabled = false;
this.compareChannelId = "";
this.compareMessageId = "";
} }
setId(id) { setIds(guildId, ownerId) {
this.guildId = id; this.guildId = guildId;
this.ownerId = ownerId === undefined ? this.ownerId : ownerId
return this; return this;
} }
setName(name) { setName(name) {
@ -38,20 +43,33 @@ module.exports = {
return this; return this;
} }
setTreeMessage(messageId, channelId) { setTreeMessage(messageId, channelId) {
this.treeMessageId = messageId; this.treeMessageId = messageId ? messageId : this.treeMessageId;
this.treeChannelId = channelId; this.treeChannelId = channelId;
return this; return this;
} }
setLeaderboardMessage(messageId, channelId) { setTreeInfo(name, height, channelId, messageId) {
this.leaderboardMessageId = messageId; this.treeName = name ? name : this.treeName;
this.leaderboardChannelId = channelId; this.treeHeight = height;
this.treeChannelId = channelId ? channelId : this.treeChannelId;
this.treeMessageId = messageId ? messageId : this.treeMessageId;
return this; return this;
} }
setReminders(waterMessage, fruitMessage, reminderChannelId, watchChannelId) { setLeaderboardMessage(messageId, channelId) {
this.waterMessage = waterMessage; this.leaderboardMessageId = messageId ? messageId : this.leaderboardMessageId;
this.fruitMessage = fruitMessage; this.leaderboardChannelId = channelId ? channelId : this.leaderboardChannelId;
this.reminderChannelId = reminderChannelId; return this;
this.watchChannelId = watchChannelId; }
setCompareMessage(channelId, messageId) {
this.compareChannelId = channelId;
this.compareMessageId = messageId;
return this;
}
setReminders(waterMessage, fruitMessage, reminderChannelId, watchChannelId, enabled) {
this.waterMessage = waterMessage === undefined ? this.waterMessage : waterMessage
this.fruitMessage = fruitMessage === undefined ? this.fruitMessage : fruitMessage;
this.reminderChannelId = reminderChannelId === undefined ? this.reminderChannelId : reminderChannelId
this.watchChannelId = watchChannelId === undefined ? this.watchChannelId : watchChannelId;
this.notificationsEnabled = enabled === undefined ? this.notificationsEnabled : enabled;
return this; return this;
} }
setRoles(waterRoleId, fruitRoleId) { setRoles(waterRoleId, fruitRoleId) {
@ -102,41 +120,125 @@ module.exports = {
break; break;
case "setReminders": case "setReminders":
queryParts = [ queryParts = [
`UPDATE guild_info SET water_message = ${db.escape(this.waterMessage)}, `, `INSERT INTO guild_info (guild_id, water_message, fruit_message, reminder_channel_id, watch_channel_id, notifications_enabled) VALUES (`,
`${db.escape(this.guildId)},`,
`${db.escape(this.waterMessage)},`,
`${db.escape(this.fruitMessage)},`,
`${db.escape(this.reminderChannelId)},`,
`${db.escape(this.watchChannelId)},`,
`${db.escape(this.notificationsEnabled)}`,
`) ON DUPLICATE KEY UPDATE water_message = ${db.escape(this.waterMessage)}, `,
`fruit_message = ${db.escape(this.fruitMessage)}, `, `fruit_message = ${db.escape(this.fruitMessage)}, `,
`reminder_channel_id = ${db.escape(this.reminderChannelId)}, `, `reminder_channel_id = ${db.escape(this.reminderChannelId)}, `,
`watch_channel_id = ${db.escape(this.watchChannelId)} `, `watch_channel_id = ${db.escape(this.watchChannelId)},`,
`WHERE guild_id = ${db.escape(this.guildId)}` `notifications_enabled = ${db.escape(this.notificationsEnabled)}`
]; ];
return queryParts.join(''); return queryParts.join('');
break; break;
case "setTreeMessage": case "setTreeMessage":
// queryParts = [
// `UPDATE guild_info SET tree_message_id = ${db.escape(this.treeMessageId)}, `,
// `tree_channel_id = ${db.escape(this.treeChannelId)} `,
// `WHERE guild_id = ${db.escape(this.guildId)}`
// ];
queryParts = [ queryParts = [
`UPDATE guild_info SET tree_message_id = ${db.escape(this.treeMessageId)}, `, `INSERT INTO guild_info (guild_id, tree_message_id, tree_channel_id)`,
`tree_channel_id = ${db.escape(this.treeChannelId)}, `, `VALUES (${db.escape(this.guildId)}, ${db.escape(this.treeMessageId)}, ${db.escape(this.treeChannelId)})`,
`WHERE guild_id = ${db.escape(this.guildId)}` `ON DUPLICATE KEY UPDATE tree_message_id = ${db.escape(this.treeMessageId)}, tree_channel_id = ${db.escape(this.treeChannelId)}`
]; ];
return queryParts.join(''); return queryParts.join('');
break; break;
case "setLeaderboardMessage": case "setLeaderboardMessage":
// queryParts = [
// `UPDATE guild_info SET leaderboard_message_id = ${db.escape(this.leaderboardMessageId)}, `,
// `leaderboard_channel_id = ${db.escape(this.leaderboardChannelId)} `,
// `WHERE guild_id = ${db.escape(this.guildId)}`
// ];
queryParts = [ queryParts = [
`UPDATE guild_info SET leaderboard_message_id = ${db.escape(this.leaderboardMessageId)}, `, `INSERT INTO guild_info (guild_id, leaderboard_message_id, leaderboard_channel_id)`,
`leaderboard_channel_id = ${db.escape(this.leaderboardChannelId)}, `, `VALUES (${db.escape(this.guildId)}, ${db.escape(this.leaderboardMessageId)}, ${db.escape(this.leaderboardChannelId)})`,
`WHERE guild_id = ${db.escape(this.guildId)}` `ON DUPLICATE KEY UPDATE leaderboard_message_id = ${db.escape(this.leaderboardMessageId)}, leaderboard_channel_id = ${db.escape(this.leaderboardChannelId)}`
]; ];
return queryParts.join(''); return queryParts.join('');
break; break;
case "setRoles": case "setRoles":
if (this.fruitRoleId != "") {
queryParts = [ queryParts = [
`UPDATE guild_info SET water_role_id = ${db.escape(this.waterRoleId)}, `, `INSERT INTO guild_info (`,
`fruit_role_id = ${db.escape(this.fruitRoleId)} `, `guild_id, water_role_id, fruit_role_id`,
`) VALUES (`,
`${db.escape(this.guildId)}, ${db.escape(this.waterRoleId)}, ${db.escape(this.fruitRoleId)}`,
`) ON DUPLICATE KEY UPDATE water_role_id = ${db.escape(this.waterRoleId)}, `,
`fruit_role_id = ${db.escape(this.fruitRoleId)}`
];
} else {
queryParts = [
`UPDATE guild_info SET water_role_id = ${db.escape(this.waterRoleId)} `,
`WHERE guild_id = ${db.escape(this.guildId)}`
];
}
return queryParts.join('');
break;
case "setTreeInfo":
queryParts = [
`INSERT INTO guild_info (`,
`guild_id, tree_name, tree_height, tree_channel_id, tree_message_id`,
`) VALUES (`,
`${db.escape(this.guildId)}, ${db.escape(this.treeName)}, ${db.escape(this.treeHeight)}, ${db.escape(this.treeChannelId)}, ${db.escape(this.treeMessageId)}`,
`) ON DUPLICATE KEY UPDATE tree_name = ${db.escape(this.treeName)}, `,
`tree_height = ${db.escape(this.treeHeight)}, `,
`tree_channel_id = ${db.escape(this.treeChannelId)}, `,
`tree_message_id = ${db.escape(this.treeMessageId)}`
];
return queryParts.join('');
case "setCompareMessage":
queryParts = [
`INSERT INTO guild_info (`,
`guild_id, compare_channel_id, compare_message_id`,
`) VALUES (`,
`${db.escape(this.guildId)}, ${db.escape(this.compareChannelId)}, ${db.escape(this.compareMessageId)}`,
`) ON DUPLICATE KEY UPDATE compare_channel_id = ${db.escape(this.compareChannelId)}, compare_message_id = ${db.escape(this.compareMessageId)}`,
];
return queryParts.join('');
// TODO This is hacked in and needs to be implemented throughout the code
case "setIds":
queryParts = [
`UPDATE guild_info SET `,
`owner_id=${db.escape(this.ownerId)} `,
`WHERE guild_id=${db.escape(this.guildId)}` `WHERE guild_id=${db.escape(this.guildId)}`
]; ];
return queryParts.join(''); return queryParts.join('');
break; case "setBasic":
queryParts = [
`INSERT INTO guild_info (`,
`guild_id, owner_id`,
`) VALUES (`,
`${db.escape(this.guildId)}, ${db.escape(this.ownerId)}`,
`) ON DUPLICATE KEY UPDATE owner_id=${db.escape(this.ownerId)}`
];
return queryParts.join('');
default: default:
break; break;
} }
} }
generateSetupInfo() {
let setupInfoParts = [
`Here is your server's configuration:`,
`Tree Name: ${this.treeName}`,
`Tree Height: ${this.treeHeight}`,
`Tree Channel: <#${this.treeChannelId}>`,
`[Tree Link](https://discord.com/channels/${this.guildId}/${this.treeChannelId}/${this.treeMessageId})`,
`Leaderboard Channel: <#${this.leaderboardChannelId}>`,
`[Leaderboard Link](https://discord.com/channels/${this.guildId}/${this.leaderboardChannelId}/${this.leaderboardMessageId})`,
`Notification Relay Enabled: ${this.notificationsEnabled}`,
`Notification Watch Channel: <#${this.watchChannelId}>`,
`Notification Relay Channel: <#${this.reminderChannelId}>`,
`Water Message: ${this.waterMessage}`,
`Fruit Message: ${this.fruitMessage}`,
`Water Role: <@&${this.waterRoleId}>`,
`Fruit Role: <@&${this.fruitRoleId}>`
]
return setupInfoParts.join('\n');
}
} }
} }

0
modules/_clear-commands.js Normal file → Executable file
View File

0
modules/_deploy-commands.js Normal file → Executable file
View File

24
modules/_deploy-global.js Normal file → Executable file
View File

@ -22,9 +22,24 @@ console.log(`Token: ${token} Client ID: ${clientId}`);
const rest = new REST({ version: '9' }).setToken(token); const rest = new REST({ version: '9' }).setToken(token);
(async () => { async function deleteCommands() {
try { try {
console.log('Started refreshing application (/) commands.'); console.log('Started deleting application (/) commands.');
await rest.put(
Routes.applicationCommands(clientId),
{ body: "" },
);
console.log('Successfully deleted application (/) commands.');
} catch (error) {
console.error(error);
}
}
async function uploadCommands() {
try {
console.log('Started reloading application (/) commands.');
await rest.put( await rest.put(
Routes.applicationCommands(clientId), Routes.applicationCommands(clientId),
@ -36,4 +51,9 @@ const rest = new REST({ version: '9' }).setToken(token);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
}
(async () => {
// await deleteCommands();
await uploadCommands();
})(); })();

48
modules/_prepareStrings.js Normal file → Executable file
View File

@ -1,33 +1,29 @@
/* const commands = [
</setup:1065407649363005561> {raw: "`/compare`", clickable: "</compare:1077058896469966889>"},
</setupinfo:1065413032374706196> {raw: "`/relay set`", clickable: "</relay set:1077322799032578152>"},
</compare:1065346941166297128> {raw: "`/relay update`", clickable: "</relay update:1077322799032578152>"},
</setping:1068373237995683902> {raw: "`/relay disable`", clickable: "</relay disable:1077322799032578152>"},
</optout:1068753032801693758> {raw: "`/rolemenu`", clickable: "</rolemenu:1077058896469966892>"},
</watertime:1066970330029113444> {raw: "`/watertime`", clickable: "</watertime:1077058896469966895>"},
</timetoheight:1067727254634889227> {raw: "`/timetoheight`", clickable: "</timetoheight:1077058896469966894>"},
</reset:1065412317052944476> {raw: "`/setup compare`", clickable: "</setup compare:1077058896469966893>"},
</help:1065346941166297129> {raw: "`/setup view`", clickable: "</setup view:1077058896469966893>"},
</tree:972648557796524032> {raw: "`/setup reset`", clickable: "</setup reset:1077058896469966893>"},
</top trees:1051840665362894950> {raw: "`/help`", clickable: "</help:1077058896469966890>"},
*/ {raw: "`/tree`", clickable: "</tree:972648557796524032>"},
{raw: "`/top trees`", clickable: "</top trees:1051840665362894950>"},
{raw: "`/commands`", clickable: "</commands:1077058896469966888>"}
];
const fs = require('fs'); const fs = require('fs');
const replaceAll = require('string.prototype.replaceall'); const replaceAll = require('string.prototype.replaceall');
const string = fs.readFileSync('./data/rawstring.txt').toString(); const path = "./modules/input.txt";
const string = fs.readFileSync(path).toString();
let newString = replaceAll(string, '\* ', ''); let newString = replaceAll(string, '\* ', '');
newString = replaceAll(newString, '\n', '\\n'); newString = replaceAll(newString, '\n', '\\n');
newString = replaceAll(newString, '\t', ' - '); newString = replaceAll(newString, '\t', ' - ');
newString = replaceAll(newString, '`/setup`', '</setup:1065407649363005561>'); commands.forEach(command => {
newString = replaceAll(newString, '`/setupinfo`', '</setupinfo:1065413032374706196>'); newString = replaceAll(newString, command.raw, command.clickable);
newString = replaceAll(newString, '`/compare`', '</compare:1065346941166297128>'); });
newString = replaceAll(newString, '`/setping`', '</setping:1068373237995683902>');
newString = replaceAll(newString, '`/optout`', '</optout:1068753032801693758>');
newString = replaceAll(newString, '`/watertime`', '</watertime:1066970330029113444>');
newString = replaceAll(newString, '`/timetoheight`', '</timetoheight:1067727254634889227>');
newString = replaceAll(newString, '`/reset`', '</reset:1065412317052944476>');
newString = replaceAll(newString, '`/help`', '</help:1065346941166297129>');
newString = replaceAll(newString, '`/commands`', '</commands:1069501270454456331>');
newString = replaceAll(newString, '`', '``'); newString = replaceAll(newString, '`', '``');
fs.writeFileSync('./data/rawstring.txt', newString); fs.writeFileSync(path, newString);
return "Done"; return "Done";

90
modules/dbfn.js Normal file → Executable file
View File

@ -56,20 +56,16 @@ module.exports = {
db.end(); db.end();
return; return;
} }
if (res.length == 0) {
reject("There is no database entry for your guild yet. Try running /setup");
db.end();
return;
}
row = res[0]; row = res[0];
const guildInfo = new GuildInfo() const guildInfo = new GuildInfo()
.setId(row.guild_id) .setIds(row.guild_id, row.owner_id)
.setName(row.tree_name) .setName(row.tree_name)
.setHeight(row.tree_height) .setHeight(row.tree_height)
.setTreeMessage(row.tree_message_id, row.tree_channel_id) .setTreeMessage(row.tree_message_id, row.tree_channel_id)
.setLeaderboardMessage(row.leaderboard_message_id, row.leaderboard_channel_id) .setLeaderboardMessage(row.leaderboard_message_id, row.leaderboard_channel_id)
.setReminders(row.water_message, row.fruit_message, row.reminder_channel_id, row.watch_channel_id) .setReminders(row.water_message, row.fruit_message, row.reminder_channel_id, row.watch_channel_id, row.notifications_enabled)
.setRoles(row.water_role_id, row.fruit_role_id); .setRoles(row.water_role_id, row.fruit_role_id)
.setCompareMessage(row.compare_channel_id, row.compare_message_id);
db.end(); db.end();
resolve(guildInfo); resolve(guildInfo);
}); });
@ -106,15 +102,17 @@ module.exports = {
for (let i = 0; i < res.length; i++) { for (let i = 0; i < res.length; i++) {
let row = res[i]; let row = res[i];
guildInfos.push(new GuildInfo() guildInfos.push(new GuildInfo()
.setId(row.guild_id) .setIds(row.guild_id, row.owner_id)
.setName(row.tree_name) .setName(row.tree_name)
.setHeight(row.tree_height) .setHeight(row.tree_height)
.setTreeMessage(row.tree_message_id, row.tree_channel_id) .setTreeMessage(row.tree_message_id, row.tree_channel_id)
.setLeaderboardMessage(row.leaderboard_message_id, row.leaderboard_channel_id) .setLeaderboardMessage(row.leaderboard_message_id, row.leaderboard_channel_id)
.setReminders(row.water_message, row.fruit_message, row.reminder_channel_id, row.watch_channel_id) .setReminders(row.water_message, row.fruit_message, row.reminder_channel_id, row.watch_channel_id, row.notifications_enabled)
.setRoles(row.water_role_id, row.fruit_role_id) .setRoles(row.water_role_id, row.fruit_role_id)
.setCompareMessage(row.compare_channel_id, row.compare_message_id)
); );
} }
// console.log(res.length + " // " + guildInfos.length);
db.end(); db.end();
resolve(guildInfos); resolve(guildInfos);
@ -141,10 +139,41 @@ module.exports = {
return; return;
} }
db.end(); db.end();
// console.log("Updated the database");
resolve(); resolve();
}); });
}); });
}, },
setTreeInfo(guildInfo) {
const db = mysql.createConnection({
host: process.env.DBHOST,
user: process.env.DBUSER,
password: process.env.DBPASS,
database: process.env.DBNAME,
port: process.env.DBPORT
});
db.connect((err) => {
if (err) throw `Error connecting to the database: ${err.message}`;
});
// Returns a Promise, resolve({ "status": "", "data": null })
// guildInfo = { "guildId": "123", "treeName": "name", "treeHeight": 123, "treeMessageId": "123", "treeChannelId": "123", "leaderboardMessageId": "123", "leaderboardChannelId": "123"}
// Set a server's tree information in the database)
const insertGuildInfoQuery = `INSERT INTO guild_info (guild_id, tree_name, tree_height, tree_message_id, tree_channel_id) VALUES (${db.escape(guildInfo.guildId)}, ${db.escape(guildInfo.treeName)}, ${db.escape(guildInfo.treeHeight)},${db.escape(guildInfo.treeMessageId)}, ${db.escape(guildInfo.treeChannelId)}) ON DUPLICATE KEY UPDATE tree_name = ${db.escape(guildInfo.treeName)},tree_height = ${db.escape(guildInfo.treeHeight)},tree_message_id = ${db.escape(guildInfo.treeMessageId)},tree_channel_id = ${db.escape(guildInfo.treeChannelId)}`;
// TODO run this query and return a promise, then resolve with { "status": , "data": null }
return new Promise((resolve, reject) => {
db.query(insertGuildInfoQuery, (err, res) => {
if (err) {
console.error(err);
db.end();
reject("Error setting the guild info: " + err.message);
return;
}
db.end();
console.log("Updated the database");
resolve({ "status": "Successfully set the guild information", "data": null });
});
});
},
setLeaderboardInfo(guildInfo) { setLeaderboardInfo(guildInfo) {
const db = mysql.createConnection({ const db = mysql.createConnection({
host: process.env.DBHOST, host: process.env.DBHOST,
@ -269,46 +298,5 @@ module.exports = {
resolve({ "status": "Successfully uploaded the leaderboard", "data": res }); resolve({ "status": "Successfully uploaded the leaderboard", "data": res });
}); });
}); });
},
get24hTree(guildId, treeName) {
const db = mysql.createConnection({
host: process.env.DBHOST,
user: process.env.DBUSER,
password: process.env.DBPASS,
database: process.env.DBNAME,
port: process.env.DBPORT
});
db.connect((err) => {
if (err) throw `Error connecting to the database: ${err.message}`;
});
// Returns a Promise, resolve({ "status": "", "data": leaderboard })
const select24hTreeQuery = `SELECT id, tree_name, tree_rank, tree_height, has_pin FROM leaderboard WHERE guild_id = ${db.escape(guildId)} AND tree_name = ${db.escape(treeName)} AND timestamp > date_sub(now(), interval 1 day) ORDER BY id ASC LIMIT 1`;
// TODO run the query and return a promise then process the results. resolve with { "status": , "data": leaderboard }
return new Promise((resolve, reject) => {
db.query(select24hTreeQuery, (err, res) => {
if (err) {
console.error(err);
db.end();
reject("Error fetching the historic 24hr tree height: " + err.message);
return;
}
let hist24hTree = {};
if (res.length > 0) {
hist24hTree = {
"treeName": res[0].tree_name,
"treeRank": res[0].tree_rank,
"treeHeight": res[0].tree_height,
"hasPin": res[0].has_pin
}
} else {
hist24hTree = {
}
}
db.end();
resolve({ "status": "Successfully fetched historic 24hr tree.", "data": hist24hTree });
});
});
} }
}; };

711
modules/functions.js Normal file → Executable file
View File

@ -2,7 +2,7 @@
// dotenv for handling environment variables // dotenv for handling environment variables
const dotenv = require('dotenv'); const dotenv = require('dotenv');
dotenv.config(); dotenv.config();
const isDev = process.env.isDev; const isDev = process.env.DEBUG === "true";
const package = require('../package.json'); const package = require('../package.json');
// filesystem // filesystem
@ -17,8 +17,10 @@ const { GuildInfo } = require('./CustomClasses');
const config = require('../data/config.json'); const config = require('../data/config.json');
const strings = require('../data/strings.json'); const strings = require('../data/strings.json');
const slashCommandFiles = fs.readdirSync('./slash-commands/').filter(file => file.endsWith('.js')); const slashCommandFiles = fs.readdirSync('./slash-commands/').filter(file => file.endsWith('.js'));
const dotCommandFiles = fs.readdirSync('./dot-commands/').filter(file => file.endsWith('.js'));
const dbfn = require('./dbfn.js'); const dbfn = require('./dbfn.js');
const { finished } = require('stream'); const { finished } = require('stream');
const https = require('https');
const functions = { const functions = {
// Functions for managing and creating Collections // Functions for managing and creating Collections
@ -35,6 +37,44 @@ const functions = {
} }
if (isDev) console.log('Slash Commands Collection Built'); if (isDev) console.log('Slash Commands Collection Built');
}, },
dotCommands(client) {
// If the dotCommands collection doesn't exist already, create it
if (!client.dotCommands) client.dotCommands = new Discord.Collection();
// Empty the dotcommands collection
client.dotCommands.clear();
// Iterate over each file within ./dot-commands that ends with .js
for (const file of dotCommandFiles) {
// Pull the file into a temporary variable
const dotCommand = require(`../dot-commands/${file}`);
// Create a new entry in the collection with the key of the command name, value of the command itself
client.dotCommands.set(dotCommand.name, dotCommand);
// Old code from NodBot to implement aliases for dot commands. Unused as of 5/16/23
// if (Array.isArray(dotCommand.alias)) {
// dotCommand.alias.forEach(element => {
// client.dotCommands.set(element, dotCommand);
// });
// } else if (dotCommand.alias != undefined) {
// client.dotCommands.set(dotCommand.alias, dotCommand);
// }
}
if (isDev) console.log('Dot Commands Collection Built');
},
setvalidCommands(client) {
// Iterate over every entry in the dotCommands collection
for (const entry of client.dotCommands.map(command => command)) {
// Add the command's name to the valid commands list for validation later
config.validCommands.push(entry.name);
// Old code from NodBot to implement aliases for dot commands. Unused as of 5/16/23
// if (Array.isArray(entry.alias)) {
// entry.alias.forEach(element => {
// config.validCommands.push(element);
// });
// } else if (entry.alias != undefined) {
// config.validCommands.push(entry.alias);
// }
}
if (isDev) console.log(`Valid Commands Added to Config\n${config.validCommands}`);
},
async guildInfos(client) { async guildInfos(client) {
const guildInfos = await dbfn.getAllGuildInfos(); const guildInfos = await dbfn.getAllGuildInfos();
if (!client.guildInfos) client.guildInfos = new Discord.Collection(); if (!client.guildInfos) client.guildInfos = new Discord.Collection();
@ -43,6 +83,27 @@ const functions = {
client.guildInfos.set(guildInfo.guildId, guildInfo); client.guildInfos.set(guildInfo.guildId, guildInfo);
} }
return 'guildInfos Collection Built'; return 'guildInfos Collection Built';
},
async messageCollectors(client) {
// Create an empty collection for MessageCollectors
if (!client.messageCollectors) client.messageCollectors = new Discord.Collection();
client.messageCollectors.clear();
// Get all of the guild infos from the client
const { guildInfos, messageCollectors } = client;
// Iterate over each guild info
await guildInfos.forEach(async guildInfo => {
await functions.collectors.create(client, guildInfo).catch(async e => {
if (e === "ERRNOGUILD") {
guildInfo.setReminders(undefined, undefined, undefined, undefined, false);
const query = guildInfo.queryBuilder("setReminders");
await dbfn.setGuildInfo(query);
await functions.collectionBuilders.guildInfos(client);
console.log("Disabled notification relay for a guild I'm no longer in: " + guildInfo.guildId);
} else {
throw e;
}
});
});
} }
}, },
builders: { builders: {
@ -70,12 +131,12 @@ const functions = {
); );
return refreshActionRow; return refreshActionRow;
}, },
treeRoleMenu() { treeRoleMenu(fruit) {
return new ActionRowBuilder() let actionRow = new ActionRowBuilder().addComponents(this.buttons.waterPing());
.addComponents( if (fruit) {
this.buttons.waterPing(), actionRow.addComponents(this.buttons.fruitPing());
this.buttons.fruitPing() }
); return actionRow;
}, },
buttons: { buttons: {
acceptRules() { acceptRules() {
@ -100,10 +161,10 @@ const functions = {
}, },
embeds: { embeds: {
treeRoleMenu(guildInfo) { treeRoleMenu(guildInfo) {
const actionRow = functions.builders.actionRows.treeRoleMenu(); const actionRow = functions.builders.actionRows.treeRoleMenu(guildInfo.fruitRoleId == "" ? false : true);
let tempStrings = strings.embeds.treeRoleMenu; let tempStrings = strings.embeds.treeRoleMenu;
let description = tempStrings[0] + tempStrings[1] + `<@&${guildInfo.waterRoleId}>` + tempStrings[2]; let description = tempStrings[0] + tempStrings[1] + `<@&${guildInfo.waterRoleId}>` + tempStrings[2];
if (guildInfo.fruitRoleId != undefined) { if (guildInfo.fruitRoleId != "") {
description += tempStrings[3] + `<@&${guildInfo.fruitRoleId}>` + tempStrings[4]; description += tempStrings[3] + `<@&${guildInfo.fruitRoleId}>` + tempStrings[4];
} }
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
@ -112,6 +173,16 @@ const functions = {
.setDescription(description) .setDescription(description)
.setFooter({ text: strings.embeds.roleMenuFooter }); .setFooter({ text: strings.embeds.roleMenuFooter });
return { embeds: [embed], components: [actionRow] }; return { embeds: [embed], components: [actionRow] };
},
information(content, fields) {
const embed = new EmbedBuilder()
.setColor(strings.embeds.color)
.setTitle('Information')
.setDescription(content)
.setFooter({ text: `v${package.version} - ${strings.embeds.footer}` });
if (fields) embed.addFields(fields);
const messageContents = { embeds: [embed], ephemeral: true };
return messageContents;
} }
}, },
comparisonEmbed(content, guildInfo) { comparisonEmbed(content, guildInfo) {
@ -124,11 +195,21 @@ const functions = {
const messageContents = { embeds: [embed], components: [this.actionRows.comparisonActionRow(guildInfo)] }; const messageContents = { embeds: [embed], components: [this.actionRows.comparisonActionRow(guildInfo)] };
return messageContents; return messageContents;
}, },
reminderEmbed(content, guildInfo) { waterReminderEmbed(content, guildInfo) {
// Create the embed using the content passed to this function // Create the embed using the content passed to this function
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor(strings.embeds.color) .setColor(strings.embeds.waterColor)
.setTitle('Notification Relay') .setTitle(strings.embeds.waterTitle)
.setDescription(`[Click here to go to your Tree](https://discord.com/channels/${guildInfo.guildId}/${guildInfo.treeChannelId}/${guildInfo.treeMessageId})`)
.setFooter({ text: `Click ♻️ to delete this message` });
const messageContents = { content: content, embeds: [embed], components: [this.actionRows.reminderActionRow()] };
return messageContents;
},
fruitReminderEmbed(content, guildInfo) {
// Create the embed using the content passed to this function
const embed = new EmbedBuilder()
.setColor(strings.embeds.fruitColor)
.setTitle(strings.embeds.fruitTitle)
.setDescription(`[Click here to go to your Tree](https://discord.com/channels/${guildInfo.guildId}/${guildInfo.treeChannelId}/${guildInfo.treeMessageId})`) .setDescription(`[Click here to go to your Tree](https://discord.com/channels/${guildInfo.guildId}/${guildInfo.treeChannelId}/${guildInfo.treeMessageId})`)
.setFooter({ text: `Click ♻️ to delete this message` }); .setFooter({ text: `Click ♻️ to delete this message` });
const messageContents = { content: content, embeds: [embed], components: [this.actionRows.reminderActionRow()] }; const messageContents = { content: content, embeds: [embed], components: [this.actionRows.reminderActionRow()] };
@ -144,11 +225,24 @@ const functions = {
const messageContents = { embeds: [embed], ephemeral: privateBool }; const messageContents = { embeds: [embed], ephemeral: privateBool };
return messageContents; return messageContents;
}, },
aboutEmbed(private) {
const embed = new EmbedBuilder()
.setColor(strings.embeds.color)
.setTitle(strings.help.aboutTitle)
.setDescription(`${strings.help.info}\n\n${strings.help.longDescription}`)
.setFooter({ text: `v${package.version} - ${strings.embeds.footer}` });
embed.addFields([
{ name: strings.help.topggTitle, value: strings.help.topggBody}
]);
const privateBool = private == 'true';
const messageContents = { embeds: [embed], ephemeral: privateBool };
return messageContents;
},
errorEmbed(content) { errorEmbed(content) {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor(0xFF0000) .setColor(0xFF0000)
.setTitle('Error!') .setTitle(strings.embeds.errorTitle)
.setDescription("Error: " + content) .setDescription(`${strings.embeds.errorPrefix}\n${content}`)
.setFooter({ text: `v${package.version} - ${strings.embeds.footer}` }); .setFooter({ text: `v${package.version} - ${strings.embeds.footer}` });
const messageContents = { embeds: [embed], ephemeral: true }; const messageContents = { embeds: [embed], ephemeral: true };
return messageContents; return messageContents;
@ -163,6 +257,55 @@ const functions = {
return messageContents; return messageContents;
} }
}, },
dotCommands: {
getCommandData(message) {
const commandData = {};
// Split the message content at the final instance of a period
const finalPeriod = message.content.lastIndexOf('.');
// if(isDev) console.log(message.content);
// If the final period is the last character, or doesn't exist
if (finalPeriod < 0) {
// if (isDev) console.log(finalPeriod);
commandData.isCommand = false;
return commandData;
}
commandData.isCommand = true;
// Get the first part of the message, everything leading up to the final period
commandData.args = message.content.slice(0, finalPeriod);
// Get the last part of the message, everything after the final period
commandData.command = message.content.slice(finalPeriod).replace('.', '').toLowerCase();
commandData.author = `${message.author.username}#${message.author.discriminator}`;
return this.checkCommand(commandData);
},
checkCommand(commandData) {
if (commandData.isCommand) {
const validCommands = require('../data/config.json').validCommands;
commandData.isValid = validCommands.includes(commandData.command);
// Add exceptions for messages that contain only a link
if (commandData.args.startsWith('http')) commandData.isValid = false;
}
else {
commandData.isValid = false;
console.error('Somehow a non-command made it to checkCommands()');
}
return commandData;
},
checkPermissions(type, userId) {
if (type === "devTeam") {
const { devTeamIds } = config;
let matchFound = false;
for (let i = 0; i < devTeamIds.length; i++) {
if (userId === devTeamIds[i]) matchFound = true;
}
return matchFound;
} else if (type === "owner") {
return config.ownerId === userId;
} else if (type === "everyone") {
return true;
}
}
},
rankings: { rankings: {
parse(interaction, guildInfo) { parse(interaction, guildInfo) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -229,7 +372,7 @@ const functions = {
}); });
}).catch(err => { }).catch(err => {
reject(strings.status.missingLeaderboardMessage); reject(strings.status.missingLeaderboardMessage);
console.error(err); // console.error(err);
return; return;
}); });
}).catch(err => { }).catch(err => {
@ -244,9 +387,9 @@ const functions = {
}); });
}, },
async compare(interaction, guildInfo) { async compare(guildInfo) {
try { try {
const getLeaderboardResponse = await dbfn.getLeaderboard(interaction.guildId); const getLeaderboardResponse = await dbfn.getLeaderboard(guildInfo.guildId);
const leaderboard = getLeaderboardResponse.data; // [ { treeName: "Name", treeHeight: 1234.5, treeRank: 67 }, {...}, {...} ] const leaderboard = getLeaderboardResponse.data; // [ { treeName: "Name", treeHeight: 1234.5, treeRank: 67 }, {...}, {...} ]
// Prepare the beginning of the comparison message // Prepare the beginning of the comparison message
@ -293,7 +436,6 @@ const functions = {
}, },
tree: { tree: {
parse(interaction, guildInfo) { parse(interaction, guildInfo) {
let input;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (guildInfo == undefined) { if (guildInfo == undefined) {
reject(`The guild entry hasn't been created yet. [${interaction.guildId || interaction.commandGuildId}]`); reject(`The guild entry hasn't been created yet. [${interaction.guildId || interaction.commandGuildId}]`);
@ -306,6 +448,7 @@ const functions = {
reject("This doesn't appear to be a valid ``/tree`` message."); reject("This doesn't appear to be a valid ``/tree`` message.");
return; return;
} }
let input;
input = m.embeds[0].data.description; input = m.embeds[0].data.description;
let treeName = m.embeds[0].data.title; let treeName = m.embeds[0].data.title;
let lines = input.split('\n'); let lines = input.split('\n');
@ -316,12 +459,12 @@ const functions = {
}); });
}).catch(err => { }).catch(err => {
reject(strings.status.missingTreeMessage); reject(strings.status.missingTreeMessage);
console.error(err); // console.error(err);
return; return;
}); });
}).catch(err => { }).catch(err => {
reject(strings.status.missingTreeChannel); reject(strings.status.missingTreeChannel);
console.error(err); // console.error(err);
return; return;
}); });
} else { } else {
@ -462,12 +605,184 @@ const functions = {
}, },
isTree(message) { isTree(message) {
if (message.embeds.length > 0) { if (message.embeds.length > 0) {
return message.embeds[0].data.description.includes("Your tree is"); // Grab the description and title
const { description, title } = message.embeds[0].data;
// Make sure it's a tree message
if (description.includes("Your tree is")) {
// Grab the name
const treeName = title;
// Grab the tree's height
const indices = [description.indexOf("Your tree is ") + 13, description.indexOf("ft")];
const treeHeightStr = description.slice(indices[0], indices[1]);
const treeHeightFloat = parseFloat(treeHeightStr).toFixed(1);
// Return the info gathered
return {
treeName: treeName,
treeHeight: treeHeightFloat
};
} else {
return false;
}
} else {
return false;
} }
}, },
// Checks if a message is a leaderboard message, and returns the entries if it is
isLeaderboard(message) { isLeaderboard(message) {
if (message.embeds.length > 0) { if (message.embeds.length > 0) {
return message.embeds[0].data.title == "Tallest Trees"; // Grab the description and title from the embed
const { title, description } = message.embeds[0].data;
// Check that it's a Top Trees message
if (title == "Tallest Trees") {
// Break the description into an array of each line
const lines = description.split("\n");
const leaderboard = {
guildId: message.guildId,
entries: []
};
lines.forEach(line => {
// Skeleton entry:
const entry = {
treeRank: 0,
treeName: "",
treeHeight: 0,
hasPin: 0
}
// Break the line into parts separated by a hyphen -
// DO NOT USE this, it breaks when any tree name contains " - " which
// isn't uncommon apparently
// const parts = line.split(" - ");
// Instead, find the indices of the first and last instances of " - "
const hyphenIndices = [line.indexOf(" - "), line.lastIndexOf(" - ")];
const parts = [
line.slice(0, hyphenIndices[0]),
line.slice(hyphenIndices[0] + 3, hyphenIndices[1]),
line.slice(hyphenIndices[1] + 3, line.length)
];
//
// Grab the rank
// Preset the indices to split the lines to get the data
const rankIndices = [parts[0].indexOf("#") + 1, parts[0].lastIndexOf("``")];
const nameIndices = [parts[1].indexOf("``") + 2, parts[1].lastIndexOf("``")];
const heightIndices = [0, parts[2].lastIndexOf("ft")];
// Set the data from the parts using the indices
entry.treeRank = parts[0].slice(...rankIndices);
entry.treeName = parts[1].slice(...nameIndices);
entry.treeHeight = parts[2].slice(...heightIndices);
// Go back and check for the first 3 as they use emojis instead of numbers, this will overwrite above
if (line.includes("🥇")) entry.treeRank = 1;
if (line.includes("🥈")) entry.treeRank = 2;
if (line.includes("🥉")) entry.treeRank = 3;
// Check for the pin showing ownership
if (line.includes("📍")) entry.hasPin = 1;
// Add the entry to the array
leaderboard.entries.push(entry);
});
return leaderboard;
} else {
return false;
}
} else {
return false;
}
},
async updateHandler(message) {
if (message.partial) {
message = await message.fetch().catch(e => {
throw e;
});
}
// Make sure the message is from Grow A Tree
if (message.author.id != strings.ids.growATree) return;
// Check and store the message types
const isLeaderboard = this.isLeaderboard(message);
const isTree = this.isTree(message);
// Check if the message is a leaderboard
if (isLeaderboard) {
if (isDev) console.log(`LU: ${message.guild.name}`);
let guildInfo;
let doDbUpdate = false;
if (message.client.guildInfos.has(message.guildId)) {
guildInfo = message.client.guildInfos.get(message.guildId);
if ((guildInfo.leaderboardChannelId != message.channel.id) || (guildInfo.leaderboardMessageId != message.id)) {
guildInfo.setLeaderboardMessage(message.id, message.channel.id);
doDbUpdate = true;
}
} else {
guildInfo = new GuildInfo().setIds(message.guildId, message.guild.ownerId)
.setLeaderboardMessage(message.id, message.channel.id);
doDbUpdate = true;
}
if (doDbUpdate) {
const query = guildInfo.queryBuilder("setLeaderboardMessage");
await dbfn.setGuildInfo(query);
await functions.collectionBuilders.guildInfos(message.client);
}
await dbfn.uploadLeaderboard(isLeaderboard);
// Update the comparison message
// Check for valid message IDs
if (guildInfo.treeMessageId === "") throw strings.error.noTreeMessage;
if (guildInfo.leaderboardMessageId === "") throw strings.error.noLeaderboardMessage;
if (guildInfo.compareMessageId === "") throw strings.error.noCompareMessage;
// Fetch the messages
const { guild } = message;
// Tree
const treeChannel = await guild.channels.fetch(guildInfo.treeChannelId);
const treeMessage = await treeChannel.messages.fetch(guildInfo.treeMessageId);
// Leaderboard
const leaderboardChannel = await guild.channels.fetch(guildInfo.leaderboardChannelId);
const leaderboardMessage = await leaderboardChannel.messages.fetch(guildInfo.leaderboardMessageId);
// Comparison
const compareChannel = await guild.channels.fetch(guildInfo.compareChannelId);
const compareMessage = await compareChannel.messages.fetch(guildInfo.compareMessageId);
// Update the tree information
// Make sure we have a tree message and parse it
const isTree = this.isTree(treeMessage);
if (!isTree) throw "Tree message isn't actually a tree message!";
guildInfo.setTreeInfo(isTree.treeName, isTree.treeHeight);
// Grab the leaderboard
// Make sure it's a leaderboard and parse it
// const isLeaderboard = this.messages.isLeaderboard(leaderboardMessage);
// if (!isLeaderboard) throw "Leaderboard message isn't actually a leaderboard message!";
// Upload the leaderboard
// await dbfn.uploadLeaderboard(isLeaderboard);
// Build the string that shows the comparison // TODO Move the string building section to fn.builders?
const comparedRankings = await functions.rankings.compare(guildInfo);
const embed = functions.builders.comparisonEmbed(comparedRankings, guildInfo);
await compareMessage.edit(embed).catch(e => console.error(e));
} else if (isTree) {
// Check if the message is a tree
// if (isDev) console.log(`TU: ${isTree.treeName}: ${isTree.treeHeight}ft`);
let guildInfo;
let doDbUpdate = false;
if (message.client.guildInfos.has(message.guildId)) {
guildInfo = message.client.guildInfos.get(message.guildId);
if ((guildInfo.treeName != isTree.treeName) || (guildInfo.treeHeight != isTree.treeHeight)) {
guildInfo.setTreeInfo(isTree.treeName, isTree.treeHeight, message.channel.id, message.id);
doDbUpdate = true;
}
} else {
guildInfo = new GuildInfo().setIds(message.guildId, message.guild.ownerId)
.setTreeInfo(isTree.treeName, isTree.treeHeight, message.channel.id, message.id);
doDbUpdate = true;
}
if (doDbUpdate) {
const query = guildInfo.queryBuilder("setTreeInfo");
await dbfn.setGuildInfo(query);
await functions.collectionBuilders.guildInfos(message.client);
}
} }
} }
}, },
@ -475,14 +790,24 @@ const functions = {
async fruitPing(interaction) { async fruitPing(interaction) {
if (interaction.client.guildInfos.has(interaction.guildId)) { if (interaction.client.guildInfos.has(interaction.guildId)) {
let guildInfo = interaction.client.guildInfos.get(interaction.guildId); let guildInfo = interaction.client.guildInfos.get(interaction.guildId);
const role = await functions.roles.fetchRole(interaction.guild, guildInfo.fruitRoleId);
let status = "No Changes Made"; let status = "No Changes Made";
let errorFlag = false;
const role = await functions.roles.fetchRole(interaction.guild, guildInfo.fruitRoleId).catch(e => {
errorFlag = true;
status = strings.error.noFetchRole;
});
if (interaction.member.roles.cache.some(role => role.id == guildInfo.fruitRoleId)) { if (interaction.member.roles.cache.some(role => role.id == guildInfo.fruitRoleId)) {
await functions.roles.takeRole(interaction.member, role); await functions.roles.takeRole(interaction.member, role).catch(e => {
status = "Removed the fruit role."; errorFlag = true;
status = strings.error.noTakeRole;
});
if (!errorFlag) status = strings.error.yesTakeRole;
} else { } else {
await functions.roles.giveRole(interaction.member, role); await functions.roles.giveRole(interaction.member, role).catch(e => {
status = "Added the fruit role."; errorFlag = true;
status = strings.error.noGiveRole;
});
if (!errorFlag) status = strings.error.yesGiveRole;
} }
return functions.builders.embed(status); return functions.builders.embed(status);
} else { } else {
@ -493,13 +818,23 @@ const functions = {
if (interaction.client.guildInfos.has(interaction.guildId)) { if (interaction.client.guildInfos.has(interaction.guildId)) {
let guildInfo = interaction.client.guildInfos.get(interaction.guildId); let guildInfo = interaction.client.guildInfos.get(interaction.guildId);
let status = "No Changes Made"; let status = "No Changes Made";
const role = await functions.roles.fetchRole(interaction.guild, guildInfo.waterRoleId); let errorFlag = false;
const role = await functions.roles.fetchRole(interaction.guild, guildInfo.waterRoleId).catch(e => {
errorFlag = true;
status = strings.error.noFetchRole;
});
if (interaction.member.roles.cache.some(role => role.id == guildInfo.waterRoleId)) { if (interaction.member.roles.cache.some(role => role.id == guildInfo.waterRoleId)) {
await functions.roles.takeRole(interaction.member, role); await functions.roles.takeRole(interaction.member, role).catch(e => {
status = "Removed the water role."; errorFlag = true;
status = strings.error.noTakeRole;
});
if (!errorFlag) status = strings.error.yesTakeRole;
} else { } else {
await functions.roles.giveRole(interaction.member, role); await functions.roles.giveRole(interaction.member, role).catch(e => {
status = "Added the water role."; errorFlag = true;
status = strings.error.noGiveRole;
});
if (!errorFlag) status = strings.error.yesGiveRole;
} }
return functions.builders.embed(status); return functions.builders.embed(status);
} else { } else {
@ -509,38 +844,120 @@ const functions = {
}, },
roles: { roles: {
async fetchRole(guild, roleId) { async fetchRole(guild, roleId) {
return await guild.roles.fetch(roleId).catch(err => console.error("Error fetching the role: " + err + "\n" + roleId)); return await guild.roles.fetch(roleId);
}, },
async giveRole(member, role) { async giveRole(member, role) {
await member.roles.add(role).catch(err => console.error("Error giving the role: " + err + "\n" + JSON.stringify(role))); await member.roles.add(role);
}, },
async takeRole(member, role) { async takeRole(member, role) {
await member.roles.remove(role).catch(err => console.error("Error taking the role: " + err + "\n" + JSON.stringify(role))); await member.roles.remove(role);
}
},
collectors: {
async create(client, guildInfo) {
// If a collector is already setup
if (client.messageCollectors.has(guildInfo.guildId)) {
// Close the collector
await this.end(client, guildInfo);
}
// Make sure guildInfo is what we expect, the watch channel isnt blank, and notifications are enabled
if ((guildInfo instanceof GuildInfo && guildInfo.watchChannelId != "") && (guildInfo.notificationsEnabled == true)) {
// Fetch the Guild
const guild = await client.guilds.fetch(guildInfo.guildId).catch(e => {
throw "ERRNOGUILD"
});
// Fetch the Channel
const channel = await guild.channels.fetch(guildInfo.watchChannelId);
// Create the filter function
const filter = message => {
// Discard any messages sent by Silvanus
return message.author.id != process.env.BOTID;
}
// Create the collector
const collector = channel.createMessageCollector({ filter });
// Add the collector to the messageCollectors Collection
client.messageCollectors.set(guildInfo.guildId, collector);
// if (isDev) console.log("Set up a collector in " + guildInfo.guildId);
collector.on('collect', message => {
// if (isDev) console.log("Collected a message in " + message.guild.id);
// Check for manual relay use with "water ping" and "fruit ping"
if (message.content.toLowerCase().includes("water ping")) {
functions.sendWaterReminder(guildInfo, guildInfo.waterMessage, guildInfo.reminderChannelId, guild).catch(e => console.error(`[${e.code}]: ${e.message} - ${e.url}`));
return;
} else if (message.content.toLowerCase().includes("fruit ping")) {
functions.sendFruitReminder(guildInfo, guildInfo.fruitMessage, guildInfo.reminderChannelId, guild).catch(e => console.error(`[${e.code}]: ${e.message} - ${e.url}`));
return;
}
// If the message doesn't contain an embed, we can ignore it
if (message.embeds == undefined) return;
if (message.embeds.length == 0) return;
// console.log(JSON.stringify(message.embeds[0].data));
if (message.embeds[0].data.description == undefined) return;
// Check the description field of the embed to determine if it matches Grow A Tree's notification texts
if (message.embeds[0].data.description.includes(strings.notifications.water)) {
functions.sendWaterReminder(guildInfo, guildInfo.waterMessage, guildInfo.reminderChannelId, guild).catch(e => console.error(`[${e.code}]: ${e.message} - ${e.url}`));
} else if (message.embeds[0].data.description.includes(strings.notifications.fruit)) {
functions.sendFruitReminder(guildInfo, guildInfo.fruitMessage, guildInfo.reminderChannelId, guild).catch(e => console.error(`[${e.code}]: ${e.message} - ${e.url}`));
}
});
}
},
async end(client, guildInfo) {
if (!client.messageCollectors) throw "No Message Collectors";
if (!client.messageCollectors.has(guildInfo.guildId)) throw "Guild doesn't have a Message Collector";
const collector = client.messageCollectors.get(guildInfo.guildId);
// Close the collector
await collector.stop();
// Remove the collector from the messageCollectors Collection
client.messageCollectors.delete(guildInfo.guildId);
} }
}, },
async refresh(interaction) { async refresh(interaction) {
// const getGuildInfoResponse = await dbfn.getGuildInfo(interaction.guildId);
// let guildInfo = getGuildInfoResponse.data;
let guildInfo = interaction.client.guildInfos.get(interaction.guild.id);
const findMessagesResponse = await this.messages.find(interaction, guildInfo);
if (findMessagesResponse.code == 1) {
guildInfo = findMessagesResponse.data;
// Parse the tree
await this.tree.parse(interaction, guildInfo);
// Parse the leaderboard
await this.rankings.parse(interaction, guildInfo);
// Build the string that shows the comparison // TODO Move the string building section to fn.builders?
const comparedRankings = await this.rankings.compare(interaction, guildInfo);
if (interaction.client.guildInfos.has(interaction.guildId)) {
let guildInfo = interaction.client.guildInfos.get(interaction.guildId);
// Check for valid message IDs
if (guildInfo.treeMessageId === "") throw strings.error.noTreeMessage;
if (guildInfo.leaderboardMessageId === "") throw strings.error.noLeaderboardMessage;
// Fetch the messages
const { guild } = interaction;
// Tree
const treeChannel = await guild.channels.fetch(guildInfo.treeChannelId);
const treeMessage = await treeChannel.messages.fetch(guildInfo.treeMessageId);
// Leaderboard
const leaderboardChannel = await guild.channels.fetch(guildInfo.leaderboardChannelId);
const leaderboardMessage = await leaderboardChannel.messages.fetch(guildInfo.leaderboardMessageId);
// Update the comparison message information
guildInfo.setCompareMessage(interaction.channel.id, interaction.message.id);
const query = guildInfo.queryBuilder("setCompareMessage");
await dbfn.setGuildInfo(query);
// Update the tree information
// Make sure we have a tree message and parse it
const isTree = this.messages.isTree(treeMessage);
if (!isTree) throw "Tree message isn't actually a tree message!";
guildInfo.setTreeInfo(isTree.treeName, isTree.treeHeight);
// Grab the leaderboard
// Make sure it's a leaderboard and parse it
const isLeaderboard = this.messages.isLeaderboard(leaderboardMessage);
if (!isLeaderboard) throw "Leaderboard message isn't actually a leaderboard message!";
// Upload the leaderboard
await dbfn.uploadLeaderboard(isLeaderboard);
// Build the string that shows the comparison // TODO Move the string building section to fn.builders?
const comparedRankings = await this.rankings.compare(guildInfo);
const embed = this.builders.comparisonEmbed(comparedRankings, guildInfo); const embed = this.builders.comparisonEmbed(comparedRankings, guildInfo);
await interaction.update(embed).catch(err => console.error(err)); await interaction.update(embed).catch(e => console.error(e));
} else { } else {
await interaction.update(this.builders.errorEmbed(findMessagesResponse.status)); throw "Guild doesn't exist in database!";
} }
}, },
reset(guildId) { reset(interaction) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
dbfn.deleteGuildInfo(guildId).then(res => { dbfn.deleteGuildInfo(interaction.guildId).then(res => {
functions.collectionBuilders.guildInfos(interaction.client);
resolve(res); resolve(res);
}).catch(err => { }).catch(err => {
console.error(err); console.error(err);
@ -549,41 +966,73 @@ const functions = {
}); });
}); });
}, },
getInfo(interaction) {
let guildInfo = interaction.client.guildInfos.get(interaction.guild.id);
let guildInfoString = "";
guildInfoString += `Tree Message: https://discord.com/channels/${guildId}/${guildInfo.treeChannelId}/${guildInfo.treeMessageId}\n`;
guildInfoString += `Rank Message: https://discord.com/channels/${guildId}/${guildInfo.leaderboardChannelId}/${guildInfo.leaderboardMessageId}\n`;
return `Here is your servers setup info:\n${guildInfoString}`;
},
getWaterTime(size) { getWaterTime(size) {
return Math.floor(Math.pow(size * 0.07 + 5, 1.1)); // Seconds return Math.floor(Math.pow(size * 0.07 + 5, 1.1)); // Seconds
}, },
timeToHeight(beginHeight, destHeight) { parseWaterTime(seconds) {
// 60 secs in min
// 3600 secs in hr
// 86400 sec in day
let waterParts = {
value: seconds,
units: "secs"
};
if (60 < seconds && seconds <= 3600) { // Minutes
waterParts.value = parseFloat(seconds / 60).toFixed(1);
waterParts.units = "mins";
} else if (3600 < seconds && seconds <= 86400) {
waterParts.value = parseFloat(seconds / 3600).toFixed(1);
waterParts.units = "hrs";
} else if (86400 < seconds) {
waterParts.value = parseFloat(seconds / 86400).toFixed(1);
waterParts.units = "days";
}
return `${waterParts.value} ${waterParts.units}`;
},
timeToHeight(beginHeight, destHeight, efficiency, quality) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let time = 0; let time = 0;
let oldTime = 0;
let compostAppliedCount = 0;
let totalWaterCount = 0;
if ((efficiency) && (quality)) {
for (let i = beginHeight; i < destHeight; i++) {
const randNum = Math.floor(Math.random() * 100);
const compostApplied = randNum <= efficiency;
if (compostApplied) {
let qualityPercent = quality / 100;
let waterTime = functions.getWaterTime(i);
let reductionTime = waterTime * qualityPercent;
let finalTime = waterTime - reductionTime;
compostAppliedCount++;
totalWaterCount++;
time += parseFloat(finalTime);
oldTime += waterTime;
} else {
totalWaterCount++;
let waterTime = parseFloat(functions.getWaterTime(i));
time += waterTime;
oldTime += waterTime;
}
}
} else {
for (let i = beginHeight; i < destHeight; i++) { for (let i = beginHeight; i < destHeight; i++) {
const waterTime = parseFloat(functions.getWaterTime(i)); const waterTime = parseFloat(functions.getWaterTime(i));
// console.log("Height: " + i + "Time: " + waterTime); // console.log("Height: " + i + "Time: " + waterTime);
time += waterTime; time += waterTime;
} }
// 60 secs in min
// 3600 secs in hr
// 86400 sec in day
let units = " secs";
if (60 < time && time <= 3600) { // Minutes
time = parseFloat(time / 60).toFixed(1);
units = " mins";
} else if (3600 < time && time <= 86400) {
time = parseFloat(time / 3600).toFixed(1);
units = " hrs";
} else if (86400 < time) {
time = parseFloat(time / 86400).toFixed(1);
units = " days";
} }
resolve(time + units); const readableWaterTime = this.parseWaterTime(time);
const savedTime = this.parseWaterTime(oldTime - time);
resolve({
time: readableWaterTime,
totalWaterCount: totalWaterCount ? totalWaterCount : undefined,
compostAppliedCount: compostAppliedCount ? compostAppliedCount : undefined,
average: totalWaterCount ? parseFloat((compostAppliedCount / totalWaterCount) * 100).toFixed(1) : undefined,
savedTime: savedTime
});
}); });
}, },
sleep(ms) { sleep(ms) {
@ -595,47 +1044,119 @@ const functions = {
}, ms); }, ms);
}); });
}, },
async sendReminder(guildInfo, message, channelId, guild) { async sendWaterReminder(guildInfo, message, channelId, guild) {
const reminderChannel = await guild.channels.fetch(channelId); const reminderChannel = await guild.channels.fetch(channelId);
const reminderEmbed = functions.builders.reminderEmbed(message, guildInfo); // const reminderEmbed = functions.builders.waterReminderEmbed(message, guildInfo);
await reminderChannel.send(reminderEmbed).catch(err => { if (isDev) console.log(`WR: ${guild.name}: ${guildInfo.treeName}`);
console.error(err); await reminderChannel.send(message).then(async m => {
if (!m.deletable) return;
await this.sleep(500).then(async () => {
await m.delete().catch(e => console.error(e));
});
}).catch(err => {
console.error(`[${err.code}]: ${err.message} - ${err.url}`);
}); });
}, },
async refreshComparisonMessage(client, guildInfo) { async sendFruitReminder(guildInfo, message, channelId, guild) {
if (guildInfo.comparisonChannelId != "" && guildInfo.comparisonMessageId != "") { const reminderChannel = await guild.channels.fetch(channelId);
const guild = await client.guilds.fetch(guildInfo.guildId); // const reminderEmbed = functions.builders.fruitReminderEmbed(message, guildInfo);
const comparisonChannel = await guild.channels.fetch(guildInfo.comparisonChannelId); if (isDev) console.log(`FR: ${guild.name}: ${guildInfo.treeName}`);
const comparisonMessage = await comparisonChannel.messages.fetch(guildInfo.comparisonMessageId); await reminderChannel.send(message).then(async m => {
const embed = comparisonMessage.embeds[0]; if (!m.deletable) return;
const actionRow = this.builders.actionRows.comparisonActionRow(guildInfo); await this.sleep(500).then(async () => {
await comparisonMessage.edit({ components: [actionRow] }); await m.delete().catch(e => console.error(e));
return; });
} }).catch(err => {
console.error(`[${err.code}]: ${err.message} - ${err.url}`);
});
}, },
async setupCollectors(client) { async setupCollectors(client) {
let guildInfos = client.guildInfos; let guildInfos = client.guildInfos;
guildInfos.set("collectors", []); let collectorsArray = [];
await guildInfos.forEach(async guildInfo => { await guildInfos.forEach(async guildInfo => {
if (guildInfo.watchChannelId != "" && guildInfo instanceof GuildInfo) { if (guildInfo instanceof GuildInfo && guildInfo.watchChannelId != "" && guildInfo.notificationsEnabled) {
const guild = await client.guilds.fetch(guildInfo.guildId); const guild = await client.guilds.fetch(guildInfo.guildId);
// console.log(guildInfo instanceof GuildInfo); // console.log(guildInfo instanceof GuildInfo);
const channel = await guild.channels.fetch(guildInfo.watchChannelId); const channel = await guild.channels.fetch(guildInfo.watchChannelId);
const filter = message => { const filter = message => {
return message.author.id != process.env.BOTID && message.embeds != undefined; return message.author.id != process.env.BOTID;
} }
const collector = channel.createMessageCollector({ filter }); const collector = channel.createMessageCollector({ filter });
collector.on('collect', message => { collector.on('collect', message => {
if (message.content.toLowerCase().includes("water ping")) {
this.sendWaterReminder(guildInfo, guildInfo.waterMessage, guildInfo.reminderChannelId, guild).catch(e => console.error(`[${e.code}]: ${e.message} - ${e.url}`));
return;
} else if (message.content.toLowerCase().includes("fruit ping")) {
this.sendFruitReminder(guildInfo, guildInfo.fruitMessage, guildInfo.reminderChannelId, guild).catch(e => console.error(`[${e.code}]: ${e.message} - ${e.url}`));
return;
}
if (message.embeds == undefined) return;
if (message.embeds.length == 0) return; if (message.embeds.length == 0) return;
console.log(message.embeds); guildInfo = client.guildInfos.get(guild.id);
if (message.embeds[0].data.description.includes(strings.notifications.water)) { if (message.embeds[0].data.description.includes(strings.notifications.water)) {
this.sendReminder(guildInfo, guildInfo.waterMessage, guildInfo.reminderChannelId, guild); this.sendWaterReminder(guildInfo, guildInfo.waterMessage, guildInfo.reminderChannelId, guild).catch(e => console.error(`[${e.code}]: ${e.message} - ${e.url}`));
} else if (message.embeds[0].data.description.includes(strings.notifications.fruit)) { } else if (message.embeds[0].data.description.includes(strings.notifications.fruit)) {
this.sendReminder(guildInfo, guildInfo.fruitMessage, guildInfo.reminderChannelId, guild); this.sendFruitReminder(guildInfo, guildInfo.fruitMessage, guildInfo.reminderChannelId, guild).catch(e => console.error(`[${e.code}]: ${e.message} - ${e.url}`));
} }
}); });
} }
}); });
guildInfos.set("collectors", collectorsArray);
},
async setupCollector(channel, interaction) {
if (interaction.client.guildInfos.has(interaction.guildId)) {
let collectors = interaction.client.guildInfos.get('collectors');
let guildInfo = interaction.client.guildInfos.get(interaction.guildId);
const filter = message => {
return message.author.id != process.env.BOTID;
}
const collector = channel.createMessageCollector({ filter });
collectors.push(collector);
collector.on('collect', message => {
if (message.content.toLowerCase().includes("water ping")) {
this.sendWaterReminder(guildInfo, guildInfo.waterMessage, guildInfo.reminderChannelId, guild).catch(e => console.error(`[${e.code}]: ${e.message} - ${e.url}`));
return;
} else if (message.content.toLowerCase().includes("fruit ping")) {
this.sendFruitReminder(guildInfo, guildInfo.fruitMessage, guildInfo.reminderChannelId, guild).catch(e => console.error(`[${e.code}]: ${e.message} - ${e.url}`));
return;
}
if (message.embeds == undefined) return;
if (message.embeds.length == 0) return;
if (message.embeds[0].data.description.includes(strings.notifications.water)) {
this.sendWaterReminder(guildInfo, guildInfo.waterMessage, guildInfo.reminderChannelId, guild).catch(e => console.error(`[${e.code}]: ${e.message} - ${e.url}`));
} else if (message.embeds[0].data.description.includes(strings.notifications.fruit)) {
this.sendFruitReminder(guildInfo, guildInfo.fruitMessage, guildInfo.reminderChannelId, guild).catch(e => console.error(`[${e.code}]: ${e.message} - ${e.url}`));
}
});
} else {
throw "Guild doesn't exist in database!";
}
},
generateErrorId() {
const digitCount = 10;
const digits = [];
for (let i = 0; i < digitCount; i++) {
const randBase = Math.random();
const randNumRaw = randBase * 10;
const randNumRound = Math.floor(randNumRaw);
digits.push(randNumRound);
}
const errorId = digits.join("");
return errorId;
},
async sendHeartbeat(url) {
https.get(url, async (response) => {
if (isDev) console.log("Sent Heartbeat Request: " + url);
let data = '';
response.on('data', (chunk) => data += chunk);
response.on('end', () => {
if (isDev) console.log("Received Heartbeat Response: " + data);
parsedData = JSON.parse(data);
if ( !(parsedData.ok) ) console.error("Heartbeat failed");
});
}).on("error", (error) => console.error(error));
} }
}; };

137
modules/testing.js Normal file → Executable file
View File

@ -1,124 +1,21 @@
const GuildInfo = class { information(content, fields) {
constructor() { const embed = new EmbedBuilder()
this.guildId = ""; .setColor(strings.embeds.color)
this.treeName = ""; .setTitle('Information')
this.treeHeight = 0; .setDescription(content)
this.treeMessageId = ""; .setFooter({ text: `v${package.version} - ${strings.embeds.footer}` });
this.treeChannelId = ""; if (fields) embed.addFields(fields);
this.leaderboardMessageId = ""; const messageContents = { embeds: [embed], ephemeral: true };
this.leaderboardChannelId = ""; return messageContents;
this.waterMessage = "";
this.fruitMessage = "";
this.reminderChannelId = "";
this.watchChannelId = "";
} }
setId(id) { replyContent = `I estimate that a tree with ${efficiency}% Composter Efficiency and ${quality}% Compost Quality growing from ${beginHeight}ft to ${endHeight}ft will take ${time}`;
this.guildId = id; replyFields = [
return this; { name: `Start Height:`, value: `**${beginHeight}ft**`, inline: true },
} { name: `End Height:`, value: `**${endHeight}**`, inline: true },
setName(name) { { name: `Efficiency:`, value: `**${efficiency}%**`, inline: true },
this.treeName = name; { name: `Quality:`, value: `**${quality}%**`, inline: true },
return this; { name: `Summary`, value: `Compost Applied **${compostAppliedCount}** times, out of **${totalWaterCount}** waterings, for an average of **${average}%**` }
}
setHeight(height) {
this.treeHeight = height;
return this;
}
setTreeMessage(messageId, channelId) {
this.treeMessageId = messageId;
this.treeChannelId = channelId;
return this;
}
setLeaderboardMessage(messageId, channelId) {
this.leaderboardMessageId = messageId;
this.leaderboardChannelId = channelId;
return this;
}
setReminders(waterMessage, fruitMessage, reminderChannelId, watchChannelId) {
this.waterMessage = waterMessage;
this.fruitMessage = fruitMessage;
this.reminderChannelId = reminderChannelId;
this.watchChannelId = watchChannelId;
return this;
}
queryBuilder(query) {
let queryParts = [];
switch (query) {
case "setAll":
queryParts = [
`INSERT INTO guild_info `,
`(guild_id, `,
`tree_name, `,
`tree_height, `,
`tree_message_id, `,
`tree_channel_id, `,
`leaderboard_message_id, `,
`leaderboard_channel_id, `,
`water_message, `,
`fruit_message, `,
`reminder_channel_id, `,
`watch_channel_id) `,
`VALUES (${db.escape(this.guildId)}, `,
`${db.escape(this.treeName)}, `,
`${db.escape(this.treeHeight)}, `,
`${db.escape(this.treeMessageId)}, `,
`${db.escape(this.treeChannelId)}, `,
`${db.escape(this.leaderboardMessageId)}, `,
`${db.escape(this.leaderboardChannelId)}, `,
`${db.escape(this.waterMessage)}, `,
`${db.escape(this.fruitMessage)}, `,
`${db.escape(this.reminderChannelId)}, `,
`${db.escape(this.watchChannelId)}) `,
`ON DUPLICATE KEY UPDATE tree_name = ${db.escape(this.treeName)}, `,
`tree_height = ${db.escape(this.treeHeight)}, `,
`tree_message_id = ${db.escape(this.treeMessageId)}, `,
`tree_channel_id = ${db.escape(this.treeChannelId)}, `,
`leaderboard_message_id = ${db.escape(this.leaderboardMessageId)}, `,
`leaderboard_channel_id = ${db.escape(this.leaderboardChannelId)}, `,
`water_message = ${db.escape(this.waterMessage)}, `,
`fruit_message = ${db.escape(this.fruitMessage)}, `,
`reminder_channel_id = ${db.escape(this.reminderChannelId)}, `,
`watch_channel_id = ${db.escape(this.watchChannelId)})`
]; ];
return queryParts.join();
break;
case "setReminders":
queryParts = [
`UPDATE guildInfo SET water_message = ${db.escape(this.waterMessage)}, `,
`fruit_message = ${db.escape(this.fruitMessage)}, `,
`reminder_channel_id = ${db.escape(this.reminderChannelId)}, `,
`watch_channel_id = ${db.escape(this.watchChannelId)} `,
`WHERE guild_id = ${db.escape(this.guildId)}`
];
return queryParts.join();
break;
case "setTreeMessage":
queryParts = [
`UPDATE guildInfo SET tree_message_id = ${db.escape(this.treeMessageId)}, `,
`tree_channel_id = ${db.escape(this.treeChannelId)}, `,
`WHERE guild_id = ${db.escape(this.guildId)}`
];
return queryParts.join();
break;
case "setLeaderboardMessage":
queryParts = [
`UPDATE guildInfo SET leaderboard_message_id = ${db.escape(this.leaderboardMessageId)}, `,
`leaderboard_channel_id = ${db.escape(this.leaderboardChannelId)}, `,
`WHERE guild_id = ${db.escape(this.guildId)}`
];
return queryParts.join();
break;
default:
break;
}
}
}
new GuildInfo() const reply = information(replyContent, replyFields);
.setId(row.guild_id)
.setName(row.tree_name)
.setHeight(row.tree_height)
.setTreeMessage(row.tree_message_id, row.tree_channel_id)
.setLeaderboardMessage(row.leaderboard_message_id, row.leaderboard_channel_id)
.setReminders(row.water_message, row.fruit_message, row.reminder_channel_id);

98
modules/utils.js Normal file
View File

@ -0,0 +1,98 @@
/* eslint-disable no-case-declarations */
/* eslint-disable indent */
// dotenv for handling environment variables
const dotenv = require('dotenv');
dotenv.config();
const token = process.env.TOKEN;
const dbfn = require('./dbfn.js');
// Discord.JS
const { Client, GatewayIntentBits, Partials } = require('discord.js');
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.GuildMessageReactions,
GatewayIntentBits.MessageContent
],
partials: [
Partials.Channel,
Partials.Message
],
});
// Various imports
const fn = require('../modules/functions.js');
client.once('ready', async () => {
// watchRequestRates();
await fn.collectionBuilders.guildInfos(client);
const guilds = client.guilds.cache;
console.log("I'm in " + guilds.size + " guilds with " + client.guildInfos.size + " guildInfos");
// guilds.each(g => {
// console.log(g.name + "," + g.id + "," + g.ownerId);
// });
await setAllGuildOwners();
process.exit();
});
client.login(token);
async function watchRequestRates() {
const axios = require('axios');
// Make a GET request to the Discord API
await axios.get('https://discord.com/api/v10/users/@me', {
headers: {
'Authorization': `Bot ${token}`
}
}).then(response => {
// Get the rate limit headers
const remaining = response.headers['x-ratelimit-remaining'];
const reset = response.headers['x-ratelimit-reset'];
// Log the rate limit headers
console.log(`Remaining requests: ${remaining}`);
console.log(`Reset time (Unix epoch seconds): ${reset}`);
}).catch(error => {
console.error(error);
});
await fn.sleep(500).then(async () => {
await watchRequestRates();
});
}
async function setAllGuildOwners() {
try {
let guildInfosArray = new Array();
let guildUpdateCount = 0;
let guildMissingCount = 0;
client.guildInfos.forEach((guildInfo) => {
guildInfosArray.push(guildInfo);
});
// console.log(guildInfosArray);
for (let i = 0; i < guildInfosArray.length; i++) {
const guildInfo = guildInfosArray[i];
let eFlag = 0;
const guild = await client.guilds.fetch(guildInfo.guildId).catch(e => {
eFlag = 1;
if (e.status === 404) {
console.log("Missing guild: " + guildInfo.guildId);
guildMissingCount++;
} else {
throw e;
}
});
if (eFlag === 1) continue;
guildInfo.setIds(guildInfo.guildId, guild.ownerId);
const query = guildInfo.queryBuilder("setIds");
console.log(query);
await dbfn.setGuildInfo(query);
guildUpdateCount++;
}
console.log(`Updated ${guildUpdateCount} guilds with ${guildMissingCount} missing guilds.`);
} catch(err) {
console.error(err);
}
}

5
package.json Normal file → Executable file
View File

@ -1,6 +1,6 @@
{ {
"name": "silvanus", "name": "silvanus",
"version": "1.1.5", "version": "1.2.9",
"description": "Grow A Tree Companion Bot", "description": "Grow A Tree Companion Bot",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
@ -17,7 +17,8 @@
}, },
"homepage": "https://github.com/voidf1sh/silvanus#readme", "homepage": "https://github.com/voidf1sh/silvanus#readme",
"dependencies": { "dependencies": {
"discord.js": "^14.7.1", "axios": "^1.4.0",
"discord.js": "^14.11.0",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
"mysql": "^2.18.1", "mysql": "^2.18.1",
"string-replace-all": "^2.0.0", "string-replace-all": "^2.0.0",

39
privacy Normal file
View File

@ -0,0 +1,39 @@
Privacy Policy for Silvanus Discord Bot
This Privacy Policy describes how Silvanus ("the Bot," "we," or "us") collects, uses, and stores your personal information when you interact with the bot. Please read this policy carefully to understand our practices regarding your personal data and how we will treat it.
Data Collection and Usage:
We collect and store the following data when you use the Silvanus Discord Bot:
User IDs: We collect user IDs to provide personalized services and ensure smooth bot functionality.
Guild IDs: We collect guild IDs to enable the bot to operate within specific servers and provide server-specific features.
Channel IDs: We collect channel IDs to facilitate bot functionality within specific channels.
Message IDs: We collect message IDs to support certain features and improve the user experience.
Role IDs: We collect role IDs to provide role-based functionality within Discord servers.
Tree Names, Ranks, and Heights: We collect tree-related data for in-game purposes and to enhance user interactions.
Data Storage and Security:
All collected data is stored securely using industry-standard practices. User IDs, guild IDs, channel IDs, message IDs, role IDs, tree names, tree ranks, and tree heights are stored in an encrypted MariaDB database. Additionally, the database is stored on a LUKS encrypted drive, providing an extra layer of security.
Data Deletion Requests:
If you would like to request the deletion of your data, please follow one of the methods below:
Discord Message: You can message @voidf1sh on Discord to request data deletion.
Email: You can send an email to SilvanusDev@gmail.com to request data deletion.
Support Server: You can join our support server at https://discord.gg/g5JRGn7PxU and submit a request for data deletion.
Please note that we will make reasonable efforts to respond to and process your data deletion requests in a timely manner, subject to any legal obligations or legitimate interests that may prevent us from complying with such requests.
Data Sharing:
We do not sell or share your personal information with third parties, except in the following circumstances:
Compliance with Laws: We may share your personal information if required to do so by applicable laws, regulations, legal processes, or governmental requests.
Protection of Rights: We may share your personal information to protect the rights, property, or safety of Silvanus, its users, or others.
Changes to the Privacy Policy:
We reserve the right to modify or update this Privacy Policy at any time. We will notify users of any material changes through the bot or other means. Your continued use of Silvanus after such modifications or updates signifies your acceptance of the revised Privacy Policy.
Contact Information:
If you have any questions, concerns, or requests regarding this Privacy Policy or our data practices, please contact us at SilvanusDev@gmail.com.
Last updated: [22 May 2023]

0
slash-commands/.DS_Store vendored Normal file → Executable file
View File

20
slash-commands/about.js Executable file
View File

@ -0,0 +1,20 @@
const { SlashCommandBuilder, messageLink } = require('discord.js');
const fn = require('../modules/functions.js');
const strings = require('../data/strings.json');
module.exports = {
data: new SlashCommandBuilder()
.setName('about')
.setDescription('Get info about the bot')
.addStringOption(o =>
o.setName('private')
.setDescription('Should the response be sent privately?')
.setRequired(true)
.addChoices(
{ name: "True", value: "true" },
{ name: "False", value: "false" })),
async execute(interaction) {
const aboutEmbed = fn.builders.aboutEmbed(interaction.options.getString('private'));
await interaction.reply(aboutEmbed);
},
};

0
slash-commands/commands.js Normal file → Executable file
View File

50
slash-commands/compare.js Normal file → Executable file
View File

@ -1,6 +1,7 @@
const { SlashCommandBuilder, Guild } = require('discord.js'); const { SlashCommandBuilder, Guild } = require('discord.js');
const dbfn = require('../modules/dbfn.js'); const dbfn = require('../modules/dbfn.js');
const fn = require('../modules/functions.js'); const fn = require('../modules/functions.js');
const strings = require('../data/strings.json');
const { GuildInfo } = require('../modules/CustomClasses.js'); const { GuildInfo } = require('../modules/CustomClasses.js');
module.exports = { module.exports = {
@ -14,36 +15,33 @@ module.exports = {
if (interaction.client.guildInfos.has(interaction.guildId)) { if (interaction.client.guildInfos.has(interaction.guildId)) {
let guildInfo = interaction.client.guildInfos.get(interaction.guildId); let guildInfo = interaction.client.guildInfos.get(interaction.guildId);
const findMessagesResponse = await fn.messages.find(interaction, guildInfo); // Handle a missing tree or leaderboard
if (findMessagesResponse.code == 1) { let errors = [];
let guildInfo = interaction.client.guildInfos.get(interaction.guildId); if (guildInfo.treeMessageId === "") {
// Parse the leaderboard message errors.push(strings.error.noTreeMessage);
await fn.rankings.parse(interaction, guildInfo); } else if (guildInfo.leaderboardMessageId === "") {
errors.push(strings.error.noLeaderboardMessage);
}
if (errors.length > 0) {
const embed = fn.builders.errorEmbed(`Unable to complete comparison, unable to find message(s):\n${errors.join("\n")}`);
await interaction.editReply(embed);
return;
}
// Build the string that shows the comparison // TODO Move the string building section to fn.builders? // Build the string that shows the comparison // TODO Move the string building section to fn.builders?
const comparedRankings = await fn.rankings.compare(interaction, guildInfo); const comparedRankings = await fn.rankings.compare(guildInfo);
const embed = fn.builders.comparisonEmbed(comparedRankings, guildInfo); const embed = fn.builders.comparisonEmbed(comparedRankings, guildInfo);
await interaction.editReply(embed).catch(err => console.error(err)); await interaction.editReply(embed).then(async m => {
guildInfo.setCompareMessage(m.channel.id, m.id);
const query = guildInfo.queryBuilder("setCompareMessage");
await dbfn.setGuildInfo(query);
}).catch(e => console.error(e));
} else { } else {
await interaction.editReply(fn.builders.errorEmbed(findMessagesResponse.status)); let errors = [];
} errors.push(strings.error.noTreeMessage);
} else { errors.push(strings.error.noLeaderboardMessage);
// Create a basic guildInfo with blank data const embed = fn.builders.errorEmbed(`Unable to complete comparison, unable to find message(s):\n${errors.join("\n")}`);
let guildInfo = new GuildInfo() await interaction.editReply(embed);
.setId(interaction.guildId)
.setTreeMessage("", interaction.channelId)
.setLeaderboardMessage("", interaction.channelId)
// Using the above guildInfo, try to find the Grow A Tree messages
const findMessagesResponse = await fn.messages.find(interaction, guildInfo);
guildInfo = findMessagesResponse.data;
if (findMessagesResponse.code == 1) {
// Build the string that shows the comparison // TODO Move the string building section to fn.builders?
const comparedRankings = await fn.rankings.compare(interaction, guildInfo);
const embed = fn.builders.comparisonEmbed(comparedRankings, guildInfo);
await interaction.editReply(embed).catch(err => console.error(err));
} else {
await interaction.editReply(fn.builders.errorEmbed(findMessagesResponse.status));
}
} }
} catch (err) { } catch (err) {
interaction.editReply(fn.builders.errorEmbed(err)).catch(err => { interaction.editReply(fn.builders.errorEmbed(err)).catch(err => {

2
slash-commands/help.js Normal file → Executable file
View File

@ -14,7 +14,7 @@ module.exports = {
{ name: "True", value: "true" }, { name: "True", value: "true" },
{ name: "False", value: "false" })), { name: "False", value: "false" })),
execute(interaction) { execute(interaction) {
const helpEmbed = fn.builders.helpEmbed(`${strings.help.info}\n\n**Setup**\n${strings.help.setup}\n\nSee </commands:0> for a list of all my commands\n\n**Support Server**\n${strings.urls.supportServer}`, interaction.options.getString('private')); const helpEmbed = fn.builders.helpEmbed(`${strings.help.info}\n\n**Setup**\n${strings.help.setup}\n\n**Support Server**\n${strings.urls.supportServer}`, interaction.options.getString('private'));
interaction.reply(helpEmbed); interaction.reply(helpEmbed);
}, },
}; };

View File

@ -1,50 +0,0 @@
const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js');
const dbfn = require('../modules/dbfn.js');
const fn = require('../modules/functions.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('notifications')
.setDescription('Setup a notification relay for improved water and fruit notifications')
.addChannelOption(o =>
o
.setName('watchchannel')
.setDescription('The channel Grow A Tree sends your notifications in')
.setRequired(true))
.addStringOption(o =>
o
.setName('watermessage')
.setDescription('Message to send for water reminders')
.setRequired(true))
.addChannelOption(o =>
o
.setName('pingchannel')
.setDescription('The channel to send the water reminder in')
.setRequired(true))
.addStringOption(o =>
o
.setName('fruitmessage')
.setDescription("Message to send for fruit reminders")
.setRequired(false))
.setDefaultMemberPermissions(PermissionFlagsBits.ManageRoles),
async execute(interaction) {
try {
await interaction.deferReply({ ephemeral: true });
if (interaction.client.guildInfos.has(interaction.guildId)) {
const watchChannel = interaction.options.getChannel('watchchannel');
const waterMessage = interaction.options.getString('watermessage');
const fruitMessage = interaction.options.getString('fruitmessage') ? interaction.options.getString('fruitmessage') : interaction.options.getString('watermessage');
const reminderChannel = interaction.options.getChannel('pingchannel');
let guildInfo = interaction.client.guildInfos.get(interaction.guildId);
guildInfo.setReminders(waterMessage, fruitMessage, reminderChannel.id, watchChannel.id);
let query = guildInfo.queryBuilder("setReminders");
console.log(query);
await dbfn.setGuildInfo(query);
await interaction.editReply(`I'll watch <#${watchChannel.id}> for Grow A Tree Notifications and relay them to <#${reminderChannel.id}>.`).catch(e => console.error(e));
fn.collectionBuilders.guildInfos(interaction.client);
}
} catch (err) {
console.error("Error occurred while setting up a notification relay: " + err);
}
},
};

View File

@ -1,20 +0,0 @@
const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js');
const dbfn = require('../modules/dbfn.js');
const fn = require('../modules/functions.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('optout')
.setDescription('Opt-out of automatic water reminders')
.setDefaultMemberPermissions(PermissionFlagsBits.ManageRoles),
async execute(interaction) {
try {
await interaction.deferReply({ ephemeral: true });
const setReminderOptInResponse = await dbfn.setReminderOptIn(interaction.guildId, 0);
interaction.editReply(setReminderOptInResponse.status);
} catch(err) {
console.error(err);
await interaction.editReply(fn.builders.errorEmbed(err));
}
},
};

30
slash-commands/permissions.js Executable file
View File

@ -0,0 +1,30 @@
const { SlashCommandBuilder, PermissionsBitField, PermissionFlagsBits } = require('discord.js');
const fn = require('../modules/functions.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('permissions')
.setDescription('Check my permissions here')
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
async execute(interaction) {
await interaction.deferReply({ ephemeral: true });
const me = interaction.guild.members.me;
const guildPerms = me.permissions;
const manageRoles = guildPerms.has(PermissionsBitField.Flags.ManageRoles);
const mentionEveryone = guildPerms.has(PermissionsBitField.Flags.MentionEveryone);
const channelPerms = me.permissionsIn(interaction.channel);
const viewChannel = channelPerms.has(PermissionsBitField.Flags.ViewChannel);
const sendMessages = channelPerms.has(PermissionsBitField.Flags.SendMessages);
const responseParts = [
`This is the status of my permissions in this server and this channel (<#${interaction.channel.id}>)`,
`**Guild Permissions**`,
`Manage Roles: ${manageRoles}`,
`Mention All Roles: ${mentionEveryone}`,
`**Channel Permissions**`,
`View Channel: ${viewChannel}`,
`Send Messages: ${sendMessages}`
];
const replyEmbed = fn.builders.embed(responseParts.join("\n"));
await interaction.editReply(replyEmbed);
}
};

205
slash-commands/relay.js Executable file
View File

@ -0,0 +1,205 @@
const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js');
const { GuildInfo } = require('../modules/CustomClasses.js');
const dbfn = require('../modules/dbfn.js');
const fn = require('../modules/functions.js');
const strings = require('../data/strings.json');
module.exports = {
data: new SlashCommandBuilder()
.setName('relay')
.setDescription('A notification relay for improved water and fruit notifications')
.addSubcommand(sc =>
sc.setName('set')
.setDescription('Set up the notification relay for the first time')
.addChannelOption(o =>
o.setName('watchchannel')
.setDescription('The channel Grow A Tree sends your notifications in')
.setRequired(true)
)
.addStringOption(o =>
o.setName('watermessage')
.setDescription('Message to send for water reminders')
.setRequired(true)
)
.addChannelOption(o =>
o.setName('pingchannel')
.setDescription('The channel to send the water reminder in')
.setRequired(true)
)
.addStringOption(o =>
o.setName('fruitmessage')
.setDescription("Message to send for fruit reminders")
.setRequired(false)
)
)
.addSubcommand(sc =>
sc.setName('update')
.setDescription('Update an already setup notification relay')
.addChannelOption(o =>
o.setName('watchchannel')
.setDescription('The channel Grow A Tree sends your notifications in')
.setRequired(false)
)
.addStringOption(o =>
o.setName('watermessage')
.setDescription('Message to send for water reminders')
.setRequired(false)
)
.addChannelOption(o =>
o.setName('pingchannel')
.setDescription('The channel to send the water reminder in')
.setRequired(false)
)
.addStringOption(o =>
o.setName('fruitmessage')
.setDescription("Message to send for fruit reminders")
.setRequired(false)
)
.addBooleanOption(o =>
o.setName('enabled')
.setDescription("Enable the relay?")
.setRequired(false)
)
)
.addSubcommand(sc =>
sc.setName('disable')
.setDescription('Disable the notification relay')
)
.setDefaultMemberPermissions(PermissionFlagsBits.ManageRoles),
async execute(interaction) {
try {
await interaction.deferReply({ ephemeral: true });
const subcommand = interaction.options.getSubcommand();
// if (process.env.DEBUG) console.log(`${typeof subcommand}: ${subcommand}`);
switch (subcommand) {
// Set all components for the first time
case "set":
// If there is already a guildInfo object for this server
if (interaction.client.guildInfos.has(interaction.guildId)) {
let guildInfo = interaction.client.guildInfos.get(interaction.guildId);
// Get options from the interaction
const watchChannel = interaction.options.getChannel('watchchannel');
const waterMessage = interaction.options.getString('watermessage');
// If the fruit message is set, use it, otherwise default to the water message.
const fruitMessage = interaction.options.getString('fruitmessage') ? interaction.options.getString('fruitmessage') : interaction.options.getString('watermessage');
const reminderChannel = interaction.options.getChannel('pingchannel');
// Set the reminder configuration in the GuildInfo object
guildInfo.setReminders(waterMessage, fruitMessage, reminderChannel.id, watchChannel.id, true);
// Update the guildInfos Collection
interaction.client.guildInfos.set(interaction.guildId, guildInfo);
// Build a query to update the database
let query = guildInfo.queryBuilder("setReminders");
// Run the query
await dbfn.setGuildInfo(query);
// Set up a collector on the watch channel
fn.collectors.create(interaction.client, guildInfo);
// Compose a reply
const reply = [
`I'll watch <#${watchChannel.id}> for Grow A Tree Notifications and relay them to <#${reminderChannel.id}>.`,
`Water Message: ${waterMessage}`,
`Fruit Message: ${fruitMessage}`
].join("\n");
// Send the reply
await interaction.editReply(fn.builders.embed(reply)).catch(e => console.error(e));
} else {
// Get options from the interaction
const watchChannel = interaction.options.getChannel('watchchannel');
const waterMessage = interaction.options.getString('watermessage');
// If the fruit message is set, use it. Otherwise default to the water message
const fruitMessage = interaction.options.getString('fruitmessage') ? interaction.options.getString('fruitmessage') : interaction.options.getString('watermessage');
const reminderChannel = interaction.options.getChannel('pingchannel');
// Create a new GuildInfo object
let guildInfo = new GuildInfo()
.setIds(interaction.guildId, interaction.guild.ownerId)
// Set the reminder configuration
.setReminders(waterMessage, fruitMessage, reminderChannel.id, watchChannel.id, true);
// Update the guildInfos Collection
interaction.client.guildInfos.set(interaction.guildId, guildInfo);
// Build a query to update the database
let query = guildInfo.queryBuilder("setReminders");
// Run the query
await dbfn.setGuildInfo(query);
// Refresh the collection
await fn.collectionBuilders.guildInfos(interaction.client);
// Create a messageCollector on the watch channel
fn.collectors.create(interaction.client, guildInfo);
// Compose a reply
const reply = [
`I'll watch <#${watchChannel.id}> for Grow A Tree Notifications and relay them to <#${reminderChannel.id}>.`,
`Water Message: ${waterMessage}`,
`Fruit Message: ${fruitMessage}`
].join("\n");
// Send the reply
await interaction.editReply(reply).catch(e => console.error(e));
}
break;
case "update": // Update the relay configuration piecemeal
if (interaction.client.guildInfos.has(interaction.guildId)) {
let guildInfo = interaction.client.guildInfos.get(interaction.guildId);
// Get all possible options from the interaction
const inWatchChannel = interaction.options.getChannel('watchchannel');
const inWaterMessage = interaction.options.getString('watermessage');
const inFruitMessage = interaction.options.getString('fruitmessage');
const inReminderChannel = interaction.options.getChannel('pingchannel');
const inEnabled = interaction.options.getBoolean('enabled');
// Check if each option is set, if it is, use it. Otherwise use what was already set
const outWatchChannelId = inWatchChannel ? inWatchChannel.id : guildInfo.watchChannelId;
const outWaterMessage = inWaterMessage ? inWaterMessage : guildInfo.waterMessage;
const outFruitMessage = inFruitMessage ? inFruitMessage : guildInfo.fruitMessage;
const outReminderChannelId = inReminderChannel ? inReminderChannel.id : guildInfo.reminderChannelId;
const outEnabled = inEnabled ? inEnabled : guildInfo.notificationsEnabled;
// Update the relay configuration
guildInfo.setReminders(outWaterMessage, outFruitMessage, outReminderChannelId, outWatchChannelId, outEnabled);
// Update the guildInfos Collection
interaction.client.guildInfos.set(interaction.guildId, guildInfo);
// Build a query to update the database
let query = guildInfo.queryBuilder("setReminders");
// Run the query
await dbfn.setGuildInfo(query);
// Refresh the collection
await fn.collectionBuilders.guildInfos(interaction.client);
// Create a messageCollector on the watch channel
fn.collectors.create(interaction.client, guildInfo);
// Compose a reply
const reply = [
`I'll watch <#${outWatchChannelId}> for Grow A Tree Notifications and relay them to <#${outReminderChannelId}>.`,
`Water Message: ${outWaterMessage}`,
`Fruit Message: ${outFruitMessage}`
].join("\n");
// Send the reply
await interaction.editReply(reply).catch(e => console.error(e));
} else {
await interaction.editReply(fn.builders.errorEmbed("There is no existing notification relay to update!")).catch(e => console.error(e));
}
break;
case 'disable': // Disable the relay
if (interaction.client.guildInfos.has(interaction.guildId)) {
let guildInfo = interaction.client.guildInfos.get(interaction.guildId);
// Update the relay config with all undefined except for `enabled` which is false
guildInfo.setReminders(undefined, undefined, undefined, undefined, false);
// Update the guildInfos Collection
interaction.client.guildInfos.set(interaction.guildId, guildInfo);
// Update the database
await dbfn.setGuildInfo(guildInfo.queryBuilder("setReminders")).catch(e => console.error(e));
// Refresh the collection
await fn.collectionBuilders.guildInfos(interaction.client);
// Close the collector
await fn.collectors.end(interaction.client, guildInfo).catch(e => console.error(e));
// Reply confirming disabling of relay
await interaction.editReply(fn.builders.embed(strings.status.optout)).catch(e => console.error(e));
} else {
await interaction.editReply(fn.builders.errorEmbed("A notification relay has not been set up yet!")).catch(e => console.error(e));
}
break;
default:
await interaction.editReply(fn.builders.errorEmbed("Invalid subcommand detected.")).catch(e => console.error(e));
break;
}
} catch (err) {
console.error("Error occurred while setting up a notification relay: " + err);
}
},
};

View File

@ -1,24 +0,0 @@
const { SlashCommandBuilder } = require('discord.js');
const fn = require('../modules/functions.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('reset')
.setDescription('Reset all message assignments in your server'),
execute(interaction) {
interaction.deferReply({ ephemeral: true }).then(() => {
fn.reset(interaction.guildId).then(res => {
interaction.editReply(fn.builders.embed("Assignments Reset")).catch(err => {
console.error(err);
});
}).catch(err => {
console.error(err);
interaction.editReply("There was a problem deleting your guild information, contact @voidf1sh#0420 for help.").catch(err => {
console.error(err);
});
});
}).catch(err => {
console.error(err);
});
},
};

21
slash-commands/rolemenu.js Normal file → Executable file
View File

@ -1,29 +1,18 @@
const { SlashCommandBuilder } = require('discord.js'); const { SlashCommandBuilder } = require('discord.js');
const dbfn = require('../modules/dbfn.js');
const fn = require('../modules/functions.js'); const fn = require('../modules/functions.js');
const strings = require('../data/strings.json');
module.exports = { module.exports = {
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
.setName('rolemenu') .setName('rolemenu')
.setDescription('Send a self-assignable role selection menu') .setDescription('Send a self-assignable role selection menu'),
.addRoleOption(o =>
o.setName('waterrole')
.setDescription('The role for water reminder pings')
.setRequired(true))
.addRoleOption(o =>
o.setName('fruitrole')
.setDescription('The role for fruit alert pings')
.setRequired(false)),
async execute(interaction) { async execute(interaction) {
await interaction.deferReply().catch(err => console.error(err)); await interaction.deferReply().catch(e => console.error(e));
if (interaction.client.guildInfos.has(interaction.guildId)) { if (interaction.client.guildInfos.has(interaction.guildId)) {
let guildInfo = interaction.client.guildInfos.get(interaction.guildId); let guildInfo = interaction.client.guildInfos.get(interaction.guildId);
guildInfo.setRoles(interaction.options.getRole('waterrole').id, interaction.options.getRole('fruitrole').id); await interaction.editReply(fn.builders.embeds.treeRoleMenu(guildInfo)).catch(e => console.error(e));
await dbfn.setGuildInfo(guildInfo.queryBuilder("setRoles"));
await fn.collectionBuilders.guildInfos(interaction.client);
await interaction.editReply(fn.builders.embeds.treeRoleMenu(guildInfo)).catch(err => console.error(err));
} else { } else {
await interaction.editReply(fn.builders.errorEmbed("No information is known about your server yet, please run /setup or /compare")).catch(err => console.error(err)); await interaction.editReply(fn.builders.errorEmbed(strings.status.noRoleMenu)).catch(e => console.error(e));
} }
}, },
}; };

114
slash-commands/setup.js Normal file → Executable file
View File

@ -1,37 +1,99 @@
const { SlashCommandBuilder } = require('discord.js'); const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js');
const fn = require('../modules/functions.js'); const fn = require('../modules/functions.js');
const strings = require('../data/strings.json'); const strings = require('../data/strings.json');
const dbfn = require('../modules/dbfn.js'); const dbfn = require('../modules/dbfn.js');
const { GuildInfo } = require('../modules/CustomClasses.js');
module.exports = { module.exports = {
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
.setName('setup') .setName('setup')
.setDescription('Attempt automatic configuration of the bot.') .setDescription('Configure some feature settings.')
.addChannelOption(o => .addSubcommand(sc =>
o.setName('treechannel') sc.setName('rolemenu')
.setDescription('What channel is your tree in?') .setDescription('Setup the roles to be used with /rolemenu')
.setRequired(true)) .addRoleOption(o =>
.addChannelOption(o => o.setName('waterrole')
o.setName('leaderboardchannel') .setDescription('The role for water reminder pings')
.setDescription('If your leaderboard isn\'t in the same channel, where is it?') .setRequired(true)
.setRequired(false)), )
.addRoleOption(o =>
o.setName('fruitrole')
.setDescription('The role for fruit alert pings')
.setRequired(false)
)
)
.addSubcommand(sc =>
sc.setName('view')
.setDescription('View your server\'s configuration'))
.addSubcommand(sc =>
sc.setName('reset')
.setDescription('Remove all server configuration from the database')
.addBooleanOption(o =>
o.setName('confirm')
.setDescription('WARNING THIS IS IRREVERSIBLE')
.setRequired(true)
)
)
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
async execute(interaction) { async execute(interaction) {
await interaction.deferReply({ ephemeral: true }); await interaction.deferReply({ ephemeral: true });
/**/ const subcommand = interaction.options.getSubcommand();
let guildInfo = { switch (subcommand) {
"guildId": interaction.guildId, case "rolemenu":
"treeName": "", let waterRoleId = interaction.options.getRole('waterrole').id;
"treeHeight": 0, let fruitRoleId = interaction.options.getRole('fruitrole') ? interaction.options.getRole('fruitrole').id : undefined;
"treeMessageId": "", if (interaction.client.guildInfos.has(interaction.guildId)) {
"treeChannelId": `${interaction.options.getChannel('treechannel').id }`, let guildInfo = interaction.client.guildInfos.get(interaction.guildId);
"leaderboardMessageId": "", guildInfo.setRoles(waterRoleId, fruitRoleId);
"leaderboardChannelId": `${interaction.options.getChannel('leaderboardchannel').id || interaction.options.getChannel('treechannel').id }`, await dbfn.setGuildInfo(guildInfo.queryBuilder("setRoles"));
"reminderMessage": "", await fn.collectionBuilders.guildInfos(interaction.client);
"reminderChannelId": "", await interaction.editReply(fn.builders.embeds.treeRoleMenu(guildInfo)).catch(e => console.error(e));
"remindedStatus": 0, } else {
"reminderOptIn": 0, let guildInfo = new GuildInfo()
}; .setIds(interaction.guildId, interaction.guild.ownerId);
const findMessagesResponse = await fn.messages.find(interaction, guildInfo); guildInfo.setRoles(waterRoleId, fruitRoleId);
interaction.editReply(findMessagesResponse.status); await dbfn.setGuildInfo(guildInfo.queryBuilder("setRoles"));
await fn.collectionBuilders.guildInfos(interaction.client);
await interaction.editReply(fn.builders.embeds.treeRoleMenu(guildInfo)).catch(e => console.error(e));
}
break;
case "view":
try {
if (interaction.client.guildInfos.has(interaction.guildId)) {
let guildInfo = interaction.client.guildInfos.get(interaction.guildId);
await interaction.editReply(fn.builders.embed(guildInfo.generateSetupInfo()));
} else {
await interaction.editReply(fn.builders.errorEmbed("Guild doesn't exist in database!"));
}
} catch (err) {
console.error(err);
await interaction.editReply(fn.builders.errorEmbed("There was an error running the command."));
}
break;
case "reset":
if (interaction.client.guildInfos.has(interaction.guildId)) {
let guildInfo = interaction.client.guildInfos.get(interaction.guildId);
if (interaction.options.getBoolean('confirm')) {
fn.reset(interaction).then(res => {
interaction.editReply(fn.builders.embed(strings.status.reset)).catch(err => {
console.error(err);
});
}).catch(err => {
console.error(err);
interaction.editReply(strings.status.resetError).catch(err => {
console.error(err);
});
});
} else {
await interaction.editReply(fn.builders.embed("You must select 'true' to confirm setup reset. No changes have been made.")).catch(e => console.error(e));
}
} else {
await interaction.editReply(fn.builders.errorEmbed("There is no configuration to delete.")).catch(e => console.error(e));
}
break;
default:
await interaction.editReply(fn.builders.errorEmbed(strings.error.invalidSubcommand)).catch(e => console.error(e));
break;
}
}, },
}; };

View File

@ -1,21 +0,0 @@
const { SlashCommandBuilder } = require('discord.js');
const fn = require('../modules/functions.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('setupinfo')
.setDescription('View information about how the bot is set up in your server'),
execute(interaction) {
interaction.deferReply({ephemeral: true}).then(() => {
fn.getInfo(interaction).then(res => {
const embed = fn.builders.embed(res);
interaction.editReply(embed);
}).catch(err => {
interaction.editReply(err);
console.error(err);
});
}).catch(err => {
console.error(err);
});
},
};

0
slash-commands/template Normal file → Executable file
View File

62
slash-commands/timetoheight.js Normal file → Executable file
View File

@ -9,25 +9,61 @@ module.exports = {
.addIntegerOption(o => .addIntegerOption(o =>
o.setName('endheight') o.setName('endheight')
.setDescription('Ending tree height in feet') .setDescription('Ending tree height in feet')
.setRequired(true)) .setRequired(true)
)
.addIntegerOption(o => .addIntegerOption(o =>
o.setName('beginheight') o.setName('beginheight')
.setDescription('Beginning tree height in feet') .setDescription('Beginning tree height in feet')
.setRequired(false)), .setRequired(false)
)
.addIntegerOption(o =>
o.setName('efficiency')
.setDescription('Composter efficiency percentage, rounded')
.setRequired(false)
)
.addIntegerOption(o =>
o.setName('quality')
.setDescription('Compost quality percentage, rounded')
.setRequired(false)
)
.addBooleanOption(o =>
o.setName('private')
.setDescription('Should the reply be visible only to you?')
.setRequired(false)
),
async execute(interaction) { async execute(interaction) {
await interaction.deferReply({ ephemeral: true }); const private = interaction.options.getBoolean('private') != undefined ? interaction.options.getBoolean('private') : true;
let beginHeight = interaction.options.getInteger('beginheight'); await interaction.deferReply({ ephemeral: private });
const inBeginHeight = interaction.options.getInteger('beginheight');
const endHeight = interaction.options.getInteger('endheight'); const endHeight = interaction.options.getInteger('endheight');
if (!beginHeight) { const efficiency = interaction.options.getInteger('efficiency') != undefined ? interaction.options.getInteger('efficiency') : 10;
const quality = interaction.options.getInteger('quality') != undefined ? interaction.options.getInteger('quality') : 5;
let beginHeight, replyContent;
if (!inBeginHeight) {
const guildInfo = interaction.client.guildInfos.get(interaction.guild.id); const guildInfo = interaction.client.guildInfos.get(interaction.guild.id);
beginHeight = guildInfo.treeHeight; beginHeight = guildInfo.treeHeight;
} else {
beginHeight = inBeginHeight;
}
const timeToHeightResults = await fn.timeToHeight(beginHeight, endHeight, efficiency, quality);
const { time, totalWaterCount, compostAppliedCount, average, savedTime } = timeToHeightResults;
let replyFields = undefined;
if (efficiency && quality) {
replyContent = `I estimate that a tree with ${efficiency}% Composter Efficiency and ${quality}% Compost Quality growing from ${beginHeight}ft to ${endHeight}ft will take ${time}`;
replyFields = [
{ name: `Height Gained:`, value: `${endHeight - beginHeight}ft`, inline: true},
{ name: `E/Q:`, value: `${efficiency}%/${quality}%`, inline: true},
{ name: `Compost Applied`, value: `${compostAppliedCount} times`, inline: true },
{ name: `Compost Average`, value: `${average}%`, inline: true },
{ name: `Saved Time`, value: savedTime, inline: true }
];
} else {
replyContent = `I estimate that a tree growing from ${beginHeight}ft to ${endHeight}ft will take ${time}`;
}
const reply = fn.builders.embeds.information(replyContent, replyFields)
await interaction.editReply(reply);
} }
fn.timeToHeight(beginHeight, endHeight).then(res => {
interaction.editReply(`It will take a tree that is ${beginHeight}ft tall ${res} to reach ${endHeight}ft.`);
}).catch(err => {
interaction.editReply("Error: " + err);
console.error(err);
return;
});
},
}; };

20
slash-commands/watertime.js Normal file → Executable file
View File

@ -6,13 +6,21 @@ module.exports = {
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
.setName('watertime') .setName('watertime')
.setDescription('Calculate the watering time for a given tree height') .setDescription('Calculate the watering time for a given tree height')
.addStringOption(o => .addIntegerOption(o =>
o.setName('height') o.setName('height')
.setDescription('Tree height in feet, numbers ONLY') .setDescription('Tree height')
.setRequired(true)), .setRequired(true))
.addBooleanOption(o =>
o.setName('private')
.setDescription('Should the response be private? Default: true')
.setRequired(false)),
async execute(interaction) { async execute(interaction) {
await interaction.deferReply(); const treeHeight = interaction.options.getInteger('height');
const treeHeight = interaction.options.getString('height'); const privateOpt = interaction.options.getBoolean('private');
await interaction.editReply(`A tree that is ${treeHeight}ft tall will have a watering time of ${fn.getWaterTime(treeHeight)} minutes.`); const private = privateOpt != undefined ? privateOpt : true;
await interaction.deferReply( {ephemeral: private });
const waterSeconds = fn.getWaterTime(treeHeight);
const waterTime = fn.parseWaterTime(waterSeconds);
await interaction.editReply(`A tree that is ${treeHeight}ft tall will have a watering time of ${waterTime}.`);
}, },
}; };